forked from zelo72/mastodon-ios
chore: add Combine style valid logic
This commit is contained in:
parent
a74ac3a41a
commit
243d3362e6
|
@ -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:
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue