├── .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 | [![Bitrise App Page](https://app.bitrise.io/app/11b5791a9dab5b3c/status.svg?token=jFpCmx9nwy200940OcGbqA&branch=master)](https://app.bitrise.io/app/11b5791a9dab5b3c) [![Astro Forms Documentation](https://github.com/plummer/astro-docs/blob/master/assets/docs-button.svg)](https://www.astroforms.com) 4 | 5 | [![Astro Forms is an approachable iOS forms framework for building beautiful, reusable and easy to maintain forms. Type Safe Row Access, Validation, Manage Multiple Themes, Custom Focus Rects, Keyboard Management](https://user-images.githubusercontent.com/580919/46787465-01d1d380-cd7b-11e8-8f23-1c1050a25270.jpg)](https://www.astroforms.com) 6 | 7 | | ![qyky5d 300x534](https://user-images.githubusercontent.com/580919/46787060-0ea1f780-cd7a-11e8-8196-cce42033e893.gif) | ![w7y7v4 300x534](https://user-images.githubusercontent.com/580919/46787062-0ea1f780-cd7a-11e8-9d96-28d22f436651.gif) | ![wmqmxm 300x534](https://user-images.githubusercontent.com/580919/46787064-0ea1f780-cd7a-11e8-9087-928870b451e2.gif) | 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 | --------------------------------------------------------------------------------