Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions Examples/UIKit/UIKitExample/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="In3-Gf-dmP">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="UIKitExample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="xZX-kb-XF8"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1064.885496183206" y="130.98591549295776"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="jvk-0h-3ho">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="In3-Gf-dmP" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="xXu-9J-MCw">
<rect key="frame" x="0.0" y="118" width="393" height="54"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="Fe5-Td-pvt"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kBa-xd-Ry9" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="138.1679389312977" y="130.98591549295776"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
16 changes: 16 additions & 0 deletions Examples/UIKit/UIKitExample/Extensions/UIView+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Validator
// Copyright © 2025 Space Code. All rights reserved.
//

import UIKit

extension UIView {
func findFirstResponder() -> UIView? {
if isFirstResponder { return self }
for sub in subviews {
if let found = sub.findFirstResponder() { return found }
}
return nil
}
}
246 changes: 37 additions & 209 deletions Examples/UIKit/UIKitExample/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,235 +10,63 @@ import ValidatorUI
// MARK: - ViewController

final class ViewController: UIViewController {
// MARK: - Properties
private let examples: [ExampleItem] = [
ExampleItem(title: "UITextField Example", controller: LoginTextFieldExampleViewController()),
ExampleItem(title: "UITextView Example", controller: FeedbackTextViewExampleViewController()),
]

// UI
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()

private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()

private lazy var firstNameTextField: UITextField = {
let textField = makeField("First Name")
textField.validationRules = [
LengthValidationRule(min: 2, max: 50, error: "First name should be 2–50 characters long"),
]
return textField
}()

private lazy var lastNameTextField: UITextField = {
let textField = makeField("Last Name")
textField.validationRules = [
LengthValidationRule(min: 2, max: 50, error: "Last name should be 2–50 characters long"),
]
return textField
}()

private lazy var emailTextField: UITextField = {
let textField = makeField("Email")
textField.keyboardType = .emailAddress
textField.validationRules = [
EmailValidationRule(error: "Please enter a valid email address"),
]
return textField
}()

private lazy var submitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Sign Up", for: .normal)
button.layer.cornerRadius = 10
button.backgroundColor = .systemGray4
button.setTitleColor(.white, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(submit), for: .touchUpInside)
return button
}()

private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 16
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()

// Private properties
private var isValid: Bool {
[firstNameTextField, lastNameTextField, emailTextField]
.allSatisfy { $0.validationResult == .valid }
}

// MARK: - Lifecycle
private let tableView = UITableView(frame: .zero, style: .insetGrouped)

override func viewDidLoad() {
super.viewDidLoad()
title = "Examples"
view.backgroundColor = .systemBackground
configure()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
registerKeyboardNotifications()
setupTableView()
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
unregisterKeyboardNotifications()
}
private func setupTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.translatesAutoresizingMaskIntoConstraints = false

// MARK: - UI Setup

private func configure() {
for item in [firstNameTextField, lastNameTextField, emailTextField] {
item.validationHandler = { [weak self] _ in
guard let self else { return }
updateSubmitButtonState()
}
}

[firstNameTextField, lastNameTextField, emailTextField, submitButton]
.forEach(stackView.addArrangedSubview)

view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(stackView)
view.addSubview(tableView)

NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor),

stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),

submitButton.heightAnchor.constraint(equalToConstant: 48),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}

private func updateSubmitButtonState() {
submitButton.isEnabled = isValid
UIView.animate(withDuration: 0.25) {
self.submitButton.backgroundColor = self.isValid ? .systemBlue : .systemGray4
}
}

// MARK: Private

private func makeField(_ placeholder: String) -> UITextField {
let textField = UITextField()
textField.placeholder = placeholder

textField.layer.cornerRadius = 10
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.systemGray4.cgColor
textField.backgroundColor = UIColor.secondarySystemBackground
textField.textColor = .label

textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0))
textField.leftViewMode = .always

textField.heightAnchor.constraint(equalToConstant: 56.0).isActive = true
textField.translatesAutoresizingMaskIntoConstraints = false
textField.validateOnInputChange(isEnabled: true)

return textField
}

// MARK: Notifications

private func registerKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}

private func unregisterKeyboardNotifications() {
NotificationCenter.default.removeObserver(self)
}

// MARK: - Actions

@objc
private func submit() {
guard isValid else {
let alert = UIAlertController(
title: "The form is invalid",
message: "Please check the highlighted fields and correct the entered information.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Got it", style: .default))

present(alert, animated: true)
return
}

let alert = UIAlertController(
title: "Success 🎉",
message: "Your account has been successfully created.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
// MARK: UITableViewDataSource, UITableViewDelegate

present(alert, animated: true)
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
examples.count
}

@objc
private func keyboardWillShow(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let frameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else { return }

let keyboardFrame = frameValue.cgRectValue
let bottomInset = keyboardFrame.height - view.safeAreaInsets.bottom

scrollView.contentInset.bottom = bottomInset

if let firstResponder = view.findFirstResponder(),
!scrollView.frame.contains(firstResponder.frame)
{
scrollView.scrollRectToVisible(firstResponder.frame, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let example = examples[indexPath.row]
cell.textLabel?.text = example.title
cell.accessoryType = .disclosureIndicator
return cell
}

@objc
private func keyboardWillHide(_: Notification) {
scrollView.contentInset.bottom = .zero
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let example = examples[indexPath.row]
navigationController?.pushViewController(example.controller, animated: true)
}
}

extension UIView {
func findFirstResponder() -> UIView? {
if isFirstResponder { return self }
for sub in subviews {
if let found = sub.findFirstResponder() { return found }
}
return nil
}
// MARK: - ExampleItem

struct ExampleItem {
let title: String
let controller: UIViewController
}
Loading
Loading