├── .swift-version
├── _Pods.xcodeproj
├── Example
├── AstroForms
│ ├── Images.xcassets
│ │ ├── Contents.json
│ │ ├── Check
│ │ │ ├── Contents.json
│ │ │ ├── Check-filled.imageset
│ │ │ │ ├── Check-filled.png
│ │ │ │ ├── Check-filled@2x.png
│ │ │ │ ├── Check-filled@3x.png
│ │ │ │ └── Contents.json
│ │ │ └── Check-unfilled.imageset
│ │ │ │ ├── Check-unfilled.png
│ │ │ │ ├── Check-unfilled@2x.png
│ │ │ │ ├── Check-unfilled@3x.png
│ │ │ │ └── Contents.json
│ │ ├── Location Marker
│ │ │ ├── Contents.json
│ │ │ └── Location-marker.imageset
│ │ │ │ ├── Location-marker.png
│ │ │ │ ├── Location-marker@2x.png
│ │ │ │ ├── Location-marker@3x.png
│ │ │ │ └── Contents.json
│ │ ├── Example Form Images
│ │ │ ├── Contents.json
│ │ │ ├── Astro Hero.imageset
│ │ │ │ ├── Astro Hero Colored.png
│ │ │ │ ├── Astro Hero Colored@2x.png
│ │ │ │ ├── Astro Hero Colored@3x.png
│ │ │ │ └── Contents.json
│ │ │ └── Astro Background.imageset
│ │ │ │ ├── Astro Background.png
│ │ │ │ ├── Astro Background@2x.png
│ │ │ │ ├── Astro Background@3x.png
│ │ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-Small.png
│ │ │ ├── Icon-Small@2x.png
│ │ │ ├── Icon-Small@3x.png
│ │ │ ├── Icon-Small-40@2x.png
│ │ │ ├── Icon-Small-40@3x.png
│ │ │ ├── iTunesArtwork@1x.png
│ │ │ ├── Icon-Notification@2x.png
│ │ │ ├── Icon-Notification@3x.png
│ │ │ └── Contents.json
│ ├── GUI
│ │ ├── Shared
│ │ │ ├── Forms
│ │ │ │ ├── Rows
│ │ │ │ │ ├── CustomViewRow
│ │ │ │ │ │ ├── CustomView.swift
│ │ │ │ │ │ ├── CustomNibRow.swift
│ │ │ │ │ │ └── Custom Views
│ │ │ │ │ │ │ ├── HeroView.swift
│ │ │ │ │ │ │ └── HeroView.xib
│ │ │ │ │ ├── TextFieldRow
│ │ │ │ │ │ ├── AstroTextField.swift
│ │ │ │ │ │ ├── TextFieldRow.swift
│ │ │ │ │ │ ├── TextFieldRowView.swift
│ │ │ │ │ │ └── TextFieldRowView.xib
│ │ │ │ │ ├── MapRow
│ │ │ │ │ │ ├── MapRowView.swift
│ │ │ │ │ │ ├── MapRow.swift
│ │ │ │ │ │ └── MapRowView.xib
│ │ │ │ │ ├── SwitchRow
│ │ │ │ │ │ ├── SwitchRowView.swift
│ │ │ │ │ │ ├── SwitchRow.swift
│ │ │ │ │ │ └── SwitchRowView.xib
│ │ │ │ │ ├── ButtonRow
│ │ │ │ │ │ ├── ButtonRow.swift
│ │ │ │ │ │ ├── ButtonRowView.swift
│ │ │ │ │ │ └── ButtonRowView.xib
│ │ │ │ │ ├── CheckListRow
│ │ │ │ │ │ ├── CheckListRowView.swift
│ │ │ │ │ │ ├── CheckListRowView.xib
│ │ │ │ │ │ ├── CheckListRow.swift
│ │ │ │ │ │ ├── CheckListRowItemView.xib
│ │ │ │ │ │ └── CheckListRowItemView.swift
│ │ │ │ │ └── TextViewRow
│ │ │ │ │ │ ├── TextViewRow.swift
│ │ │ │ │ │ ├── TextViewRowView.swift
│ │ │ │ │ │ └── TextViewRowView.xib
│ │ │ │ ├── Theme
│ │ │ │ │ ├── Theme.swift
│ │ │ │ │ ├── Themeable.swift
│ │ │ │ │ ├── AstroThemeImage.swift
│ │ │ │ │ └── AstroThemeColor.swift
│ │ │ │ └── Helpers
│ │ │ │ │ ├── HintView
│ │ │ │ │ ├── HintView.swift
│ │ │ │ │ └── HintView.xib
│ │ │ │ │ └── ErrorView
│ │ │ │ │ ├── ErrorView.swift
│ │ │ │ │ └── ErrorView.xib
│ │ │ ├── Views
│ │ │ │ └── ThemeableImageView
│ │ │ │ │ └── ThemeableImageView.swift
│ │ │ ├── Controllers
│ │ │ │ └── SlideNavigationController.swift
│ │ │ ├── Extensions
│ │ │ │ └── UIImage+UIColor.swift
│ │ │ └── Transitions
│ │ │ │ └── SlideAnimatedTransition.swift
│ │ └── Features
│ │ │ ├── Additional Info
│ │ │ ├── AdditionalInfoViewController.swift
│ │ │ └── AdditionalInfoForm.swift
│ │ │ └── Login
│ │ │ ├── BackgroundContainerViewController.swift
│ │ │ ├── LoginViewController.swift
│ │ │ └── LoginForm.swift
│ ├── Info.plist
│ ├── AppDelegate.swift
│ └── Base.lproj
│ │ ├── LaunchScreen.xib
│ │ └── Main.storyboard
├── Podfile
├── AstroForms.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── AstroForms-Example.xcscheme
├── AstroForms.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Podfile.lock
└── Tests
│ ├── Info.plist
│ └── ValidationTests.swift
├── AstroForms
├── Theming
│ ├── ThemeableView.swift
│ ├── ThemeabeImageTraits.swift
│ ├── ThemeableColorTraits.swift
│ └── ThemeableTraits.swift
├── Protocols
│ ├── RowTag.swift
│ ├── HelperView.swift
│ ├── RowUpdateResponder.swift
│ ├── DefaultKeyboardToolbarDelegate.swift
│ ├── ValueRow.swift
│ ├── RowDelegate.swift
│ ├── ValueViewDelegate.swift
│ ├── DefaultKeyboardTraits.swift
│ ├── FocusableRow.swift
│ └── Row.swift
├── Types
│ ├── FocusChange.swift
│ └── RowUpdate.swift
├── Validation
│ ├── Factory
│ │ └── ValidationRule.swift
│ └── Protocols
│ │ └── ValidatableForm.swift
├── Extensions
│ ├── UIView+FirstResponder.swift
│ ├── UIView+AnimationCurve.swift
│ └── UIView+XibInitializable.swift
└── Abstract
│ ├── Form+Collection.swift
│ ├── Form.swift
│ ├── Form+FocusRow.swift
│ ├── Form+Keyboard.swift
│ └── Form+Initialize.swift
├── .gitignore
├── LICENSE
├── AstroForms.podspec
└── README.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 4.2
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Location Marker/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/iTunesArtwork@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/iTunesArtwork@1x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Check-filled@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Check-unfilled@3x.png
--------------------------------------------------------------------------------
/Example/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '11.4'
2 | use_frameworks!
3 |
4 | target 'AstroForms_Example' do
5 | pod 'AstroForms', :path => '../'
6 |
7 | target 'AstroForms_Tests' do
8 | inherit! :search_paths
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Location-marker@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Astro Hero Colored@3x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background@2x.png
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plummer/astro-forms/HEAD/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Astro Background@3x.png
--------------------------------------------------------------------------------
/AstroForms/Theming/ThemeableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeableView.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol ThemeableView {
11 | func updateTheme()
12 | }
13 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/RowTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RowTag.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 25/8/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol RowTag {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/AstroForms/Types/FocusChange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusChange.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 5/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum FocusChange {
11 | case
12 | focus,
13 | focusNext,
14 | focusPrevious,
15 | blur
16 | }
17 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/HelperView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelperView.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 25/8/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol HelperView {
12 | var row: AnyRow? { get set }
13 | }
14 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/RowUpdateResponder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RowUpdateResponder.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 23/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol RowUpdateResponder {
11 |
12 | var onRowUpdate: ((RowUpdate) -> Void)? { get set }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Example/AstroForms.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/AstroForms.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/AstroForms.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - AstroForms (0.1.0)
3 |
4 | DEPENDENCIES:
5 | - AstroForms (from `../`)
6 |
7 | EXTERNAL SOURCES:
8 | AstroForms:
9 | :path: "../"
10 |
11 | SPEC CHECKSUMS:
12 | AstroForms: d08952ef1757936efa3562cb700ec43a58eff8cd
13 |
14 | PODFILE CHECKSUM: eeddd3da33b26a284ca0d4b1802457e77b47abae
15 |
16 | COCOAPODS: 1.6.0.beta.1
17 |
--------------------------------------------------------------------------------
/AstroForms/Theming/ThemeabeImageTraits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeabeImageTraits.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | public protocol ThemeableImageTraits {
12 |
13 | associatedtype ThemeImageType
14 |
15 | func image(_ requirement: ThemeImageType) -> UIImage
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CustomViewRow/CustomView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | protocol CustomView where Self: UIView {
14 | var row: CustomViewRow? { get set }
15 | }
16 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/DefaultKeyboardToolbarDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultKeyboardToolbarDelegate.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 7/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | @objc public protocol DefaultKeyboardToolbarDelegate {
11 | @objc func keyboardToolbarDoneTapped()
12 | @objc func keyboardToolbarNextTapped()
13 | @objc func keyboardToolbarPreviousTapped()
14 | }
15 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Theme/Theme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AstroTheme.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | /// The themes available to the project
13 | enum AstroTheme: Theme {
14 | case
15 | normal,
16 | grey,
17 | light
18 | }
19 |
--------------------------------------------------------------------------------
/AstroForms/Theming/ThemeableColorTraits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeableColorTraits.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | /// There traits of having a themeable color scheme, for Themeable
12 | public protocol ThemeableColorTraits {
13 |
14 | associatedtype ThemeColorType
15 |
16 | func color(_ requirement: ThemeColorType) -> UIColor
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/ValueRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValueRow.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 1/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Distinguishes rows with values from those without.
12 | public protocol ValueRow: ValueViewDelegate {
13 |
14 | associatedtype Value
15 |
16 | /// The value for the row.
17 | var value: Value { get set }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/RowDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RowDelegate.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 17/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol RowDelegate {
11 |
12 | func rowDidStartEditing(row: AnyRow)
13 |
14 | func rowDidEndEditing(row: AnyRow)
15 |
16 | func rowDidEdit(row: AnyRow)
17 |
18 | func rowDidFocus(row: AnyRow)
19 |
20 | func rowDidBlur(row: AnyRow)
21 |
22 | func rowUpdate(type: RowUpdate, row: AnyRow)
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Astro Background.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Astro Background@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Astro Background@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Example Form Images/Astro Hero.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Astro Hero Colored.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Astro Hero Colored@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Astro Hero Colored@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Views/ThemeableImageView/ThemeableImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeableImageView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// A UIImageView that conforms to the project Themeable protocol.
13 | class ThemeableImageView: UIImageView, Themeable {
14 |
15 | var theme: AstroTheme? {
16 | didSet { updateTheme() }
17 | }
18 |
19 | func updateTheme() { self.image = image(.formBackground) }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/AstroForms/Types/RowUpdate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RowUpdate.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 23/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum RowUpdate {
11 | case
12 |
13 | /// Every time the row is changed
14 | live,
15 |
16 | /// Each time the row is blurred
17 | onResignActive,
18 |
19 | /// On blur after the row has changed at least once
20 | onResignActiveAfterChange,
21 |
22 | /// Confusing to name, but live changes after blur, and on the initial
23 | /// blur event if the form has changed
24 | regular
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-filled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Check-filled.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Check-filled@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Check-filled@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Check/Check-unfilled.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Check-unfilled.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Check-unfilled@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Check-unfilled@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/Location Marker/Location-marker.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Location-marker.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "Location-marker@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "Location-marker@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Features/Additional Info/AdditionalInfoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdditionalInfoViewController.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class AdditionalInfoViewController: UIViewController {
13 |
14 | @IBOutlet weak var exampleFieldsForm: AdditionalInfoForm!
15 |
16 | var loginFormData: LoginFormData?
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | exampleFieldsForm.loginFormData = loginFormData
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/AstroForms/Validation/Factory/ValidationRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValidationRule.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 20/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct ValidationRule {
11 |
12 | public static var isEmail: (String) -> Bool {
13 |
14 | return {
15 | let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
16 |
17 | return NSPredicate(format:"SELF MATCHES %@", regex).evaluate(
18 | with: $0
19 | )
20 | }
21 |
22 | }
23 |
24 | public static var required: (String) -> Bool {
25 | return { !$0.isEmpty }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CustomViewRow/CustomNibRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomViewRow
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 24/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | class CustomViewRow: Row where T: CustomView {
13 |
14 | typealias View = T
15 |
16 | var view: T
17 |
18 | var tag: RowTag
19 |
20 | init(tag: RowTag, config: ((CustomViewRow) -> Void)? = nil) {
21 |
22 | let view: T = T.fromXib()
23 | self.tag = tag
24 | self.view = view
25 | self.view.row = self
26 | config?(self)
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/AstroForms/Extensions/UIView+FirstResponder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+FirstResponder.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 24/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 |
13 | /// The first responder within this view's tree of views if it exists.
14 | public var firstResponder: UIView? {
15 |
16 | guard !isFirstResponder else { return self }
17 |
18 | for subview in subviews {
19 | if let firstResponder = subview.firstResponder {
20 | return firstResponder
21 | }
22 | }
23 |
24 | return nil
25 |
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Helpers/HintView/HintView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HintView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 23/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class HintView: UIView, Themeable {
12 |
13 | var theme: AstroTheme?
14 |
15 | @IBOutlet weak var label: UILabel!
16 |
17 | var title: String {
18 |
19 | get { return label.text ?? "" }
20 |
21 | set { label.text = newValue }
22 |
23 | }
24 |
25 | func updateTheme() {
26 | backgroundColor = color(.primaryBackground)
27 | label.textColor = color(.hintTextColor)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Helpers/ErrorView/ErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 23/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ErrorView: UIView, Themeable {
12 |
13 | var theme: AstroTheme?
14 |
15 | @IBOutlet weak var label: UILabel!
16 |
17 | var title: String {
18 |
19 | get { return label.text ?? "" }
20 |
21 | set { label.text = newValue }
22 |
23 | }
24 |
25 | func updateTheme() {
26 | backgroundColor = color(.primaryBackground)
27 | label.textColor = color(.errorTextColor)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/AstroForms/Extensions/UIView+AnimationCurve.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+AnimationCurve.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 6/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView.AnimationCurve {
12 |
13 | /// Converts a `UIView.AnimationCurve` into `UIView.AnimationOptions`
14 | /// as required by `UIView.animateWithDuration` when the curve
15 | /// is the only animation option.
16 | ///
17 | /// - Returns: THe `UIView.AnimationOptions` of a `UIView.AnimationCurve`
18 | func toOptions() -> UIView.AnimationOptions {
19 |
20 | return UIView.AnimationOptions(rawValue: UInt(rawValue << 16))
21 |
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CustomViewRow/Custom Views/HeroView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeroView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 24/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | final class HeroView: UIView, Themeable, CustomView {
14 |
15 | var theme: AstroTheme? = nil
16 |
17 | weak var row: CustomViewRow?
18 |
19 | @IBOutlet weak var imageView: UIImageView!
20 |
21 | func updateTheme() {
22 | imageView.image = image(.astroHero)
23 | imageView.tintColor = color(.secondaryTint)
24 | backgroundColor = color(.primaryBackground)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Features/Login/BackgroundContainerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BackgroundContainerViewController.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class BackgroundViewController: UIViewController {
13 |
14 | let theme: AstroTheme = AstroTheme.light
15 |
16 | override var preferredStatusBarStyle: UIStatusBarStyle {
17 | return .lightContent
18 | }
19 |
20 | @IBOutlet weak var backgroundImageView: ThemeableImageView!
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | backgroundImageView.theme = theme
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow/AstroTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AstroTextField.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import UIKit
12 |
13 | // This implementation uses a UITextField subclass just to allow inset changes
14 | @IBDesignable
15 | class AstroTextField: UITextField {
16 |
17 | @IBInspectable var inset: CGFloat = 0
18 |
19 | override func textRect(forBounds bounds: CGRect) -> CGRect {
20 | return bounds.insetBy(dx: inset, dy: inset)
21 | }
22 |
23 | override func editingRect(forBounds bounds: CGRect) -> CGRect {
24 | return textRect(forBounds: bounds)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/MapRow/MapRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapRowView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 30/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import MapKit
12 |
13 | class MapRowView: UIView, Themeable {
14 |
15 | var theme: AstroTheme?
16 |
17 | weak var row: MapRow?
18 |
19 | @IBOutlet weak var marker: UIImageView!
20 |
21 | @IBOutlet weak var label: UILabel!
22 |
23 | @IBOutlet weak var map: MKMapView!
24 |
25 | func updateTheme() {
26 | backgroundColor = color(.primaryBackground)
27 | marker.tintColor = color(.primaryTint)
28 | label.textColor = color(.inputLabelColor)
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Example/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
26 | # Carthage/Checkouts
27 |
28 | Carthage/Build
29 |
30 | # We recommend against adding the Pods directory to your .gitignore. However
31 | # you should judge for yourself, the pros and cons are mentioned at:
32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
33 | #
34 | # Note: if you ignore the Pods directory, make sure to uncomment
35 | # `pod install` in .travis.yml
36 | #
37 | Pods/
38 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Theme/Themeable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Themeable.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | /// An implementation interface that binds theming associated types
13 | /// to concrete types with generic constraints.
14 | protocol Themeable:
15 | ThemeableColorTraits,
16 | ThemeableImageTraits,
17 | ThemeableTraits
18 | where
19 | ThemeType == AstroTheme,
20 | ThemeColorType == AstroThemeColor,
21 | ThemeImageType == AstroThemeImage {
22 | // This protocol does nothing except specify the types for `Theme` and
23 | // consolidate the various types of theming that are required in a given
24 | // project, so that a concrete type only needs implement one protocol
25 | // for theming.
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/SwitchRow/SwitchRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchRowView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 8/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | class SwitchRowView: UIView {
14 |
15 | var theme: AstroTheme? = nil
16 |
17 | @IBOutlet weak var `switch`: UISwitch!
18 |
19 | @IBOutlet weak var label: UILabel!
20 |
21 | weak var row: SwitchRow?
22 |
23 | override func awakeFromNib() {
24 | super.awakeFromNib()
25 |
26 | `switch`.addTarget(
27 | self,
28 | action: #selector(switchValueChanged(_:)),
29 | for: .valueChanged
30 | )
31 |
32 | }
33 |
34 | @objc func switchValueChanged(_ sender: UISwitch) {
35 | row?.valueDidEndEditing()
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Controllers/SlideNavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlideNavigationController.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 27/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class SlideNavigationController: UINavigationController, UINavigationControllerDelegate {
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 | self.delegate = self
17 | }
18 |
19 | func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
20 |
21 | return operation == .push
22 | ? SlideAnimatedTransition(direction: .right)
23 | : SlideAnimatedTransition(direction: .left)
24 |
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Extensions/UIImage+UIColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+UIColor.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIImage {
13 |
14 | /// Generates a 1x1pt UIImage from a UIColor.
15 | ///
16 | /// - Parameter color: The intended colour
17 | /// - Returns: The UIImage
18 | static func from(color: UIColor) -> UIImage {
19 |
20 | let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
21 |
22 | UIGraphicsBeginImageContext(rect.size)
23 |
24 | let context = UIGraphicsGetCurrentContext()!
25 | context.setFillColor(color.cgColor)
26 | context.fill(rect)
27 | let image = UIGraphicsGetImageFromCurrentImageContext()
28 |
29 | UIGraphicsEndImageContext()
30 |
31 | return image!
32 |
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/ButtonRow/ButtonRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonRow.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 9/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | class ButtonRow: Row {
13 |
14 | var view: ButtonRowView
15 |
16 | typealias View = ButtonRowView
17 |
18 | var tag: RowTag
19 |
20 | var buttonTapBlock: (() -> Void)?
21 |
22 | func buttonTapped() {
23 | buttonTapBlock?()
24 | }
25 |
26 | var title: String? {
27 | get {
28 | return view.button.title(for: .normal)
29 | }
30 | set {
31 | view.button.setTitle(newValue, for: .normal)
32 | }
33 | }
34 |
35 | init(tag: RowTag, config: ((ButtonRow) -> Void)? = nil) {
36 |
37 | let view: View = View.fromXib()
38 | self.view = view
39 | self.tag = tag
40 | self.view.row = self
41 | config?(self)
42 |
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Andrew Plummer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CheckListRow/CheckListRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckListRowView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 12/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class CheckListRowView: UIView, Themeable {
13 |
14 | @IBOutlet weak var stackView: UIStackView!
15 |
16 | @IBOutlet weak var label: UILabel!
17 |
18 | weak var row: CheckListRow?
19 |
20 | @IBOutlet weak var checkListBackground: UIView!
21 |
22 | var theme: AstroTheme?
23 |
24 | func updateTheme() {
25 |
26 | backgroundColor = color(.primaryBackground)
27 | checkListBackground.backgroundColor = color(.inputBackground)
28 | label.textColor = color(.inputLabelColor)
29 |
30 | stackView.arrangedSubviews.forEach {
31 | ($0 as? CheckListRowItemView)?.updateTheme()
32 | }
33 |
34 | }
35 |
36 | override func layoutSubviews() {
37 | super.layoutSubviews()
38 | row?.updateCorners()
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/SwitchRow/SwitchRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchRow.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 8/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | class SwitchRow: Row, ValueRow {
14 |
15 | var valueHasStartedEditing: Bool = false
16 |
17 | var valueHasChanged: Bool = false
18 |
19 | var valueHasEndedEditing: Bool = false
20 |
21 | typealias Value = Bool
22 |
23 | var tag: RowTag
24 |
25 | var view: SwitchRowView
26 |
27 | var value: Value {
28 |
29 | get { return view.switch.isOn }
30 |
31 | set { view.switch.setOn(newValue, animated: false) }
32 |
33 | }
34 |
35 | init(tag: RowTag, config: ((SwitchRow) -> Void)? = nil) {
36 |
37 | let view: View = View.fromXib()
38 | self.view = view
39 | self.tag = tag
40 | self.view.row = self
41 | config?(self)
42 | self.view.switch.accessibilityLabel = self.view.label.text
43 |
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/AstroForms/Abstract/Form+Collection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form+Collection.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 23/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// A form can be represented as a collection of any row types.
13 | extension Form {
14 |
15 | private func stackWrapped(_ view: UIView) -> UIStackView {
16 |
17 | let wrapStackView = UIStackView()
18 | wrapStackView.axis = .vertical
19 | wrapStackView.addArrangedSubview(view)
20 |
21 | return wrapStackView
22 |
23 | }
24 |
25 | public func add(_ row: AnyRow) {
26 |
27 | rows.append(row)
28 |
29 | stackView.addArrangedSubview(stackWrapped(row.baseView))
30 |
31 | (row.baseView as? ThemeableView)?.updateTheme()
32 |
33 | }
34 |
35 | public func insert(_ row: AnyRow, at index: Int) {
36 |
37 | rows.insert(row, at: index)
38 | stackView.insertArrangedSubview(stackWrapped(row.baseView), at: index)
39 |
40 | }
41 |
42 | public func remove(at index: Int) {
43 |
44 | rows.remove(at: index)
45 | stackView.subviews[index].removeFromSuperview()
46 |
47 | }
48 |
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/AstroForms/Extensions/UIView+XibInitializable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+XibInitializable.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 1/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 |
13 | /// Creates a UIView from a nib.
14 | ///
15 | ///
16 | /// - Returns: The UIView from Xib
17 | /// - Throws: An error if the view couldn't be instantiated.
18 | ///
19 | /// To use a subclass, add the type annotation.
20 | /// i.e. `let subclass: SubclassUIView = try SubclassUIView.fromXib()`
21 | public static func fromXib() -> T {
22 |
23 | guard
24 | let nib = Bundle.main.loadNibNamed(
25 | String(describing: T.self),
26 | owner: nil,
27 | options: nil
28 | )?.first,
29 | let view = nib as? T else {
30 |
31 | fatalError(
32 | """
33 | ---
34 | 🚨 Fatal Error: Attempt to instantiate Nib:
35 | \(String(describing: T.self)) however it does not exist.
36 | ---
37 | """
38 | )
39 |
40 | }
41 |
42 | return view
43 |
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Example/AstroForms/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Astro Forms
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/MapRow/MapRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapRow.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 30/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 | import CoreLocation
13 |
14 | class MapRow: Row, ValueRow {
15 |
16 | typealias View = MapRowView
17 |
18 | typealias Value = CLLocationCoordinate2D
19 |
20 | var view: MapRowView
21 |
22 | var value: CLLocationCoordinate2D {
23 |
24 | set {
25 | self.view.map.centerCoordinate = newValue
26 | valueDidEndEditing()
27 | }
28 |
29 | get {
30 | return self.view.map.centerCoordinate
31 | }
32 |
33 | }
34 |
35 | var tag: RowTag
36 |
37 | var valueHasChanged: Bool = false
38 |
39 | var valueHasEndedEditing: Bool = false
40 |
41 | var valueHasStartedEditing: Bool = false
42 |
43 | init(
44 | tag: RowTag,
45 | location: CLLocationCoordinate2D,
46 | config: ((MapRow) -> Void)? = nil
47 | ) {
48 |
49 | let view: MapRowView = View.fromXib()
50 | self.tag = tag
51 | self.view = view
52 | self.value = location
53 | view.row = self
54 | config?(self)
55 |
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow/TextFieldRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FieldRow.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 1/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | /// A basic text field and label row.
14 | class TextFieldRow: Row, ValueRow, FocusableRow {
15 |
16 | var valueHasStartedEditing: Bool = false
17 |
18 | var valueHasChanged: Bool = false
19 |
20 | var valueHasEndedEditing: Bool = false
21 |
22 | var focusRect: () -> CGRect? = { return nil }
23 |
24 | typealias Value = String
25 |
26 | var tag: RowTag
27 |
28 | var view: TextFieldRowView
29 |
30 | var focusElement: UIResponder { return view.textField }
31 |
32 | /// The `UITextField` input value.
33 | var value: Value {
34 |
35 | get { return view.textField.text ?? "" }
36 |
37 | set { view.textField.text = newValue }
38 |
39 | }
40 |
41 | init(tag: RowTag, config: ((TextFieldRow) -> Void)? = nil) {
42 |
43 | let view: View = View.fromXib()
44 | self.view = view
45 | self.tag = tag
46 | self.view.row = self
47 |
48 | config?(self)
49 |
50 | self.view.textField.accessibilityLabel = self.view.label.text ?? ""
51 |
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextViewRow/TextViewRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextViewRow.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 7/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | class TextViewRow: Row, ValueRow, FocusableRow {
13 |
14 | var valueHasStartedEditing: Bool = false
15 |
16 | var valueHasChanged: Bool = false
17 |
18 | var valueHasEndedEditing: Bool = false
19 |
20 | var focusRect: () -> CGRect? = { nil }
21 |
22 | typealias Value = String
23 |
24 | var tag: RowTag
25 |
26 | var view: TextViewRowView
27 |
28 | var focusElement: UIResponder { return view.textView }
29 |
30 | var value: Value {
31 |
32 | get { return view.textView.text }
33 |
34 | set { view.textView.text = newValue }
35 |
36 | }
37 |
38 | var height: CGFloat {
39 |
40 | get { return view.textViewHeightConstraint.constant }
41 |
42 | set { view.textViewHeightConstraint.constant = newValue }
43 |
44 | }
45 |
46 | init(tag: RowTag, config: ((TextViewRow) -> Void)? = nil) {
47 |
48 | let view: View = View.fromXib()
49 | self.view = view
50 | self.tag = tag
51 | self.view.row = self
52 | config?(self)
53 | self.view.textView.accessibilityLabel = self.view.label.text ?? ""
54 |
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Features/Login/LoginViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewController.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class LoginViewController: UIViewController, LoginFormDelegate {
13 |
14 | enum SegueKeys: String {
15 | case showExampleFields
16 | }
17 |
18 | var theme: AstroTheme = AstroTheme.light
19 |
20 | @IBOutlet weak var loginForm: LoginForm!
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 |
25 | loginForm.theme = theme
26 | loginForm.delegate = self
27 |
28 | }
29 |
30 | func didSubmit(result: LoginFormData) {
31 |
32 | performSegue(
33 | withIdentifier: SegueKeys.showExampleFields.rawValue,
34 | sender: result
35 | )
36 |
37 | }
38 |
39 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
40 |
41 | if segue.identifier == SegueKeys.showExampleFields.rawValue {
42 |
43 | guard
44 | let formData = sender as? LoginFormData,
45 | let dest = segue.destination as? AdditionalInfoViewController
46 | else {
47 | return
48 | }
49 |
50 | dest.loginFormData = formData
51 |
52 | }
53 |
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/Example/AstroForms/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-Notification@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-Notification@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-Small.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-Small@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-Small@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-Small-40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-Small-40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "1024x1024",
59 | "idiom" : "ios-marketing",
60 | "filename" : "iTunesArtwork@1x.png",
61 | "scale" : "1x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/AstroForms/Protocols/ValueViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValueViewDelegate.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 2/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Optional delegate methods for views with changing values
12 | public protocol ValueViewDelegate: class {
13 |
14 | func valueDidEdit()
15 |
16 | func valueDidStartEditing()
17 |
18 | func valueDidEndEditing()
19 |
20 | var valueHasChanged: Bool { get set }
21 |
22 | var valueHasEndedEditing: Bool { get set }
23 |
24 | var valueHasStartedEditing: Bool { get set }
25 |
26 | }
27 |
28 | public extension ValueViewDelegate where Self: AnyRow {
29 |
30 | func valueDidEdit() {
31 |
32 | form?.rowDidEdit(row: self)
33 | self.valueHasChanged = true
34 |
35 | form?.rowUpdate(type: .live, row: self)
36 |
37 | if valueHasEndedEditing {
38 | form?.rowUpdate(type: .regular, row: self)
39 | }
40 |
41 | }
42 |
43 | func valueDidStartEditing() {
44 |
45 | form?.rowDidStartEditing(row: self)
46 | self.valueHasStartedEditing = true
47 |
48 | }
49 |
50 | func valueDidEndEditing() {
51 |
52 | form?.rowDidEndEditing(row: self)
53 | self.valueHasEndedEditing = true
54 |
55 | form?.rowUpdate(type: .onResignActive, row: self)
56 |
57 | if valueHasChanged {
58 | form?.rowUpdate(type: .onResignActiveAfterChange, row: self)
59 | form?.rowUpdate(type: .regular, row: self)
60 | }
61 |
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/DefaultKeyboardTraits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultKeyboardTraits.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 7/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 |
12 | // TODO: This is a really awkward way of exposing shared toolbar functionality
13 | // between view classes. Refactor to a more appropriate structure to support
14 | // many keyboard types.
15 | public extension DefaultKeyboardToolbarDelegate {
16 |
17 | func defaultToolbar(row: T?) -> UIToolbar {
18 |
19 | let toolbar = UIToolbar()
20 | toolbar.barStyle = .default
21 | toolbar.isTranslucent = true
22 | toolbar.sizeToFit()
23 |
24 | let doneButton = UIBarButtonItem(
25 | title: "Done",
26 | style: .plain,
27 | target: self,
28 | action: #selector(keyboardToolbarDoneTapped)
29 | )
30 |
31 | let flexibleSpace = UIBarButtonItem(
32 | barButtonSystemItem: .flexibleSpace,
33 | target: nil,
34 | action: nil
35 | )
36 |
37 | let nextButton = UIBarButtonItem(
38 | title: "Next",
39 | style: .plain,
40 | target: self,
41 | action: #selector(keyboardToolbarNextTapped)
42 | )
43 |
44 | nextButton.isEnabled = row?.nextFocusableRow != nil
45 |
46 | let previousButton = UIBarButtonItem(
47 | title: "Previous",
48 | style: .plain,
49 | target: self,
50 | action: #selector(keyboardToolbarPreviousTapped)
51 | )
52 |
53 | previousButton.isEnabled = row?.previousFocusableRow != nil
54 |
55 | toolbar.setItems([
56 | previousButton,
57 | nextButton,
58 | flexibleSpace,
59 | doneButton
60 | ], animated: false
61 | )
62 |
63 | return toolbar
64 |
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Theme/AstroThemeImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AstroThemeImage.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | enum AstroThemeImage {
13 |
14 | case
15 | formBackground,
16 | astroHero
17 |
18 | }
19 |
20 | extension Themeable {
21 |
22 | /// Generate a UIImage for each theme in the project.
23 | ///
24 | /// - Parameter requirement: The `ThemeImageType` requirement
25 | /// - Returns: The UIIMage for the current theme.
26 | func image(_ requirement: ThemeImageType) -> UIImage {
27 |
28 | let theme = getTheme() ?? .normal
29 |
30 | switch theme {
31 |
32 | case .normal:
33 |
34 | switch requirement {
35 |
36 | case .formBackground: return UIImage.from(color: .white)
37 |
38 | case .astroHero:
39 | let image = #imageLiteral(resourceName: "Astro Hero").withRenderingMode(.alwaysTemplate)
40 | return image
41 | }
42 |
43 | case .grey:
44 |
45 | switch requirement {
46 |
47 | case .formBackground: return UIImage.from(color: #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1))
48 |
49 | case .astroHero:
50 | let image = #imageLiteral(resourceName: "Astro Hero").withRenderingMode(.alwaysTemplate)
51 | return image
52 | }
53 |
54 | case .light:
55 |
56 | switch requirement {
57 |
58 | case .formBackground: return #imageLiteral(resourceName: "Astro Background")
59 |
60 | case .astroHero: return #imageLiteral(resourceName: "Astro Hero")
61 |
62 | }
63 |
64 | }
65 |
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/AstroForms/Validation/Protocols/ValidatableForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValidatableForm.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 18/9/18.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol ValidatableForm {
11 |
12 | typealias ValidationRuleMsgBlock = ((R.Value) -> Bool, String)
13 |
14 | typealias ValidationRuleBlock = (R.Value) -> Bool
15 |
16 | func validate(
17 | row: R,
18 | _ rules: ValidationRuleMsgBlock...) -> (Bool, String?)
19 |
20 | func validateList(
21 | row: R,
22 | _ rules: ValidationRuleMsgBlock...) -> [(Bool, String?)]
23 |
24 | func validate(
25 | row: R,
26 | _ rules: ValidationRuleBlock...) -> Bool
27 |
28 | }
29 |
30 | public extension ValidatableForm {
31 |
32 | @discardableResult
33 | public func validate(
34 | row: R,
35 | _ rules: ValidationRuleMsgBlock...) -> (Bool, String?) {
36 |
37 | for rule in rules {
38 |
39 | if rule.0(row.value) == false {
40 | return (false, rule.1)
41 | }
42 |
43 | }
44 |
45 | return (true, nil)
46 |
47 | }
48 |
49 | @discardableResult
50 | public func validate(
51 | row: R,
52 | _ rules: ValidationRuleBlock...) -> Bool {
53 |
54 | for rule in rules {
55 |
56 | if rule(row.value) == false {
57 | return false
58 | }
59 |
60 | }
61 |
62 | return true
63 |
64 | }
65 |
66 | @discardableResult
67 | func validateList(
68 | row: R,
69 | _ rules: ValidationRuleMsgBlock...) -> [(Bool, String?)] {
70 |
71 | let rulesResults: [(Bool, String?)] = rules.map({
72 | return ($0.0(row.value), $0.1)
73 | })
74 |
75 | return rulesResults
76 |
77 | }
78 |
79 | }
80 |
81 |
82 |
--------------------------------------------------------------------------------
/AstroForms/Abstract/Form.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 4/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | typealias VoidCallback = () -> Void
12 |
13 | open class Form: UIView, RowDelegate, ValidatableForm {
14 |
15 | let scrollView = UIScrollView()
16 |
17 | let containerView = UIView()
18 |
19 | /// The primary UI stackview for the form.
20 | let stackView = UIStackView()
21 |
22 | /// The rows for the row. Access via the form collection.
23 | public var rows: [AnyRow] = []
24 |
25 | /// Override this function to setup your rows with their initial state.
26 | /// The base implementation just clears the rows, so it is safe to
27 | /// call multiple times.
28 | open func setupRows() {
29 |
30 | // Remove all existing rows and views, so this function always refreshes
31 | //the view entirely.
32 | rows = []
33 |
34 | }
35 |
36 | open func findRow(
37 | tag: TagType
38 | ) -> RowType? {
39 |
40 | for row in self.rows {
41 | if (row.tag as? TagType) == tag {
42 | return row as? RowType
43 | }
44 | }
45 |
46 | return nil
47 |
48 | }
49 |
50 | public override init(frame: CGRect) {
51 |
52 | super.init(frame: frame)
53 | initialize()
54 | setupRows()
55 |
56 | }
57 |
58 | public required init?(coder aDecoder: NSCoder) {
59 |
60 | super.init(coder: aDecoder)
61 | initialize()
62 | setupRows()
63 |
64 | }
65 |
66 | open func submit() {
67 | self.firstResponder?.resignFirstResponder()
68 | }
69 |
70 | // Subclasses cannot override protocol extension of superclasses
71 |
72 | open func rowDidStartEditing(row: AnyRow) {}
73 |
74 | open func rowDidEndEditing(row: AnyRow) {}
75 |
76 | open func rowDidEdit(row: AnyRow) {}
77 |
78 | open func rowDidFocus(row: AnyRow) {}
79 |
80 | open func rowDidBlur(row: AnyRow) {}
81 |
82 | open func rowUpdate(type: RowUpdate, row: AnyRow) { }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/AstroForms.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint AstroForms.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'AstroForms'
11 | s.version = '0.1.1'
12 | s.summary = 'An approachable iOS forms framework for building beautiful, reusable and easy to maintain forms.'
13 |
14 | # This description is used to generate tags and improve search results.
15 | # * Think: What does it do? Why did you write it? What is the focus?
16 | # * Try to keep it short, snappy and to the point.
17 | # * Write the description between the DESC delimiters below.
18 | # * Finally, don't worry about the indent, CocoaPods strips it!
19 |
20 | s.description = <<-DESC
21 | Astro Forms is a framework that provides the structure to build your own highly custom and reusable forms for a project.
22 | In this way it's different to other frameworks - it's specifically not a drop in set of subclassable elements or abstraction around UIKit.
23 | Instead, it's a set of protocols and a minimum of abstract classes you compose to build your forms.
24 | This is an opinionated way of doing things, however you'll have far less code (less 🐛s) and far more flexibility for anything but trivial stock-standard looking forms.
25 | DESC
26 |
27 | s.homepage = 'https://github.com/plummer/astro-forms'
28 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
29 | s.license = { :type => 'MIT', :file => 'LICENSE' }
30 | s.author = { 'plummer' => 'andrewplummer@me.com' }
31 | s.source = { :git => 'https://github.com/plummer/astro-forms.git', :tag => s.version.to_s }
32 | # s.social_media_url = 'https://twitter.com/'
33 |
34 | s.ios.deployment_target = '11.4'
35 |
36 | s.source_files = 'AstroForms/**/*'
37 |
38 | # s.resource_bundles = {
39 | # 'AstroForms' => ['AstroForms/Assets/*.png']
40 | # }
41 |
42 | # s.public_header_files = 'Pod/Classes/**/*.h'
43 | # s.frameworks = 'UIKit', 'MapKit'
44 | # s.dependency 'AFNetworking', '~> 2.3'
45 | end
46 |
--------------------------------------------------------------------------------
/Example/AstroForms/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // AstroForms
4 | //
5 | // Created by plummer on 09/04/2018.
6 | // Copyright (c) 2018 plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/AstroForms/Theming/ThemeableTraits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeableTraits.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | public protocol Theme { }
12 |
13 | /// The traits of having a theme associated.
14 | public protocol ThemeableTraits: AnyThemeableTraits, ThemeableView {
15 |
16 | associatedtype ThemeType: Theme
17 |
18 | var theme: ThemeType? { get }
19 |
20 | func getTheme() -> ThemeType?
21 |
22 | }
23 |
24 | public protocol AnyThemeableTraits {
25 |
26 | var anyTheme: Theme? { get }
27 |
28 | func updateTheme()
29 |
30 | }
31 |
32 | public extension ThemeableTraits {
33 |
34 | var anyTheme: Theme? { return theme }
35 |
36 | }
37 |
38 | public extension ThemeableTraits where Self: UIView {
39 |
40 | func updateTheme() { }
41 |
42 | func getTheme() -> ThemeType? {
43 |
44 | var parentForm: Form?
45 |
46 | // Shortcut the loop if the current view is a form
47 | // This prevents a form inheriting from a parent for styles
48 | // however that doesn't seem like a reasonable case.
49 | var superview: UIView? = (self as? Form) ?? self.superview
50 |
51 | // This is convenient but slow (On^2 for n rows)
52 | // TODO: Look at changing the way form is referenced generally
53 | // to speed this up if necessary.
54 |
55 | while superview != nil {
56 |
57 | if let _form = superview as? Form {
58 | parentForm = _form
59 | break
60 | }
61 |
62 | superview = superview?.superview
63 |
64 | }
65 |
66 | let theme: ThemeType? =
67 | self.theme
68 | ?? (parentForm as? AnyThemeableTraits)?.anyTheme as? ThemeType
69 |
70 | return theme
71 |
72 | }
73 |
74 | }
75 |
76 | public extension ThemeableTraits where Self: Form {
77 |
78 | func updateTheme() {
79 |
80 | func updateSubviews(for view: UIView) {
81 | for view in view.subviews {
82 | (view as? ThemeableView)?.updateTheme()
83 | updateSubviews(for: view)
84 | }
85 | }
86 |
87 | updateSubviews(for: self)
88 |
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/AstroForms/Abstract/Form+FocusRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form+FocusRow.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 24/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension Form {
12 |
13 | /// Scroll to a focused row's preferred focus rect if there is an active
14 | /// first responder inside it.
15 | func focusRow(duration: Double, options: UIView.AnimationOptions) {
16 |
17 | for row in rows {
18 |
19 | // Check if this is the currently selected row
20 | guard
21 | row.baseView.firstResponder != nil,
22 | let focusableRow = row as? FocusableRow else { continue }
23 |
24 | let focusRect = focusableRow.focusRect() ?? row.baseView.frame
25 |
26 | var view: UIView? = row.baseView
27 |
28 | var scrollView: UIScrollView?
29 |
30 | while scrollView == nil {
31 |
32 | guard let _superview = view?.superview else { break }
33 | scrollView = _superview as? UIScrollView
34 | view = _superview
35 |
36 | }
37 |
38 | guard
39 | let _scrollView = scrollView,
40 | let _directSuperview = row.baseView.superview else { continue }
41 |
42 | let rectInScrollView = _directSuperview.convert(
43 | focusRect,
44 | to: _scrollView
45 | )
46 |
47 | UIView.animate(
48 | withDuration: duration,
49 | delay: 0,
50 | options: options,
51 | animations: {
52 | // The animated property can't be used here, so this is
53 | // wrapping in a standard UIView animation. If you use the
54 | // animation property, it will conflict with the built in
55 | // `UITextField` focus behaviour of UIKit, and will only
56 | // work sometimes. Incidentally this provides more control over
57 | // the focus timing / curve.
58 | _scrollView.scrollRectToVisible(
59 | rectInScrollView,
60 | animated: false
61 | )
62 | }, completion: nil)
63 |
64 | }
65 |
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/ButtonRow/ButtonRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonRowView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 9/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | class ButtonRowView: UIView, Themeable {
14 |
15 | var theme: AstroTheme?
16 |
17 | weak var row: ButtonRow?
18 |
19 | @IBOutlet weak var button: UIButton!
20 |
21 | @IBAction func buttonTapped(_ sender: Any) {
22 | row?.buttonTapped()
23 | }
24 |
25 | override func awakeFromNib() {
26 | super.awakeFromNib()
27 | button.clipsToBounds = true
28 | button.addTarget(self, action: #selector(buttonTouchDown), for: .touchDown)
29 | button.addTarget(self, action: #selector(buttonTouchUp), for: .touchUpInside)
30 | button.addTarget(self, action: #selector(buttonTouchUp), for: .touchUpOutside)
31 | button.addTarget(self, action: #selector(buttonTouchUp), for: .touchDragOutside)
32 | }
33 |
34 | @objc func buttonTouchDown() {
35 |
36 | UIView.animate(withDuration: 0.2) {[weak self] in
37 | guard let strongSelf = self else { return }
38 | strongSelf.button.transform = CGAffineTransform.init(
39 | scaleX: 1.03, y: 1.03
40 | )
41 | }
42 |
43 | }
44 |
45 | @objc func buttonTouchUp() {
46 |
47 | UIView.animate(withDuration: 0.2) {[weak self] in
48 | guard let strongSelf = self else { return }
49 | strongSelf.button.transform = CGAffineTransform.init(
50 | scaleX: 1.00, y: 1.00
51 | )
52 | }
53 |
54 | }
55 |
56 | func updateTheme() {
57 |
58 | button.setBackgroundImage(
59 | UIImage.from(color: color(.buttonBackground)),
60 | for: .normal
61 | )
62 |
63 | button.setBackgroundImage(
64 | UIImage.from(color: color(.buttonHighlightedBackground)),
65 | for: .highlighted
66 | )
67 |
68 | button.setBackgroundImage(
69 | UIImage.from(color: color(.buttonDisabledBackground)),
70 | for: .disabled
71 | )
72 |
73 | button.setTitleColor(color(.buttonText), for: .normal)
74 |
75 | button.setTitleColor(color(.buttonDisabledText), for: .disabled)
76 |
77 | backgroundColor = color(.primaryBackground)
78 |
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Helpers/ErrorView/ErrorView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CustomViewRow/Custom Views/HeroView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Helpers/HintView/HintView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextViewRow/TextViewRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextViewView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 7/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | class TextViewRowView: UIView,
14 | UITextViewDelegate,
15 | DefaultKeyboardToolbarDelegate, Themeable {
16 |
17 | @IBOutlet weak var label: UILabel!
18 |
19 | @IBOutlet weak var textView: UITextView!
20 |
21 | var theme: AstroTheme?
22 |
23 | @IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
24 |
25 | weak var row: TextViewRow?
26 |
27 | func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
28 |
29 | // This will need updating in the event that the form shows / hides
30 | // a row before / after the TextFieldRow.
31 | textView.inputAccessoryView = defaultToolbar(row: self.row)
32 | textView.inputAccessoryView?.tintColor = color(.primaryTint)
33 |
34 | return true
35 |
36 | }
37 |
38 | func textViewDidBeginEditing(_ textView: UITextView) {
39 | textView.backgroundColor = color(.inputHighlightedBackground)
40 |
41 | UIView.animate(
42 | withDuration: 0.6,
43 | delay: 0,
44 | usingSpringWithDamping: 0.5,
45 | initialSpringVelocity: 0.3, options: [], animations: {
46 |
47 | textView.transform = CGAffineTransform(scaleX: 1.03, y: 1.03)
48 |
49 | }, completion: nil)
50 |
51 | row?.valueDidStartEditing()
52 |
53 | }
54 |
55 | func textViewDidEndEditing(_ textView: UITextView) {
56 | textView.backgroundColor = color(.inputBackground)
57 |
58 | UIView.animate(withDuration: 0.2) {
59 | textView.transform = CGAffineTransform(scaleX: 1.00, y: 1.00)
60 | }
61 |
62 | row?.valueDidEndEditing()
63 |
64 | }
65 |
66 | func textViewDidChange(_ textView: UITextView) {
67 |
68 | row?.valueDidEdit()
69 |
70 | }
71 |
72 |
73 | @objc func keyboardToolbarDoneTapped() {
74 |
75 | row?.focusChange(.blur)
76 |
77 | }
78 |
79 | @objc func keyboardToolbarNextTapped() {
80 |
81 | row?.focusChange(.focusNext)
82 |
83 | }
84 |
85 | @objc func keyboardToolbarPreviousTapped() {
86 |
87 | row?.focusChange(.focusPrevious)
88 |
89 | }
90 |
91 | func updateTheme() {
92 |
93 | backgroundColor = color(.primaryBackground)
94 | label.textColor = color(.inputLabelColor)
95 | textView.textColor = color(.inputTextColor)
96 | textView.tintColor = color(.primaryTint)
97 |
98 | }
99 |
100 | override func awakeFromNib() {
101 |
102 | super.awakeFromNib()
103 |
104 | textView.delegate = self
105 | textView.textContainerInset = UIEdgeInsets(
106 | top: 12,
107 | left: 8,
108 | bottom: 12,
109 | right: 8
110 | )
111 |
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow/TextFieldRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextFieldRowView.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 1/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 |
13 | class TextFieldRowView: UIView,
14 | UITextFieldDelegate,
15 | DefaultKeyboardToolbarDelegate, Themeable {
16 |
17 | var theme: AstroTheme?
18 |
19 | @IBOutlet var textField: AstroTextField!
20 |
21 | @IBOutlet var label: UILabel!
22 |
23 | weak var row: TextFieldRow?
24 |
25 | override func awakeFromNib() {
26 |
27 | super.awakeFromNib()
28 |
29 | textField.delegate = self
30 |
31 | textField.addTarget(
32 | self,
33 | action: #selector(textFieldDidChange),
34 | for: .editingChanged
35 | )
36 |
37 | }
38 |
39 | func updateTheme() {
40 | backgroundColor = color(.primaryBackground)
41 | label.textColor = color(.inputLabelColor)
42 | textField.textColor = color(.inputTextColor)
43 | textField.tintColor = color(.primaryTint)
44 | }
45 |
46 | func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
47 |
48 | // This will need updating in the event that the form shows / hides
49 | // a row before / after the TextFieldRow.
50 | textField.inputAccessoryView = defaultToolbar(row: self.row)
51 | textField.inputAccessoryView?.tintColor = color(.primaryTint)
52 |
53 | return true
54 |
55 | }
56 |
57 | func textFieldDidBeginEditing(_ textField: UITextField) {
58 | textField.backgroundColor = color(.inputHighlightedBackground)
59 |
60 | UIView.animate(
61 | withDuration: 0.6,
62 | delay: 0,
63 | usingSpringWithDamping: 0.5,
64 | initialSpringVelocity: 0.3, options: [], animations: {
65 |
66 | textField.transform = CGAffineTransform(scaleX: 1.03, y: 1.03)
67 |
68 | }, completion: nil)
69 |
70 | row?.valueDidStartEditing()
71 |
72 | }
73 |
74 | func textFieldDidEndEditing(_ textField: UITextField) {
75 | textField.backgroundColor = color(.inputBackground)
76 |
77 | UIView.animate(withDuration: 0.2) {
78 | textField.transform = CGAffineTransform(scaleX: 1.00, y: 1.00)
79 | }
80 |
81 | row?.valueDidEndEditing()
82 |
83 | }
84 |
85 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
86 |
87 | if row?.nextFocusableRow != nil {
88 | row?.focusChange(.focusNext)
89 | } else {
90 | row?.focusChange(.blur)
91 | }
92 |
93 | return true
94 |
95 | }
96 |
97 | @objc func textFieldDidChange() {
98 |
99 | row?.valueDidEdit()
100 |
101 | }
102 |
103 | @objc func keyboardToolbarDoneTapped() {
104 |
105 | row?.focusChange(.blur)
106 |
107 | }
108 |
109 | @objc func keyboardToolbarNextTapped() {
110 |
111 | row?.focusChange(.focusNext)
112 |
113 | }
114 |
115 | @objc func keyboardToolbarPreviousTapped() {
116 |
117 | row?.focusChange(.focusPrevious)
118 |
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Example/Tests/ValidationTests.swift:
--------------------------------------------------------------------------------
1 | import AstroForms
2 | import XCTest
3 |
4 |
5 | class TestForm: Form {
6 |
7 | }
8 |
9 | class StringRow: ValueRow {
10 |
11 | func valueDidEdit() {}
12 | func valueDidStartEditing() {}
13 | func valueDidEndEditing() {}
14 | var valueHasChanged: Bool = false
15 | var valueHasEndedEditing: Bool = false
16 | var valueHasStartedEditing: Bool = false
17 |
18 | typealias Value = String
19 |
20 | var value: String
21 |
22 | init(value: String) {
23 | self.value = value
24 | }
25 |
26 | }
27 |
28 | class ValidationTests: XCTestCase {
29 |
30 | var form = TestForm(frame: .zero)
31 |
32 | var stringRow = StringRow(value: "astro")
33 |
34 | override func setUp() { super.setUp() }
35 |
36 | override func tearDown() { super.tearDown() }
37 |
38 | /// If all tests pass or fail, validation passes or fails.
39 | func testValidationPassFail() {
40 |
41 | let passResult = form.validate(
42 | row: stringRow,
43 | { $0 == "astro" },
44 | { $0.count == 5 }
45 | )
46 |
47 | XCTAssert(passResult == true)
48 |
49 | let failResult = form.validate(
50 | row: stringRow,
51 | { $0 == "foo" },
52 | { $0.count == 8 }
53 | )
54 |
55 | XCTAssert(failResult == false)
56 |
57 | }
58 |
59 | /// If only one test fails, the whole chain fails
60 | func testChainFailure() {
61 |
62 | let failResult = form.validate(
63 | row: stringRow,
64 | { $0 == "astro" },
65 | { $0.count == 20 }
66 | )
67 |
68 | XCTAssert(failResult == false)
69 |
70 | }
71 |
72 | /// The validation error status and message should be correct
73 | func testMessageValidation() {
74 |
75 | let message = "The input string must equal astro"
76 |
77 | let failResult = form.validate(
78 | row: stringRow,
79 | ({ $0 == "forms" }, message)
80 | )
81 |
82 | XCTAssert(failResult.0 == false)
83 | XCTAssert(failResult.1 == message)
84 |
85 | }
86 |
87 | /// If the second in chain fails, that message should be returned
88 | func testSecondInChainMessage() {
89 |
90 | let message = "The character count must be correct"
91 |
92 | let failResult = form.validate(
93 | row: stringRow,
94 | ({ $0 == "astro" }, "This should pass"),
95 | ({ $0.count == 10 }, message)
96 | )
97 |
98 | XCTAssert(failResult.0 == false)
99 | XCTAssert(failResult.1 == message)
100 |
101 | }
102 |
103 | /// If using a validation list, each line should have a correct status
104 | /// and message
105 | func testValidationList() {
106 |
107 | let equalityMessage = "String should equal astro"
108 | let countMessage = "Character count should be correct"
109 | let atSymbolMessage = "@ Symbol should be included"
110 |
111 | let mixedResult = form.validateList(
112 | row: stringRow,
113 | ({$0 == "astro" }, equalityMessage),
114 | ({$0.count == 10}, countMessage),
115 | ({$0.contains("@")}, atSymbolMessage)
116 | )
117 |
118 | XCTAssert(mixedResult[0].0 == true)
119 | XCTAssert(mixedResult[0].1 == equalityMessage)
120 | XCTAssert(mixedResult[1].0 == false)
121 | XCTAssert(mixedResult[1].1 == countMessage)
122 | XCTAssert(mixedResult[2].0 == false)
123 | XCTAssert(mixedResult[2].1 == atSymbolMessage)
124 |
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/FocusableRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusableRow.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 24/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public protocol FocusableRow {
12 |
13 | /// An offset modifier for keyboard management.
14 | ///
15 | /// Use this to have a custom focus rect when row is being edited.
16 | /// If you implement this as nil, the frame of the `baseView` will be
17 | /// focused by default. Computes lazily so that it will be accessed
18 | /// when the row is actually being focused.
19 | var focusRect: () -> CGRect? { get set }
20 |
21 | /// The UIResponder element to focus when the Next / Previous keyboard
22 | /// buttons are tapped, or the row is otherwise given focus.
23 | var focusElement: UIResponder { get }
24 |
25 | /// The next row in the form, if it exists.
26 | var nextFocusableRow: FocusableRow? { get }
27 |
28 | /// The previous row in the form, if it exists.
29 | var previousFocusableRow: FocusableRow? { get }
30 |
31 | /// Change the focus state of this row.
32 | ///
33 | /// - Parameter change: The type of focus update.
34 | func focusChange(_ change: FocusChange)
35 |
36 | }
37 |
38 | public extension AnyRow where Self: FocusableRow {
39 |
40 | func focusChange(_ change: FocusChange) {
41 |
42 | switch change {
43 | case .focus:
44 | focusElement.becomeFirstResponder()
45 | case .focusNext:
46 | nextFocusableRow?.focusChange(.focus)
47 | case .focusPrevious:
48 | previousFocusableRow?.focusChange(.focus)
49 | case .blur:
50 | focusElement.resignFirstResponder()
51 | }
52 |
53 | }
54 |
55 | /// Get the row before or after a given row.
56 | ///
57 | /// - Parameters:
58 | /// - currentRow: The row to check before / after for.
59 | /// - reversedDirection: Check in the reverse direction
60 | /// - Returns: The row before / after the given row
61 | ///
62 | /// Use the computed variables `nextFocusableRow` and `previousFocusableRow`
63 | /// instead of this function.
64 | private func nextFocusableRow(
65 | currentRow: AnyRow,
66 | reversedDirection: Bool = false
67 | ) -> FocusableRow? {
68 |
69 | guard let _form = form else { return nil }
70 |
71 | var passedCurrentRow = false
72 |
73 | for index in stride(
74 | from: reversedDirection ? _form.rows.endIndex - 1 : _form.rows.startIndex,
75 | to: reversedDirection ? _form.rows.startIndex - 1 : _form.rows.endIndex,
76 | by: reversedDirection ? -1 : 1
77 | ) {
78 |
79 | // If the baseView refers to the same instance then
80 | // this is the same row.
81 | if _form.rows[index].baseView === self.baseView {
82 | passedCurrentRow = true
83 | continue
84 | }
85 |
86 | guard passedCurrentRow == true else {
87 | continue
88 | }
89 |
90 | guard let _focusableRow = _form.rows[index] as? FocusableRow else {
91 | continue
92 | }
93 |
94 | return _focusableRow
95 |
96 | }
97 |
98 | return nil
99 |
100 | }
101 |
102 | var previousFocusableRow: FocusableRow? {
103 | return nextFocusableRow(currentRow: self, reversedDirection: true)
104 | }
105 |
106 | var nextFocusableRow: FocusableRow? {
107 | return nextFocusableRow(currentRow: self, reversedDirection: false)
108 | }
109 |
110 | }
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Example/AstroForms/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Transitions/SlideAnimatedTransition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlideAnimatedTransition.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 27/9/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// A transition that simply slides view controllers, rather than the
13 | /// default `UINavigationController` animation.
14 | ///
15 | /// Useful for view controller transitions with backgrounds that remain static.
16 | class SlideAnimatedTransition: NSObject, UIViewControllerAnimatedTransitioning {
17 |
18 | var direction: Direction = .right
19 | var duration: TimeInterval = 0.35
20 |
21 | enum Direction {
22 | case left, right
23 | }
24 |
25 | /// Initialise the transition with a left or right direction.
26 | ///
27 | /// - Parameters:
28 | /// - direction: The `Direction` from which the new view arrives.
29 | /// - duration: The duration of the transition
30 | convenience init(direction: Direction, duration: TimeInterval = 0.35) {
31 | self.init()
32 | self.direction = direction
33 | self.duration = duration
34 | }
35 |
36 | func transitionDuration(
37 | using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
38 | return duration
39 | }
40 |
41 | func animateTransition(
42 | using transitionContext: UIViewControllerContextTransitioning) {
43 |
44 | guard
45 | let toView = transitionContext.view(forKey: .to),
46 | let fromView = transitionContext.view(forKey: .from) else {
47 | return
48 | }
49 |
50 | let containerBounds = transitionContext.containerView.bounds
51 |
52 | func setFinalToViewState() {
53 | toView.frame = CGRect(
54 | x: containerBounds.origin.x,
55 | y: containerBounds.origin.y,
56 | width: toView.frame.width,
57 | height: toView.frame.height
58 | )
59 | }
60 |
61 | func setFinalFromViewState() {
62 | fromView.frame = CGRect(
63 | x: direction == .right
64 | ? containerBounds.origin.x - fromView.frame.width
65 | : containerBounds.maxX
66 | ,
67 | y: containerBounds.origin.y,
68 | width: fromView.frame.width,
69 | height: fromView.frame.height
70 | )
71 | }
72 |
73 | transitionContext.containerView.addSubview(toView)
74 |
75 | // Check for animated state, otherwise finish immediately
76 | guard transitionContext.isAnimated else {
77 |
78 | setFinalToViewState()
79 | setFinalFromViewState()
80 |
81 | transitionContext.completeTransition(
82 | !transitionContext.transitionWasCancelled
83 | )
84 |
85 | return
86 |
87 | }
88 |
89 | toView.frame = CGRect(
90 | x: direction == .right
91 | ? containerBounds.maxX
92 | : containerBounds.origin.y - toView.frame.width,
93 | y: containerBounds.origin.y,
94 | width: toView.frame.width,
95 | height: toView.frame.height
96 | )
97 |
98 | UIView.animate(
99 | withDuration: transitionDuration(using: transitionContext),
100 | delay: 0,
101 | options: [.curveEaseInOut],
102 | animations: {
103 |
104 | setFinalToViewState()
105 | setFinalFromViewState()
106 |
107 | }) { _ in
108 | transitionContext.completeTransition(
109 | !transitionContext.transitionWasCancelled
110 | )
111 | }
112 |
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/ButtonRow/ButtonRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/AstroForms/Abstract/Form+Keyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form+Keyboard.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 3/7/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // An extension to handle keyboard management
12 | extension Form {
13 |
14 | /// A block that takes the new ideal scrollView offset.
15 | typealias KeyboardSizeChangeBlock = (
16 | CGFloat,
17 | Double,
18 | UIView.AnimationOptions
19 | ) -> Void
20 |
21 | /// A general handler for keyboard hiding / showing.
22 | ///
23 | /// - Parameters:
24 | /// - notification: The keyboard notification
25 | /// - animationBlock: The block to run as the keyboard changes size
26 | private func keyboardSizeChangeHandler(
27 | notification: Notification,
28 | animationBlock: @escaping KeyboardSizeChangeBlock
29 | ) {
30 |
31 | guard
32 |
33 | let keyboardInfo = notification.userInfo,
34 |
35 | // The frame of the keyboard when it is open
36 | let endFrame = keyboardInfo[
37 | UIResponder.keyboardFrameEndUserInfoKey
38 | ] as? NSValue,
39 |
40 | // The duration the keyboard takes to appear
41 | let duration = keyboardInfo[
42 | UIResponder.keyboardAnimationDurationUserInfoKey
43 | ] as? Double,
44 |
45 | // The animation curve for the keyboard as it zooms up
46 | let curveValue = keyboardInfo[
47 | UIResponder.keyboardAnimationCurveUserInfoKey
48 | ] as? Int,
49 | let curve = UIView.AnimationCurve(
50 | rawValue: curveValue
51 | )?.toOptions() else {
52 | return
53 | }
54 |
55 | /// The final keyboard frame in the context of the Form scrollView.
56 | let kbFrame = scrollView.convert(
57 | endFrame.cgRectValue,
58 | to: scrollView
59 | )
60 |
61 | let newBottomInset =
62 | frame.origin.y
63 | + frame.size.height
64 | - kbFrame.origin.y
65 |
66 | animationBlock(newBottomInset, duration, curve)
67 |
68 | }
69 |
70 | @objc func didBeginEditingTextField(_ notification: Notification) { }
71 |
72 | @objc func keyboardWillShow(_ notification: Notification) {
73 |
74 | keyboardSizeChangeHandler(
75 | notification: notification,
76 | animationBlock: {
77 | [unowned self] (newBottomInset, duration, options) in
78 |
79 | self.scrollView.contentInset.bottom = newBottomInset
80 | self.scrollView.scrollIndicatorInsets.bottom = newBottomInset
81 |
82 | // Handle additional complex keyboard management here.
83 | // For example, showing multiple fields, and offset from the
84 | // field to the keyboard.
85 | DispatchQueue.main.async {[unowned self] in
86 | // By default, animate rows with the same animation
87 | // duration and options as the keyboard show / hide.
88 | // This creates a smooth animation in this context,
89 | // and seems fine for others.
90 | self.focusRow(duration: duration, options: options)
91 | }
92 |
93 | })
94 |
95 | }
96 |
97 | @objc func keyboardWillHide(_ notification: Notification) {
98 |
99 | keyboardSizeChangeHandler(
100 | notification: notification,
101 | animationBlock: {
102 | [unowned self] (newBottomInset, duration, options) in
103 |
104 | self.scrollView.contentInset.bottom = newBottomInset
105 | self.scrollView.scrollIndicatorInsets.bottom = newBottomInset
106 |
107 | })
108 |
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Features/Additional Info/AdditionalInfoForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdditionalInfoForm.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 26/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AstroForms
12 | import CoreLocation
13 |
14 | class AdditionalInfoForm: Form, Themeable {
15 |
16 | enum AdditionalInfoRowTag: RowTag, Equatable {
17 | case aboutMe, favouriteMovie, characterLikeRow, location, submit
18 | }
19 |
20 | var loginFormData: LoginFormData? {
21 | didSet {
22 | if let _loginFormData = loginFormData {
23 | setupForm(loginFormData: _loginFormData)
24 | }
25 | }
26 | }
27 |
28 | var theme: AstroTheme? = .light {
29 | didSet {
30 | updateTheme()
31 | }
32 | }
33 |
34 | func setupForm(loginFormData: LoginFormData) {
35 |
36 | let aboutRow = TextViewRow(tag: AdditionalInfoRowTag.aboutMe) {
37 |
38 | $0.view.label.text = "Your Bio"
39 |
40 | }
41 |
42 | let mapRow = MapRow(
43 | tag: AdditionalInfoRowTag.location,
44 | location: CLLocationCoordinate2D(
45 | latitude: -37.8136,
46 | longitude: 144.9631
47 | )
48 | )
49 |
50 | let favouriteMovieRow = CheckListRow(
51 | tag: AdditionalInfoRowTag.favouriteMovie,
52 | selectionType: .single,
53 | label: "Your Favourite Mars Mission",
54 | options: [
55 | "Mars Science Laboratory: Investigating Mars' habitability",
56 | "Mars Orbiter Mission: Developing interplanetary mission technology",
57 | "MAVEN: Examining the planet's atmosphere and water"
58 | ]
59 | )
60 |
61 | let charactersYouLikeRow = CheckListRow(
62 | tag: AdditionalInfoRowTag.characterLikeRow,
63 | selectionType: .multiple,
64 | label: "Favourite Planets (Select multiple)",
65 | options: [
66 | "Mercury",
67 | "Venus",
68 | "Earth",
69 | "Mars",
70 | "Jupiter",
71 | "Saturn",
72 | "Uranus",
73 | "Neptune"
74 | ]
75 | )
76 |
77 | let submitRow = ButtonRow(tag: AdditionalInfoRowTag.submit) {
78 | $0.title = "Submit"
79 | }
80 |
81 | add(favouriteMovieRow)
82 | add(aboutRow)
83 | add(mapRow)
84 | add(charactersYouLikeRow)
85 | add(submitRow)
86 |
87 | }
88 |
89 | func updateAboutMeHintError(row: TextViewRow) {
90 |
91 | guard validate(row: row, ValidationRule.required) else {
92 | row.showHelper(viewType: ErrorView.self, animated: true) {
93 | $0.label.text = "This field is required"
94 | }
95 | return
96 | }
97 |
98 | guard validate(row: row, { $0.count <= 200 }) else {
99 |
100 | row.showHelper(viewType: ErrorView.self, animated: true) {
101 | $0.label.text =
102 | """
103 | \(row.value.count) / \(200), Too many characters
104 | """
105 | }
106 | return
107 | }
108 |
109 | row.showHelper(viewType: HintView.self, animated: true) {
110 | $0.label.text = "\(row.value.count) / \(200)"
111 | }
112 |
113 | }
114 |
115 | override func rowUpdate(type: RowUpdate, row: AnyRow) {
116 |
117 | guard let tag = row.tag as? AdditionalInfoRowTag else { return }
118 |
119 | switch type {
120 | case .live:
121 |
122 | if tag == .aboutMe, let row = row as? TextViewRow {
123 | updateAboutMeHintError(row: row)
124 | }
125 |
126 | default: break
127 |
128 | }
129 |
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Forms
2 |
3 | [](https://app.bitrise.io/app/11b5791a9dab5b3c) [](https://www.astroforms.com)
4 |
5 | [](https://www.astroforms.com)
6 |
7 | |  |  |  |
8 | | ------------- | ------------- | ------------- |
9 |
10 | ## Getting Started
11 |
12 | For a getting started guide, reference docs and included forms, head to the [documentation](https://www.astroforms.com).
13 |
14 | Want to dive right in?
15 |
16 | ```ruby
17 | pod install 'AstroForms'
18 | ```
19 |
20 | ## What is Astro Forms?
21 |
22 | Astro Forms is a framework that provides the structure to build your own highly custom and reusable forms for a project. In this way it's different to other frameworks - it's specifically _not_ a drop in set of subclassable elements or abstraction around UIKit. Instead, it's a set of protocols and a minimum of abstract classes you compose to build _your_ forms. This is an opinionated way of doing things, however you'll have far less code (less 🐛s) and far more flexibility for anything but trivial stock-standard looking forms.
23 |
24 | ### Talk straight - what is a `Form`?
25 |
26 | It's a `UIView` subclass that contains a single `UIStackView` - it controls rendering, show/hide, validation and all the other form like features you might need.
27 |
28 | ### ... and a `Row`?
29 |
30 | It's a class that conforms to various `Row` protocols (`ValueRow`, `FocusableRow`...) so it can interact with its `Form`, plus a plain old `UIView` and a Nib.
31 |
32 | ## Examples
33 |
34 | For a getting started guide, reference docs and a info on the included forms, head to the [documentation](https://www.astroforms.com).
35 |
36 | If you'd prefer to read the code, here's an [example login form](https://github.com/plummer/astro-forms/blob/master/Example/AstroForms/GUI/Features/Login/LoginForm.swift) and [text field row](https://github.com/plummer/astro-forms/tree/master/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow).
37 |
38 | As a basic overview however to give you an idea of what it's like to use Astro Forms:
39 |
40 | ### Rendering a row
41 |
42 | Rendering a row is as simple as giving it a tag and configuring it's view:
43 |
44 | ```swift
45 | let emailRow = TextFieldRow(tag: LoginFormTag.email) {
46 | $0.view.label.text = "Email"
47 | $0.view.textField.placeholder = "example@astroforms.com"
48 | $0.view.textField.keyboardType = .emailAddress
49 | $0.view.textField.autocorrectionType = UITextAutocorrectionType.no
50 | }
51 |
52 | // inside a Form subclass
53 | add(emailRow)
54 | ```
55 | You can see the implementation for this row [here](https://github.com/plummer/astro-forms/tree/master/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow).
56 |
57 | ### Finding a row
58 |
59 | Finding a row is easy and type safe:
60 |
61 | ```swift
62 | let emailRow: TextFieldRow? = findRow(tag: LoginFormTag.email)
63 | ```
64 |
65 | The value is typed for every row too:
66 |
67 | ```swift
68 | let stringValue = emailRow?.value
69 | ```
70 |
71 | `RowTag` implementations can have associated values, so dynamic row rendering and access is easy too:
72 |
73 | ```swift
74 | let addressLine4Row: TextFieldRow? = findRow(tag: LoginFormTag.custom("address-line-4"))
75 | ```
76 |
77 | ### Validating a row
78 |
79 | Forms can validate rows with a convenient block-chaining syntax:
80 |
81 | ```swift
82 | let isValid: Bool = validate(
83 | row: emailRow,
84 | { $0.count > 0 }, // The rows typed value (String) is passed into each validation block
85 | { $0.contains "@" }
86 | )
87 | ```
88 |
89 | A built in factory `ValidationRule` provides validation methods that can be extended for reusability. For example, instead of the (hilariously poor) email validation above, `ValidationRule.isEmail` is built in. This can be mixed with inline rules.
90 |
91 |
92 | ```swift
93 | let isValid: Bool = validate(
94 | row: emailRow,
95 | { $0.count > 0 }, // The rows typed valued is passed into each validation block
96 | ValidationRule.isEmail
97 | )
98 | ```
99 |
--------------------------------------------------------------------------------
/Example/AstroForms.xcodeproj/xcshareddata/xcschemes/AstroForms-Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
99 |
105 |
106 |
107 |
108 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CheckListRow/CheckListRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextViewRow/TextViewRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/SwitchRow/SwitchRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CheckListRow/CheckListRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckListRow.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 12/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | class CheckListRow: Row, ValueRow, CheckListRowItemViewDelegate {
13 |
14 | var valueHasStartedEditing: Bool = false
15 |
16 | var valueHasChanged: Bool = false
17 |
18 | var valueHasEndedEditing: Bool = false
19 |
20 | typealias Value = [Bool]
21 |
22 | typealias View = CheckListRowView
23 |
24 | var tag: RowTag
25 |
26 | var view: View
27 |
28 | enum SelectionType {
29 | case single, multiple
30 | }
31 |
32 | var selectionType: SelectionType = .single
33 |
34 | var value: Value {
35 | get {
36 |
37 | let views = view.stackView.arrangedSubviews.compactMap {
38 | view -> CheckListRowItemView? in
39 | return view as? CheckListRowItemView
40 | }
41 |
42 | return views.map({ return $0.isChecked })
43 |
44 | }
45 | set {
46 |
47 | let views = view.stackView.arrangedSubviews.compactMap {
48 | view -> CheckListRowItemView? in
49 | return view as? CheckListRowItemView
50 | }
51 |
52 | for (i, value) in newValue.enumerated() {
53 | if views.indices.contains(i) {
54 | views[i].isChecked = value
55 | }
56 | }
57 |
58 | }
59 | }
60 |
61 | func didCheck(newValue: Bool, view: UIView) {
62 | print("did check \(newValue), \(view)")
63 | }
64 |
65 | func addItems(labels: [String]) {
66 |
67 | for (i, label) in labels.enumerated() {
68 |
69 | let item: CheckListRowItemView = CheckListRowItemView.fromXib()
70 |
71 | item.label.text = label
72 |
73 | view.stackView.addArrangedSubview(item)
74 |
75 | item.updateTheme()
76 |
77 | item.delegate = self
78 |
79 | switch i {
80 | case 0:
81 | item.listPosition = labels.count == 0 ? .single : .start
82 | case labels.count - 1:
83 | item.listPosition = .end
84 | default:
85 | item.listPosition = .middle
86 | }
87 |
88 | }
89 |
90 | }
91 |
92 | func willSelect(_ view: CheckListRowItemView) {
93 |
94 | switch selectionType {
95 | case .single:
96 | self.view.stackView.arrangedSubviews
97 | .filter({$0 != view })
98 | .forEach {
99 | ($0 as? CheckListRowItemView)?.isChecked = false
100 | }
101 | case .multiple:
102 | break
103 | }
104 |
105 | }
106 |
107 | func updateCorners() {
108 |
109 | let views = self.view.stackView.arrangedSubviews.compactMap {
110 | _view -> CheckListRowItemView? in
111 | return _view as? CheckListRowItemView
112 | }
113 |
114 | guard selectionType == .multiple else {
115 |
116 | views.forEach { $0.roundCorners(corners: [.allCorners])}
117 | return
118 |
119 | }
120 |
121 | for (i, _view) in views.enumerated() {
122 |
123 | if
124 | views.indices.contains(i - 1),
125 | views[i - 1].isChecked,
126 | views.indices.contains(i + 1),
127 | views[i + 1].isChecked {
128 |
129 | // Remove rounded corners
130 | _view.roundCorners(radius: 0, corners: [.allCorners])
131 |
132 | } else if
133 | views.indices.contains(i - 1),
134 | views[i - 1].isChecked {
135 |
136 | // Round bottom only
137 | _view.roundCorners(corners: [.bottomLeft, .bottomRight])
138 |
139 | } else if
140 | views.indices.contains(i + 1),
141 | views[i + 1].isChecked {
142 |
143 | _view.roundCorners(corners: [.topLeft, .topRight])
144 |
145 | } else {
146 | _view.roundCorners(corners: [.allCorners])
147 | }
148 |
149 | }
150 |
151 | }
152 |
153 | init(tag: RowTag) {
154 |
155 | self.tag = tag
156 | let view: View = View.fromXib()
157 | self.view = view
158 |
159 | }
160 |
161 | convenience init(
162 | tag: RowTag,
163 | selectionType: SelectionType,
164 | label: String,
165 | options: [String],
166 | config: ((CheckListRow) -> Void)? = nil) {
167 |
168 | self.init(tag: tag)
169 | self.selectionType = selectionType
170 | self.view.label.text = label
171 | self.view.row = self
172 | self.addItems(labels: options)
173 | config?(self)
174 |
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CheckListRow/CheckListRowItemView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/TextFieldRow/TextFieldRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/MapRow/MapRowView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Rows/CheckListRow/CheckListRowItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckListRowItemView.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 30/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | protocol CheckListRowItemViewDelegate: class {
13 |
14 | func willSelect(_ view: CheckListRowItemView)
15 |
16 | func updateCorners()
17 |
18 | }
19 |
20 | class CheckListRowItemView: UIView, Themeable {
21 |
22 | @IBOutlet weak var imageView: UIImageView!
23 |
24 | @IBOutlet weak var label: UILabel!
25 |
26 | var theme: AstroTheme? = nil
27 |
28 | weak var delegate: CheckListRowItemViewDelegate?
29 |
30 | var isChecked: Bool {
31 | get {
32 | return self.state == .checked
33 | }
34 | set {
35 | state = newValue == true ? .checked : .unchecked
36 | }
37 | }
38 |
39 | enum State {
40 | case
41 | checked,
42 | unchecked
43 | }
44 |
45 | enum ListPosition {
46 | case
47 | start,
48 | middle,
49 | end,
50 | single
51 | }
52 |
53 | var state: State = .unchecked {
54 | didSet {
55 | setSelectedState(
56 | state: state,
57 | listPosition: listPosition
58 | )
59 | }
60 | }
61 |
62 | var listPosition: ListPosition = .middle {
63 | didSet {
64 | setSelectedState(
65 | state: state,
66 | listPosition: listPosition
67 | )
68 | }
69 | }
70 |
71 | func setSelectedState(
72 | state: State,
73 | listPosition: ListPosition
74 | ) {
75 |
76 | self.delegate?.updateCorners()
77 |
78 | switch state {
79 | case .checked:
80 |
81 | updateTheme()
82 |
83 | UIView.animate(
84 | withDuration: 0.6,
85 | delay: 0,
86 | usingSpringWithDamping: 0.5,
87 | initialSpringVelocity: 0.3, options: [], animations: {
88 | [unowned self] in
89 |
90 | self.transform = CGAffineTransform(scaleX: 1.03, y: 1.03)
91 |
92 | }, completion: nil)
93 |
94 | case .unchecked:
95 |
96 | self.layer.zPosition = 0
97 |
98 | UIView.animate(
99 | withDuration: 0.15,
100 | delay: 0,
101 | options: [.curveEaseOut],
102 | animations: {[unowned self] in
103 | self.transform = CGAffineTransform(
104 | scaleX: 1.00,
105 | y: 1.00
106 | )
107 |
108 | self.updateTheme()
109 |
110 | },
111 | completion: {[unowned self] _ in
112 |
113 | switch listPosition {
114 | case .start:
115 | self.roundCorners(corners: [.topLeft, .topRight])
116 | case .middle:
117 | self.roundCorners(radius: 0, corners: [.allCorners])
118 | case .end:
119 | self.roundCorners(corners: [.bottomLeft, .bottomRight])
120 | case .single:
121 | self.roundCorners(corners: [.allCorners])
122 |
123 | }
124 |
125 | }
126 | )
127 |
128 | }
129 |
130 | }
131 |
132 | func roundCorners(radius: CGFloat = 12, corners: UIRectCorner) {
133 |
134 | // Layout required to round corners because otherwise the bounds
135 | // may not have correct size
136 | self.setNeedsLayout()
137 | self.layoutIfNeeded()
138 |
139 |
140 | let path = UIBezierPath(
141 | roundedRect: bounds,
142 | byRoundingCorners: corners,
143 | cornerRadii: CGSize(width: radius, height: radius)
144 | )
145 |
146 | let mask = CAShapeLayer()
147 | mask.path = path.cgPath
148 | layer.mask = mask
149 |
150 | }
151 |
152 | override func awakeFromNib() {
153 | super.awakeFromNib()
154 |
155 | self.addGestureRecognizer(
156 | UITapGestureRecognizer(
157 | target: self,
158 | action: #selector(viewTapped(_:))
159 | )
160 | )
161 |
162 | }
163 |
164 | func select() {
165 |
166 | if !self.isChecked { delegate?.willSelect(self) }
167 | self.isChecked.toggle()
168 |
169 | }
170 |
171 | @objc func viewTapped(_ sender: UIView) {
172 | select()
173 | self.window?
174 | .rootViewController?
175 | .view.firstResponder?
176 | .resignFirstResponder()
177 | }
178 |
179 | func updateTheme() {
180 |
181 | switch state {
182 |
183 | case .checked:
184 |
185 | imageView.image = #imageLiteral(resourceName: "Check-filled")
186 | imageView.tintColor = .white
187 | label.textColor = color(.inputLabelColor)
188 | self.backgroundColor = color(.primaryTint)
189 | self.layer.zPosition = 1
190 |
191 | case .unchecked:
192 |
193 | self.imageView.image = #imageLiteral(resourceName: "Check-unfilled")
194 | self.imageView.tintColor = self.color(.inputDisabledBackground)
195 | self.label.textColor = self.color(.inputTextColor)
196 | self.backgroundColor = self.color(.inputBackground)
197 |
198 | }
199 |
200 | }
201 |
202 | }
203 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Features/Login/LoginForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginForm.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 24/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AstroForms
11 |
12 | protocol LoginFormDelegate: class {
13 | func didSubmit(result: LoginFormData)
14 | }
15 |
16 | class LoginForm: Form, Themeable {
17 |
18 | weak var delegate: LoginFormDelegate?
19 |
20 | var theme: AstroTheme? = .light {
21 | didSet {
22 | updateTheme()
23 | }
24 | }
25 |
26 | enum LoginFormTag: RowTag, Equatable {
27 | case
28 | hero,
29 | email,
30 | password,
31 | submit
32 | }
33 |
34 | override func awakeFromNib() {
35 | super.awakeFromNib()
36 | setupForm()
37 | }
38 |
39 | override func submit() {
40 | super.submit()
41 |
42 | guard isValid else { return }
43 |
44 | guard
45 | let emailRow: TextFieldRow = findRow(tag: LoginFormTag.email),
46 | let passwordRow: TextFieldRow = findRow(tag: LoginFormTag.password) else {
47 | return
48 | }
49 |
50 | let requestData = LoginFormData(
51 | email: emailRow.value,
52 | password: passwordRow.value
53 | )
54 |
55 | delegate?.didSubmit(result: requestData)
56 |
57 | }
58 |
59 | func setupForm() {
60 |
61 | let heroRow = CustomViewRow(tag: LoginFormTag.hero)
62 |
63 | let emailRow = TextFieldRow(tag: LoginFormTag.email) {
64 | $0.view.label.text = "Email"
65 | $0.view.textField.placeholder = "example@astroforms.com"
66 | $0.view.textField.keyboardType = .emailAddress
67 | $0.view.textField.autocorrectionType = UITextAutocorrectionType.no
68 | }
69 |
70 | let passwordRow = TextFieldRow(tag: LoginFormTag.password) {
71 | $0.view.label.text = "Password"
72 | $0.view.textField.placeholder = "••••••"
73 | $0.view.textField.isSecureTextEntry = true
74 | }
75 |
76 | let submitRow = ButtonRow(tag: LoginFormTag.submit) {[unowned self] in
77 | $0.view.button.isEnabled = false
78 | $0.buttonTapBlock = self.submit
79 | $0.title = "Login"
80 | }
81 |
82 | emailRow.focusRect = {
83 | CGRect(
84 | x: emailRow.baseView.frame.origin.x,
85 | y: emailRow.baseView.frame.origin.y,
86 | width: emailRow.baseView.frame.width,
87 | height:
88 | emailRow.baseView.frame.height
89 | + passwordRow.baseView.frame.height
90 | + submitRow.baseView.frame.height
91 | )
92 | }
93 |
94 | passwordRow.focusRect = {
95 | CGRect(
96 | x: passwordRow.baseView.frame.origin.x,
97 | y: passwordRow.baseView.frame.origin.y,
98 | width: passwordRow.baseView.frame.width,
99 | height:
100 | passwordRow.baseView.frame.height
101 | + submitRow.baseView.frame.height
102 | )
103 | }
104 |
105 | add(heroRow)
106 | add(emailRow)
107 | add(passwordRow)
108 | add(submitRow)
109 |
110 | }
111 |
112 | func updateEmailRowHintUI(row: TextFieldRow) {
113 |
114 | // Required
115 | guard validate(row: row, ValidationRule.required) else {
116 | row.showHelper(viewType: ErrorView.self, animated: true) {
117 | $0.label.text = "This field is required."
118 | }
119 | return
120 | }
121 |
122 | // Valid email in the format example@astroforms.com
123 | guard validate(row: row, ValidationRule.isEmail) else {
124 | row.showHelper(viewType: ErrorView.self, animated: true) {
125 | $0.label.text = "Please enter a valid email address."
126 | }
127 | return
128 | }
129 |
130 | row.hideHelper(animated: true)
131 |
132 | }
133 |
134 | func updatePasswordRowHint(row: TextFieldRow) {
135 |
136 | // Required
137 | guard validate(row: row, ValidationRule.required) else {
138 | row.showHelper(viewType: ErrorView.self, animated: true) {
139 | $0.label.text = "This field is required."
140 | }
141 | return
142 | }
143 |
144 | row.hideHelper(animated: true)
145 |
146 | }
147 |
148 | var isValid: Bool {
149 |
150 | guard
151 | let emailRow: TextFieldRow = findRow(tag: LoginFormTag.email),
152 | let passwordRow: TextFieldRow = findRow(tag: LoginFormTag.password)
153 | else {
154 | return false
155 | }
156 |
157 | guard
158 | validate(
159 | row: emailRow,
160 | ValidationRule.required,
161 | ValidationRule.isEmail
162 | ),
163 | validate(
164 | row: passwordRow,
165 | ValidationRule.required
166 | ) else {
167 | return false
168 | }
169 |
170 | return true
171 |
172 | }
173 |
174 | func updateButtonEnabledStateUI(row: ButtonRow) {
175 | row.view.button.isEnabled = isValid
176 | }
177 |
178 | override func rowUpdate(type: RowUpdate, row: AnyRow) {
179 |
180 | guard let tag = row.tag as? LoginFormTag else { return }
181 |
182 | switch type {
183 | case .live:
184 |
185 | // When any row is updated, update button row validity
186 | guard let buttonRow: ButtonRow = findRow(
187 | tag: LoginFormTag.submit
188 | ) else {
189 | return
190 | }
191 |
192 | updateButtonEnabledStateUI(row: buttonRow)
193 |
194 | case .regular:
195 |
196 | if tag == .email, let row = row as? TextFieldRow {
197 | updateEmailRowHintUI(row: row)
198 | return
199 | }
200 |
201 | if tag == .password, let row = row as? TextFieldRow {
202 | updatePasswordRowHint(row: row)
203 | return
204 | }
205 |
206 | default: break
207 | }
208 |
209 | }
210 |
211 | }
212 |
213 | struct LoginFormData: Codable {
214 | let email: String
215 | let password: String
216 | }
217 |
--------------------------------------------------------------------------------
/AstroForms/Abstract/Form+Initialize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form+Initialize.swift
3 | // Astro
4 | //
5 | // Created by Andrew Plummer on 21/6/18.
6 | // Copyright © 2018 Andrew Plummer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension Form {
12 |
13 | /// The general initialization function when forms are instantiated from
14 | /// code or storyboard.
15 | ///
16 | /// Sets up th overall form structure and keyboard handling.
17 | /// Only call this once per view, however call setupRows() many times.
18 | func initialize() {
19 |
20 | NotificationCenter.default.removeObserver(self)
21 |
22 | NotificationCenter.default.addObserver(
23 | self,
24 | selector: #selector(keyboardWillHide),
25 | name: UIResponder.keyboardWillHideNotification,
26 | object: nil
27 | )
28 |
29 | NotificationCenter.default.addObserver(
30 | self,
31 | selector: #selector(keyboardWillShow),
32 | name: UIResponder.keyboardWillShowNotification,
33 | object: nil
34 | )
35 |
36 | NotificationCenter.default.addObserver(
37 | self,
38 | selector: #selector(didBeginEditingTextField),
39 | name: UITextField.textDidBeginEditingNotification,
40 | // Also for UITextView
41 | object: nil
42 | )
43 |
44 | if !self.subviews.contains(scrollView) {
45 | scrollView.translatesAutoresizingMaskIntoConstraints = false
46 | self.addSubview(scrollView)
47 |
48 | self.addConstraints([
49 | NSLayoutConstraint(
50 | item: scrollView,
51 | attribute: .top,
52 | relatedBy: .equal,
53 | toItem: self,
54 | attribute: .top,
55 | multiplier: 1.0,
56 | constant: 0
57 | ),
58 | NSLayoutConstraint(
59 | item: scrollView,
60 | attribute: .leading,
61 | relatedBy: .equal,
62 | toItem: self,
63 | attribute: .leading,
64 | multiplier: 1.0,
65 | constant: 0
66 | ),
67 | NSLayoutConstraint(
68 | item: scrollView,
69 | attribute: .trailing,
70 | relatedBy: .equal,
71 | toItem: self,
72 | attribute: .trailing,
73 | multiplier: 1.0,
74 | constant: 0
75 | ),
76 | NSLayoutConstraint(
77 | item: scrollView,
78 | attribute: .bottom,
79 | relatedBy: .equal,
80 | toItem: self,
81 | attribute: .bottom,
82 | multiplier: 1.0,
83 | constant: 0
84 | )
85 | ])
86 | }
87 |
88 | // If the container view is not a subview, add it
89 | if !scrollView.subviews.contains(containerView) {
90 | containerView.translatesAutoresizingMaskIntoConstraints = false
91 | scrollView.addSubview(containerView)
92 |
93 | scrollView.addConstraints([
94 | NSLayoutConstraint(
95 | item: containerView,
96 | attribute: .centerX,
97 | relatedBy: .equal,
98 | toItem: scrollView,
99 | attribute: .centerX,
100 | multiplier: 1.0,
101 | constant: 0
102 | ),
103 | NSLayoutConstraint(
104 | item: containerView,
105 | attribute: .top,
106 | relatedBy: .equal,
107 | toItem: scrollView,
108 | attribute: .top,
109 | multiplier: 1.0,
110 | constant: 0
111 | ),
112 | NSLayoutConstraint(
113 | item: containerView,
114 | attribute: .leading,
115 | relatedBy: .equal,
116 | toItem: scrollView,
117 | attribute: .leading,
118 | multiplier: 1.0,
119 | constant: 0
120 | ),
121 | NSLayoutConstraint(
122 | item: containerView,
123 | attribute: .trailing,
124 | relatedBy: .equal,
125 | toItem: scrollView,
126 | attribute: .trailing,
127 | multiplier: 1.0,
128 | constant: 0
129 | ),
130 | NSLayoutConstraint(
131 | item: containerView,
132 | attribute: .bottom,
133 | relatedBy: .equal,
134 | toItem: scrollView,
135 | attribute: .bottom,
136 | multiplier: 1.0,
137 | constant: 0
138 | )
139 | ])
140 |
141 | stackView.translatesAutoresizingMaskIntoConstraints = false
142 | containerView.addSubview(stackView)
143 |
144 | containerView.addConstraints([
145 | NSLayoutConstraint(
146 | item: stackView,
147 | attribute: .top,
148 | relatedBy: .equal,
149 | toItem: containerView,
150 | attribute: .top,
151 | multiplier: 1.0,
152 | constant: 0
153 | ),
154 | NSLayoutConstraint(
155 | item: stackView,
156 | attribute: .leading,
157 | relatedBy: .equal,
158 | toItem: containerView,
159 | attribute: .leading,
160 | multiplier: 1.0,
161 | constant: 0
162 | ),
163 | NSLayoutConstraint(
164 | item: stackView,
165 | attribute: .trailing,
166 | relatedBy: .equal,
167 | toItem: containerView,
168 | attribute: .trailing,
169 | multiplier: 1.0,
170 | constant: 0
171 | ),
172 | NSLayoutConstraint(
173 | item: stackView,
174 | attribute: .bottom,
175 | relatedBy: .lessThanOrEqual,
176 | toItem: containerView,
177 | attribute: .bottom,
178 | multiplier: 1.0,
179 | constant: 0
180 | )
181 | ])
182 |
183 | }
184 |
185 | stackView.axis = .vertical
186 |
187 | }
188 |
189 | }
190 |
--------------------------------------------------------------------------------
/Example/AstroForms/GUI/Shared/Forms/Theme/AstroThemeColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AstroThemeColor.swift
3 | // AstroForms_Example
4 | //
5 | // Created by Andrew Plummer on 25/9/18.
6 | // Copyright © 2018 CocoaPods. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | enum AstroThemeColor {
13 | case
14 |
15 | /// The primary tint color for the form
16 | primaryTint,
17 |
18 | /// For two-tone themes, another tint color
19 | secondaryTint,
20 |
21 | /// The background for rows
22 | primaryBackground,
23 |
24 | /// UIButton
25 | buttonBackground,
26 | buttonDisabledBackground,
27 | buttonHighlightedBackground,
28 | buttonText,
29 | buttonDisabledText,
30 |
31 | // UITextField, UITextArea
32 | inputBackground,
33 | inputDisabledBackground,
34 | inputHighlightedBackground,
35 | inputTextColor,
36 | inputLabelColor,
37 |
38 | // Helpers
39 | errorTextColor,
40 | hintTextColor
41 |
42 | }
43 |
44 | extension Themeable {
45 |
46 | /// Generates a UIColor for the project's current theme.
47 | ///
48 | /// - Parameter requirement: The contextual color requirement
49 | /// - Returns: The `UIColor` required for the given context and theme
50 | func color(_ requirement: ThemeColorType) -> UIColor {
51 |
52 | let theme = getTheme() ?? .normal
53 |
54 | switch theme {
55 |
56 | case .normal:
57 |
58 | switch requirement {
59 |
60 | case .primaryTint: return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1)
61 |
62 | case .secondaryTint: return self.color(.primaryTint)
63 |
64 | case .primaryBackground: return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
65 |
66 | case .inputBackground: return #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1)
67 |
68 | case .inputDisabledBackground: return #colorLiteral(red: 0.7058823529, green: 0.7058823529, blue: 0.7058823529, alpha: 1)
69 |
70 | case .inputHighlightedBackground: return #colorLiteral(red: 0.9019607843, green: 0.9019607843, blue: 0.9019607843, alpha: 1)
71 |
72 | case .buttonBackground: return self.color(.primaryTint)
73 |
74 | case .buttonDisabledBackground: return #colorLiteral(red: 0.7058823529, green: 0.7058823529, blue: 0.7058823529, alpha: 1)
75 |
76 | case .buttonHighlightedBackground: return #colorLiteral(red: 0, green: 0.3920759949, blue: 0.8128569162, alpha: 1)
77 |
78 | case .buttonText: return .black
79 |
80 | case .buttonDisabledText: return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.7514447774)
81 |
82 | case .inputTextColor: return .black
83 |
84 | case .inputLabelColor: return .black
85 |
86 | case .errorTextColor: return #colorLiteral(red: 0.8, green: 0, blue: 0, alpha: 1)
87 |
88 | case .hintTextColor: return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1)
89 |
90 | }
91 |
92 | case .grey:
93 |
94 | switch requirement {
95 |
96 | case .primaryTint: return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1)
97 |
98 | case .secondaryTint: return self.color(.primaryTint)
99 |
100 | case .primaryBackground: return #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
101 |
102 | case .inputBackground: return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
103 |
104 | case .inputDisabledBackground: return #colorLiteral(red: 0.7058823529, green: 0.7058823529, blue: 0.7058823529, alpha: 1)
105 |
106 | case .inputHighlightedBackground: return #colorLiteral(red: 0.9647058824, green: 0.9647058824, blue: 0.9647058824, alpha: 1)
107 |
108 | case .buttonBackground: return self.color(.primaryTint)
109 |
110 | case .buttonDisabledBackground: return #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
111 |
112 | case .buttonHighlightedBackground: return #colorLiteral(red: 0, green: 0.3970302654, blue: 0.8231281726, alpha: 1)
113 |
114 | case .buttonText: return .black
115 |
116 | case .buttonDisabledText: return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.7514447774)
117 |
118 | case .inputTextColor: return .black
119 |
120 | case .inputLabelColor: return .black
121 |
122 | case .errorTextColor: return #colorLiteral(red: 0.8, green: 0, blue: 0, alpha: 1)
123 |
124 | case .hintTextColor: return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1)
125 |
126 | }
127 |
128 | case .light:
129 |
130 | switch requirement {
131 |
132 | case .primaryTint: return #colorLiteral(red: 0.5647058824, green: 0.07450980392, blue: 0.9960784314, alpha: 1)
133 |
134 | case .secondaryTint: return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
135 |
136 | case .primaryBackground: return UIColor.clear
137 |
138 | case .inputBackground: return #colorLiteral(red: 0.8847963012, green: 0.8847963012, blue: 0.8847963012, alpha: 1)
139 |
140 | case .inputDisabledBackground: return #colorLiteral(red: 0.7058823529, green: 0.7058823529, blue: 0.7058823529, alpha: 1)
141 |
142 | case .inputHighlightedBackground: return #colorLiteral(red: 0.9999960065, green: 1, blue: 1, alpha: 1)
143 |
144 | case .buttonBackground: return self.color(.primaryTint)
145 |
146 | case .buttonDisabledBackground: return #colorLiteral(red: 0.246659416, green: 0.03052198005, blue: 0.4460903286, alpha: 1)
147 |
148 | case .buttonHighlightedBackground: return #colorLiteral(red: 0.598350899, green: 0.1961166696, blue: 1, alpha: 1)
149 |
150 | case .buttonText: return .white
151 |
152 | case .buttonDisabledText: return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5)
153 |
154 | case .inputTextColor: return .black
155 |
156 | case .inputLabelColor: return .white
157 |
158 | case .errorTextColor: return .white
159 |
160 | case .hintTextColor: return .white
161 |
162 | }
163 |
164 | }
165 |
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/AstroForms/Protocols/Row.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Row.swift
3 | // AstroForms
4 | //
5 | // Created by Andrew Plummer on 4/9/18.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | /// A basic row, the only requirement is that it has a view.
12 | ///
13 | /// Use this type alone if all you want is to display some custom view.
14 | public protocol Row: AnyRow {
15 |
16 | associatedtype View: UIView
17 |
18 | /// The view for the row, which every row has.
19 | var view: View { get set }
20 |
21 | }
22 |
23 | /// A type erased version of `Row`
24 | public protocol AnyRow: class {
25 |
26 | var baseView: UIView { get }
27 |
28 | var form: Form? { get }
29 |
30 | func hide(animated: Bool)
31 |
32 | func show(animated: Bool)
33 |
34 | func showHelper(
35 | viewType: T.Type,
36 | animated: Bool,
37 | config: ((T) -> Void)?
38 | )
39 |
40 | func hideHelper(
41 | animated: Bool,
42 | callback: (() -> Void)?
43 | )
44 |
45 | var tag: RowTag { get }
46 |
47 | }
48 |
49 | public extension Row {
50 |
51 | var baseView: UIView {
52 |
53 | get { return view as UIView }
54 |
55 | }
56 |
57 | func hide(animated: Bool = false) {
58 |
59 | guard animated else {
60 | self.baseView.superview?.alpha = 0
61 | self.baseView.superview?.isHidden = true
62 | return
63 | }
64 |
65 | UIView.animate(withDuration: 0.1, animations: {
66 | self.baseView.superview?.alpha = 0
67 | })
68 |
69 | UIView.animate(withDuration: 0.3, animations: {
70 | self.baseView.superview?.isHidden = true
71 | self.baseView.superview?.superview?.layoutIfNeeded()
72 | })
73 |
74 | }
75 |
76 | func show(animated: Bool = false) {
77 |
78 | guard animated else {
79 | self.baseView.superview?.isHidden = false
80 | self.baseView.superview?.alpha = 1
81 | return
82 | }
83 |
84 | UIView.animate(withDuration: 0.1, delay: 0.2, options: [], animations: {
85 | self.baseView.superview?.alpha = 1
86 | })
87 |
88 | UIView.animate(withDuration: 0.3, animations: {
89 | self.baseView.superview?.isHidden = false
90 | self.baseView.superview?.alpha = 1
91 |
92 | // TODO: iOS 11 specific code. Figure out if it is still required.
93 | // Breaks in iOS 12.
94 | //self.superview?.layoutIfNeeded()
95 |
96 | })
97 |
98 | }
99 |
100 | var form: Form? {
101 |
102 | var parentForm: Form?
103 |
104 | var superview: UIView? = self.baseView.superview
105 |
106 | while superview != nil {
107 |
108 | if let _form = superview as? Form {
109 | parentForm = _form
110 | break
111 | }
112 |
113 | superview = superview?.superview
114 |
115 | }
116 |
117 | return parentForm
118 |
119 | }
120 |
121 | // Need to distinguish between showing and adding items
122 | // Limitation at present, a row can only show one helper (although this
123 | // might be a stackview itself.
124 |
125 |
126 |
127 | /// Show a helper view with an optional animation.
128 | ///
129 | /// - Parameters:
130 | /// - viewType: The helper view to show
131 | /// - animated: Whether or not to use the default fade in animation
132 | /// - config: A block for configuring the new helper
133 | /// - Throws: An error if the helper cannot be shown
134 | ///
135 | /// This method takes a UIView subclass `type`, rather than an instance.
136 | /// This makes usage easier, particularly for cases where the show
137 | /// method is called multiple times.
138 | func showHelper(
139 | viewType: T.Type,
140 | animated: Bool,
141 | config: ((T) -> Void)?
142 | ) {
143 |
144 | // If there is no helper stackview, add it
145 | guard let topStackView = self.baseView.superview as? UIStackView else {
146 | return
147 | }
148 |
149 | let view: T =
150 | (topStackView.arrangedSubviews.last as? T)
151 | ?? viewType.fromXib()
152 |
153 | view.isHidden = true
154 | view.alpha = 0
155 |
156 | func animateAndConfigureNewView(animated: Bool) {
157 |
158 | // Add the view if it doesn't exist
159 | let viewExists = topStackView.arrangedSubviews.count > 1
160 |
161 | config?(view)
162 |
163 | if !viewExists {
164 | topStackView.addArrangedSubview(view)
165 | } else {
166 | view.isHidden = false
167 | view.alpha = 1
168 | return
169 | }
170 |
171 | (view as? ThemeableView)?.updateTheme()
172 |
173 | guard animated else {
174 | view.isHidden = false
175 | view.alpha = 1
176 | return
177 | }
178 |
179 | UIView.animate(withDuration: 0.1, delay: 0.2, options: [], animations: {
180 | view.alpha = 1
181 | })
182 |
183 | UIView.animate(withDuration: 0.3, animations: {
184 | view.isHidden = false
185 | view.alpha = 1
186 |
187 | // TODO: iOS 11 specific code. Figure out if it is still required.
188 | // Breaks in iOS 12.
189 | //view.superview?.layoutIfNeeded()
190 |
191 | })
192 |
193 | }
194 |
195 | let lastViewOfDifferentType: UIView? =
196 | topStackView.arrangedSubviews.count > 1
197 | && topStackView.arrangedSubviews.last as? T == nil
198 | ? topStackView.arrangedSubviews.last
199 | : nil
200 |
201 | if lastViewOfDifferentType != nil {
202 |
203 | hideHelper(animated: animated) {
204 | animateAndConfigureNewView(animated: animated)
205 | }
206 |
207 | } else {
208 | animateAndConfigureNewView(animated: animated)
209 | }
210 |
211 | }
212 |
213 | func hideHelper(animated: Bool = false, callback: (() -> Void)? = nil) {
214 |
215 | guard let topStackView = self.baseView.superview as? UIStackView else {
216 | return
217 | }
218 |
219 | guard
220 | topStackView.arrangedSubviews.count > 1,
221 | let helperView = topStackView.arrangedSubviews.last else {
222 | return
223 | }
224 |
225 | guard animated else {
226 | helperView.alpha = 0
227 | helperView.isHidden = true
228 | topStackView.removeArrangedSubview(helperView)
229 | helperView.removeFromSuperview()
230 | callback?()
231 | return
232 | }
233 |
234 | UIView.animate(withDuration: 0.1, animations: {
235 | helperView.alpha = 0
236 |
237 | }) { _ in
238 |
239 | UIView.animate(withDuration: 0.2, animations: {
240 |
241 | helperView.isHidden = true
242 |
243 | }, completion: { _ in
244 |
245 | DispatchQueue.main.async {
246 | topStackView.removeArrangedSubview(helperView)
247 | helperView.removeFromSuperview()
248 | callback?()
249 | }
250 |
251 | })
252 |
253 | }
254 |
255 | }
256 |
257 | }
258 |
--------------------------------------------------------------------------------
/Example/AstroForms/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------