UML
import UIKit
class ViewController: UIViewController {
let stackView = UIStackView()
let newPasswordTextField = PasswordTextField(placeHolderText: "Enter your password")
override func viewDidLoad() {
super.viewDidLoad()
style()
layout()
}
}
extension ViewController {
func style() {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 20
newPasswordTextField.translatesAutoresizingMaskIntoConstraints = false
}
func layout() {
stackView.addArrangedSubview(newPasswordTextField)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 2),
view.trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 2)
])
}
}
import UIKit
class PasswordTextField: UIView {
let lockImageView = UIImageView(image: UIImage(systemName: "lock.fill"))
let textField = UITextField()
let placeHolderText: String
let eyeButton = UIButton(type: .custom)
let dividerView = UIView()
let errorLabel = UILabel()
init(placeHolderText: String) {
self.placeHolderText = placeHolderText
super.init(frame: .zero)
style()
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return CGSize(width: 200, height: 60)
}
}
extension PasswordTextField {
func style() {
translatesAutoresizingMaskIntoConstraints = false
lockImageView.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
textField.isSecureTextEntry = false // true
textField.placeholder = placeHolderText
//textField.delegate = self
textField.keyboardType = .asciiCapable
textField.attributedPlaceholder = NSAttributedString(string:placeHolderText,
attributes: [NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel])
eyeButton.translatesAutoresizingMaskIntoConstraints = false
eyeButton.setImage(UIImage(systemName: "eye.circle"), for: .normal)
eyeButton.setImage(UIImage(systemName: "eye.slash.circle"), for: .selected)
eyeButton.addTarget(self, action: #selector(togglePasswordView), for: .touchUpInside)
errorLabel.translatesAutoresizingMaskIntoConstraints = false
errorLabel.textColor = .systemRed
errorLabel.font = .preferredFont(forTextStyle: .footnote)
errorLabel.text = "Your password must meet the requirements below"
// errorLabel.text = "Enter your password"
// errorLabel.adjustsFontSizeToFitWidth = true
// errorLabel.minimumScaleFactor = 0.8
errorLabel.numberOfLines = 0
errorLabel.lineBreakMode = .byWordWrapping
errorLabel.isHidden = false
dividerView.translatesAutoresizingMaskIntoConstraints = false
dividerView.backgroundColor = .separator
}
func layout() {
addSubview(lockImageView)
addSubview(textField)
addSubview(eyeButton)
addSubview(dividerView)
addSubview(errorLabel)
// lock
NSLayoutConstraint.activate([
lockImageView.centerYAnchor.constraint(equalTo: textField.centerYAnchor),
lockImageView.leadingAnchor.constraint(equalTo: leadingAnchor)
])
// textfield
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: topAnchor),
textField.leadingAnchor.constraint(equalToSystemSpacingAfter: lockImageView.trailingAnchor, multiplier: 1)
])
// eye
NSLayoutConstraint.activate([
eyeButton.centerYAnchor.constraint(equalTo: textField.centerYAnchor),
eyeButton.leadingAnchor.constraint(equalToSystemSpacingAfter: textField.trailingAnchor, multiplier: 1),
eyeButton.trailingAnchor.constraint(equalTo: trailingAnchor)
])
// divider
NSLayoutConstraint.activate([
dividerView.leadingAnchor.constraint(equalTo: leadingAnchor),
dividerView.trailingAnchor.constraint(equalTo: trailingAnchor),
dividerView.heightAnchor.constraint(equalToConstant: 1),
dividerView.topAnchor.constraint(equalToSystemSpacingBelow: textField.bottomAnchor, multiplier: 1)
])
// error
NSLayoutConstraint.activate([
errorLabel.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 4),
errorLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
errorLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
])
// CHCR
lockImageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
textField.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
eyeButton.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
}
}
// MARK: - Actions
extension PasswordTextField {
@objc func togglePasswordView(_ sender: Any) {
textField.isSecureTextEntry.toggle()
eyeButton.isSelected.toggle()
}
}
Build the PasswordCriteriaView And the PasswordStatus View
import UIKit
class ViewController: UIViewController {
let stackView = UIStackView()
let newPasswordTextField = PasswordTextField(placeHolderText: "Enter your password")
let statusView = PasswordStatusView()
let confirmPasswordTextField = PasswordTextField(placeHolderText: "Re-enter new password")
let resetButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
style()
layout()
}
}
extension ViewController {
func style() {
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 20
statusView.layer.cornerRadius = 5
statusView.clipsToBounds = true
resetButton.translatesAutoresizingMaskIntoConstraints = false
resetButton.configuration = .filled()
resetButton.setTitle("Reset password", for: [])
// resetButton.addTarget(self, action: #selector(resetPasswordButtonTapped), for: .primaryActionTriggered)
stackView.addArrangedSubview(resetButton)
}
func layout() {
stackView.addArrangedSubview(newPasswordTextField)
stackView.addArrangedSubview(statusView)
stackView.addArrangedSubview(confirmPasswordTextField)
stackView.addArrangedSubview(resetButton)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 2),
view.trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 2)
])
}
}
import UIKit
class PasswordCriteriaView: UIView {
let stackView = UIStackView()
let imageView = UIImageView()
let label = UILabel()
let checkmarkImage = UIImage(systemName: "checkmark.circle")!.withTintColor(.systemGreen, renderingMode: .alwaysOriginal)
let xmarkImage = UIImage(systemName: "xmark.circle")!.withTintColor(.systemRed, renderingMode: .alwaysOriginal)
let circleImage = UIImage(systemName: "circle")!.withTintColor(.tertiaryLabel, renderingMode: .alwaysOriginal)
var isCriteriaMet: Bool = false {
didSet {
if isCriteriaMet {
imageView.image = checkmarkImage
} else {
imageView.image = xmarkImage
}
}
}
func reset() {
isCriteriaMet = false
imageView.image = circleImage
}
init(text: String) {
super.init(frame: .zero)
label.text = text
style()
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return CGSize(width: 200, height: 40)
}
}
extension PasswordCriteriaView {
func style() {
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(systemName: "circle")!.withTintColor(.tertiaryLabel, renderingMode: .alwaysOriginal)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .subheadline)
label.textColor = .secondaryLabel
stackView.addArrangedSubview(label)
imageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical)
}
func layout() {
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(label)
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
// Image
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor)
])
// CHCR
imageView.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
label.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
}
}
import UIKit
class PasswordStatusView: UIView {
let stackView = UIStackView()
let criteriaLabel = UILabel()
let lengthCriteriaView = PasswordCriteriaView(text: "8-32 characters (no spaces)")
let uppercaseCriteriaView = PasswordCriteriaView(text: "uppercase letter (A-Z)")
let lowerCaseCriteriaView = PasswordCriteriaView(text: "lowercase (a-z)")
let digitCriteriaView = PasswordCriteriaView(text: "digit (0-9)")
let specialCharacterCriteriaView = PasswordCriteriaView(text: "special character (e.g. !@#$%^)")
override init(frame: CGRect) {
super.init(frame: .zero)
style()
layout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return CGSize(width: 200, height: 200)
}
}
extension PasswordStatusView {
func style() {
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .tertiarySystemFill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 8
stackView.distribution = .equalCentering
criteriaLabel.numberOfLines = 0
criteriaLabel.lineBreakMode = .byWordWrapping
criteriaLabel.attributedText = makeCriteriaMessage()
lengthCriteriaView.translatesAutoresizingMaskIntoConstraints = false
uppercaseCriteriaView.translatesAutoresizingMaskIntoConstraints = false
lowerCaseCriteriaView.translatesAutoresizingMaskIntoConstraints = false
digitCriteriaView.translatesAutoresizingMaskIntoConstraints = false
specialCharacterCriteriaView.translatesAutoresizingMaskIntoConstraints = false
}
func layout() {
stackView.addArrangedSubview(lengthCriteriaView)
stackView.addArrangedSubview(criteriaLabel)
stackView.addArrangedSubview(uppercaseCriteriaView)
stackView.addArrangedSubview(lowerCaseCriteriaView)
stackView.addArrangedSubview(digitCriteriaView)
stackView.addArrangedSubview(specialCharacterCriteriaView)
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
stackView.leftAnchor.constraint(equalToSystemSpacingAfter: leftAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: stackView.bottomAnchor, multiplier: 2)
])
}
private func makeCriteriaMessage() -> NSAttributedString {
var plainTextAttributes = [NSAttributedString.Key: AnyObject]()
plainTextAttributes[.font] = UIFont.preferredFont(forTextStyle: .subheadline)
plainTextAttributes[.foregroundColor] = UIColor.secondaryLabel
var boldTextAttributes = [NSAttributedString.Key: AnyObject]()
boldTextAttributes[.foregroundColor] = UIColor.label
boldTextAttributes[.font] = UIFont.preferredFont(forTextStyle: .subheadline)
let attrText = NSMutableAttributedString(string: "Use at least ", attributes: plainTextAttributes)
attrText.append(NSAttributedString(string: "3 of these 4 ", attributes: boldTextAttributes))
attrText.append(NSAttributedString(string: "criteria when setting your password:", attributes: plainTextAttributes))
return attrText
}
}
Inline Interactions
import Foundation
struct PasswordCriteria {
static func lengthCriteriaMet(_ text: String) -> Bool {
text.count >= 8 && text.count <= 32
}
static func noSpaceCriteriaMet(_ text: String) -> Bool {
text.rangeOfCharacter(from: NSCharacterSet.whitespaces) == nil
}
static func lengthAndNoSpaceMet(_ text: String) -> Bool {
lengthCriteriaMet(text) && noSpaceCriteriaMet(text)
}
static func uppercaseMet(_ text: String) -> Bool {
text.range(of: "[A-Z]+", options: .regularExpression) != nil
}
static func lowercaseMet(_ text: String) -> Bool {
text.range(of: "[a-z]+", options: .regularExpression) != nil
}
static func digitMet(_ text: String) -> Bool {
text.range(of: "[0-9]+", options: .regularExpression) != nil
}
static func specialCharacterMet(_ text: String) -> Bool {
// regex escaped @:?!()$#,.\/
return text.range(of: "[@:?!()$#,./\\\\]+", options: .regularExpression) != nil
}
}
// MARK: Actions
extension PasswordStatusView {
func updateDisplay(_ text: String) {
let lengthAndNoSpaceMet = PasswordCriteria.lengthAndNoSpaceMet(text)
let uppercaseMet = PasswordCriteria.uppercaseMet(text)
let lowercaseMet = PasswordCriteria.lowercaseMet(text)
let digitMet = PasswordCriteria.digitMet(text)
let specialCharacterMet = PasswordCriteria.specialCharacterMet(text)
if shouldResetCriteria {
// Inline validation (✅ or ⚪️)
lengthAndNoSpaceMet
? lengthCriteriaView.isCriteriaMet = true
: lengthCriteriaView.reset()
uppercaseMet
? uppercaseCriteriaView.isCriteriaMet = true
: uppercaseCriteriaView.reset()
lowercaseMet
? lowerCaseCriteriaView.isCriteriaMet = true
: lowerCaseCriteriaView.reset()
digitMet
? digitCriteriaView.isCriteriaMet = true
: digitCriteriaView.reset()
specialCharacterMet
? specialCharacterCriteriaView.isCriteriaMet = true
: specialCharacterCriteriaView.reset()
}
}
}
Dealing with Keyboards
Problems
Solutions
private func setupKeyboardHiding() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc func keyboardWillShow(sender: NSNotification) {
view.frame.origin.y = view.frame.origin.y - 200
}
@objc func keyboardWillHide(notification: NSNotification) {
view.frame.origin.y = 0
}
Problems
Solution
import UIKit
extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
/// Finds the current first responder
/// - Returns: the current UIResponder if it exists
static func currentFirst() -> UIResponder? {
Static.responder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return Static.responder
}
@objc private func _trap() {
Static.responder = self
}
}
@objc func keyboardWillShow(sender: NSNotification) {
guard let userInfo = sender.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let currentTextField = UIResponder.currentFirst() as? UITextField else { return }
print("foo - userInfo: \(userInfo)")
print("foo - keyboardFrame: \(keyboardFrame)")
print("foo - currentTextField: \(currentTextField)")
let keyboardTopY = keyboardFrame.cgRectValue.origin.y
let convertedTextFieldFrame = view.convert(currentTextField.frame, from: currentTextField.superview)
let textFieldBottomY = convertedTextFieldFrame.origin.y + convertedTextFieldFrame.size.height
// if textField bottom is below keyboard bottom - bump the frame up
if textFieldBottomY > keyboardTopY {
let textBoxY = convertedTextFieldFrame.origin.y
let newFrameY = (textBoxY - keyboardTopY / 2) * -1
view.frame.origin.y = newFrameY
}
print("foo - currentTextFieldFrame: \(currentTextField.frame)")
print("foo - convertedTextFieldFrame: \(convertedTextFieldFrame)")
}
GitHub
View Github