// // MastodonRegisterViewModel+Diffable.swift // Mastodon // // Created by MainasuK on 2022-1-5. // import UIKit import Combine import MastodonAsset import MastodonLocalization extension MastodonRegisterViewModel { func setupDiffableDataSource( tableView: UITableView ) { tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) tableView.register(MastodonRegisterAvatarTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self)) tableView.register(MastodonRegisterTextFieldTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self)) tableView.register(MastodonRegisterPasswordHintTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self)) diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { case .header(let domain): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell cell.titleLabel.text = L10n.Scene.Register.title(domain) cell.subTitleLabel.isHidden = true return cell case .avatar: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self), for: indexPath) as! MastodonRegisterAvatarTableViewCell self.configureAvatar(cell: cell) return cell case .name: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.DisplayName.placeholder) cell.textField.keyboardType = .default cell.textField.autocapitalizationType = .words cell.textField.text = self.name NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) .receive(on: DispatchQueue.main) .compactMap { notification in guard let textField = notification.object as? UITextField else { assertionFailure() return nil } return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .assign(to: \.name, on: self) .store(in: &cell.disposeBag) return cell case .username: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell cell.setupTextViewRightView(text: "@" + self.domain) cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Username.placeholder) cell.textField.keyboardType = .alphabet cell.textField.autocorrectionType = .no cell.textField.text = self.username NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) .receive(on: DispatchQueue.main) .compactMap { notification in guard let textField = notification.object as? UITextField else { assertionFailure() return nil } return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .assign(to: \.username, on: self) .store(in: &cell.disposeBag) self.configureTextFieldCell(cell: cell, validateState: self.$usernameValidateState) return cell case .email: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Email.placeholder) cell.textField.keyboardType = .emailAddress cell.textField.autocorrectionType = .no cell.textField.text = self.email NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) .receive(on: DispatchQueue.main) .compactMap { notification in guard let textField = notification.object as? UITextField else { assertionFailure() return nil } return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .assign(to: \.email, on: self) .store(in: &cell.disposeBag) self.configureTextFieldCell(cell: cell, validateState: self.$emailValidateState) return cell case .password: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Password.placeholder) cell.textField.keyboardType = .alphabet cell.textField.autocorrectionType = .no cell.textField.isSecureTextEntry = true cell.textField.text = self.password NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) .receive(on: DispatchQueue.main) .compactMap { notification in guard let textField = notification.object as? UITextField else { assertionFailure() return nil } return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .assign(to: \.password, on: self) .store(in: &cell.disposeBag) self.configureTextFieldCell(cell: cell, validateState: self.$passwordValidateState) return cell case .hint: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self), for: indexPath) as! MastodonRegisterPasswordHintTableViewCell return cell case .reason: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest) cell.textField.keyboardType = .default cell.textField.text = self.reason NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) .receive(on: DispatchQueue.main) .compactMap { notification in guard let textField = notification.object as? UITextField else { assertionFailure() return nil } return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .assign(to: \.reason, on: self) .store(in: &cell.disposeBag) self.configureTextFieldCell(cell: cell, validateState: self.$reasonValidateState) return cell default: assertionFailure() return UITableViewCell() } } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems([.header(domain: domain)], toSection: .main) snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main) if approvalRequired { snapshot.appendItems([.reason], toSection: .main) } diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil) } } extension MastodonRegisterViewModel { private func configureAvatar(cell: MastodonRegisterAvatarTableViewCell) { self.$avatarImage .receive(on: DispatchQueue.main) .sink { [weak self, weak cell] image in guard let self = self else { return } guard let cell = cell else { return } let image = image ?? Asset.Scene.Onboarding.avatarPlaceholder.image cell.avatarButton.setImage(image, for: .normal) cell.avatarButton.menu = self.createAvatarMediaContextMenu() cell.avatarButton.showsMenuAsPrimaryAction = true } .store(in: &cell.disposeBag) } enum AvatarMediaMenuAction { case photoLibrary case camera case browse case delete } private func createAvatarMediaContextMenu() -> UIMenu { var children: [UIMenuElement] = [] // Photo Library let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in guard let self = self else { return } self.avatarMediaMenuActionPublisher.send(.photoLibrary) } children.append(photoLibraryAction) // Camera if UIImagePickerController.isSourceTypeAvailable(.camera) { let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in guard let self = self else { return } self.avatarMediaMenuActionPublisher.send(.camera) }) children.append(cameraAction) } // Browse let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in guard let self = self else { return } self.avatarMediaMenuActionPublisher.send(.browse) } children.append(browseAction) // Delete if avatarImage != nil { let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in guard let self = self else { return } self.avatarMediaMenuActionPublisher.send(.delete) } children.append(deleteAction) } return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) } private func configureTextFieldCell( cell: MastodonRegisterTextFieldTableViewCell, validateState: Published.Publisher ) { Publishers.CombineLatest( validateState, cell.textField.publisher(for: \.isFirstResponder) ) .receive(on: DispatchQueue.main) .sink { [weak cell] validateState, isFirstResponder in guard let cell = cell else { return } switch validateState { case .empty: cell.textFieldShadowContainer.shadowColor = isFirstResponder ? Asset.Colors.brandBlue.color : .black cell.textFieldShadowContainer.shadowAlpha = isFirstResponder ? 1 : 0.25 case .valid: cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.valid.color cell.textFieldShadowContainer.shadowAlpha = 1 case .invalid: cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.invalid.color cell.textFieldShadowContainer.shadowAlpha = 1 } } .store(in: &cell.disposeBag) } }