chore: add Combine style valid logic

This commit is contained in:
sunxiaojian 2021-02-20 18:24:23 +08:00
parent a74ac3a41a
commit 243d3362e6
3 changed files with 117 additions and 74 deletions

View File

@ -34,7 +34,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency {
let largeTitleLabel: UILabel = { let largeTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .largeTitle) label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34))
label.textColor = Asset.Colors.Label.black.color label.textColor = Asset.Colors.Label.black.color
label.text = "Tell us about you." label.text = "Tell us about you."
return label return label
@ -60,6 +60,16 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency {
return button return button
}() }()
let plusIconBackground: UIImageView = {
let icon = UIImageView()
let boldFont = UIFont.systemFont(ofSize: 24)
let configuration = UIImage.SymbolConfiguration(font: boldFont)
let image = UIImage(systemName: "plus.circle", withConfiguration: configuration)
icon.image = image
icon.tintColor = .white
return icon
}()
let plusIcon: UIImageView = { let plusIcon: UIImageView = {
let icon = UIImageView() let icon = UIImageView()
let boldFont = UIFont.systemFont(ofSize: 24) let boldFont = UIFont.systemFont(ofSize: 24)
@ -246,6 +256,12 @@ extension MastodonRegisterViewController {
photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor),
photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor),
]) ])
plusIconBackground.translatesAutoresizingMaskIntoConstraints = false
photoView.addSubview(plusIconBackground)
NSLayoutConstraint.activate([
plusIconBackground.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor),
plusIconBackground.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor),
])
plusIcon.translatesAutoresizingMaskIntoConstraints = false plusIcon.translatesAutoresizingMaskIntoConstraints = false
photoView.addSubview(plusIcon) photoView.addSubview(plusIcon)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -309,7 +325,40 @@ extension MastodonRegisterViewController {
self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid) self.setTextFieldValidAppearance(self.usernameTextField, isValid: isValid)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
viewModel.isDisplaynameValid
.receive(on: DispatchQueue.main)
.sink { [weak self] isValid in
guard let self = self else { return }
self.setTextFieldValidAppearance(self.displayNameTextField, isValid: isValid)
}
.store(in: &disposeBag)
viewModel.isEmailValid
.receive(on: DispatchQueue.main)
.sink { [weak self] isValid in
guard let self = self else { return }
self.setTextFieldValidAppearance(self.emailTextField, isValid: isValid)
}
.store(in: &disposeBag)
viewModel.isPasswordValid
.receive(on: DispatchQueue.main)
.sink { [weak self] isValid in
guard let self = self else { return }
self.setTextFieldValidAppearance(self.passwordTextField, isValid: isValid)
}
.store(in: &disposeBag)
Publishers.CombineLatest4(
viewModel.isUsernameValid,
viewModel.isDisplaynameValid,
viewModel.isEmailValid,
viewModel.isPasswordValid
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isUsernameValid, isDisplaynameValid, isEmailValid, isPasswordValid in
guard let self = self else { return }
self.signUpButton.isEnabled = isUsernameValid ?? false && isDisplaynameValid ?? false && isEmailValid ?? false && isPasswordValid ?? false
}
.store(in: &disposeBag)
viewModel.error viewModel.error
.compactMap { $0 } .compactMap { $0 }
@ -326,16 +375,13 @@ extension MastodonRegisterViewController {
) )
} }
.store(in: &disposeBag) .store(in: &disposeBag)
NotificationCenter.default NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
guard let text = self.passwordTextField.text else { return } guard let text = self.passwordTextField.text else { return }
let validations = self.viewModel.validatePassword(text: text) let validations = self.viewModel.validatePassword(text: text)
self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -360,13 +406,13 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
switch textField { switch textField {
case usernameTextField: case usernameTextField:
viewModel.username.value = textField.text viewModel.username.value = textField.text
default: case displayNameTextField:
let valid = validateTextField(textField: textField) viewModel.displayname.value = textField.text
if valid { case emailTextField:
if validateAllTextField() { viewModel.email.value = textField.text
signUpButton.isEnabled = true case passwordTextField:
} viewModel.password.value = textField.text
} default: break
} }
} }
@ -374,60 +420,24 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
// To apply Shadow // To apply Shadow
textField.layer.shadowOpacity = 1 textField.layer.shadowOpacity = 1
textField.layer.shadowRadius = 2.0 textField.layer.shadowRadius = 2.0
textField.layer.shadowOffset = CGSize.zero // Use any CGSize textField.layer.shadowOffset = CGSize.zero
textField.layer.shadowColor = color.cgColor textField.layer.shadowColor = color.cgColor
textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath
}
func validateUsername() -> Bool {
if usernameTextField.text?.count ?? 0 > 0 {
showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: usernameTextField)
return true
} else {
return false
}
}
func validateDisplayName() -> Bool {
if displayNameTextField.text?.count ?? 0 > 0 {
return true
} else {
return false
}
}
func validateEmail() -> Bool {
guard let email = emailTextField.text else {
return false
}
if !viewModel.isValidEmail(email) {
return false
}
return true
}
func validatePassword() -> Bool {
guard let password = passwordTextField.text else {
return false
} }
let result = viewModel.validatePassword(text: password)
if !(result.0 && result.1 && result.2) {
return false
}
return true
}
func validateTextField(textField: UITextField) -> Bool { func validateTextField(textField: UITextField) -> Bool {
signUpButton.isEnabled = false
var isvalid = false var isvalid = false
// if textField == usernameTextField { if textField == usernameTextField {
// isvalid = validateUsername() isvalid = viewModel.isUsernameValid.value ?? false
// } }
if textField == displayNameTextField { if textField == displayNameTextField {
isvalid = validateDisplayName() isvalid = viewModel.isDisplaynameValid.value ?? false
} }
if textField == emailTextField { if textField == emailTextField {
isvalid = validateEmail() isvalid = viewModel.isEmailValid.value ?? false
} }
if textField == passwordTextField { if textField == passwordTextField {
isvalid = validatePassword() isvalid = viewModel.isPasswordValid.value ?? false
} }
if isvalid { if isvalid {
showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField)
@ -437,8 +447,9 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
} }
return isvalid return isvalid
} }
func validateAllTextField() -> Bool { func validateAllTextField() -> Bool {
return validateUsername() && validateDisplayName() && validateEmail() && validatePassword() return viewModel.isUsernameValid.value ?? false && viewModel.isDisplaynameValid.value ?? false && viewModel.isEmailValid.value ?? false && viewModel.isPasswordValid.value ?? false
} }
private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) { private func setTextFieldValidAppearance(_ textField: UITextField, isValid: Bool?) {
@ -454,7 +465,6 @@ extension MastodonRegisterViewController: UITextFieldDelegate {
showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField)
} }
} }
} }
extension MastodonRegisterViewController { extension MastodonRegisterViewController {
@ -476,10 +486,9 @@ extension MastodonRegisterViewController {
let query = Mastodon.API.Account.RegisterQuery( let query = Mastodon.API.Account.RegisterQuery(
reason: nil, reason: nil,
username: usernameTextField.text!, username: viewModel.username.value!,
displayname: displayNameTextField.text!, email: viewModel.email.value!,
email: emailTextField.text!, password: viewModel.password.value!,
password: passwordTextField.text!,
agreement: true, // TODO: agreement: true, // TODO:
locale: "en" // TODO: locale: "en" // TODO:
) )

View File

@ -11,7 +11,6 @@ import MastodonSDK
import UIKit import UIKit
final class MastodonRegisterViewModel { final class MastodonRegisterViewModel {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
@ -19,10 +18,18 @@ final class MastodonRegisterViewModel {
let applicationToken: Mastodon.Entity.Token let applicationToken: Mastodon.Entity.Token
let isRegistering = CurrentValueSubject<Bool, Never>(false) let isRegistering = CurrentValueSubject<Bool, Never>(false)
let username = CurrentValueSubject<String?, Never>(nil) let username = CurrentValueSubject<String?, Never>(nil)
let displayname = CurrentValueSubject<String?, Never>(nil)
let email = CurrentValueSubject<String?, Never>(nil)
let password = CurrentValueSubject<String?, Never>(nil)
// output // output
let applicationAuthorization: Mastodon.API.OAuth.Authorization let applicationAuthorization: Mastodon.API.OAuth.Authorization
let isUsernameValid = CurrentValueSubject<Bool?, Never>(nil) let isUsernameValid = CurrentValueSubject<Bool?, Never>(nil)
let isDisplaynameValid = CurrentValueSubject<Bool?, Never>(nil)
let isEmailValid = CurrentValueSubject<Bool?, Never>(nil)
let isPasswordValid = CurrentValueSubject<Bool?, Never>(nil)
let error = CurrentValueSubject<Error?, Never>(nil) let error = CurrentValueSubject<Error?, Never>(nil)
init(domain: String, applicationToken: Mastodon.Entity.Token) { init(domain: String, applicationToken: Mastodon.Entity.Token) {
@ -39,15 +46,44 @@ final class MastodonRegisterViewModel {
} }
.assign(to: \.value, on: isUsernameValid) .assign(to: \.value, on: isUsernameValid)
.store(in: &disposeBag) .store(in: &disposeBag)
displayname
.map { displayname in
guard let displayname = displayname else {
return nil
}
return !displayname.isEmpty
}
.assign(to: \.value, on: isDisplaynameValid)
.store(in: &disposeBag)
email
.map { [weak self] email in
guard let self = self else { return nil }
guard let email = email else {
return nil
}
return !email.isEmpty && self.isValidEmail(email)
}
.assign(to: \.value, on: isEmailValid)
.store(in: &disposeBag)
password
.map { [weak self] password in
guard let self = self else { return nil }
guard let password = password else {
return nil
}
let result = self.validatePassword(text: password)
return !password.isEmpty && result.0 && result.1 && result.2
}
.assign(to: \.value, on: isPasswordValid)
.store(in: &disposeBag)
} }
} }
extension MastodonRegisterViewModel { extension MastodonRegisterViewModel {
func isValidEmail(_ email: String) -> Bool { func isValidEmail(_ email: String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
return emailPred.evaluate(with: email) return emailPred.evaluate(with: email)
} }
@ -81,26 +117,26 @@ extension MastodonRegisterViewModel {
let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color])
attributeString.append(start) attributeString.append(start)
attributeString.append(checkImage(color: eightCharacters ? color : falseColor)) attributeString.append(checkmarkImage(color: eightCharacters ? color : falseColor))
let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color])
attributeString.append(eightCharactersDescription) attributeString.append(eightCharactersDescription)
attributeString.append(checkImage(color: oneNumber ? color : falseColor)) attributeString.append(checkmarkImage(color: oneNumber ? color : falseColor))
let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color])
attributeString.append(oneNumberDescription) attributeString.append(oneNumberDescription)
attributeString.append(checkImage(color: oneSpecialCharacter ? color : falseColor)) attributeString.append(checkmarkImage(color: oneSpecialCharacter ? color : falseColor))
let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color])
attributeString.append(oneSpecialCharacterDescription) attributeString.append(oneSpecialCharacterDescription)
return attributeString return attributeString
} }
func checkImage(color: UIColor) -> NSAttributedString { func checkmarkImage(color: UIColor) -> NSAttributedString {
let checkImage = NSTextAttachment() let checkmarkImage = NSTextAttachment()
let font = UIFont.preferredFont(forTextStyle: .caption1) let font = UIFont.preferredFont(forTextStyle: .caption1)
let configuration = UIImage.SymbolConfiguration(font: font) let configuration = UIImage.SymbolConfiguration(font: font)
checkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color) checkmarkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color)
return NSAttributedString(attachment: checkImage) return NSAttributedString(attachment: checkmarkImage)
} }
} }

View File

@ -161,16 +161,14 @@ extension Mastodon.API.Account {
public struct RegisterQuery: Codable, PostQuery { public struct RegisterQuery: Codable, PostQuery {
public let reason: String? public let reason: String?
public let username: String public let username: String
public let displayname: String
public let email: String public let email: String
public let password: String public let password: String
public let agreement: Bool public let agreement: Bool
public let locale: String public let locale: String
public init(reason: String? = nil, username: String, displayname: String, email: String, password: String, agreement: Bool, locale: String) { public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) {
self.reason = reason self.reason = reason
self.username = username self.username = username
self.displayname = displayname
self.email = email self.email = email
self.password = password self.password = password
self.agreement = agreement self.agreement = agreement