diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4d26a731..889bd465 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; }; 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; + 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; @@ -196,6 +197,7 @@ 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; 2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = ""; }; 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; + 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = ""; }; 2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = ""; }; 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = ""; }; 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = ""; }; @@ -815,6 +817,7 @@ children = ( DB084B5125CBC56300F898ED /* CoreDataStack */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, + 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, 2D46976325C2A71500CF4AA9 /* UIIamge.swift */, @@ -1243,6 +1246,7 @@ 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, diff --git a/Mastodon/Extension/UITapGestureRecognizer.swift b/Mastodon/Extension/UITapGestureRecognizer.swift new file mode 100644 index 00000000..7e1628f3 --- /dev/null +++ b/Mastodon/Extension/UITapGestureRecognizer.swift @@ -0,0 +1,26 @@ +// +// UITapGestureRecognizer.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/19. +// + +import UIKit + +extension UITapGestureRecognizer { + + static var singleTapGestureRecognizer: UITapGestureRecognizer { + let tapGestureRecognizer = UITapGestureRecognizer() + tapGestureRecognizer.numberOfTapsRequired = 1 + tapGestureRecognizer.numberOfTouchesRequired = 1 + return tapGestureRecognizer + } + + static var doubleTapGestureRecognizer: UITapGestureRecognizer { + let tapGestureRecognizer = UITapGestureRecognizer() + tapGestureRecognizer.numberOfTapsRequired = 2 + tapGestureRecognizer.numberOfTouchesRequired = 1 + return tapGestureRecognizer + } + +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index ba540499..84de0ae9 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -44,6 +44,9 @@ internal enum Asset { internal static let primary = ColorAsset(name: "Colors/Label/primary") internal static let secondary = ColorAsset(name: "Colors/Label/secondary") } + internal enum TextField { + internal static let successGreen = ColorAsset(name: "Colors/TextField/successGreen") + } internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow") internal static let lightBackground = ColorAsset(name: "Colors/lightBackground") internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json index 275226a1..a47dfc69 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x84", - "green" : "0x69", - "red" : "0x60" + "blue" : "132", + "green" : "105", + "red" : "96" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json new file mode 100644 index 00000000..7ccf54a1 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/TextField/successGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "89", + "green" : "199", + "red" : "52" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift index db9f3085..247841c2 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewController.swift @@ -5,79 +5,165 @@ // Created by MainasuK Cirno on 2021-2-5. // -import os.log -import UIKit import Combine import MastodonSDK +import os.log +import UIKit import UITextField_Shake final class MastodonRegisterViewController: UIViewController, NeedsDependency { - var disposeBag = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var viewModel: MastodonRegisterViewModel! + + let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - let usernameLabel: UILabel = { + let stackViewTopDistance: CGFloat = 16 + + var keyboardFrame: CGRect! + + var scrollview: UIScrollView = { + let scrollview = UIScrollView() + scrollview.showsVerticalScrollIndicator = false + scrollview.translatesAutoresizingMaskIntoConstraints = false + return scrollview + }() + + let largeTitleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .largeTitle) + label.textColor = Asset.Colors.Label.black.color + label.text = "Tell us about you." + return label + }() + + let photoView: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + let photoButton: UIButton = { + let button = UIButton(type: .custom) + let boldFont = UIFont.systemFont(ofSize: 42) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) + + button.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) + button.imageView?.tintColor = Asset.Colors.Icon.photo.color + button.backgroundColor = .white + button.layer.cornerRadius = 45 + button.clipsToBounds = true + return button + }() + + let plusIcon: UIImageView = { + let icon = UIImageView() + let boldFont = UIFont.systemFont(ofSize: 24) + let configuration = UIImage.SymbolConfiguration(font: boldFont) + let image = UIImage(systemName: "plus.circle.fill", withConfiguration: configuration) + icon.image = image + icon.tintColor = Asset.Colors.Icon.plus.color + return icon + }() + + let domainLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Username:" + label.textColor = Asset.Colors.Label.black.color return label }() let usernameTextField: UITextField = { let textField = UITextField() - textField.placeholder = "Username" + textField.autocapitalizationType = .none textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "username", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() - let emailLabel: UILabel = { + let usernameIsTakenLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Email:" return label }() + let displayNameTextField: UITextField = { + let textField = UITextField() + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "display name", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always + return textField + }() + let emailTextField: UITextField = { let textField = UITextField() - textField.placeholder = "example@gmail.com" textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.keyboardType = .emailAddress + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "email", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() - let passwordLabel: UILabel = { + let passwordCheckLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Password:" + label.numberOfLines = 4 return label }() let passwordTextField: UITextField = { let textField = UITextField() - textField.placeholder = "Password" textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.keyboardType = .asciiCapable textField.isSecureTextEntry = true + textField.backgroundColor = .white + textField.textColor = .black + textField.attributedPlaceholder = NSAttributedString(string: "password", + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + textField.borderStyle = UITextField.BorderStyle.roundedRect + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) + textField.leftView = paddingView + textField.leftViewMode = .always return textField }() let signUpButton: UIButton = { let button = UIButton(type: .system) button.titleLabel?.font = .preferredFont(forTextStyle: .headline) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal) + button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled) + button.isEnabled = false button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) - button.setTitle("Sign up", for: .normal) + button.setTitle("Continue", for: .normal) button.layer.masksToBounds = true button.layer.cornerRadius = 8 button.layer.cornerCurve = .continuous @@ -89,36 +175,97 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency { activityIndicatorView.hidesWhenStopped = true return activityIndicatorView }() - } extension MastodonRegisterViewController { - override func viewDidLoad() { super.viewDidLoad() - title = "Sign Up" - view.backgroundColor = Asset.Colors.Background.systemBackground.color + navigationController?.navigationBar.isHidden = true + view.backgroundColor = Asset.Colors.Background.signUpSystemBackground.color + domainLabel.text = "@" + viewModel.domain + " " + domainLabel.sizeToFit() + passwordCheckLabel.attributedText = viewModel.attributeStringForPassword() + usernameTextField.rightView = domainLabel + usernameTextField.rightViewMode = .always + usernameTextField.delegate = self + displayNameTextField.delegate = self + emailTextField.delegate = self + passwordTextField.delegate = self + // gesture + view.addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(_resignFirstResponder)) + + // stackview let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(stackView) + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 40 + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.addArrangedSubview(largeTitleLabel) + stackView.addArrangedSubview(photoView) + stackView.addArrangedSubview(usernameTextField) + stackView.addArrangedSubview(displayNameTextField) + stackView.addArrangedSubview(emailTextField) + stackView.addArrangedSubview(passwordTextField) + stackView.addArrangedSubview(passwordCheckLabel) + + // scrollview + view.addSubview(scrollview) NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16), - stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + view.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor, constant: 20), + scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), ]) - stackView.axis = .vertical - stackView.spacing = 8 + // stackview + scrollview.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + let bottomEdgeLayoutConstraint: NSLayoutConstraint = scrollview.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: scrollview.contentLayoutGuide.topAnchor, constant: stackViewTopDistance), + stackView.leadingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollview.contentLayoutGuide.trailingAnchor), + stackView.widthAnchor.constraint(equalTo: scrollview.frameLayoutGuide.widthAnchor), + bottomEdgeLayoutConstraint, + ]) + + // photoview + photoView.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(photoButton) + NSLayoutConstraint.activate([ + photoView.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + ]) + photoButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + photoButton.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.widthAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + photoButton.centerXAnchor.constraint(equalTo: photoView.centerXAnchor), + photoButton.centerYAnchor.constraint(equalTo: photoView.centerYAnchor), + ]) + plusIcon.translatesAutoresizingMaskIntoConstraints = false + photoView.addSubview(plusIcon) + NSLayoutConstraint.activate([ + plusIcon.trailingAnchor.constraint(equalTo: photoButton.trailingAnchor), + plusIcon.bottomAnchor.constraint(equalTo: photoButton.bottomAnchor), + ]) - stackView.addArrangedSubview(usernameLabel) - stackView.addArrangedSubview(usernameTextField) - stackView.addArrangedSubview(emailLabel) - stackView.addArrangedSubview(emailTextField) - stackView.addArrangedSubview(passwordLabel) - stackView.addArrangedSubview(passwordTextField) + // textfield + NSLayoutConstraint.activate([ + usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + ]) + + // password + stackView.setCustomSpacing(6, after: passwordTextField) + stackView.setCustomSpacing(32, after: passwordCheckLabel) + // button signUpButton.translatesAutoresizingMaskIntoConstraints = false stackView.addArrangedSubview(signUpButton) NSLayoutConstraint.activate([ @@ -126,18 +273,31 @@ extension MastodonRegisterViewController { ]) signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signUpActivityIndicatorView) + scrollview.addSubview(signUpActivityIndicatorView) NSLayoutConstraint.activate([ signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), ]) + NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil) + .sink { [weak self] notification in + guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } + self?.keyboardFrame = endFrame + UIView.animate(withDuration: 0.3) { + bottomEdgeLayoutConstraint.constant = UIScreen.main.bounds.height - endFrame.origin.y + 26 + self?.view.layoutIfNeeded() + } + } + .store(in: &disposeBag) + viewModel.isRegistering .receive(on: DispatchQueue.main) .sink { [weak self] isRegistering in guard let self = self else { return } isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal) + self.signUpButton.setTitle(isRegistering ? "" : "Continue", for: .normal) self.signUpButton.isEnabled = !isRegistering } .store(in: &disposeBag) @@ -157,28 +317,125 @@ extension MastodonRegisterViewController { ) } .store(in: &disposeBag) + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + guard let text = self.passwordTextField.text else { return } + + let validations = self.viewModel.validatePassword(text: text) + + self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validations.0, oneNumber: validations.1, oneSpecialCharacter: validations.2) + } + .store(in: &disposeBag) signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } - +} + +extension MastodonRegisterViewController: UITextFieldDelegate { + func textFieldDidBeginEditing(_ textField: UITextField) { + var bottomOffsetY: CGFloat = textField.frame.origin.y + textField.frame.height - scrollview.frame.height + keyboardFrame.size.height + stackViewTopDistance + if textField == passwordTextField { + bottomOffsetY += passwordCheckLabel.frame.height + } + + if bottomOffsetY > 0 { + scrollview.setContentOffset(CGPoint(x: 0, y: bottomOffsetY), animated: true) + } + } + + func textFieldDidEndEditing(_ textField: UITextField) { + let valid = validateTextField(textField: textField) + if valid { + if validateAllTextField() { + signUpButton.isEnabled = true + } + } + } + + func showShadowWithColor(color: UIColor, textField: UITextField) { + // To apply Shadow + textField.layer.shadowOpacity = 1 + textField.layer.shadowRadius = 2.0 + textField.layer.shadowOffset = CGSize.zero // Use any CGSize + textField.layer.shadowColor = color.cgColor + } + 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 { + signUpButton.isEnabled = false + var isvalid = false + if textField == usernameTextField { + isvalid = validateUsername() + } + if textField == displayNameTextField { + isvalid = validateDisplayName() + } + if textField == emailTextField { + isvalid = validateEmail() + } + if textField == passwordTextField { + isvalid = validatePassword() + } + if isvalid { + showShadowWithColor(color: Asset.Colors.TextField.successGreen.color, textField: textField) + } else { + textField.shake() + showShadowWithColor(color: Asset.Colors.lightDangerRed.color, textField: textField) + } + return isvalid + } + func validateAllTextField() -> Bool { + return validateUsername() && validateDisplayName() && validateEmail() && validatePassword() + } } extension MastodonRegisterViewController { - + @objc private func _resignFirstResponder() { + usernameTextField.resignFirstResponder() + displayNameTextField.resignFirstResponder() + emailTextField.resignFirstResponder() + passwordTextField.resignFirstResponder() + } + @objc private func signUpButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let username = usernameTextField.text else { - usernameTextField.shake() - return - } - - guard let email = emailTextField.text else { - emailTextField.shake() - return - } - - guard let password = passwordTextField.text else { - passwordTextField.shake() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) + if !validateAllTextField() { return } @@ -187,11 +444,12 @@ extension MastodonRegisterViewController { let query = Mastodon.API.Account.RegisterQuery( reason: nil, - username: username, - email: email, - password: password, - agreement: true, // TODO: - locale: "en" // TODO: + username: usernameTextField.text!, + displayname: displayNameTextField.text!, + email: emailTextField.text!, + password: passwordTextField.text!, + agreement: true, // TODO: + locale: "en" // TODO: ) context.apiService.accountRegister( @@ -211,7 +469,7 @@ extension MastodonRegisterViewController { } } receiveValue: { [weak self] response in guard let self = self else { return } - let _ = response.value + _ = response.value // TODO: let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in @@ -222,7 +480,5 @@ extension MastodonRegisterViewController { self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } .store(in: &disposeBag) - } - } diff --git a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift index f2404bb0..09897442 100644 --- a/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Authentication/Register/MastodonRegisterViewModel.swift @@ -5,12 +5,12 @@ // Created by MainasuK Cirno on 2021-2-5. // -import Foundation import Combine +import Foundation import MastodonSDK +import UIKit final class MastodonRegisterViewModel { - // input let domain: String let applicationToken: Mastodon.Entity.Token @@ -25,5 +25,67 @@ final class MastodonRegisterViewModel { self.applicationToken = applicationToken self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) } - +} + +extension MastodonRegisterViewModel { + + func isValidEmail(_ email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: email) + } + + func validatePassword(text: String) -> (Bool, Bool, Bool) { + let trimmedText = text.trimmingCharacters(in: .whitespaces) + let isEightCharacters = trimmedText.count >= 8 + let isOneNumber = trimmedText.range(of: ".*[0-9]", options: .regularExpression) != nil + let isOneSpecialCharacter = trimmedText.trimmingCharacters(in: .decimalDigits).trimmingCharacters(in: .letters).count > 0 + return (isEightCharacters, isOneNumber, isOneSpecialCharacter) + } + + func attributeStringForUsername() -> NSAttributedString { + let resultAttributeString = NSMutableAttributedString() + let redImage = NSTextAttachment() + let font = UIFont.preferredFont(forTextStyle: .caption1) + let configuration = UIImage.SymbolConfiguration(font: font) + redImage.image = UIImage(systemName: "xmark.octagon.fill", withConfiguration: configuration)?.withTintColor(Asset.Colors.lightDangerRed.color) + let imageAttribute = NSAttributedString(attachment: redImage) + let stringAttribute = NSAttributedString(string: "This username is taken.", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.lightDangerRed.color]) + resultAttributeString.append(imageAttribute) + resultAttributeString.append(stringAttribute) + return resultAttributeString + } + + func attributeStringForPassword(eightCharacters: Bool = false, oneNumber: Bool = false, oneSpecialCharacter: Bool = false) -> NSAttributedString { + let font = UIFont.preferredFont(forTextStyle: .caption1) + let color = UIColor.black + let falseColor = UIColor.clear + let attributeString = NSMutableAttributedString() + + let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(start) + + attributeString.append(checkImage(color: eightCharacters ? color : falseColor)) + let eightCharactersDescription = NSAttributedString(string: "Eight characters\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(eightCharactersDescription) + + attributeString.append(checkImage(color: oneNumber ? color : falseColor)) + let oneNumberDescription = NSAttributedString(string: "One number\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(oneNumberDescription) + + attributeString.append(checkImage(color: oneSpecialCharacter ? color : falseColor)) + let oneSpecialCharacterDescription = NSAttributedString(string: "One special character\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(oneSpecialCharacterDescription) + + return attributeString + } + + func checkImage(color: UIColor) -> NSAttributedString { + let checkImage = NSTextAttachment() + let font = UIFont.preferredFont(forTextStyle: .caption1) + let configuration = UIImage.SymbolConfiguration(font: font) + checkImage.image = UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)?.withTintColor(color) + return NSAttributedString(attachment: checkImage) + } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index d9b2a444..97659533 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -161,14 +161,16 @@ extension Mastodon.API.Account { public struct RegisterQuery: Codable, PostQuery { public let reason: String? public let username: String + public let displayname: String public let email: String public let password: String public let agreement: Bool public let locale: String - public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) { + public init(reason: String? = nil, username: String, displayname: String, email: String, password: String, agreement: Bool, locale: String) { self.reason = reason self.username = username + self.displayname = displayname self.email = email self.password = password self.agreement = agreement