mastodon-ios/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+D...

238 lines
13 KiB
Swift

//
// 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
cell.textField.textAlignment = .left
cell.textField.semanticContentAttribute = .forceLeftToRight
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
cell.textField.textAlignment = .left
cell.textField.semanticContentAttribute = .forceLeftToRight
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<RegisterSection, RegisterItem>()
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<ValidateState>.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)
}
}