feat: finish sign up page

This commit is contained in:
sunxiaojian 2021-02-20 13:55:06 +08:00
parent c827d7630b
commit 8ef5a34a40
9 changed files with 451 additions and 69 deletions

View File

@ -23,6 +23,7 @@
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; }; 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */; };
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; };
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.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 */; }; 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.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 = "<group>"; }; 2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; }; 2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; };
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; }; 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = "<group>"; };
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; }; 2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; };
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; }; 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; }; 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
@ -815,6 +817,7 @@
children = ( children = (
DB084B5125CBC56300F898ED /* CoreDataStack */, DB084B5125CBC56300F898ED /* CoreDataStack */,
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
2D46976325C2A71500CF4AA9 /* UIIamge.swift */, 2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
@ -1243,6 +1246,7 @@
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,

View File

@ -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
}
}

View File

@ -44,6 +44,9 @@ internal enum Asset {
internal static let primary = ColorAsset(name: "Colors/Label/primary") internal static let primary = ColorAsset(name: "Colors/Label/primary")
internal static let secondary = ColorAsset(name: "Colors/Label/secondary") 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 lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground") internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue") internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")

View File

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0x84", "blue" : "132",
"green" : "0x69", "green" : "105",
"red" : "0x60" "red" : "96"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -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
}
}

View File

@ -5,79 +5,165 @@
// Created by MainasuK Cirno on 2021-2-5. // Created by MainasuK Cirno on 2021-2-5.
// //
import os.log
import UIKit
import Combine import Combine
import MastodonSDK import MastodonSDK
import os.log
import UIKit
import UITextField_Shake import UITextField_Shake
final class MastodonRegisterViewController: UIViewController, NeedsDependency { final class MastodonRegisterViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: MastodonRegisterViewModel! 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() let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline) label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.black.color
label.text = "Username:"
return label return label
}() }()
let usernameTextField: UITextField = { let usernameTextField: UITextField = {
let textField = UITextField() let textField = UITextField()
textField.placeholder = "Username"
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no 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 return textField
}() }()
let emailLabel: UILabel = { let usernameIsTakenLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Email:"
return label 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 emailTextField: UITextField = {
let textField = UITextField() let textField = UITextField()
textField.placeholder = "example@gmail.com"
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.keyboardType = .emailAddress 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 return textField
}() }()
let passwordLabel: UILabel = { let passwordCheckLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline) label.numberOfLines = 4
label.textColor = Asset.Colors.Label.primary.color
label.text = "Password:"
return label return label
}() }()
let passwordTextField: UITextField = { let passwordTextField: UITextField = {
let textField = UITextField() let textField = UITextField()
textField.placeholder = "Password"
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.keyboardType = .asciiCapable textField.keyboardType = .asciiCapable
textField.isSecureTextEntry = true 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 return textField
}() }()
let signUpButton: UIButton = { let signUpButton: UIButton = {
let button = UIButton(type: .system) let button = UIButton(type: .system)
button.titleLabel?.font = .preferredFont(forTextStyle: .headline) 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.lightBrandBlue.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.lightDisabled.color), for: .disabled)
button.isEnabled = false
button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) 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.masksToBounds = true
button.layer.cornerRadius = 8 button.layer.cornerRadius = 8
button.layer.cornerCurve = .continuous button.layer.cornerCurve = .continuous
@ -89,36 +175,97 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency {
activityIndicatorView.hidesWhenStopped = true activityIndicatorView.hidesWhenStopped = true
return activityIndicatorView return activityIndicatorView
}() }()
} }
extension MastodonRegisterViewController { extension MastodonRegisterViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = "Sign Up" navigationController?.navigationBar.isHidden = true
view.backgroundColor = Asset.Colors.Background.systemBackground.color 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() let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical
view.addSubview(stackView) 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([ NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16), scrollview.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), scrollview.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), view.trailingAnchor.constraint(equalTo: scrollview.frameLayoutGuide.trailingAnchor, constant: 20),
scrollview.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
]) ])
stackView.axis = .vertical // stackview
stackView.spacing = 8 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) // textfield
stackView.addArrangedSubview(usernameTextField) NSLayoutConstraint.activate([
stackView.addArrangedSubview(emailLabel) usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh),
stackView.addArrangedSubview(emailTextField) displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh),
stackView.addArrangedSubview(passwordLabel) emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh),
stackView.addArrangedSubview(passwordTextField) passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh),
])
// password
stackView.setCustomSpacing(6, after: passwordTextField)
stackView.setCustomSpacing(32, after: passwordCheckLabel)
// button
signUpButton.translatesAutoresizingMaskIntoConstraints = false signUpButton.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(signUpButton) stackView.addArrangedSubview(signUpButton)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -126,18 +273,31 @@ extension MastodonRegisterViewController {
]) ])
signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(signUpActivityIndicatorView) scrollview.addSubview(signUpActivityIndicatorView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor),
signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), 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 viewModel.isRegistering
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isRegistering in .sink { [weak self] isRegistering in
guard let self = self else { return } guard let self = self else { return }
isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() 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 self.signUpButton.isEnabled = !isRegistering
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -157,28 +317,125 @@ extension MastodonRegisterViewController {
) )
} }
.store(in: &disposeBag) .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) 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 { extension MastodonRegisterViewController {
@objc private func _resignFirstResponder() {
usernameTextField.resignFirstResponder()
displayNameTextField.resignFirstResponder()
emailTextField.resignFirstResponder()
passwordTextField.resignFirstResponder()
}
@objc private func signUpButtonPressed(_ sender: UIButton) { @objc private func signUpButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
guard let username = usernameTextField.text else { if !validateAllTextField() {
usernameTextField.shake()
return
}
guard let email = emailTextField.text else {
emailTextField.shake()
return
}
guard let password = passwordTextField.text else {
passwordTextField.shake()
return return
} }
@ -187,11 +444,12 @@ extension MastodonRegisterViewController {
let query = Mastodon.API.Account.RegisterQuery( let query = Mastodon.API.Account.RegisterQuery(
reason: nil, reason: nil,
username: username, username: usernameTextField.text!,
email: email, displayname: displayNameTextField.text!,
password: password, email: emailTextField.text!,
agreement: true, // TODO: password: passwordTextField.text!,
locale: "en" // TODO: agreement: true, // TODO:
locale: "en" // TODO:
) )
context.apiService.accountRegister( context.apiService.accountRegister(
@ -211,7 +469,7 @@ extension MastodonRegisterViewController {
} }
} receiveValue: { [weak self] response in } receiveValue: { [weak self] response in
guard let self = self else { return } guard let self = self else { return }
let _ = response.value _ = response.value
// TODO: // TODO:
let alertController = UIAlertController(title: "Success", message: "Regsiter request sent. Please check your email.\n(Auto sign in not implement yet.)", preferredStyle: .alert) 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 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)) self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }

View File

@ -5,12 +5,12 @@
// Created by MainasuK Cirno on 2021-2-5. // Created by MainasuK Cirno on 2021-2-5.
// //
import Foundation
import Combine import Combine
import Foundation
import MastodonSDK import MastodonSDK
import UIKit
final class MastodonRegisterViewModel { final class MastodonRegisterViewModel {
// input // input
let domain: String let domain: String
let applicationToken: Mastodon.Entity.Token let applicationToken: Mastodon.Entity.Token
@ -25,5 +25,67 @@ final class MastodonRegisterViewModel {
self.applicationToken = applicationToken self.applicationToken = applicationToken
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) 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)
}
} }

View File

@ -161,14 +161,16 @@ 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, 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.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