├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── Builder.xcscheme
├── BuilderDemo
├── BuilderDemo.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── BuilderDemo-Mock.xcscheme
│ │ └── BuilderDemo.xcscheme
├── BuilderDemo
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-40.png
│ │ │ ├── Icon-40@2x.png
│ │ │ ├── Icon-40@3x.png
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-72.png
│ │ │ ├── Icon-72@2x.png
│ │ │ ├── Icon-76.png
│ │ │ ├── Icon-76@2x.png
│ │ │ ├── Icon-83.5@2x.png
│ │ │ ├── Icon-Small-50.png
│ │ │ ├── Icon-Small-50@2x.png
│ │ │ ├── Icon-Small.png
│ │ │ ├── Icon-Small@2x.png
│ │ │ ├── Icon-Small@3x.png
│ │ │ ├── Icon.png
│ │ │ ├── Icon@2x.png
│ │ │ ├── NotificationIcon@2x.png
│ │ │ ├── NotificationIcon@3x.png
│ │ │ ├── NotificationIcon~ipad.png
│ │ │ ├── NotificationIcon~ipad@2x.png
│ │ │ └── ios-marketing.png
│ │ ├── Contents.json
│ │ ├── Logo-DK.imageset
│ │ │ ├── Contents.json
│ │ │ └── RxSwiftWidgets-Logo-DK.png
│ │ ├── Logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── RxSwiftWidgets-DK.png
│ │ ├── User-JQ.imageset
│ │ │ ├── Contents.json
│ │ │ └── User-JQ.jpg
│ │ ├── User-ML.imageset
│ │ │ ├── Contents.json
│ │ │ └── HML Animoji-G.png
│ │ ├── User-TS.imageset
│ │ │ ├── Contents.json
│ │ │ └── User-TS.png
│ │ ├── User-Unknown.imageset
│ │ │ ├── Contents.json
│ │ │ └── unknown.png
│ │ ├── cardImagePlaceholder.imageset
│ │ │ ├── Contents.json
│ │ │ ├── cardImagePlaceholder.png
│ │ │ ├── cardImagePlaceholder@2x.png
│ │ │ └── cardImagePlaceholder@3x.png
│ │ ├── checkboxChecked.imageset
│ │ │ ├── Contents.json
│ │ │ ├── checkboxChecked.png
│ │ │ ├── checkboxChecked@2x.png
│ │ │ └── checkboxChecked@3x.png
│ │ ├── checkboxCheckedBlue.imageset
│ │ │ ├── Contents.json
│ │ │ ├── checkboxCheckedBlue.png
│ │ │ ├── checkboxCheckedBlue@2x.png
│ │ │ └── checkboxCheckedBlue@3x.png
│ │ ├── checkboxEmpty.imageset
│ │ │ ├── Contents.json
│ │ │ ├── checkboxEmpty.png
│ │ │ ├── checkboxEmpty@2x.png
│ │ │ └── checkboxEmpty@3x.png
│ │ ├── checkboxEmptyBlue.imageset
│ │ │ ├── Contents.json
│ │ │ ├── checkboxEmptyBlue.png
│ │ │ ├── checkboxEmptyBlue@2x.png
│ │ │ └── checkboxEmptyBlue@3x.png
│ │ ├── close.imageset
│ │ │ ├── Contents.json
│ │ │ ├── close.png
│ │ │ ├── close@2x.png
│ │ │ └── close@3x.png
│ │ ├── heartEmpty.imageset
│ │ │ ├── Contents.json
│ │ │ ├── heartEmpty.png
│ │ │ ├── heartEmpty@2x.png
│ │ │ └── heartEmpty@3x.png
│ │ ├── search-template.imageset
│ │ │ ├── Contents.json
│ │ │ ├── search.png
│ │ │ ├── search@2x.png
│ │ │ └── search@3x.png
│ │ ├── universalGlyphsEmail.imageset
│ │ │ ├── Contents.json
│ │ │ ├── universalGlyphsEmail.png
│ │ │ ├── universalGlyphsEmail@2x.png
│ │ │ └── universalGlyphsEmail@3x.png
│ │ ├── universalGlyphsHome.imageset
│ │ │ ├── Contents.json
│ │ │ ├── universalGlyphsHome.png
│ │ │ ├── universalGlyphsHome@2x.png
│ │ │ └── universalGlyphsHome@3x.png
│ │ ├── universalGlyphsPhone.imageset
│ │ │ ├── Contents.json
│ │ │ ├── universalGlyphsPhone.png
│ │ │ ├── universalGlyphsPhone@2x.png
│ │ │ └── universalGlyphsPhone@3x.png
│ │ ├── vector.imageset
│ │ │ ├── Contents.json
│ │ │ ├── vector1.jpg
│ │ │ └── vector2.jpg
│ │ ├── vector1.imageset
│ │ │ ├── Contents.json
│ │ │ └── vector1.jpg
│ │ └── vector2.imageset
│ │ │ ├── Contents.json
│ │ │ └── vector2.jpg
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Configuration
│ │ ├── API.xcconfig
│ │ └── MOCK.xcconfig
│ ├── ContactForm
│ │ ├── ContactFormViewController.swift
│ │ ├── ContactFormViewModel.swift
│ │ └── FABMenuView.swift
│ ├── Details
│ │ ├── DetailCardView.swift
│ │ ├── DetailViewController.swift
│ │ ├── DetailViewModel.swift
│ │ └── _Details+Injection.swift
│ ├── Forms
│ │ ├── FormField.swift
│ │ └── FormFieldManager.swift
│ ├── Info.plist
│ ├── Login
│ │ ├── LoadingCardView.swift
│ │ ├── LoginCardView.swift
│ │ ├── LoginErrorView.swift
│ │ ├── LoginStatusView.swift
│ │ ├── LoginViewController.swift
│ │ ├── LoginViewModel.swift
│ │ └── NotYetImplementedCardView.swift
│ ├── Main
│ │ ├── MainStackViewController.swift
│ │ ├── MainUsersStackView.swift
│ │ ├── MainUsersTableView.swift
│ │ ├── MainViewController.swift
│ │ └── MainViewModel.swift
│ ├── Menu
│ │ ├── Base.lproj
│ │ │ └── Main.storyboard
│ │ ├── MenuOption.swift
│ │ ├── MenuTableViewController.swift
│ │ └── MenuViewController.swift
│ ├── Models
│ │ └── User.swift
│ ├── SceneDelegate.swift
│ ├── Services
│ │ ├── Dismissible.swift
│ │ ├── TestServices.swift
│ │ ├── UserImageCache.swift
│ │ ├── UserService.swift
│ │ └── _Services+Injection.swift
│ ├── Shared
│ │ ├── Extensions
│ │ │ ├── Functions+Extensions.swift
│ │ │ ├── Rx+Extensions.swift
│ │ │ ├── String+Extensions.swift
│ │ │ ├── UIColor+Extensions.swift
│ │ │ ├── UIImage+Extensions.swift
│ │ │ ├── UITextField+Styles.swift
│ │ │ ├── UIView+Extensions.swift
│ │ │ └── UIViewController+Extensions.swift
│ │ ├── Fields
│ │ │ ├── BuilderInternalTextField.swift
│ │ │ ├── CurrencyTextField.swift
│ │ │ ├── MaskedTextField.swift
│ │ │ ├── MaxWidthTextField.swift
│ │ │ ├── NextAccessoryView.swift
│ │ │ └── TextFieldBehaviorAggregator.swift
│ │ ├── Networking
│ │ │ ├── APIError.swift
│ │ │ ├── Core
│ │ │ │ ├── BaseSessionManager.swift
│ │ │ │ ├── ClientRequestBuilder.swift
│ │ │ │ ├── ClientSessionManager.swift
│ │ │ │ ├── ClientSessionManagerInterceptor.swift
│ │ │ │ └── MockURLProtocol.swift
│ │ │ ├── Extensions
│ │ │ │ ├── ClientRequestBuilder+Combine.swift
│ │ │ │ ├── ClientRequestBuilder+Result.swift
│ │ │ │ └── ClientRequestBuilder+RxSwift.swift
│ │ │ ├── Inteceptors
│ │ │ │ ├── MockDelayInterceptor.swift
│ │ │ │ ├── MockSessionManagerWrapper.swift
│ │ │ │ ├── SSOAuthenticationInterceptor.swift
│ │ │ │ ├── SessionLoggingInterceptorr.swift
│ │ │ │ ├── StandardHeadersInterceptor.swift
│ │ │ │ └── StatusErrorMappingInterceptor.swift
│ │ │ └── _Networking+Injection.swift
│ │ └── Styles
│ │ │ ├── ButtonView+Styles.swift
│ │ │ ├── LabelView+Styles.swift
│ │ │ └── TextField+Styles.swift
│ ├── Test
│ │ ├── CornerCardViewController.swift
│ │ ├── GeneralTestView.swift
│ │ ├── ScrollingTabBarTest.swift
│ │ ├── TabBarTest.swift
│ │ ├── TestViewController.swift
│ │ └── TestViews.swift
│ ├── ViewController.swift
│ ├── Views
│ │ ├── DLSCardView.swift
│ │ ├── MetaTextField.swift
│ │ ├── StandardEmptyPage.swift
│ │ ├── StandardErrorPage.swift
│ │ └── StandardLoadingPage.swift
│ ├── _Main+Injection.swift
│ └── json
│ │ ├── get.json
│ │ └── get_users.json
├── BuilderDemoTests
│ ├── BuilderDemoTests.swift
│ ├── Factory+XCTest.swift
│ ├── MainViewModelSpec.swift
│ ├── README.md
│ ├── TestExrtensionsSpec.swift
│ ├── UserImageCacheSpec.swift
│ └── XCTTest+Extensions.swift
└── BuilderDemoUITests
│ ├── BuilderDemoUITests.swift
│ └── BuilderDemoUITestsLaunchTests.swift
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── SampleDetail.png
├── Sources
└── Builder
│ ├── Builder
│ ├── Builder+Attributes.swift
│ ├── Builder+Button.swift
│ ├── Builder+Constraints.swift
│ ├── Builder+Container.swift
│ ├── Builder+Context.swift
│ ├── Builder+Controls.swift
│ ├── Builder+Divider.swift
│ ├── Builder+Dynamic.swift
│ ├── Builder+Extensions.swift
│ ├── Builder+ForEach.swift
│ ├── Builder+Gestures.swift
│ ├── Builder+Group.swift
│ ├── Builder+Image.swift
│ ├── Builder+Label.swift
│ ├── Builder+Navigation.swift
│ ├── Builder+Padding.swift
│ ├── Builder+RxSwift.swift
│ ├── Builder+ScrollView.swift
│ ├── Builder+Spacer.swift
│ ├── Builder+Stack.swift
│ ├── Builder+Styles.swift
│ ├── Builder+Switch.swift
│ ├── Builder+TableView.swift
│ ├── Builder+TextField.swift
│ ├── Builder+Variable.swift
│ ├── Builder+View.swift
│ ├── Builder+ViewController.swift
│ ├── Builder+With.swift
│ ├── Builder+ZStack.swift
│ └── Builder.swift
│ └── Extensions
│ ├── UIColor+Extensions.swift
│ └── UIImage+Extensions.swift
└── Tests
└── BuilderTests
└── BuilderTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | .build/
41 |
42 | # CocoaPods
43 | #
44 | # We recommend against adding the Pods directory to your .gitignore. However
45 | # you should judge for yourself, the pros and cons are mentioned at:
46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
47 | #
48 | # Pods/
49 |
50 | # Carthage
51 | #
52 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
53 | # Carthage/Checkouts
54 |
55 | Carthage/Build
56 |
57 | # fastlane
58 | #
59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
60 | # screenshots whenever they are needed.
61 | # For more information about the recommended setup visit:
62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
63 |
64 | fastlane/report.xml
65 | fastlane/Preview.html
66 | fastlane/screenshots
67 | fastlane/test_output
68 | IDEWorkspaceChecks.plist
69 | .DS_Store
70 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Builder.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "factory",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/hmlongco/Factory.git",
7 | "state" : {
8 | "branch" : "main",
9 | "revision" : "59f9a10aa635a9e34b61e8a1ac43b5798c090093"
10 | }
11 | },
12 | {
13 | "identity" : "rxswift",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/ReactiveX/RxSwift.git",
16 | "state" : {
17 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
18 | "version" : "6.5.0"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // BuilderDemo
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | ViewBuilderEnvironment.defaultLabelFont = UIFont.preferredFont(forTextStyle: .body)
17 |
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "NotificationIcon@2x.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "NotificationIcon@3x.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "Icon-Small.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "Icon-Small@2x.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "Icon-Small@3x.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "Icon-40@2x.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "Icon-40@3x.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "Icon.png",
47 | "idiom" : "iphone",
48 | "scale" : "1x",
49 | "size" : "57x57"
50 | },
51 | {
52 | "filename" : "Icon@2x.png",
53 | "idiom" : "iphone",
54 | "scale" : "2x",
55 | "size" : "57x57"
56 | },
57 | {
58 | "filename" : "Icon-60@2x.png",
59 | "idiom" : "iphone",
60 | "scale" : "2x",
61 | "size" : "60x60"
62 | },
63 | {
64 | "filename" : "Icon-60@3x.png",
65 | "idiom" : "iphone",
66 | "scale" : "3x",
67 | "size" : "60x60"
68 | },
69 | {
70 | "filename" : "NotificationIcon~ipad.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "20x20"
74 | },
75 | {
76 | "filename" : "NotificationIcon~ipad@2x.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "20x20"
80 | },
81 | {
82 | "filename" : "Icon-Small.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "29x29"
86 | },
87 | {
88 | "filename" : "Icon-Small@2x.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "29x29"
92 | },
93 | {
94 | "filename" : "Icon-40.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "40x40"
98 | },
99 | {
100 | "filename" : "Icon-40@2x.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "40x40"
104 | },
105 | {
106 | "filename" : "Icon-Small-50.png",
107 | "idiom" : "ipad",
108 | "scale" : "1x",
109 | "size" : "50x50"
110 | },
111 | {
112 | "filename" : "Icon-Small-50@2x.png",
113 | "idiom" : "ipad",
114 | "scale" : "2x",
115 | "size" : "50x50"
116 | },
117 | {
118 | "filename" : "Icon-72.png",
119 | "idiom" : "ipad",
120 | "scale" : "1x",
121 | "size" : "72x72"
122 | },
123 | {
124 | "filename" : "Icon-72@2x.png",
125 | "idiom" : "ipad",
126 | "scale" : "2x",
127 | "size" : "72x72"
128 | },
129 | {
130 | "filename" : "Icon-76.png",
131 | "idiom" : "ipad",
132 | "scale" : "1x",
133 | "size" : "76x76"
134 | },
135 | {
136 | "filename" : "Icon-76@2x.png",
137 | "idiom" : "ipad",
138 | "scale" : "2x",
139 | "size" : "76x76"
140 | },
141 | {
142 | "filename" : "Icon-83.5@2x.png",
143 | "idiom" : "ipad",
144 | "scale" : "2x",
145 | "size" : "83.5x83.5"
146 | },
147 | {
148 | "filename" : "ios-marketing.png",
149 | "idiom" : "ios-marketing",
150 | "scale" : "1x",
151 | "size" : "1024x1024"
152 | }
153 | ],
154 | "info" : {
155 | "author" : "xcode",
156 | "version" : 1
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-72.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/ios-marketing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/Logo-DK.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "RxSwiftWidgets-Logo-DK.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/Logo-DK.imageset/RxSwiftWidgets-Logo-DK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/Logo-DK.imageset/RxSwiftWidgets-Logo-DK.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "RxSwiftWidgets-DK.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/Logo.imageset/RxSwiftWidgets-DK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/Logo.imageset/RxSwiftWidgets-DK.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-JQ.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "User-JQ.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-JQ.imageset/User-JQ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/User-JQ.imageset/User-JQ.jpg
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-ML.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "HML Animoji-G.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-ML.imageset/HML Animoji-G.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/User-ML.imageset/HML Animoji-G.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-TS.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "User-TS.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-TS.imageset/User-TS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/User-TS.imageset/User-TS.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-Unknown.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "unknown.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/User-Unknown.imageset/unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/User-Unknown.imageset/unknown.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "cardImagePlaceholder.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "cardImagePlaceholder@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "cardImagePlaceholder@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/cardImagePlaceholder.imageset/cardImagePlaceholder@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "checkboxChecked.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "checkboxChecked@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "checkboxChecked@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxChecked.imageset/checkboxChecked@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "checkboxCheckedBlue.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "checkboxCheckedBlue@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "checkboxCheckedBlue@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxCheckedBlue.imageset/checkboxCheckedBlue@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "checkboxEmpty.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "checkboxEmpty@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "checkboxEmpty@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmpty.imageset/checkboxEmpty@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "checkboxEmptyBlue.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "checkboxEmptyBlue@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "checkboxEmptyBlue@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/checkboxEmptyBlue.imageset/checkboxEmptyBlue@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "close.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "close@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "close@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/close.imageset/close@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "heartEmpty.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "heartEmpty@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "heartEmpty@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/heartEmpty.imageset/heartEmpty@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "search.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "search@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "search@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/search-template.imageset/search@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "universalGlyphsEmail.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "universalGlyphsEmail@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "universalGlyphsEmail@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsEmail.imageset/universalGlyphsEmail@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "universalGlyphsHome.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "universalGlyphsHome@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "universalGlyphsHome@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsHome.imageset/universalGlyphsHome@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "universalGlyphsPhone.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "universalGlyphsPhone@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "universalGlyphsPhone@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone@2x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/universalGlyphsPhone.imageset/universalGlyphsPhone@3x.png
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "vector2.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "filename" : "vector1.jpg",
16 | "idiom" : "universal",
17 | "scale" : "1x"
18 | },
19 | {
20 | "idiom" : "universal",
21 | "scale" : "2x"
22 | },
23 | {
24 | "appearances" : [
25 | {
26 | "appearance" : "luminosity",
27 | "value" : "dark"
28 | }
29 | ],
30 | "idiom" : "universal",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "universal",
35 | "scale" : "3x"
36 | },
37 | {
38 | "appearances" : [
39 | {
40 | "appearance" : "luminosity",
41 | "value" : "dark"
42 | }
43 | ],
44 | "idiom" : "universal",
45 | "scale" : "3x"
46 | }
47 | ],
48 | "info" : {
49 | "author" : "xcode",
50 | "version" : 1
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector.imageset/vector1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/vector.imageset/vector1.jpg
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector.imageset/vector2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/vector.imageset/vector2.jpg
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "vector1.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector1.imageset/vector1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/vector1.imageset/vector1.jpg
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "vector2.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Assets.xcassets/vector2.imageset/vector2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/BuilderDemo/BuilderDemo/Assets.xcassets/vector2.imageset/vector2.jpg
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Base.lproj/LaunchScreen.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 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Configuration/API.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // API.xcconfig
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/18/21.
6 | //
7 |
8 | // Configuration settings file format documentation can be found at:
9 | // https://help.apple.com/xcode/#/dev745c5c974
10 |
11 | API_BASE_PROTOCOL = https
12 | API_BASE_URL = "//randomuser.me/api"
13 | BUNDLE_SUFFIX =
14 | PRODUCT_SUFFIX =
15 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Configuration/MOCK.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // MOCK.xcconfig
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/18/21.
6 | //
7 |
8 | // Configuration settings file format documentation can be found at:
9 | // https://help.apple.com/xcode/#/dev745c5c974
10 |
11 | API_BASE_PROTOCOL = mock
12 | API_BASE_URL = "//mock"
13 | BUNDLE_SUFFIX = .mock
14 | PRODUCT_SUFFIX = -MOCK
15 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/ContactForm/ContactFormViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactFormViewModel.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/10/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import RxSwift
11 |
12 | enum ContactFormIDS: String, CaseIterable {
13 | case first
14 | case last
15 | case address1
16 | case address2
17 | case city
18 | case state
19 | case zip
20 | case email
21 | case alternateEmail
22 | case phone
23 | case agree
24 | }
25 |
26 | class ContactFormViewModel: FormFieldManager {
27 |
28 | @Variable var error: String? = nil
29 |
30 | var hideEmailNotRequired: Observable {
31 | Observable.zip(state(.enabled, for: .email), state(.enabled, for: .alternateEmail))
32 | .map { $0 || $1 }
33 | }
34 |
35 | var termsTextColor: Observable {
36 | error(for: .agree)
37 | .map { $0 == nil ? .label : .red }
38 | }
39 |
40 | func configure() {
41 | fields = [
42 | FormField(id: ContactFormIDS.first, value: "Michael")
43 | .isRequired(),
44 |
45 | FormField(id: ContactFormIDS.last, value: "Long")
46 | .isRequired(),
47 |
48 | FormField(id: ContactFormIDS.address1, value: "1234 East West St")
49 | .isRequired(),
50 |
51 | FormField(id: ContactFormIDS.address2, value: ""),
52 |
53 | FormField(id: ContactFormIDS.city, value: "Boulder")
54 | .isRequired(),
55 |
56 | FormField(id: ContactFormIDS.state, value: "CO")
57 | .isRequired()
58 | .length(2, "Two letter abbreviation."),
59 |
60 | FormField(id: ContactFormIDS.zip, value: "80303")
61 | .isRequired(),
62 |
63 | FormField(id: ContactFormIDS.email, value: "hmlongexample.com")
64 | .isRequired()
65 | .isEmail(),
66 |
67 | FormField(id: ContactFormIDS.alternateEmail, value: "")
68 | .isEmail(),
69 |
70 | FormField(id: ContactFormIDS.phone, value: "303-895-0000")
71 | .validate {
72 | $0.stripReturningDigitsOnly().count == 10 ? nil : "Phone number must be 10 digits long"
73 | },
74 |
75 | FormField(id: ContactFormIDS.agree, value: true)
76 | .isRequired()
77 | ]
78 |
79 | // print("INITIAL DISABLE")
80 | // states[ContactFormIDS.email.rawValue] = .disabled
81 | // states[ContactFormIDS.alternateEmail.rawValue] = .disabled
82 | }
83 |
84 | override func placeholder(for id: ContactFormIDS) -> String {
85 | switch id {
86 | case .first:
87 | return "First Name"
88 | case .last:
89 | return "Last Name"
90 | case .address1:
91 | return "Address Line 1"
92 | case .address2:
93 | return "Address Line 2 (optional)"
94 | case .alternateEmail:
95 | return "Alternate Email (optional)"
96 | default:
97 | return id.rawValue.capitalized
98 | }
99 | }
100 |
101 | override func validate() {
102 | super.validate() // default behavior
103 | error = errors.isEmpty ? nil : "Please correct the following errors..."
104 | }
105 |
106 | }
107 |
108 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/ContactForm/FABMenuView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FABMenuView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/15/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import RxSwift
11 |
12 | struct FABMenuView: ViewBuilder {
13 |
14 | let menuItems: [String]
15 | var content: () -> ViewConvertable
16 |
17 | @Variable var showFABMenu = false
18 |
19 | init(menuItems: [String], @ViewResultBuilder _ builder: @escaping () -> ViewConvertable) {
20 | self.menuItems = menuItems
21 | self.content = builder
22 | }
23 |
24 | var body: View {
25 | ZStackView {
26 | content()
27 |
28 | ContainerView {
29 | ImageView(systemName: "plus")
30 | .tintColor(.white)
31 | .contentMode(.center)
32 | .frame(height: 50, width: 50)
33 | .cornerRadius(25)
34 | .backgroundColor(.red)
35 | .position(.topCenter)
36 | .onTapGesture { context in
37 | showFABMenu.toggle()
38 | }
39 | }
40 | .position(.bottomCenter)
41 | .frame(height: 80, width: 50)
42 |
43 | // disabled area
44 | ContainerView {
45 | VStackView {
46 | SpacerView()
47 |
48 | // close area
49 | ZStackView {
50 | ContainerView()
51 | .backgroundColor(.black)
52 | .position(.bottom)
53 | .height(25)
54 |
55 | ImageView(systemName: "xmark")
56 | .tintColor(.white)
57 | .contentMode(.center)
58 | .frame(height: 50, width: 50)
59 | .cornerRadius(25)
60 | .backgroundColor(.red)
61 | .position(.bottomCenter)
62 | .onTapGesture { context in
63 | showFABMenu.toggle()
64 | }
65 | }
66 |
67 | // menu area
68 | ContainerView {
69 | VStackView {
70 | ForEach(menuItems) { item in
71 | LabelView(item)
72 | .color(.white)
73 | .height(44)
74 | .onTapGesture { context in
75 | showFABMenu.toggle()
76 | print("TAPPED \(item)")
77 | }
78 | }
79 | }
80 | .alignment(.center)
81 | .padding(20)
82 | .spacing(0)
83 | }
84 | .backgroundColor(.black)
85 | .position(.bottom)
86 | }
87 | .spacing(0)
88 | }
89 | .backgroundColor(UIColor(white: 0.5, alpha: 0.3))
90 | .width(UIScreen.main.bounds.width)
91 | .hidden(bind: $showFABMenu.asObservable().map { !$0 })
92 | .onTapGesture { context in
93 | showFABMenu.toggle()
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Details/DetailCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailCardView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/18/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | struct DetailCardView: ViewBuilder {
14 |
15 | @Injected(Container.detailViewModel) var viewModel: DetailViewModel
16 |
17 | init(user: User) {
18 | viewModel.configure(user)
19 | }
20 |
21 | var body: View {
22 | DLSCardView {
23 | VStackView {
24 | DetailPhotoView(photo: viewModel.photo(), name: viewModel.fullname)
25 |
26 | VStackView {
27 | NameValueView(name: "Address", value: viewModel.street)
28 | NameValueView(name: "", value: viewModel.cityStateZip)
29 | SpacerView(8)
30 | NameValueView(name: "Email", value: viewModel.email)
31 | NameValueView(name: "Phone1", value: viewModel.phone)
32 | SpacerView(8)
33 | NameValueView(name: "Age", value: viewModel.age)
34 | }
35 | .spacing(0)
36 | .padding(20)
37 | }
38 | .onAppear { _ in
39 | print("DLS Card Appeared")
40 | }
41 | .onDisappear { _ in
42 | print("DLS Card Disappeared")
43 | }
44 | }
45 | }
46 |
47 | }
48 |
49 | struct DetailPhotoView: ViewBuilder {
50 |
51 | let photo: Observable
52 | let name: String
53 |
54 | var body: View {
55 | ZStackView {
56 | ImageView(photo)
57 | .contentMode(.scaleAspectFill)
58 | .clipsToBounds(true)
59 | .height(300)
60 | LabelView(name)
61 | .alignment(.right)
62 | .font(.title2)
63 | .color(.white)
64 | .padding(h: 20, v: 8)
65 | .backgroundColor(.black)
66 | .alpha(0.7)
67 | .position(.bottom)
68 | }
69 | }
70 | }
71 |
72 | struct NameValueView: ViewBuilder {
73 |
74 | let name: String?
75 | let value: String?
76 |
77 | var body: View {
78 | HStackView {
79 | LabelView(name)
80 | .color(.secondaryLabel)
81 | SpacerView()
82 | LabelView(value)
83 | .alignment(.right)
84 | }
85 | .spacing(4)
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Details/DetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | class DetailViewController: UIViewController {
15 |
16 | @Injected(Container.detailViewModel) var viewModel: DetailViewModel
17 |
18 | lazy var dismissible = Dismissible(self)
19 |
20 | @Variable var testSwtichValue = false
21 |
22 | convenience init(user: User) {
23 | self.init()
24 | viewModel.configure(user)
25 | }
26 |
27 | deinit {
28 | print("deinit DetailViewController")
29 | }
30 |
31 | override func viewDidLoad() {
32 | super.viewDidLoad()
33 | title = viewModel.fullname
34 | view.embed(contents())
35 | view.backgroundColor = .systemBackground
36 | }
37 |
38 | func contents() -> View {
39 | VerticalScrollView {
40 | VStackView {
41 | DetailCardView(user: viewModel.user)
42 |
43 | HStackView {
44 | LabelView("Accept Terms")
45 | SpacerView()
46 | SwitchView(viewModel.$accepted)
47 | .onTintColor(.blue)
48 | }
49 |
50 | ButtonView("Submit")
51 | .enabled(bind: viewModel.$accepted)
52 | .style(StyleButtonFilled())
53 | .onTap { [weak dismissible] _ in
54 | dismissible?.dismiss()
55 | }
56 |
57 | LabelView("Inforamtion presented above is not repesentative of any person, living, dead, undead, or fictional.")
58 | .style(StyleLabelFootnote())
59 |
60 | SpacerView()
61 | }
62 | .padding(20)
63 | .spacing(20)
64 | .onReceive(viewModel.$accepted) { context in
65 | context.view.accessibilityLabel = context.value ? "Terms accepted." : "Terms Not yet accepted."
66 | }
67 | }
68 | .backgroundColor(.quaternarySystemFill)
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Details/DetailViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailViewModel.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | class DetailViewModel {
14 |
15 | @Injected(Container.userImageCache) var cache: UserImageCache
16 |
17 | @Variable var accepted = false
18 |
19 | var user: User!
20 |
21 | var fullname: String { user.fullname }
22 |
23 | var street: String? {
24 | guard let loc = user.location, let number = loc.street?.number, let name = loc.street?.name else { return nil }
25 | return "\(number) \(name)"
26 | }
27 |
28 | var cityStateZip: String? {
29 | guard let loc = user.location, let city = loc.city, let state = loc.state, let zip = loc.postcode else { return nil }
30 | return "\(city) \(state) \(zip)"
31 | }
32 |
33 | var email: String { user.email ?? "n/a" }
34 | var phone: String { user.phone ?? "n/a"}
35 |
36 | var age: String {
37 | guard let age = user.dob?.age else { return "n/a" }
38 | return "\(age)"
39 | }
40 |
41 | var disposeBag = DisposeBag()
42 |
43 | init() {
44 | $accepted.asObservable()
45 | .debug()
46 | .subscribe { event in
47 | print(event)
48 | }
49 | .disposed(by: disposeBag)
50 | }
51 |
52 | deinit {
53 | print("deinit DetailViewModel")
54 | }
55 |
56 | func configure(_ user: User) {
57 | self.user = user
58 | }
59 |
60 | func photo() -> Observable {
61 | return cache.photo(forUser: user)
62 | .map { $0 ?? UIImage(named: "User-Unknown") }
63 | .observe(on: MainScheduler.instance)
64 | }
65 |
66 | #if MOCK
67 | func configuredViewModel() -> DetailViewModel {
68 | return with(DetailViewModel()) {
69 | $0.configure(User.mockJQ)
70 | }
71 |
72 | }
73 |
74 | func makeButton(_ title: String?) -> UIButton {
75 | with(UIButton()) {
76 | $0.translatesAutoresizingMaskIntoConstraints = false
77 | $0.titleLabel?.text = title
78 | $0.titleLabel?.font = .preferredFont(forTextStyle: .headline)
79 | $0.setTitleColor(.red, for: .normal)
80 | }
81 | }
82 |
83 | func makeButton2(_ title: String?) -> UIButton {
84 | let button = UIButton()
85 | button.translatesAutoresizingMaskIntoConstraints = false
86 | button.titleLabel?.text = title
87 | button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
88 | button.setTitleColor(.red, for: .normal)
89 | return button
90 | }
91 |
92 | #endif
93 | }
94 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Details/_Details+Injection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Details+Injection.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import Foundation
9 | import Factory
10 |
11 | extension Container {
12 | static let detailViewModel = Factory(scope: .shared) {
13 | DetailViewModel()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Forms/FormField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormField.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/12/21.
6 | //
7 |
8 | import Foundation
9 | import Builder
10 | import RxSwift
11 | import UIKit
12 |
13 |
14 | protocol AnyFormField {
15 | var id: String { get }
16 | func value() -> T?
17 | func validates() -> String?
18 | }
19 |
20 | struct FormField: AnyFormField {
21 |
22 | var id: String
23 |
24 | @Variable var variable: Value
25 |
26 | private(set) var validators: [(Value) -> String?] = []
27 |
28 | init(id: String, value: Value) {
29 | self.id = id
30 | self._variable = Variable(wrappedValue: value)
31 | }
32 |
33 | init(id: ID, value: Value) where ID.RawValue == String {
34 | self.id = id.rawValue
35 | self._variable = Variable(wrappedValue: value)
36 | }
37 |
38 | func value() -> T? {
39 | variable as? T
40 | }
41 |
42 | func validates() -> String? {
43 | for validator in validators {
44 | if let error = validator(variable) {
45 | return error
46 | }
47 | }
48 | return nil
49 | }
50 |
51 | }
52 |
53 |
54 | extension FormField where Value == String {
55 |
56 | func validate(_ validation: @escaping (_ value: String) -> String?) -> Self {
57 | validateEmpty { $0.isEmpty ? nil : validation($0) }
58 | }
59 |
60 | func validateEmpty(_ validation: @escaping (_ value: String) -> String?) -> Self {
61 | var field = self
62 | field.validators.append(validation)
63 | return field
64 | }
65 |
66 | func isRequired(_ message: String? = nil) -> Self {
67 | validateEmpty { $0.isEmpty ? message ?? "Required" : nil }
68 | }
69 |
70 | func isEmail(_ message: String? = nil) -> Self {
71 | validate { $0 .contains("@") ? nil : message ?? "Must be valid email address" }
72 | }
73 |
74 | func length(_ count: Int, _ message: String? = nil) -> Self {
75 | validate { $0.count == count ? nil : message ?? "Must be \(count) characters long" }
76 | }
77 |
78 | func maxLength(_ count: Int, _ message: String? = nil) -> Self {
79 | validate { $0.count > count ? nil : message ?? "Must be \(count) characters or less" }
80 | }
81 |
82 | func minLength(_ count: Int, _ message: String? = nil) -> Self {
83 | validate { $0.count <= count ? nil : message ?? "Must be \(count) characters or more" }
84 | }
85 |
86 | }
87 |
88 | extension FormField where Value == Bool {
89 |
90 | func validate(_ validation: @escaping (_ value: Bool) -> String?) -> Self {
91 | var field = self
92 | field.validators.append(validation)
93 | return field
94 | }
95 |
96 | func isRequired(_ message: String? = nil) -> Self {
97 | validate { $0 ? nil : message ?? "Required" }
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Forms/FormFieldManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormFieldManager.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/12/21.
6 | //
7 |
8 | import Foundation
9 | import Builder
10 | import RxSwift
11 |
12 | enum FormFieldState {
13 | case enabled
14 | case disabled
15 | case hidden
16 | }
17 |
18 | class FormFieldManager where IDS.RawValue == String {
19 |
20 | @Variable var fields: [AnyFormField] = []
21 | @Variable var errors: [String:String] = [:]
22 | @Variable var states: [String:FormFieldState] = [:]
23 |
24 | @inlinable
25 | func field(for id: IDS) -> AnyFormField? {
26 | fields.first(where: { $0.id == id.rawValue })
27 | }
28 |
29 | @inlinable
30 | func value(for id: IDS) -> T? {
31 | field(for: id)?.value()
32 | }
33 |
34 | func variable(for id: IDS) -> Variable {
35 | guard let field = field(for: id) as? FormField else {
36 | fatalError("Missing '\(id.rawValue)' of type \(T.self) in field list")
37 | }
38 | return field.$variable
39 | }
40 |
41 | func placeholder(for id: IDS) -> String {
42 | return id.rawValue.capitalized
43 | }
44 |
45 | func error(for id: IDS) -> Observable {
46 | $errors.asObservable()
47 | .map { $0[id.rawValue] }
48 | }
49 |
50 | func hasErrors() -> Observable {
51 | $errors.asObservable()
52 | .skip(1)
53 | .debounce(.milliseconds(100), scheduler: MainScheduler.instance)
54 | .map { !$0.isEmpty }
55 | }
56 |
57 | var isValid: Bool {
58 | errors.isEmpty
59 | }
60 |
61 | func state(for id: IDS) -> Observable {
62 | $states.asObservable()
63 | .map { $0[id.rawValue] ?? .enabled }
64 | .distinctUntilChanged()
65 | }
66 |
67 | func state(_ state: FormFieldState, for id: IDS) -> Observable {
68 | self.state(for: id)
69 | .map { $0 == state }
70 | }
71 |
72 | func validate() {
73 | validate(fields, filtering: [.disabled, .hidden])
74 | }
75 |
76 | func validate(_ fields: [AnyFormField], filtering states: [FormFieldState]) {
77 | errors = fields
78 | .compactMap { (field) -> AnyFormField? in
79 | let state = self.states[field.id] ?? .enabled
80 | return states.contains(state) ? nil : field
81 | }
82 | .reduce(into: [:]) { errors, field in
83 | if let error = field.validates() {
84 | errors[field.id] = error
85 | }
86 | }
87 | }
88 |
89 | func nextID(from id: IDS) -> String? {
90 | let ids = fields.map { $0.id }
91 | if let index = ids.firstIndex(where: { $0 == id.rawValue }), index < (ids.count - 1) {
92 | return ids[index + 1]
93 | }
94 | return nil
95 | }
96 |
97 | }
98 |
99 |
100 | extension MetaTextField {
101 |
102 | init(manager: FormFieldManager, id: ID) where ID: RawRepresentable, ID.RawValue == String {
103 | self
104 | .identifier(id.rawValue)
105 | .text(bidirectionalBind: manager.variable(for: id))
106 | .placeholder(manager.placeholder(for: id))
107 | .error(bind: manager.error(for: id))
108 | .returnKeyType(.next)
109 | .onControlEvent(.editingDidBegin) { context in
110 | context.view.scrollIntoView()
111 | }
112 | .onControlEvent(.editingDidEndOnExit) { [weak manager] context in
113 | if let id = manager?.nextID(from: id), let field = context.find(id), field.canBecomeFirstResponder {
114 | field.becomeFirstResponder()
115 | }
116 | }
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API_BASE_PROTOCOL
6 | $(API_BASE_PROTOCOL)
7 | API_BASE_URL
8 | $(API_BASE_URL)
9 | UIApplicationSceneManifest
10 |
11 | UIApplicationSupportsMultipleScenes
12 |
13 | UISceneConfigurations
14 |
15 | UIWindowSceneSessionRoleApplication
16 |
17 |
18 | UISceneConfigurationName
19 | Default Configuration
20 | UISceneDelegateClassName
21 | $(PRODUCT_MODULE_NAME).SceneDelegate
22 | UISceneStoryboardFile
23 | Main
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/LoadingCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | struct LoadingCardView: ViewBuilder {
15 | var body: View {
16 | DLSCardView {
17 | VStackView {
18 | With(UIActivityIndicatorView()) {
19 | $0.startAnimating()
20 | }
21 | }
22 | .padding(16)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/LoginCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginCardView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | struct LoginCardView: ViewBuilder {
15 |
16 | let viewModel: LoginViewModel
17 |
18 | var body: View {
19 | DLSCardView {
20 | VStackView {
21 | LabelView("Welcome")
22 | .alignment(.center)
23 | .font(.headline)
24 | .color(.label)
25 |
26 | LoginErrorView(error: viewModel.$error)
27 |
28 | VStackView {
29 | TextField(viewModel.$username)
30 | .placeholder("Login ID")
31 | .set(keyPath: \.borderStyle, value: .roundedRect) // properties w/o dedicated builder
32 | .set(keyPath: \.textContentType, value: .username) // properties w/o dedicated builder
33 | .tag(456) // testing identifiers
34 | LabelView(viewModel.$usernameError)
35 | .font(.footnote)
36 | .color(.red)
37 | .hidden(bind: viewModel.hideUsernameError)
38 | .padding(h: 8, v: 0)
39 | }
40 | .spacing(2)
41 |
42 | VStackView {
43 | TextField(viewModel.$password)
44 | .placeholder("Password")
45 | .set(keyPath: \.borderStyle, value: .roundedRect) // properties w/o dedicated builder
46 | .set(keyPath: \.textContentType, value: .password) // properties w/o dedicated builder
47 | .set(keyPath: \.isSecureTextEntry, value: true) // properties w/o dedicated builder
48 | LabelView(viewModel.$passwordError)
49 | .font(.footnote)
50 | .color(.red)
51 | .hidden(bind: viewModel.hidePasswordError)
52 | .padding(h: 8, v: 0)
53 | }
54 | .spacing(2)
55 |
56 | VStackView {
57 | ButtonView("Login") { [weak viewModel] _ in
58 | viewModel?.login()
59 | }
60 | .style(StyleButtonFilled())
61 |
62 | ButtonView("Enroll / Login Help") { context in
63 | print(context.view.find(superview: "dlscard")!) // testing identifiers
64 | print(context.view.find(456)!) // testing identifiers
65 | }
66 | .height(44)
67 | }
68 | .spacing(6)
69 |
70 | }
71 | .padding(top: 30, left: 40, bottom: 16, right: 40)
72 | .spacing(20)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/LoginErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginErrorView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | struct LoginErrorView: ViewBuilder {
15 |
16 | @Variable var error: String?
17 |
18 | var body: View {
19 | ContainerView {
20 | LabelView($error)
21 | .alignment(.center)
22 | .font(.headline)
23 | .color(.white)
24 | .numberOfLines(0)
25 | }
26 | .backgroundColor(.red)
27 | .cornerRadius(2)
28 | .padding(8)
29 | .hidden(true)
30 | .onReceive($error.asObservable().skip(1), handler: { context in
31 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
32 | UIView.animate(withDuration: 0.2) {
33 | context.view.isHidden = context.value == nil
34 | }
35 | }
36 | })
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/LoginStatusView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginStatusView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | struct LoginStatusView: ViewBuilder {
15 |
16 | @Variable private var status: String? = nil
17 |
18 | var body: View {
19 | LabelView($status)
20 | .alignment(.center)
21 | .font(.body)
22 | .color(.yellow)
23 | .numberOfLines(0)
24 | .hidden(true)
25 | .onReceive($status.asObservable().skip(1), handler: { context in
26 | UIView.animate(withDuration: 0.3) {
27 | context.view.isHidden = context.value == nil
28 | }
29 | })
30 | .onAppear { _ in
31 | self.load()
32 | }
33 | }
34 |
35 | func load() {
36 | DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
37 | self.status = "This is a system status message that should be shown to the user."
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/LoginViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewModel.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/22/21.
6 | //
7 |
8 | import Foundation
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import SwiftUI
13 |
14 | class LoginViewModel {
15 |
16 | enum State {
17 | case loading
18 | case loaded
19 | }
20 |
21 | @Variable var state: State = .loading
22 |
23 | @Variable var username: String? = "michael"
24 | @Variable var usernameError: String? = nil
25 |
26 | @Variable var password: String? = ""
27 | @Variable var passwordError: String? = nil
28 |
29 | @Variable var done: Bool = false
30 | @Variable var error: String? = nil
31 |
32 | var hideUsernameError: Observable {
33 | $usernameError.asObservable().map { $0 == nil }
34 | }
35 |
36 | var hidePasswordError: Observable {
37 | $passwordError.asObservable().map { $0 == nil }
38 | }
39 |
40 | func login() {
41 | usernameError = (username?.isEmpty ?? true) ? "Required" : nil
42 | passwordError = (password?.isEmpty ?? true) ? "Required" : nil
43 |
44 | let showError = usernameError != nil || passwordError != nil
45 | error = showError ? "This is an error message that should be shown to the user." : nil
46 |
47 | if error == nil {
48 | state = .loading
49 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
50 | self.done = true
51 | }
52 | }
53 | }
54 |
55 | func load() {
56 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
57 | self.state = .loaded
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Login/NotYetImplementedCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotYetImplementedCardView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | struct NotYetImplementedCardView: ViewBuilder {
15 | var body: View {
16 | DLSCardView {
17 | VStackView {
18 | LabelView("Not yet implemented...")
19 | .alignment(.center)
20 | .font(.headline)
21 | .color(.secondaryLabel)
22 | }
23 | .padding(16)
24 | }
25 | .height(150)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Main/MainStackViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | class MainStackViewController: UIViewController {
14 |
15 | @Injected(Container.mainViewModel) var viewModel: MainViewModel
16 |
17 | let disposeBag = DisposeBag()
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | title = "Stack View Test"
22 | view.backgroundColor = .systemBackground
23 | setupSubscriptions()
24 | }
25 |
26 | func setupSubscriptions() {
27 | viewModel.$state
28 | .observe(on: MainScheduler.instance)
29 | .subscribe(onNext: { [weak self] (state) in
30 | guard let self = self else { return }
31 | switch state {
32 | case .initial:
33 | self.viewModel.load()
34 | case .loading:
35 | self.transition(to: StandardLoadingPage())
36 | case .loaded(let users):
37 | self.transition(to: MainUsersStackView(users: users))
38 | case .empty(let message):
39 | self.transition(to: StandardEmptyPage(message: message))
40 | case .error(let error):
41 | self.transition(to: StandardErrorPage(error: error))
42 | }
43 | })
44 | .disposed(by: disposeBag)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Main/MainUsersStackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainUsersStackView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 3/1/21.
6 | //
7 |
8 | //
9 | // MainCardView.swift
10 | // Builder
11 | //
12 | // Created by Michael Long on 1/18/21.
13 | //
14 |
15 | import UIKit
16 | import Builder
17 | import Factory
18 | import RxSwift
19 |
20 |
21 | struct MainUsersStackView: ViewBuilder {
22 |
23 | let users: [User]
24 |
25 | var body: View {
26 | VerticalScrollView {
27 | VStackView(DynamicItemViewBuilder(users) { user in
28 | StackCardView(user: user)
29 | .backgroundColor(.secondarySystemBackground)
30 | .border(color: .gray)
31 | .shadow(color: .black, radius: 3, opacity: 0.2, offset: CGSize(width: 3, height: 3))
32 | .onTapGesture { context in
33 | let vc = DetailViewController(user: user)
34 | context.present(vc)
35 | }
36 | })
37 | .padding(h: 16, v: 8)
38 | .spacing(16)
39 | }
40 | }
41 | }
42 |
43 |
44 | struct StackCardView: ViewBuilder {
45 |
46 | @Injected(Container.userImageCache) var cache: UserImageCache
47 |
48 | let user: User
49 |
50 | var body: View {
51 | ContainerView {
52 | HStackView {
53 | ImageView(thumbnail())
54 | .frame(height: 60, width: 60)
55 | VStackView {
56 | LabelView(user.fullname)
57 | .font(.preferredFont(forTextStyle: .body))
58 | LabelView(user.email)
59 | .font(.preferredFont(forTextStyle: .footnote))
60 | .color(.secondaryLabel)
61 | SpacerView()
62 | }
63 | .padding(h: 8, v: 8)
64 | .spacing(4)
65 |
66 | }
67 | }
68 | }
69 |
70 | func thumbnail() -> Observable {
71 | return cache.thumbnailOrPlaceholder(forUser: user)
72 | .asObservable()
73 | .observe(on: MainScheduler.instance)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Main/MainUsersTableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainUsersView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 3/1/21.
6 | //
7 |
8 | //
9 | // MainCardView.swift
10 | // Builder
11 | //
12 | // Created by Michael Long on 1/18/21.
13 | //
14 |
15 | import UIKit
16 | import Builder
17 | import Factory
18 | import RxSwift
19 |
20 |
21 | struct MainUsersTableView: ViewBuilder {
22 |
23 | let users: [User]
24 |
25 | var body: View {
26 | TableView(DynamicItemViewBuilder(users) { user in
27 | TableViewCell {
28 | MainCardView(user: user)
29 | }
30 | .accessoryType(.disclosureIndicator)
31 | .onSelect { (context) in
32 | context.push(DetailViewController(user: user))
33 | return false
34 | }
35 | })
36 | }
37 |
38 | }
39 |
40 | struct MainCardView: ViewBuilder {
41 |
42 | @Injected(Container.userImageCache) var cache: UserImageCache
43 |
44 | let user: User
45 |
46 | var body: View {
47 | HStackView {
48 | ImageView(thumbnail())
49 | .cornerRadius(25)
50 | .frame(height: 50, width: 50)
51 | VStackView {
52 | LabelView(user.fullname)
53 | .font(.preferredFont(forTextStyle: .body))
54 | LabelView(user.email)
55 | .font(.preferredFont(forTextStyle: .footnote))
56 | .color(.secondaryLabel)
57 | SpacerView()
58 | }
59 | .spacing(4)
60 | }
61 | }
62 |
63 | func thumbnail() -> Observable {
64 | return cache.thumbnailOrPlaceholder(forUser: user)
65 | .asObservable()
66 | .observe(on: MainScheduler.instance)
67 | }
68 |
69 | }
70 |
71 | class UserTableViewCell: BuilderInternalTableViewCell {
72 |
73 | var nameLabel: UILabel?
74 | var emailLabel: UILabel?
75 |
76 | func configure(_ user: User) {
77 | if contentView.subviews.isEmpty {
78 | contentView.embed(body)
79 | }
80 | nameLabel?.text = user.fullname
81 | emailLabel?.text = user.fullname
82 | }
83 |
84 | var body: View {
85 | HStackView {
86 | VStackView {
87 | LabelView("")
88 | .reference(&nameLabel)
89 | .font(.preferredFont(forTextStyle: .body))
90 | LabelView("")
91 | .reference(&emailLabel)
92 | .font(.preferredFont(forTextStyle: .footnote))
93 | .color(.secondaryLabel)
94 | SpacerView()
95 | }
96 | .padding(h: 16, v: 8)
97 | .spacing(4)
98 | }
99 | }
100 |
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Main/MainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | class MainViewController: UIViewController {
14 |
15 | @Injected(Container.mainViewModel) var viewModel: MainViewModel
16 |
17 | var mainView: UIView?
18 | let disposeBag = DisposeBag()
19 |
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 | title = "Table View Test"
23 | setupSubscriptions()
24 | }
25 |
26 | func setupSubscriptions() {
27 | viewModel.$state
28 | .observe(on: MainScheduler.instance)
29 | .subscribe(onNext: { [weak self] (state) in
30 | guard let self = self else { return }
31 | switch state {
32 | case .initial:
33 | self.viewModel.load()
34 | case .loading:
35 | self.transition(to: StandardLoadingPage())
36 | case .loaded(let users):
37 | self.transition(to: MainUsersTableView(users: users).reference(&self.mainView))
38 | case .empty(let message):
39 | self.transition(to: StandardEmptyPage(message: message))
40 | case .error(let error):
41 | self.transition(to: StandardErrorPage(error: error))
42 | }
43 | })
44 | .disposed(by: disposeBag)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Main/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 | import RxCocoa
13 |
14 | class MainViewModel {
15 |
16 | @Injected(Container.userImageCache) var images: UserImageCache
17 | @Injected(Container.userServiceType) var service: UserServiceType
18 |
19 | enum State: Equatable {
20 | case initial
21 | case loading
22 | case loaded([User])
23 | case empty(String)
24 | case error(String)
25 | }
26 |
27 | @Variable private(set) var state = State.initial
28 |
29 | private var disposeBag = DisposeBag()
30 |
31 | func load() {
32 | state = .loading
33 | service.list()
34 | .map { $0.sorted(by: { ($0.name.last + $0.name.first) < ($1.name.last + $1.name.first) }) }
35 | .subscribe { [weak self] (users) in
36 | if users.isEmpty {
37 | self?.state = .empty("No current users found...")
38 | } else {
39 | self?.state = .loaded(users)
40 | }
41 | } onFailure: { [weak self] (e) in
42 | self?.state = .error(e.localizedDescription)
43 | }
44 | .disposed(by: disposeBag)
45 | }
46 |
47 | func refresh() {
48 | load()
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Menu/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 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Menu/MenuOption.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuOption.swift
3 | // BuilderDemo
4 | //
5 | // Created by Michael Long on 9/17/22.
6 | //
7 |
8 | import UIKit
9 |
10 | struct MenuOption {
11 | let name: String
12 | let description: String
13 | let destination: () -> UIViewController
14 | }
15 |
16 | extension MenuOption {
17 | static let options: [MenuOption] = [
18 | MenuOption(name: "Login View Test", description: "A basic username/password login screen.") {
19 | LoginViewController()
20 | },
21 | MenuOption(name: "Contact Form Test", description: "A basic contact form screen with a few twists.") {
22 | ContactFormViewController()
23 | },
24 | MenuOption(name: "Table View Test", description: "A basic master/detail table view with user data pulled from an API.") {
25 | MainViewController()
26 | },
27 | MenuOption(name: "Stack View Test", description: "A basic master/detail dynamic stack view with user data pulled from an API.") {
28 | MainStackViewController()
29 | },
30 | MenuOption(name: "Table View Menu Options", description: "An alternative table view that displays our menu options.") {
31 | MenuTableViewController()
32 | },
33 | MenuOption(name: "Tab Bar Test View", description: "A basic view that tests a new tab bar layour.") {
34 | CustomTabBarViewController()
35 | },
36 | MenuOption(name: "Test Views", description: "A basic view that tests many elements of ViewBuilder.") {
37 | TestViewController()
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Menu/MenuTableViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | class MenuTableViewController: UIViewController {
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | title = "Builder Demo"
18 | view.backgroundColor = .systemBackground
19 | view.embed(MenuTableView())
20 |
21 | // let vc = ContactFormViewController()
22 | // navigationController?.pushViewController(vc, animated: false)
23 | }
24 |
25 | }
26 |
27 | struct MenuTableView: ViewBuilder {
28 |
29 | let options = MenuOption.options
30 |
31 | var body: View {
32 | TableView(StaticViewBuilder {
33 | ForEach(options) {
34 | MenuTableViewCell(option: $0)
35 | }
36 | })
37 | }
38 |
39 | }
40 |
41 | struct MenuTableViewCell: ViewBuilder {
42 |
43 | let option: MenuOption
44 |
45 | var body: View {
46 | TableViewCell {
47 | VStackView {
48 | LabelView(option.name)
49 | .font(.headline)
50 | LabelView(option.description)
51 | .font(.footnote)
52 | .color(.secondaryLabel)
53 | .numberOfLines(0)
54 | }
55 | .spacing(2)
56 | }
57 | .accessoryType(.disclosureIndicator)
58 | .onSelect { (context) in
59 | context.push(option.destination())
60 | return false
61 | }
62 | }
63 |
64 | }
65 |
66 | struct MenuTableViewOLD: ViewBuilder {
67 |
68 | var body: View {
69 | TableView(StaticViewBuilder {
70 | MenuTableViewCellOLD(name: "Login View Test", description: "A basic login field.") {
71 | LoginViewController()
72 | }
73 |
74 | MenuTableViewCellOLD(name: "Table View Test", description: "A basic master/detail table view with user data pulled from an API.") {
75 | MainViewController()
76 | }
77 | MenuTableViewCellOLD(name: "Stack View Test", description: "A basic master/detail dynamic stack view with user data pulled from an API.") {
78 | MainStackViewController()
79 | }
80 | MenuTableViewCellOLD(name: "Test Views", description: "A basic view that tests many elements of ViewBuilder.") {
81 | TestViewController()
82 | }
83 | })
84 | }
85 |
86 | }
87 |
88 | struct MenuTableViewCellOLD: ViewBuilder {
89 |
90 | let name: String
91 | let description: String
92 | let destination: () -> UIViewController
93 |
94 | var body: View {
95 | TableViewCell {
96 | VStackView {
97 | LabelView(name)
98 | .font(.headline)
99 | LabelView(description)
100 | .font(.footnote)
101 | .color(.secondaryLabel)
102 | .numberOfLines(0)
103 | }
104 | .spacing(2)
105 | }
106 | .accessoryType(.disclosureIndicator)
107 | .onSelect { (context) in
108 | context.push(destination())
109 | return false
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Menu/MenuViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuViewController.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/11/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | class MenuViewController: UIViewController {
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 |
18 | navigationItem.titleView = ImageView(named: "Logo-DK")
19 | .frame(height: 50, width: 50)
20 | .build()
21 |
22 | view.backgroundColor = .systemBackground
23 | view.embed(MenuStackView())
24 |
25 | // let vc = ContactFormViewController()
26 | // navigationController?.pushViewController(vc, animated: false)
27 | }
28 |
29 | }
30 |
31 | struct MenuStackView: ViewBuilder {
32 | var body: View {
33 | ZStackView {
34 | ImageView(named: "vector")
35 | VerticalScrollView {
36 | VStackView {
37 | MenuHeaderView()
38 | ForEach(MenuOption.options) { option in
39 | MenuOptionView(option: option)
40 | }
41 | MenuFootnoteView()
42 | }
43 | .spacing(15)
44 | .padding(30)
45 | }
46 | }
47 | }
48 | }
49 |
50 | struct MenuHeaderView: ViewBuilder {
51 | var body: View {
52 | VStackView {
53 | LabelView("Builder Demo")
54 | .font(.title1)
55 | LabelView("Version 1.0")
56 | .font(.footnote)
57 | .color(.secondaryLabel)
58 | }
59 | .alignment(.center)
60 | .spacing(0)
61 | }
62 | }
63 |
64 | struct MenuOptionView: ViewBuilder {
65 | let option: MenuOption
66 | var body: View {
67 | ContainerView {
68 | VStackView {
69 | LabelView(option.name)
70 | .font(.headline)
71 | LabelView(option.description)
72 | .font(.footnote)
73 | .color(.secondaryLabel)
74 | .numberOfLines(0)
75 | }
76 | .spacing(2)
77 | .padding(16)
78 | }
79 | .backgroundColor(UIColor.systemGroupedBackground.withAlphaComponent(0.5))
80 | .cornerRadius(10)
81 | .shadow(color: .black.withAlphaComponent(0.5), radius: 3, offset: CGSize(width: 3, height: 3))
82 | .onTapGesture { context in
83 | context.view.addHighlightOverlay(animated: true, removeAfter: 0.3)
84 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
85 | context.push(option.destination())
86 | }
87 | }
88 | }
89 | }
90 |
91 | struct MenuFootnoteView: ViewBuilder {
92 | var body: View {
93 | LabelView("Created by Michael Long, 2022")
94 | .alignment(.center)
95 | .font(.footnote)
96 | .color(.secondaryLabel)
97 | }
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // BuilderDemo
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Services/TestServices.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestServices.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 10/5/21.
6 | //
7 |
8 | import Foundation
9 | import Builder
10 | import RxSwift
11 | import RxCocoa
12 |
13 | class TestVariable {
14 |
15 | @Variable var name = "Michael"
16 | @Variable var switched = true
17 |
18 | func test() {
19 | _ = SwitchView($switched)
20 |
21 | _ = $switched.asObservable()
22 | .subscribe { name in
23 | print(name)
24 | }
25 |
26 | _ = $name.asObservable()
27 | .subscribe { name in
28 | print(name)
29 | }
30 |
31 | name = "Test1"
32 |
33 | $name.wrappedValue = "Test2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Services/UserImageCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserImageCache.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Factory
10 | import RxSwift
11 |
12 | class UserImageCache {
13 |
14 | @Injected(Container.userServiceType) var userService: UserServiceType
15 |
16 | func thumbnail(forUser user: User) -> Observable {
17 | guard let path = user.picture?.medium else {
18 | return .just(nil)
19 | }
20 | if let image = imageCache.object(forKey: NSString(string: path)) {
21 | return .just(image)
22 | }
23 | let image = userService.thumbnail(forUser: user)
24 | .asObservable()
25 | return Observable.merge(.just(nil), image)
26 | .do(onNext: { [weak self] (image) in
27 | if let image = image {
28 | self?.imageCache.setObject(image, forKey: NSString(string: path))
29 | }
30 | })
31 | }
32 |
33 | func thumbnailOrPlaceholder(forUser user: User) -> Observable {
34 | thumbnail(forUser: user)
35 | .catchAndReturn(UIImage(named: "User-Unknown"))
36 | .map { $0 ?? UIImage(named: "User-Unknown") }
37 | }
38 |
39 | func photo(forUser user: User) -> Observable {
40 | guard let path = user.picture?.large else {
41 | return .just(nil)
42 | }
43 | if let image = imageCache.object(forKey: NSString(string: path)) {
44 | return .just(image)
45 | }
46 | let image = userService.photo(forUser: user)
47 | .asObservable()
48 | return Observable.merge(.just(nil), image)
49 | .do(onNext: { [weak self] (image) in
50 | if let image = image {
51 | self?.imageCache.setObject(image, forKey: NSString(string: path))
52 | }
53 | })
54 | }
55 |
56 | func photoOrPlaceholder(forUser user: User) -> Observable {
57 | photo(forUser: user)
58 | .catchAndReturn(UIImage(named: "User-Unknown"))
59 | .map { $0 ?? UIImage(named: "User-Unknown") }
60 | }
61 |
62 | private var imageCache = NSCache()
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Services/UserService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserService.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Factory
10 | import RxSwift
11 | import RxCocoa
12 |
13 | protocol UserServiceType {
14 | func list() -> Single<[User]>
15 | func thumbnail(forUser user: User) -> Single
16 | func photo(forUser user: User) -> Single
17 | }
18 |
19 | struct UserService: UserServiceType {
20 |
21 | @Injected(Container.clientSessionManager) var session: ClientSessionManager
22 |
23 | init() {
24 | session.configure { (interceptor: SSOAuthenticationInterceptor) in
25 | interceptor.token = "F129038AF30912830E8120938"
26 | }
27 | }
28 |
29 | func list() -> Single<[User]> {
30 | session.builder()
31 | .add(queryItems: [
32 | URLQueryItem(name: "results", value: "20"),
33 | URLQueryItem(name: "seed", value: "999"),
34 | URLQueryItem(name: "nat", value: "us")
35 | ])
36 | .with { r in
37 | print(r)
38 | }
39 | .get()
40 | .decode(type: UserResultType.self, decoder: JSONDecoder())
41 | .map { $0.results }
42 | .observe(on: MainScheduler.instance)
43 | }
44 |
45 | func thumbnail(forUser user: User) -> Single {
46 | guard let path = user.picture?.medium else {
47 | return .just(nil)
48 | }
49 | return session.builder(forURL: URL(string: path))
50 | .get()
51 | .map { (data: Data) -> UIImage? in UIImage(data: data) }
52 | .observe(on: MainScheduler.instance)
53 | }
54 |
55 | func photo(forUser user: User) -> Single {
56 | guard let path = user.picture?.large else {
57 | return .just(nil)
58 | }
59 | return session.builder(forURL: URL(string: path))
60 | .get()
61 | .map { (data: Data) -> UIImage? in UIImage(data: data) }
62 | .observe(on: MainScheduler.instance)
63 | }
64 |
65 | private struct UserResultType: Codable {
66 | let results: [User]
67 | }
68 |
69 | }
70 |
71 | #if MOCK
72 | struct MockUserService: UserServiceType {
73 |
74 | func list() -> Single<[User]> {
75 | return .just(User.users)
76 | }
77 |
78 | func thumbnail(forUser user: User) -> Single {
79 | if let name = user.picture?.thumbnail, let image = UIImage(named: name) {
80 | return .just(image)
81 | }
82 | return .just(nil)
83 | }
84 |
85 | func photo(forUser user: User) -> Single {
86 | if let name = user.picture?.thumbnail, let image = UIImage(named: name) {
87 | return .just(image)
88 | }
89 | return .just(nil)
90 | }
91 |
92 | }
93 |
94 | struct MockEmptyUserService: UserServiceType {
95 | func list() -> Single<[User]> {
96 | return .just([])
97 | }
98 | func thumbnail(forUser user: User) -> Single {
99 | return .just(nil)
100 | }
101 | func photo(forUser user: User) -> Single {
102 | return .just(nil)
103 | }
104 | }
105 |
106 | struct MockErrorUserService: UserServiceType {
107 | func list() -> Single<[User]> {
108 | return .error(APIError.unexpected)
109 | }
110 | func thumbnail(forUser user: User) -> Single {
111 | return .error(APIError.unexpected)
112 | }
113 | func photo(forUser user: User) -> Single {
114 | return .error(APIError.unexpected)
115 | }
116 | }
117 | #endif
118 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Services/_Services+Injection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Services+Injection.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import Foundation
9 | import Factory
10 |
11 | extension Container {
12 | static let userImageCache = Factory(scope: .shared) {
13 | UserImageCache()
14 | }
15 | static let userServiceType = Factory {
16 | UserService()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/Functions+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Functions+Extensions.swift
3 | // Functions+Extensions
4 | //
5 | // Created by Michael Long on 7/29/21.
6 | //
7 |
8 | import Foundation
9 |
10 | @discardableResult
11 | @inlinable
12 | public func with(_ value: T, _ configuration: ((_ value: T) -> Void)) -> T {
13 | configuration(value)
14 | return value
15 | }
16 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/Rx+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Rx+Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 2/3/21.
6 | //
7 |
8 | import Foundation
9 | import RxSwift
10 |
11 | extension RxSwift.ObservableType where Element == Bool {
12 | func toggle() -> Observable {
13 | self.map { !$0 }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/String+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Extensions.swift
3 | //
4 | // Copyright (c) 2017 Client Resources Inc. All rights reserved.
5 | //
6 |
7 | import Foundation
8 |
9 | extension String {
10 | func removingWhitespaces() -> String {
11 | return components(separatedBy: .whitespaces).joined()
12 | }
13 |
14 | // Returns: A formatted phone number in the format: (XXX) XXX-XXXX
15 | func asPhoneNumber() -> String {
16 | return self.formatDigitsWith(mask:"(999) 999-9999")
17 | }
18 |
19 | // Returns: A formatted phone number in the format: XXX-XXX-XXXX
20 | // This is used for when we need a URL of the number to call
21 | func asCallablePhoneNumber() -> String {
22 | return self.formatDigitsWith(mask: "999-999-9999")
23 | }
24 | }
25 |
26 | extension String {
27 | func stripReturningDigitsOnly() -> String {
28 | return self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
29 | }
30 |
31 | func replaceNewlinesWithSpaces() -> String {
32 | return self.replacingOccurrences(of: "\n", with: " ")
33 | }
34 |
35 | func formatDigitsWith(mask: String) -> String {
36 | let cleanNumber = self.stripReturningDigitsOnly()
37 | var result = ""
38 | var index = cleanNumber.startIndex
39 | for ch in mask {
40 | if index == cleanNumber.endIndex {
41 | break
42 | }
43 | if ch == "9" {
44 | result.append(cleanNumber[index])
45 | index = cleanNumber.index(after: index)
46 | } else {
47 | result.append(ch)
48 | }
49 | }
50 | return result
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/UIColor+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 |
12 | func lighter(by percentage: CGFloat = 30.0) -> UIColor {
13 | return self.adjust(by: abs(percentage))
14 | }
15 |
16 | func darker(by percentage: CGFloat = 30.0) -> UIColor {
17 | return self.adjust(by: -1 * abs(percentage))
18 | }
19 |
20 | private func adjust(by percentage: CGFloat = 30.0) -> UIColor {
21 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
22 | if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
23 | return UIColor(red: min(red + percentage/100, 1.0),
24 | green: min(green + percentage/100, 1.0),
25 | blue: min(blue + percentage/100, 1.0),
26 | alpha: alpha)
27 | }
28 | return self
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/UIImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Extensions.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 9/26/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIImage {
12 |
13 | /**
14 | Initializes and returns the image object of the specified color and size.
15 | */
16 | public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
17 | let rect = CGRect(origin: .zero, size: size)
18 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
19 | color.setFill()
20 | UIRectFill(rect)
21 | let image = UIGraphicsGetImageFromCurrentImageContext()
22 | UIGraphicsEndImageContext()
23 |
24 | guard let cgImage = image?.cgImage else {
25 | return nil
26 | }
27 | self.init(cgImage: cgImage)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/UITextField+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UITextField+Styles.swift
3 | //
4 |
5 | import Foundation
6 | import UIKit
7 |
8 | private var bottomLineColorAssociationKey: UInt8 = 0
9 | private var selectedBottomLineColorAssociationKey: UInt8 = 1
10 | private var errorColorAssociationKey: UInt8 = 2
11 |
12 | // Custom UIAppearance properties
13 | extension UITextField {
14 |
15 | override open var accessibilityValue: String? {
16 | get { return self.text }
17 | set { super.accessibilityValue = newValue }
18 | }
19 |
20 | @objc dynamic var placeholderTextColor: UIColor? {
21 | get {
22 | return self.placeholderTextColor
23 | }
24 | set {
25 | let placeholderText = placeholder != nil ? placeholder! : ""
26 | attributedPlaceholder = NSAttributedString(string: placeholderText, attributes: [NSAttributedString.Key.foregroundColor: newValue!])
27 | setNeedsDisplay()
28 | }
29 | }
30 |
31 | @objc dynamic var bottomLineColor: UIColor? {
32 | get {
33 | return objc_getAssociatedObject(self, &bottomLineColorAssociationKey) as? UIColor
34 | }
35 | set(newValue) {
36 | objc_setAssociatedObject(self, &bottomLineColorAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
37 | setNeedsDisplay()
38 | }
39 | }
40 |
41 | @objc dynamic var selectedBottomLineColor: UIColor? {
42 | get {
43 | return objc_getAssociatedObject(self, &selectedBottomLineColorAssociationKey) as? UIColor
44 | }
45 | set(newValue) {
46 | objc_setAssociatedObject(self, &selectedBottomLineColorAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
47 | setNeedsDisplay()
48 | }
49 | }
50 |
51 | @objc dynamic var errorColor: UIColor? {
52 | get {
53 | return objc_getAssociatedObject(self, &errorColorAssociationKey) as? UIColor
54 | }
55 | set(newValue) {
56 | objc_setAssociatedObject(self, &errorColorAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
57 | setNeedsDisplay()
58 | }
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/UIView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/7/21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 |
12 | public func addUnderline(lineThickness: CGFloat, lineColor: UIColor, dashed: Bool = false) -> CALayer {
13 |
14 | let width = frame.size.width
15 | let height = frame.size.height
16 |
17 | // Create a line at the bottom of the view
18 | let lineShape = CAShapeLayer()
19 | lineShape.strokeStart = 0.0
20 | lineShape.strokeColor = lineColor.cgColor
21 | lineShape.fillColor = UIColor.clear.cgColor
22 | lineShape.lineWidth = lineThickness
23 |
24 | // We might draw the line with a dashed style
25 | if dashed {
26 | let dashThickness = lineThickness as NSNumber
27 | let emptyThickness = lineThickness * 4 as NSNumber
28 | lineShape.lineDashPattern = [dashThickness, emptyThickness]
29 | }
30 |
31 | let path = CGMutablePath()
32 | path.move(to: CGPoint(x: 0, y: height - (lineThickness / 2)))
33 | path.addLine(to: CGPoint(x: width, y: height - (lineThickness / 2)))
34 | lineShape.path = path
35 |
36 | // Modify this view with a clear background
37 | backgroundColor = UIColor.clear
38 |
39 | // Attach the line at the bottom of the ivew
40 | layer.addSublayer(lineShape)
41 |
42 | return lineShape
43 | }
44 |
45 | }
46 |
47 |
48 | extension UIView {
49 |
50 | func addHighlightOverlay(animated: Bool = true, removeAfter delay: TimeInterval? = nil) {
51 | let overlay = UIView(frame: self.bounds)
52 | overlay.backgroundColor = .label
53 | overlay.alpha = animated ? 0.0 : 0.15
54 | overlay.tag = 999
55 | addSubview(overlay)
56 | if animated {
57 | UIView.animate(withDuration: 0.1) {
58 | overlay.alpha = 0.15
59 | }
60 | }
61 | if let delay = delay {
62 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
63 | if animated {
64 | UIView.animate(withDuration: 0.1) {
65 | overlay.alpha = 0.15
66 | } completion: { _ in
67 | overlay.removeFromSuperview()
68 | }
69 | } else {
70 | overlay.removeFromSuperview()
71 | }
72 | }
73 | }
74 | }
75 |
76 | func removeHighlightOverlay(animated: Bool = true) {
77 | if let overlay = find(subview: 999) {
78 | overlay.removeFromSuperview()
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Extensions/UIViewController+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Extensions.swift
3 | // UIViewController+Extensions
4 | //
5 | // Created by Michael Long on 8/10/21.
6 | //
7 |
8 | import UIKit
9 |
10 | //extension UIStoryboard {
11 | // static func instantiate(_ identifier: String, storyboard: String, configure: (_ vc: VC) -> Void) -> UIViewController {
12 | // let sb = UIStoryboard(name: storyboard, bundle: .main)
13 | // let vc = sb.instantiateViewController(identifier: identifier)
14 | // if let vc = vc as? VC {
15 | // configure(vc)
16 | // } else if let nc = vc as? UINavigationController, let vc = nc.topViewController as? VC {
17 | // configure(vc)
18 | // }
19 | // return vc
20 | // }
21 | //}
22 | //
23 | //extension UIViewController {
24 | //
25 | // func push(_ identifier: String, storyboard: String, configure: (_ vc: VC) -> Void) {
26 | // let vc = UIStoryboard.instantiate(identifier, storyboard: storyboard, configure: configure)
27 | // navigationController?.pushViewController(vc, animated: true)
28 | // }
29 | //
30 | // func present(_ identifier: String, storyboard: String, style: UIModalPresentationStyle = .pageSheet, configure: (_ vc: VC) -> Void) {
31 | // let vc = UIStoryboard.instantiate(identifier, storyboard: storyboard, configure: configure)
32 | // vc.modalPresentationStyle = style
33 | // navigationController?.present(vc, animated: true)
34 | // }
35 | //
36 | //}
37 | //
38 | //extension UIViewController {
39 | //
40 | // func push(_ vc: VC, configure: ((_ vc: VC) -> Void)? = nil) {
41 | // configure?(vc)
42 | // navigationController?.pushViewController(vc, animated: true)
43 | // }
44 | //
45 | // func present(_ vc: VC, style: UIModalPresentationStyle = .pageSheet, configure: ((_ vc: VC) -> Void)? = nil) {
46 | // configure?(vc)
47 | // vc.modalPresentationStyle = style
48 | // navigationController?.present(vc, animated: true)
49 | // }
50 | //
51 | // func present(wrapped vc: VC, style: UIModalPresentationStyle = .pageSheet, configure: ((_ vc: VC) -> Void)? = nil) {
52 | // let nc = UINavigationController(rootViewController: vc)
53 | // configure?(vc)
54 | // vc.modalPresentationStyle = style
55 | // navigationController?.present(nc, animated: true)
56 | // }
57 | //
58 | //}
59 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Fields/MaskedTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | class MaskedTextField: BuilderInternalTextField {
12 |
13 | var maskFormat: String? {
14 | didSet {
15 | if let maskFormat = maskFormat {
16 | self.behavior = MaskedTextFieldBehavior(maskFormat)
17 | } else {
18 | self.behavior = nil
19 | }
20 | }
21 | }
22 |
23 | }
24 |
25 | class MaskedTextFieldBehavior: TextFieldBehaviorDelegate {
26 |
27 | var maskFormat: String?
28 |
29 | init(_ maskFormat: String?) {
30 | self.maskFormat = maskFormat
31 | }
32 |
33 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
34 |
35 | let oldText = (textField.text ?? "") as NSString
36 | let newText: String = oldText.replacingCharacters(in: range, with: string)
37 |
38 | // masking?
39 | if let mask = maskFormat, !mask.isEmpty {
40 | let maskedText = newText.formatDigitsWith(mask: mask)
41 | if oldText as String != maskedText {
42 | textField.text = maskedText
43 | textField.sendActions(for: .editingChanged)
44 | }
45 | return false
46 | }
47 |
48 | return true
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Fields/MaxWidthTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 |
9 | import UIKit
10 |
11 | class MaxWidthTextField: BuilderInternalTextField {
12 |
13 | var maxWidth: Int = 0 {
14 | didSet {
15 | if maxWidth > 0 {
16 | self.behavior = MaxWidthTextFieldBehavior(maxWidth)
17 | } else {
18 | self.behavior = nil
19 | }
20 | }
21 | }
22 |
23 | }
24 |
25 | class MaxWidthTextFieldBehavior: TextFieldBehaviorDelegate {
26 |
27 | var maxWidth: Int = 0
28 |
29 | init(_ maxWidth: Int) {
30 | self.maxWidth = maxWidth
31 | }
32 |
33 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
34 |
35 | let oldText = (textField.text ?? "") as NSString
36 | let newText = oldText.replacingCharacters(in: range, with: string) as NSString
37 |
38 | if maxWidth > 0 && newText.length > maxWidth {
39 | return false
40 | }
41 |
42 | return true
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Fields/NextAccessoryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class NextAccessoryView: UIToolbar {
11 |
12 | weak var textField: UITextField?
13 |
14 | static func add(to field: UITextField) {
15 | _ = NextAccessoryView(field)
16 | }
17 |
18 | init(_ textField: UITextField) {
19 | self.textField = textField
20 | super.init(frame: CGRect(x: 0, y: 0, width: 320, height: 50))
21 |
22 | barStyle = .default
23 | barTintColor = .lightGray
24 | tintColor = .black
25 |
26 | let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil)
27 | let title = textField.returnKeyType == .next ? "Next" : "Done"
28 | let next: UIBarButtonItem = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(nextButtonAction))
29 | next.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .normal)
30 |
31 | var items = [UIBarButtonItem]()
32 | items.append(flexSpace)
33 | items.append(next)
34 |
35 | self.items = items
36 | sizeToFit()
37 |
38 | textField.inputAccessoryView = self
39 | }
40 |
41 | required init?(coder aDecoder: NSCoder) {
42 | super.init(coder: aDecoder)
43 | }
44 |
45 | @objc func nextButtonAction() {
46 | textField?.resignFirstResponder()
47 | textField?.sendActions(for: .editingDidEndOnExit)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Fields/TextFieldBehaviorAggregator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class TextFieldBehaviorAggregator: TextFieldBehaviorDelegate {
11 |
12 | private let behaviorList: [TextFieldBehaviorDelegate]
13 |
14 | init(behaviors: TextFieldBehaviorDelegate...) {
15 | self.behaviorList = behaviors
16 | }
17 |
18 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
19 | for behavior in self.behaviorList {
20 | if !behavior.textField(textField, shouldChangeCharactersIn: range, replacementString: string) {
21 | return false
22 | }
23 | }
24 | return true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/APIError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIError.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/18/21.
6 | //
7 |
8 | import Foundation
9 |
10 | enum APIError: Error {
11 | case application
12 | case connection
13 | case server
14 | case unknown
15 | case unexpected
16 | }
17 |
18 | extension APIError: CustomStringConvertible {
19 | var description: String {
20 | switch self {
21 | case .connection:
22 | return "Unable to connect to server at this time."
23 | case .unknown:
24 | return "An unknown error occurred."
25 | default:
26 | return "An unexpected error occurred."
27 | }
28 | }
29 | }
30 |
31 | extension APIError: LocalizedError {
32 | public var errorDescription: String? {
33 | return self.description
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Core/BaseSessionManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseSessionManager.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/19/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class BaseSessionManager: ClientSessionManager {
12 |
13 | var base: String
14 | var session: URLSession
15 |
16 | init(base: String, session: URLSession = URLSession.shared) {
17 | self.base = base
18 | self.session = session
19 | }
20 |
21 | func request(forURL url: URL?) -> URLRequest {
22 | URLRequest(url: url ?? URL(string: base)!)
23 | }
24 |
25 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
26 | session.dataTask(with: request, completionHandler: completionHandler)
27 | }
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Core/ClientSessionManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientSessionManager.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/19/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 |
13 | protocol ClientSessionManager: AnyObject {
14 |
15 | func builder(forURL url: URL?) -> ClientRequestBuilder
16 |
17 | func request(forURL url: URL?) -> URLRequest
18 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask?
19 |
20 | func interceptor(_ interceptor: ClientSessionManagerInterceptor) -> ClientSessionManager
21 | func configure(_ handler: (_ interceptor: T) -> Void)
22 |
23 | }
24 |
25 | extension ClientSessionManager {
26 |
27 | func interceptor(_ parent: ClientSessionManagerInterceptor) -> ClientSessionManager {
28 | parent.parentSessionManager = self
29 | return parent
30 | }
31 |
32 | func configure(_ handler: (_ manager: T) -> Void) {
33 | if let manager = self as? T {
34 | handler(manager)
35 | }
36 | }
37 |
38 | func builder() -> ClientRequestBuilder {
39 | ClientRequestBuilder(self)
40 | }
41 |
42 | func builder(forURL url: URL?) -> ClientRequestBuilder {
43 | ClientRequestBuilder(self, forURL: url)
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Core/ClientSessionManagerInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientSessionManagerInterceptor.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/19/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 |
13 | protocol ClientSessionManagerInterceptor: ClientSessionManager {
14 | var parentSessionManager: ClientSessionManager! { get set }
15 | }
16 |
17 | extension ClientSessionManagerInterceptor {
18 |
19 | func configure(_ handler: (_ manager: T) -> Void) {
20 | if let manager = self as? T {
21 | handler(manager)
22 | return
23 | }
24 | return parentSessionManager.configure(handler)
25 | }
26 |
27 | func request(forURL url: URL?) -> URLRequest {
28 | return parentSessionManager.request(forURL: url)
29 | }
30 |
31 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
32 | return parentSessionManager.execute(request: request, completionHandler: completionHandler)
33 | }
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Extensions/ClientRequestBuilder+Combine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientRequestBuilder+RX.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/20/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | //extension ClientRequestBuilder {
13 | //
14 | // struct CombineExtension {
15 | //
16 | // let builder: ClientRequestBuilder
17 | //
18 | // func execute(_ method: HTTPMethod = .get) -> AnyPublisher {
19 | // send(method)
20 | // .decode(type: ResultType.self, decoder: JSONDecoder())
21 | // .mapError { (error) -> APIError in
22 | // error as? APIError ?? .application
23 | // }
24 | // .eraseToAnyPublisher()
25 | // }
26 | //
27 | // func execute(_ method: HTTPMethod = .get) -> AnyPublisher {
28 | // let publisher = PassthroughSubject()
29 | // let task = builder
30 | // .method(method)
31 | // .send { (data, _, error) in
32 | // if let data = data {
33 | // publisher.send(data)
34 | // } else {
35 | // publisher.send(completion: .failure(error as? APIError ?? .unexpected))
36 | // }
37 | // }
38 | // task.resume()
39 | // return publisher.eraseToAnyPublisher()
40 | // }
41 | //
42 | // }
43 | //
44 | // var combine: CombineExtension {
45 | // CombineExtension(builder: self)
46 | // }
47 | //
48 | //}
49 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Extensions/ClientRequestBuilder+Result.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientRequestBuilder+Result.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/20/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | extension ClientRequestBuilder {
13 |
14 | // struct ResultExtension {
15 | //
16 | // let builder: ClientRequestBuilder
17 | //
18 | // func execute(_ method: HTTPMethod, completionHandler: @escaping (_ result: Result) -> Void) -> Void {
19 | // builder
20 | // .method(method)
21 | // .execute { (data, _, error) in
22 | // if let data = data, let result = try? JSONDecoder().decode(ResultType.self, from: data) {
23 | // completionHandler(.success(result))
24 | // } else {
25 | // completionHandler(.failure(error as? APIError ?? .application))
26 | // }
27 | // }
28 | // .resume()
29 | // }
30 | //
31 | // func execute(_ method: HTTPMethod, completionHandler: @escaping (_ result: Result) -> Void) -> Void {
32 | // builder
33 | // .method(method)
34 | // .send { (data, _, error) in
35 | // if let data = data {
36 | // completionHandler(.success(data))
37 | // } else {
38 | // completionHandler(.failure(error as? APIError ?? .application))
39 | // }
40 | // }
41 | // .resume()
42 | // }
43 | //
44 | // }
45 | //
46 | // var result: ResultExtension {
47 | // ResultExtension(builder: self)
48 | // }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Extensions/ClientRequestBuilder+RxSwift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientRequestBuilder+RX.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/20/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | extension ClientRequestBuilder {
13 |
14 | func get() -> Single {
15 | execute(.get)
16 | }
17 |
18 | func post() -> Single {
19 | execute(.post)
20 | }
21 |
22 | func put() -> Single {
23 | execute(.put)
24 | }
25 |
26 | func create() -> Single {
27 | execute(.create)
28 | }
29 |
30 | func delete() -> Single {
31 | execute(.delete)
32 | }
33 |
34 | func execute(_ method: HTTPMethod = .get) -> Single {
35 | execute(method)
36 | .map { (data, response, error) -> Data in
37 | if let error = error {
38 | throw error //as? APIError ?? .unexpected
39 | } else {
40 | return data ?? Data()
41 | }
42 | }
43 | }
44 |
45 | func execute(_ method: HTTPMethod = .get) -> Single<(Data?, URLResponse?, Error?)> {
46 | Single.create { (single) -> Disposable in
47 | let task = self
48 | .method(method)
49 | .execute { (data, response, error) in
50 | single(.success((data, response, error)))
51 | }
52 | task?.resume()
53 | return Disposables.create { task?.cancel() }
54 | }
55 | }
56 |
57 | // struct RxExtension {
58 | //
59 | // let builder: ClientRequestBuilder
60 | //
61 | // func send(_ method: HTTPMethod = .get) -> Single {
62 | // Single.create { (single) -> Disposable in
63 | // let task = builder
64 | // .method(method)
65 | // .send { (data, _, error) in
66 | // if let data = data {
67 | // single(.success(data))
68 | // } else {
69 | // single(.failure(error as? APIError ?? .unexpected))
70 | // }
71 | // }
72 | // task.resume()
73 | // return Disposables.create { task.cancel() }
74 | // }
75 | // }
76 | //
77 | // }
78 | //
79 | // var rx: RxExtension {
80 | // RxExtension(builder: self)
81 | // }
82 |
83 | }
84 |
85 |
86 | public extension Single where Element == Data, Trait == SingleTrait {
87 | func decode(type: Item.Type, decoder: Decoder) -> Single- {
88 | map { try decoder.decode(type, from: $0) }
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/MockDelayInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockDelayInterceptor.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/13/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MockDelayInterceptor: ClientSessionManagerInterceptor {
12 |
13 | var parentSessionManager: ClientSessionManager!
14 |
15 | static var delay: Double = 0.2
16 |
17 | init(delay: Double = 0.2) {
18 | MockDelayInterceptor.delay = delay
19 | }
20 |
21 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
22 | let interceptor: (Data?, URLResponse?, Error?) -> Void = { (data, response, error) in
23 | if MockDelayInterceptor.delay > 0 {
24 | DispatchQueue.main.asyncAfter(deadline: .now() + MockDelayInterceptor.delay) {
25 | completionHandler(data, response, error)
26 | }
27 | } else {
28 | completionHandler(data, response, error)
29 | }
30 | }
31 | return parentSessionManager.execute(request: request, completionHandler: interceptor)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/MockSessionManagerWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientSessionManager.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/19/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class MockSessionManagerWrapper: ClientSessionManagerWrapper {
12 |
13 | var wrappedSessionManager: ClientSessionManager!
14 | var session = URLSession.mock
15 |
16 | func request(forURL url: URL?) -> URLRequest {
17 | wrappedSessionManager.request(forURL: url)
18 | }
19 |
20 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
21 | if let path = request.url?.path, MockURLProtocol.responses[path] != nil {
22 | return session.dataTask(with: request, completionHandler: completionHandler)
23 | }
24 | return wrappedSessionManager.execute(request: request, completionHandler: completionHandler)
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/SSOAuthenticationInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interceptors.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/13/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | class SSOAuthenticationInterceptor: ClientSessionManagerInterceptor {
13 |
14 | var parentSessionManager: ClientSessionManager!
15 |
16 | var token: String?
17 |
18 | init() {}
19 |
20 | func request(forURL url: URL?) -> URLRequest {
21 | var request = parentSessionManager.request(forURL: url)
22 | if let token = token {
23 | request.setValue(token, forHTTPHeaderField: "Authorization")
24 | print("Added authentication headers")
25 | }
26 | return request
27 | }
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/SessionLoggingInterceptorr.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interceptors.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/13/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 |
13 | class SessionLoggingInterceptor: ClientSessionManagerInterceptor {
14 |
15 | var parentSessionManager: ClientSessionManager!
16 |
17 | init() {}
18 |
19 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
20 | print("REQ: \(request)")
21 | let interceptor: (Data?, URLResponse?, Error?) -> Void = { (data, response, error) in
22 | let status: Int = (response as? HTTPURLResponse)?.statusCode ?? 999
23 | print("\(status): \(request)")
24 | completionHandler(data, response, error)
25 | }
26 | return parentSessionManager.execute(request: request, completionHandler: interceptor)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/StandardHeadersInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interceptors.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/13/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | class StandardHeadersInterceptor: ClientSessionManagerInterceptor {
13 |
14 | var parentSessionManager: ClientSessionManager!
15 |
16 | init() {}
17 |
18 | func request(forURL url: URL?) -> URLRequest {
19 | var request = parentSessionManager.request(forURL: url)
20 | request.setValue("something", forHTTPHeaderField: "header")
21 | print("Added standard headers")
22 | return request
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/Inteceptors/StatusErrorMappingInterceptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interceptors.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/13/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | class StatusErrorMappingInterceptor: ClientSessionManagerInterceptor {
13 |
14 | var parentSessionManager: ClientSessionManager!
15 |
16 | init() {}
17 |
18 | func execute(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask? {
19 | let interceptor: (Data?, URLResponse?, Error?) -> Void = { (data, response, error) in
20 | let status: Int = (response as? HTTPURLResponse)?.statusCode ?? 999
21 | switch status {
22 | case 404:
23 | completionHandler(data, response, APIError.unknown)
24 | case 500:
25 | completionHandler(data, response, APIError.unknown)
26 | default:
27 | completionHandler(data, response, error)
28 | }
29 | }
30 | return parentSessionManager.execute(request: request, completionHandler: interceptor)
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Networking/_Networking+Injection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Netowrking+Injection.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import UIKit
9 | import Factory
10 |
11 | extension Container {
12 | enum APIMode: String {
13 | case api
14 | case mock
15 | case test
16 | }
17 |
18 | #if MOCK
19 | static var apiMode = APIMode.mock
20 | #else
21 | static var apiMode = APIMode.api
22 | #endif
23 | }
24 |
25 | extension Container {
26 |
27 | static let clientSessionManager = Factory(scope: .singleton) {
28 | switch apiMode {
29 | case .api:
30 | return clientSessionManagerAPI()
31 | case .mock:
32 | return clientSessionManagerMock()
33 | case .test:
34 | return clientSessionManagerTest()
35 | }
36 | }
37 |
38 | private static let urlSession = Factory(scope: .singleton) {
39 | let configuration = URLSessionConfiguration.ephemeral
40 | configuration.protocolClasses = [MockURLProtocol.self] // add mock protocol handler
41 | return URLSession(configuration: configuration)
42 | }
43 |
44 | private static let clientSessionManagerAPI = Factory {
45 | BaseSessionManager(base: "https://randomuser.me/api", session: urlSession())
46 | .interceptor(StatusErrorMappingInterceptor())
47 | .interceptor(StandardHeadersInterceptor())
48 | .interceptor(SSOAuthenticationInterceptor())
49 | .interceptor(SessionLoggingInterceptor())
50 | }
51 |
52 | private static let clientSessionManagerMock = Factory {
53 | MockURLProtocol.set(forPath: "/api/portraits/men/11.jpg", status: 200, data: UIImage(named: "User-JQ")?.pngData())
54 | MockURLProtocol.set(forPath: "/api/portraits/med/men/11.jpg", status: 200, data: UIImage(named: "User-JQ")?.pngData())
55 | MockURLProtocol.set(forPath: "User-JQ", status: 200, data: UIImage(named: "User-JQ")?.pngData())
56 | MockURLProtocol.set(forPath: "User-TS", status: 404)
57 | MockURLProtocol.set(forPath: "/test", status: 200, json: "{\"name\":\"Michael\":}")
58 | MockURLProtocol.setupDefaultJSONBundleHandler()
59 |
60 | return BaseSessionManager(base: "/", session: urlSession())
61 | .interceptor(StatusErrorMappingInterceptor())
62 | .interceptor(StandardHeadersInterceptor())
63 | .interceptor(SSOAuthenticationInterceptor())
64 | .interceptor(SessionLoggingInterceptor())
65 | .interceptor(MockDelayInterceptor(delay: 0.3))
66 | }
67 |
68 | private static let clientSessionManagerTest = Factory {
69 | BaseSessionManager(base: "/", session: urlSession())
70 | .interceptor(StatusErrorMappingInterceptor())
71 | .interceptor(StandardHeadersInterceptor())
72 | .interceptor(SSOAuthenticationInterceptor())
73 | .interceptor(SessionLoggingInterceptor())
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Styles/ButtonView+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonView+Styles.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 9/8/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | struct StyleButtonFilled: BuilderStyle {
12 | public func apply(to view: ButtonView.Base) {
13 | With(view)
14 | .cornerRadius(8)
15 | .color(.white)
16 | .backgroundColor(.blue, for: .normal)
17 | .backgroundColor(.blue.darker(), for: .highlighted)
18 | .backgroundColor(.gray, for: .disabled)
19 | .padding(10)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Styles/LabelView+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelView+Styles.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 9/8/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | extension UIColor {
12 | static let standardAccentColor = UIColor(red: 1/255, green: 50/255, blue: 159/255, alpha: 1)
13 | }
14 |
15 | struct StyleLabelAccentTitle: BuilderStyle {
16 | public func apply(to view: LabelView.Base) {
17 | With(view)
18 | .font(.footnote)
19 | .color(.standardAccentColor)
20 | .with {
21 | $0.text = $0.text?.uppercased()
22 | }
23 | }
24 | }
25 |
26 | struct StyleLabelFootnote: BuilderStyle {
27 | public func apply(to view: LabelView.Base) {
28 | With(view)
29 | .font(.footnote)
30 | .color(.secondaryLabel)
31 | .numberOfLines(0)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Shared/Styles/TextField+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextField+Styles.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 9/8/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | struct StyleStandardMetaTextField: BuilderStyle {
12 | public func apply(to view: MetaTextField.Base) {
13 | With(view)
14 | .autocorrectionType(.no)
15 | .set(keyPath: \.clearButtonMode, value: .whileEditing)
16 | .height(34)
17 | .tintColor(.red)
18 | .with {
19 | $0.bottomLineColor = .standardAccentColor
20 | $0.selectedBottomLineColor = .standardAccentColor
21 | }
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Test/GeneralTestView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeneralTestView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/6/22.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import RxSwift
11 | import Factory
12 |
13 | class GeneralTestViewController: UIViewController {
14 |
15 | let disposeBag = DisposeBag()
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 | view.backgroundColor = .systemBackground
20 | view.embed(content())
21 |
22 | print(UITraitCollection.current.userInterfaceStyle == .light ? "Light Mode" : "Dark Mode")
23 |
24 | }
25 |
26 | func content() -> View {
27 | VStackView {
28 | VerticalScrollView {
29 | VStackView {
30 | ContainerView {
31 | LabelView("This is a test!")
32 | .backgroundColor(.red)
33 | .color(.white)
34 | // .margins(20) // only within container
35 | .padding(10)
36 | }
37 | .padding(20)
38 | .backgroundColor(.yellow)
39 | .height(100)
40 |
41 | SpacerView()
42 | }
43 | .padding(20)
44 | }
45 | ContainerView {
46 | MyButtonView()
47 | .safeArea(true)
48 | }
49 | }
50 | }
51 |
52 | }
53 |
54 | struct MyButtonView: ViewBuilder {
55 | var body: View {
56 | ViewModifier(DLSInsetFilledActionButton("Submit"))
57 | .height(58)
58 | .onTap { context in
59 | print("tapped")
60 | }
61 |
62 | }
63 | }
64 |
65 |
66 | class DLSInsetFilledActionButton: UIButton {
67 |
68 | var backgroundView: UIView!
69 |
70 | public init(_ title: String = "") {
71 | super.init(frame: .zero)
72 | self.setTitle(title, for: .normal)
73 | setupCommon()
74 | }
75 |
76 | public override init(frame: CGRect) {
77 | super.init(frame: frame)
78 | setupCommon()
79 | }
80 |
81 | required init?(coder: NSCoder) {
82 | super.init(coder: coder)
83 | setupCommon()
84 | }
85 |
86 | func setupCommon() {
87 | backgroundView = BuilderDemo.with(UIView()) {
88 | $0.translatesAutoresizingMaskIntoConstraints = false
89 | $0.backgroundColor = .red
90 | $0.isUserInteractionEnabled = false
91 | $0.layer.cornerRadius = 2.0
92 | }
93 | insertConstrainedSubview(backgroundView, at: 0, position: .fill, padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8))
94 | titleLabel?.font = .preferredFont(forTextStyle: .headline)
95 | backgroundColor = .systemBackground
96 | setTitleColor(.systemBackground, for: .normal)
97 | }
98 |
99 | override var isHighlighted: Bool {
100 | didSet {
101 | UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
102 | self.backgroundView.backgroundColor = self.isHighlighted ? .red.darker() : .red
103 | }, completion: nil)
104 | }
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Test/TestViews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestViews.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | struct SomeViewBuilder2: ViewBuilder {
12 | var body: View {
13 | LabelView("This is some text!")
14 | .hidden(false)
15 | .font(.preferredFont(forTextStyle: .largeTitle))
16 | .with {
17 | $0.heightAnchor.constraint(equalToConstant: 200).isActive = true
18 | $0.widthAnchor.constraint(equalToConstant: 350).isActive = true
19 | }
20 | }
21 | }
22 |
23 | struct AnotherViewBuilder2: ViewBuilder {
24 | var body: View {
25 | SomeViewBuilder1()
26 | .hidden(true)
27 | }
28 | }
29 |
30 | struct AnotherViewBuilder3: ViewBuilder {
31 | var body: View {
32 | UITextView()
33 | .hidden(true)
34 | .with {
35 | $0.heightAnchor.constraint(equalToConstant: 200).isActive = true
36 | $0.widthAnchor.constraint(equalToConstant: 350).isActive = true
37 | }
38 | }
39 | }
40 |
41 | struct SomeViewBuilder1: ViewBuilder {
42 | var body: View {
43 | LabelView("This is VB1 Text!")
44 | }
45 | }
46 |
47 | struct AnotherViewBuilder1: ViewBuilder {
48 | var body: View {
49 | SomeViewBuilder1()
50 | }
51 | }
52 |
53 | struct AnotherViewBuilder4: ViewBuilder {
54 | var body: View {
55 | UITextView()
56 | }
57 | }
58 |
59 | struct AnotherViewBuilder5: ViewBuilder {
60 | var body: View {
61 | SomeViewBuilder1()
62 | .hidden(true)
63 | .with {
64 | $0.heightAnchor.constraint(equalToConstant: 200).isActive = true
65 | $0.widthAnchor.constraint(equalToConstant: 350).isActive = true
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // BuilderDemo
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import UIKit
9 |
10 | class ViewController: UIViewController {
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | // Do any additional setup after loading the view.
15 | }
16 |
17 |
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Views/DLSCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DLSCardView.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/22/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 |
11 | struct DLSCardView: ViewBuilder {
12 |
13 | let content: () -> ViewConvertable
14 |
15 | init(@ViewResultBuilder _ content: @escaping () -> ViewConvertable) {
16 | self.content = content
17 | }
18 |
19 | var body: View {
20 | ContainerView {
21 | ContainerView {
22 | content()
23 | }
24 | .roundedCorners(radius: 16, corners: [.layerMinXMinYCorner])
25 | .clipsToBounds(true)
26 | }
27 | .backgroundColor(.secondarySystemGroupedBackground)
28 | .roundedCorners(radius: 16, corners: [.layerMinXMinYCorner])
29 | .shadow(color: .black, radius: 2, opacity: 0.25, offset: CGSize(width: 0, height: 2))
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Views/MetaTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetaTextField.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 12/7/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import RxSwift
11 | import RxRelay
12 |
13 | public struct MetaTextField: ModifiableView {
14 |
15 | public let modifiableView = Modified(BuilderInternalTextField()) {
16 | $0.setContentCompressionResistancePriority(.required, for: .vertical)
17 | $0.font = UIFont.preferredFont(forTextStyle: .body)
18 | $0.bottomLineColor = .gray
19 | $0.selectedBottomLineColor = .blue
20 | $0.errorColor = .red
21 | }
22 |
23 | // lifecycle
24 | public init() {
25 | // with later binding
26 | }
27 |
28 | public init(_ text: String?) {
29 | modifiableView.text = text
30 | }
31 |
32 | public init(_ binding: Binding) where Binding.T == String? {
33 | text(bind: binding)
34 | }
35 |
36 | public init(_ binding: Binding) where Binding.T == String {
37 | text(bidirectionalBind: binding)
38 | }
39 |
40 | public init(_ binding: Binding) where Binding.T == String? {
41 | text(bidirectionalBind: binding)
42 | }
43 |
44 | }
45 |
46 | extension ModifiableView where Base: BuilderInternalTextField {
47 |
48 | @discardableResult
49 | func error(bind binding: Binding) -> ViewModifier where Binding.T == String? {
50 | ViewModifier(modifiableView) {
51 | binding.asObservable()
52 | .skip(1)
53 | .observe(on: ConcurrentMainScheduler.instance)
54 | .map { $0 ?? "" }
55 | .bind(to: $0.errorText)
56 | .disposed(by: $0.rxDisposeBag)
57 | }
58 | }
59 |
60 | @discardableResult
61 | func maxWidth(_ width: Int) -> ViewModifier {
62 | ViewModifier(modifiableView) { $0.behavior = MaxWidthTextFieldBehavior(width) }
63 | }
64 |
65 | @discardableResult
66 | func mask(_ mask: String) -> ViewModifier {
67 | ViewModifier(modifiableView) { $0.behavior = MaskedTextFieldBehavior(mask) }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Views/StandardEmptyPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardEmptyPage.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 3/1/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | struct StandardEmptyPage: ViewBuilder {
14 |
15 | let message: String
16 |
17 | var body: View {
18 | return VerticalScrollView {
19 | VStackView {
20 | LabelView(message)
21 | .color(.systemGray)
22 | SpacerView()
23 | }
24 | .padding(16)
25 | }
26 | .backgroundColor(.systemBackground)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Views/StandardErrorPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardErrorPage.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 3/1/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | struct StandardErrorPage: ViewBuilder {
14 |
15 | let error: String
16 |
17 | var body: View {
18 | return VerticalScrollView {
19 | VStackView {
20 | LabelView(error)
21 | .color(.red)
22 | SpacerView()
23 | }
24 | .padding(16)
25 | }
26 | .backgroundColor(.systemBackground)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/Views/StandardLoadingPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardLoadingPage.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 3/1/21.
6 | //
7 |
8 | import UIKit
9 | import Builder
10 | import Factory
11 | import RxSwift
12 |
13 | struct StandardLoadingPage: ViewBuilder {
14 |
15 | var body: View {
16 | return VerticalScrollView {
17 | VStackView {
18 | With(UIActivityIndicatorView()) {
19 | $0.color = .systemGray
20 | $0.startAnimating()
21 | }
22 | SpacerView()
23 | }
24 | .padding(16)
25 | }
26 | .backgroundColor(.systemBackground)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/_Main+Injection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Main+Injection.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 1/17/21.
6 | //
7 |
8 | import Foundation
9 | import Factory
10 |
11 | extension Container {
12 | static let mainViewModel = Factory(scope: .shared) {
13 | MainViewModel()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/json/get.json:
--------------------------------------------------------------------------------
1 | {"results":[{"gender":"M","id":{"name":"21","value":"21"},"email":"tomswift@swiftenterprises.com","phone":"402-555-9999","name":{"title":"Mr.","first":"Tom","last":"Swift"},"nat":"US"},{"gender":"M","phone":"303-555-8888","nat":"US","id":{"name":"21","value":"21"},"picture":{"large":"User-JQ","thumbnail":"User-JQ","medium":"User-JQ"},"email":"jquest@quest.com","name":{"title":"Mr.","first":"Jonny","last":"Quest"}}]}
2 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemo/json/get_users.json:
--------------------------------------------------------------------------------
1 | {"results":[{"gender":"M","id":{"name":"21","value":"21"},"email":"tomswift@swiftenterprises.com","phone":"402-555-9999","name":{"title":"Mr.","first":"Tom","last":"Swift"},"nat":"US"},{"gender":"M","phone":"303-555-8888","nat":"US","id":{"name":"21","value":"21"},"picture":{"large":"User-JQ","thumbnail":"User-JQ","medium":"User-JQ"},"email":"jquest@quest.com","name":{"title":"Mr.","first":"Jonny","last":"Quest"}}]}
2 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoTests/BuilderDemoTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderDemoTests.swift
3 | // BuilderDemoTests
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import XCTest
9 | @testable import BuilderDemo
10 |
11 | class BuilderDemoTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoTests/Factory+XCTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Factory+XCTest.swift
3 | // BuilderTests
4 | //
5 | // Created by Michael Long on 2/28/21.
6 | //
7 |
8 | import XCTest
9 | import Factory
10 | import RxSwift
11 | import RxCocoa
12 |
13 | @testable import BuilderDemo
14 |
15 | extension Container {
16 |
17 | static func resetUnitTestRegistrations() {
18 |
19 | userServiceType.register { MockUserService() }
20 | userImageCache.register { UserImageCache() } // use our own and not global
21 |
22 | MockDelayInterceptor.delay = 0.0
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoTests/TestExrtensionsSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModelSpec.swift
3 | // BuilderTests
4 | //
5 | // Created by Michael Long on 2/22/21.
6 | //
7 |
8 | import XCTest
9 | import Factory
10 | import RxSwift
11 | import RxCocoa
12 |
13 | @testable import BuilderDemo
14 |
15 | class TestExtensionsSpec: XCTestCase {
16 |
17 | func testTestFunctions() throws {
18 | let relay = BehaviorRelay(value: "initial")
19 |
20 | DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) {
21 | relay.accept("updated")
22 | }
23 |
24 | test("Test relay current value", value: relay.asObservable()) {
25 | $0 == "initial"
26 | }
27 |
28 | test("Test relay eventual value", value: relay.asObservable()) {
29 | $0 == "updated"
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoTests/UserImageCacheSpec.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserImageCacheSpec.swift
3 | // BuilderTests
4 | //
5 | // Created by Michael Long on 2/22/21.
6 | //
7 |
8 | import XCTest
9 | import Factory
10 | import RxSwift
11 | import RxCocoa
12 |
13 | @testable import BuilderDemo
14 |
15 | class UserImageCacheSpec: XCTestCase {
16 |
17 | override func setUp() {
18 | Container.Registrations.push()
19 | Container.resetUnitTestRegistrations()
20 | }
21 |
22 | override func tearDown() {
23 | Container.Registrations.pop()
24 | }
25 |
26 | func testImageThumbnails() throws {
27 | let cache = UserImageCache()
28 |
29 | let image1 = cache.thumbnail(forUser: User.mockJQ).asObservable()
30 | test("Test has thumbnail for user", value: image1) {
31 | $0 == UIImage(named: "User-JQ")
32 | }
33 |
34 | let image2 = cache.thumbnail(forUser: User.mockTS).asObservable()
35 | test("Test no image for user", value: image2) {
36 | $0 == nil
37 | }
38 | }
39 |
40 | func testImagePlaceholders() throws {
41 | let cache = UserImageCache()
42 |
43 | let image1 = cache.thumbnailOrPlaceholder(forUser: User.mockJQ).asObservable()
44 | test("Test has thumbnail for user", value: image1) {
45 | $0 == UIImage(named: "User-JQ")
46 | }
47 |
48 | let image2 = cache.thumbnailOrPlaceholder(forUser: User.mockTS).asObservable()
49 | test("Test no image for user", value: image2) {
50 | $0 == UIImage(named: "User-Unknown")
51 | }
52 | }
53 |
54 | func testThunbnailError() throws {
55 | Container.userServiceType.register { MockErrorUserService() as UserServiceType }
56 |
57 | let cache = UserImageCache()
58 | let image1 = cache.thumbnail(forUser: User.mockJQ).asObservable()
59 |
60 | test("Test receiving nothing on error", error: image1) {
61 | $0 is APIError
62 | }
63 |
64 | let image2 = cache.thumbnailOrPlaceholder(forUser: User.mockJQ).asObservable()
65 | test("Test receiving placeholder image on error", value: image2) {
66 | $0 == UIImage(named: "User-Unknown")
67 | }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoTests/XCTTest+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTTest+Extensions.swift
3 | // BuilderTests
4 | //
5 | // Created by Michael Long on 2/22/21.
6 | //
7 |
8 | import Foundation
9 | import XCTest
10 | import RxSwift
11 |
12 |
13 | extension XCTestCase {
14 |
15 | static var defaultDelay: Double = 2.0
16 |
17 | func test(_ description: String, delay: Double = defaultDelay, handler: (_ done: @escaping () -> Void) -> Void) {
18 | let expectation = XCTestExpectation(description: description)
19 | let done = { () in expectation.fulfill() }
20 | handler(done)
21 | wait(for: [expectation], timeout: delay)
22 | }
23 |
24 | }
25 |
26 | extension XCTestCase {
27 |
28 | func test(_ description: String, value o: Observable, delay: Double = defaultDelay, test: @escaping (_ value: T) -> Bool) {
29 | let expectation = XCTestExpectation(description: description)
30 | var success = false
31 | let disposable = o
32 | .subscribe(onNext: { (value) in
33 | guard success == false else { return }
34 | if test(value) {
35 | success = true
36 | expectation.fulfill()
37 | }
38 | }, onCompleted: {
39 | if success == false {
40 | XCTFail(description)
41 | expectation.fulfill()
42 | }
43 | })
44 | wait(for: [expectation], timeout: delay)
45 | disposable.dispose()
46 | }
47 |
48 | func test(_ description: String, completed o: Observable, delay: Double = defaultDelay) {
49 | let expectation = XCTestExpectation(description: description)
50 | let disposable = o
51 | .subscribe(onError: { (e) in
52 | XCTFail(description)
53 | expectation.fulfill()
54 | }, onCompleted: {
55 | expectation.fulfill()
56 | })
57 | wait(for: [expectation], timeout: delay)
58 | disposable.dispose()
59 | }
60 |
61 | func test(_ description: String, error o: Observable, delay: Double = defaultDelay, test: @escaping (_ error: Error) -> Bool) {
62 | let expectation = XCTestExpectation(description: description)
63 | let disposable = o
64 | .subscribe(onError: { (e) in
65 | if !test(e) {
66 | XCTFail(description)
67 | }
68 | expectation.fulfill()
69 | }, onCompleted: {
70 | XCTFail(description)
71 | expectation.fulfill()
72 | })
73 | wait(for: [expectation], timeout: delay)
74 | disposable.dispose()
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoUITests/BuilderDemoUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderDemoUITests.swift
3 | // BuilderDemoUITests
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import XCTest
9 |
10 | class BuilderDemoUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/BuilderDemo/BuilderDemoUITests/BuilderDemoUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderDemoUITestsLaunchTests.swift
3 | // BuilderDemoUITests
4 | //
5 | // Created by Michael Long on 6/18/22.
6 | //
7 |
8 | import XCTest
9 |
10 | class BuilderDemoUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Michael Long
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 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "rxswift",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/ReactiveX/RxSwift.git",
7 | "state" : {
8 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
9 | "version" : "6.5.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Builder",
8 | platforms: [
9 | .iOS(.v12)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "Builder",
15 | targets: ["Builder"]
16 | ),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.5.0"),
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "Builder",
27 | dependencies: ["RxSwift", .product(name: "RxCocoa", package: "RxSwift")]),
28 | .testTarget(
29 | name: "BuilderTests",
30 | dependencies: ["Builder", "RxSwift", .product(name: "RxCocoa", package: "RxSwift")]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/SampleDetail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/Builder/2445c003dec35fa06917898629f8d1cd91d99b8f/SampleDetail.png
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Button.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Button
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/29/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | public struct ButtonView: ModifiableView {
15 |
16 | public let modifiableView = Modified(UIButton()) {
17 | $0.setTitleColor(ViewBuilderEnvironment.defaultButtonColor ?? $0.tintColor, for: .normal)
18 | $0.titleLabel?.font = ViewBuilderEnvironment.defaultButtonFont ?? .preferredFont(forTextStyle: .headline)
19 | $0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
20 | $0.setContentCompressionResistancePriority(.required, for: .vertical)
21 | }
22 |
23 | // lifecycle
24 | public init(_ title: String? = nil) {
25 | modifiableView.setTitle(title, for: .normal)
26 | }
27 |
28 | public init(_ title: String? = nil, action: @escaping (_ context: ViewBuilderContext) -> Void) {
29 | modifiableView.setTitle(title, for: .normal)
30 | onTap(action)
31 | }
32 | }
33 |
34 |
35 | // Custom UIImageView modifiers
36 | extension ModifiableView where Base: UIButton {
37 |
38 | @discardableResult
39 | public func alignment(_ alignment: UIControl.ContentHorizontalAlignment) -> ViewModifier {
40 | ViewModifier(modifiableView, keyPath: \.contentHorizontalAlignment, value: alignment)
41 | }
42 |
43 | @discardableResult
44 | public func backgroundColor(_ color: UIColor, for state: UIControl.State) -> ViewModifier {
45 | ViewModifier(modifiableView) { $0.setBackgroundImage(UIImage(color: color), for: state) }
46 | }
47 |
48 | @discardableResult
49 | public func color(_ color: UIColor, for state: UIControl.State = .normal) -> ViewModifier {
50 | ViewModifier(modifiableView) { $0.setTitleColor(color, for: state) }
51 | }
52 |
53 | @discardableResult
54 | public func font(_ font: UIFont?) -> ViewModifier {
55 | ViewModifier(modifiableView) { $0.titleLabel?.font = font }
56 | }
57 |
58 | @discardableResult
59 | public func font(_ style: UIFont.TextStyle) -> ViewModifier {
60 | ViewModifier(modifiableView) { $0.titleLabel?.font = .preferredFont(forTextStyle: style) }
61 | }
62 |
63 | @discardableResult
64 | public func onTap(_ handler: @escaping (_ context: ViewBuilderContext) -> Void) -> ViewModifier {
65 | ViewModifier(modifiableView) { [unowned modifiableView] view in
66 | view.rx.tap
67 | .throttle(.milliseconds(300), latest: false, scheduler: MainScheduler.instance)
68 | .subscribe(onNext: { () in handler(ViewBuilderContext(view: modifiableView)) })
69 | .disposed(by: view.rxDisposeBag)
70 | }
71 | }
72 |
73 | }
74 |
75 | extension UIButton: ViewBuilderPaddable {
76 |
77 | public func setPadding(_ padding: UIEdgeInsets) {
78 | self.contentEdgeInsets = padding
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Container.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Build+Container.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 9/28/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | public struct ContainerView: ModifiableView {
13 |
14 | public var modifiableView = Modified(BuilderInternalContainerView(frame: .zero)) {
15 | $0.backgroundColor = .clear
16 | $0.isUserInteractionEnabled = true
17 | }
18 |
19 | public init(_ view: View? = nil) {
20 | modifiableView.views = view
21 | }
22 |
23 | public init(@ViewResultBuilder _ builder: () -> ViewConvertable) {
24 | modifiableView.views = builder()
25 | }
26 |
27 | }
28 |
29 | public typealias DynamicContainerView = ContainerView
30 |
31 | extension DynamicContainerView {
32 | public init(_ binding: Binding, @ViewResultBuilder _ builder: @escaping (_ value: Value) -> ViewConvertable)
33 | where Binding.T == Value {
34 | binding.asObservable()
35 | .subscribe(onNext: { [weak modifiableView] value in
36 | modifiableView?.transition(to: builder(value))
37 | })
38 | .disposed(by: modifiableView.rxDisposeBag)
39 | }
40 |
41 | }
42 |
43 | extension ModifiableView where Base: BuilderInternalContainerView {
44 |
45 | @discardableResult
46 | func defaultPosition(_ position: UIView.EmbedPosition) -> ViewModifier {
47 | ViewModifier(modifiableView, keyPath: \.position, value: position)
48 | }
49 |
50 | @discardableResult
51 | func defaultSafeArea(_ safeArea: Bool) -> ViewModifier {
52 | ViewModifier(modifiableView, keyPath: \.safeArea, value: safeArea)
53 | }
54 |
55 | }
56 |
57 | public class BuilderInternalContainerView: UIView, ViewBuilderEventHandling {
58 |
59 | fileprivate var views: ViewConvertable?
60 | fileprivate var padding: UIEdgeInsets = .zero
61 | fileprivate var position: EmbedPosition = .fill
62 | fileprivate var safeArea: Bool = false
63 |
64 | convenience public init(_ view: View?) {
65 | self.init(frame: .zero)
66 | self.views = view
67 | }
68 |
69 | convenience public init(@ViewResultBuilder _ builder: () -> ViewConvertable) {
70 | self.init(frame: .zero)
71 | self.views = builder()
72 | }
73 |
74 | public func transition(to views: ViewConvertable?) {
75 | if superview == nil {
76 | self.views = views
77 | } else if let view = views?.asViews().first {
78 | transition(to: view)
79 | }
80 | }
81 |
82 | override public func didMoveToSuperview() {
83 | embed(views?.asViews() ?? [], padding: padding, safeArea: safeArea)
84 | super.didMoveToSuperview()
85 | }
86 |
87 | override public func didMoveToWindow() {
88 | optionalBuilderAttributes()?.commonDidMoveToWindow(self)
89 | }
90 |
91 | }
92 |
93 | extension BuilderInternalContainerView: ViewBuilderPaddable {
94 |
95 | public func setPadding(_ padding: UIEdgeInsets) {
96 | self.padding = padding
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Controls.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Controls.swift
3 | //
4 | // Created by Michael Long on 11/25/21.
5 | // Copyright © 2021 Client Resources Inc. All rights reserved.
6 | //
7 |
8 | import UIKit
9 |
10 | extension ModifiableView where Base: UIControl {
11 |
12 | @discardableResult
13 | public func contentHorizontalAlignment(_ alignment: UIControl.ContentHorizontalAlignment) -> ViewModifier {
14 | ViewModifier(modifiableView, keyPath: \.contentHorizontalAlignment, value: alignment)
15 | }
16 |
17 | @discardableResult
18 | public func contentVerticalAlignment(_ alignment: UIControl.ContentVerticalAlignment) -> ViewModifier {
19 | ViewModifier(modifiableView, keyPath: \.contentVerticalAlignment, value: alignment)
20 | }
21 |
22 | @discardableResult
23 | public func enabled(_ enabled: Bool) -> ViewModifier {
24 | ViewModifier(modifiableView, keyPath: \.isEnabled, value: enabled)
25 | }
26 |
27 | @discardableResult
28 | public func highlighted(_ highlighted: Bool) -> ViewModifier {
29 | ViewModifier(modifiableView, keyPath: \.isEnabled, value: highlighted)
30 | }
31 |
32 | @discardableResult
33 | public func selected(_ selected: Bool) -> ViewModifier {
34 | ViewModifier(modifiableView, keyPath: \.isEnabled, value: selected)
35 | }
36 |
37 | }
38 |
39 | extension ModifiableView where Base: UIControl {
40 |
41 | @discardableResult
42 | public func enabled(bind binding: Binding) -> ViewModifier where Binding.T == Bool {
43 | ViewModifier(modifiableView, binding: binding) { $0.isEnabled = $1 }
44 | }
45 |
46 | @discardableResult
47 | public func highlighted(bind binding: Binding) -> ViewModifier where Binding.T == Bool {
48 | ViewModifier(modifiableView, binding: binding) { $0.isHighlighted = $1 }
49 | }
50 |
51 | @discardableResult
52 | public func selected(bind binding: Binding) -> ViewModifier where Binding.T == Bool {
53 | ViewModifier(modifiableView, binding: binding) { $0.isSelected = $1 }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Divider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Divider.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 11/9/21.
6 | //
7 |
8 | import UIKit
9 |
10 | // Custom builder fot UILabel
11 | public struct DividerView: ModifiableView {
12 |
13 | public let modifiableView = Modified(BuilderInternalDividerView(frame: .zero)) {
14 | let subview = UIView(frame: .zero)
15 | $0.addSubview(subview)
16 | subview.translatesAutoresizingMaskIntoConstraints = false
17 | if #available(iOS 13, *) {
18 | subview.backgroundColor = ViewBuilderEnvironment.defaultSeparatorColor ?? UIColor.secondaryLabel
19 | } else {
20 | subview.backgroundColor = ViewBuilderEnvironment.defaultSeparatorColor ?? UIColor.black
21 | }
22 | subview.topAnchor.constraint(equalTo: $0.topAnchor, constant: 4.0).isActive = true
23 | subview.leftAnchor.constraint(equalTo: $0.leftAnchor).isActive = true
24 | subview.rightAnchor.constraint(equalTo: $0.rightAnchor).isActive = true
25 | subview.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
26 | subview.bottomAnchor.constraint(equalTo: $0.bottomAnchor, constant: -4.5).isActive = true
27 | $0.backgroundColor = .clear
28 | }
29 |
30 | // lifecycle
31 | public init() {}
32 |
33 | }
34 |
35 | extension ModifiableView where Base: BuilderInternalDividerView {
36 |
37 | @discardableResult
38 | public func color(_ color: UIColor?) -> ViewModifier {
39 | ViewModifier(modifiableView) { $0.subviews.first?.backgroundColor = color }
40 | }
41 |
42 | }
43 |
44 | public class BuilderInternalDividerView: UIView {}
45 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Dynamic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+ForEach.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 9/30/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | public protocol AnyIndexableViewBuilder: ViewConvertable {
13 | var count: Int { get }
14 | var updated: Observable? { get }
15 | func view(at index: Int) -> View?
16 | }
17 |
18 | public struct StaticViewBuilder: AnyIndexableViewBuilder {
19 |
20 | private var views: [View]
21 |
22 | public init(@ViewResultBuilder _ views: () -> ViewConvertable) {
23 | self.views = views().asViews()
24 | }
25 |
26 | public var count: Int { views.count }
27 | public var updated: Observable?
28 |
29 | public func view(at index: Int) -> View? {
30 | guard views.indices.contains(index) else { return nil }
31 | return views[index]
32 | }
33 |
34 | public func asViews() -> [View] {
35 | views
36 | }
37 |
38 | }
39 |
40 | public class DynamicItemViewBuilder
- : AnyIndexableViewBuilder {
41 |
42 | public var items: [Item] {
43 | didSet {
44 | updatePublisher.onNext(())
45 | }
46 | }
47 |
48 | public var count: Int { items.count }
49 | public var updated: Observable? { updatePublisher }
50 |
51 | private let updatePublisher = PublishSubject()
52 | private let builder: (_ item: Item) -> View?
53 |
54 | public init(_ items: [Item]?, builder: @escaping (_ item: Item) -> View?) {
55 | self.items = items ?? []
56 | self.builder = builder
57 | }
58 |
59 | public func item(at index: Int) -> Item? {
60 | guard items.indices.contains(index) else { return nil }
61 | return items[index]
62 | }
63 |
64 | public func view(at index: Int) -> View? {
65 | guard let item = item(at: index) else { return nil }
66 | return builder(item)
67 | }
68 |
69 | public func asViews() -> [View] {
70 | return items.compactMap { self.builder($0) }
71 | }
72 |
73 | }
74 |
75 | public class DynamicObservableViewBuilder: AnyIndexableViewBuilder {
76 |
77 | public var count: Int { view == nil ? 0 : 1 }
78 | public var updated: Observable?
79 |
80 | private var view: View?
81 | private var disposeBag = DisposeBag()
82 |
83 | public init(_ observable: Observable, builder: @escaping (_ value: Value) -> View) {
84 | self.updated = observable
85 | .do(onNext: { [weak self] value in
86 | self?.view = builder(value)
87 | })
88 | .map { _ in () }
89 | }
90 |
91 | public func view(at index: Int) -> View? {
92 | guard index == 0 else { return nil }
93 | return view
94 | }
95 |
96 | public func asViews() -> [View] {
97 | guard let view = view else { return [] }
98 | return [view]
99 | }
100 |
101 | }
102 |
103 | public class DynamicValueViewBuilder: AnyIndexableViewBuilder {
104 |
105 | public var value: Value {
106 | didSet {
107 | updatePublisher.onNext(())
108 | }
109 | }
110 |
111 | public var count: Int = 1
112 | public var updated: Observable? { updatePublisher }
113 |
114 | private let updatePublisher = PublishSubject()
115 | private let builder: (_ value: Value) -> View
116 |
117 | public init(_ value: Value, builder: @escaping (_ value: Value) -> View) {
118 | self.value = value
119 | self.builder = builder
120 | }
121 |
122 | public func view(at index: Int) -> View? {
123 | guard index == 0 else { return nil }
124 | return builder(value)
125 | }
126 |
127 | public func asViews() -> [View] {
128 | return [builder(value)]
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+ForEach.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+ForEach.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 11/7/21.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct ForEach: ViewConvertable {
11 |
12 | private var views: [View] = []
13 |
14 | public init(_ count: Int, _ builder: (_ index: Int) -> View) {
15 | for index in 0..(_ array: [Element], _ builder: (_ element: Element) -> View) {
21 | views = array.map { builder($0) }
22 | }
23 |
24 | public func asViews() -> [View] {
25 | views
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Group.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Group.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 7/7/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Group: ViewConvertable {
11 |
12 | private var views: [View]
13 |
14 | public init(@ViewResultBuilder _ views: () -> ViewConvertable) {
15 | self.views = views().asViews()
16 | }
17 |
18 | func asViews() -> [View] {
19 | views
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Image.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Image.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/29/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | public struct ImageView: ModifiableView {
13 |
14 | public let modifiableView = Modified(UIImageView())
15 |
16 | // lifecycle
17 | public init(_ image: UIImage?) {
18 | modifiableView.image = image
19 | }
20 |
21 | public init(named name: String) {
22 | modifiableView.image = UIImage(named: name)
23 | }
24 |
25 | @available(iOS 13, *)
26 | public init(systemName name: String) {
27 | modifiableView.image = UIImage(systemName: name)
28 | }
29 |
30 | public init(_ image: Binding) where Binding.T == UIImage {
31 | self.image(bind: image)
32 | }
33 |
34 | public init(_ image: Binding) where Binding.T == UIImage? {
35 | self.image(bind: image)
36 | }
37 |
38 | // deprecated
39 | public init(configuration: (_ view: UIImageView) -> Void) {
40 | configuration(modifiableView)
41 | }
42 |
43 | }
44 |
45 |
46 | // Custom UIImageView modifiers
47 |
48 | extension ModifiableView where Base: UIImageView {
49 |
50 | @discardableResult
51 | public func tintColor(_ color: UIColor?) -> ViewModifier {
52 | ViewModifier(modifiableView, keyPath: \.tintColor, value: color)
53 | }
54 |
55 | }
56 |
57 | extension ModifiableView where Base: UIImageView {
58 |
59 | @discardableResult
60 | public func image(bind binding: Binding) -> ViewModifier where Binding.T == UIImage {
61 | ViewModifier(modifiableView, binding: binding) { $0.image = $1 }
62 | }
63 |
64 | @discardableResult
65 | public func image(bind binding: Binding) -> ViewModifier where Binding.T == UIImage? {
66 | ViewModifier(modifiableView, binding: binding) { $0.image = $1 }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Navigation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Navigation.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/4/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | extension UINavigationController {
13 |
14 | @discardableResult
15 | public func push(view: View, animated: Bool) -> Self {
16 | pushViewController(UIViewController(view), animated: animated)
17 | return self
18 | }
19 |
20 | }
21 |
22 | extension ModifiableView {
23 |
24 | @discardableResult
25 | public func navigation(title: String?) -> ViewModifier {
26 | ViewModifier(modifiableView) {
27 | $0.builderAttributes()?.onAppearOnceHandlers.append({ context in
28 | context.navigationItem?.title = title
29 | })
30 | }
31 | }
32 |
33 | }
34 |
35 | extension UIBarButtonItem {
36 |
37 | convenience public init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem) {
38 | self.init(barButtonSystemItem: systemItem, target: nil, action: nil)
39 | }
40 |
41 | convenience public init(image: UIImage?, style: UIBarButtonItem.Style) {
42 | self.init(image: image, style: style, target: nil, action: nil)
43 | }
44 |
45 | convenience public init(title: String?, style: UIBarButtonItem.Style) {
46 | self.init(title: title, style: style, target: nil, action: nil)
47 | }
48 |
49 | @discardableResult
50 | public func onTap(_ handler: @escaping (_ item: UIBarButtonItem) -> Void) -> Self {
51 | self.rx.tap
52 | .throttle(.milliseconds(300), latest: false, scheduler: MainScheduler.instance)
53 | .subscribe(onNext: { [unowned self] () in handler(self) })
54 | .disposed(by: rxDisposeBag)
55 | return self
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Padding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Padding.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 11/9/21.
6 | //
7 |
8 | import UIKit
9 |
10 |
11 | public protocol ViewBuilderPaddable {
12 | func setPadding(_ padding: UIEdgeInsets)
13 | }
14 |
15 |
16 | extension ModifiableView where Base: ViewBuilderPaddable {
17 |
18 | @discardableResult
19 | public func padding(_ value: CGFloat) -> ViewModifier {
20 | padding(insets: UIEdgeInsets(top: value, left: value, bottom: value, right: value))
21 | }
22 |
23 | @discardableResult
24 | public func padding(h: CGFloat, v: CGFloat) -> ViewModifier {
25 | padding(insets: UIEdgeInsets(top: v, left: h, bottom: v, right: h))
26 | }
27 |
28 | @discardableResult
29 | public func padding(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) -> ViewModifier {
30 | padding(insets: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right))
31 | }
32 |
33 | @discardableResult
34 | public func padding(insets: UIEdgeInsets) -> ViewModifier {
35 | ViewModifier(modifiableView) { $0.setPadding(insets) }
36 | }
37 |
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+RxSwift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+RxSwift.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 11/9/21.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxCocoa
11 |
12 |
13 | public protocol RxBinding {
14 | associatedtype T
15 | func asObservable() -> Observable
16 | }
17 |
18 | extension Observable: RxBinding {
19 | // previously defined
20 | }
21 |
22 |
23 |
24 |
25 | public protocol RxBidirectionalBinding: RxBinding {
26 | associatedtype T
27 | func asRelay() -> BehaviorRelay
28 | }
29 |
30 | extension BehaviorRelay: RxBidirectionalBinding {
31 | public func asRelay() -> BehaviorRelay { self }
32 | }
33 |
34 |
35 |
36 | extension ViewModifier {
37 |
38 | public init(_ view: Base, binding: B, handler: @escaping (_ view: Base, _ value: T) -> Void) where B.T == T {
39 | self.modifiableView = view
40 | binding.asObservable()
41 | .observe(on: ConcurrentMainScheduler.instance)
42 | .subscribe(onNext: { [weak view] value in
43 | if let view = view {
44 | handler(view, value)
45 | }
46 | })
47 | .disposed(by: view.rxDisposeBag)
48 | }
49 |
50 | public init(_ view: Base, binding: B, keyPath: ReferenceWritableKeyPath) where B.T == T {
51 | self.modifiableView = view
52 | binding.asObservable()
53 | .observe(on: ConcurrentMainScheduler.instance)
54 | .subscribe(onNext: { [weak view] value in
55 | if let view = view, view[keyPath: keyPath] != value {
56 | view[keyPath: keyPath] = value
57 | }
58 | })
59 | .disposed(by: view.rxDisposeBag)
60 | }
61 |
62 | }
63 |
64 | extension ModifiableView {
65 |
66 | @discardableResult
67 | public func bind(keyPath: ReferenceWritableKeyPath, binding: B) -> ViewModifier where B.T == T {
68 | ViewModifier(modifiableView) {
69 | binding.asObservable()
70 | .observe(on: ConcurrentMainScheduler.instance)
71 | .subscribe(onNext: { [weak modifiableView] value in
72 | modifiableView?[keyPath: keyPath] = value
73 | })
74 | .disposed(by: $0.rxDisposeBag)
75 | }
76 | }
77 |
78 | @discardableResult
79 | public func onReceive(_ binding: B, handler: @escaping (_ context: ViewBuilderValueContext) -> Void)
80 | -> ViewModifier where B.T == T {
81 | ViewModifier(modifiableView) {
82 | binding.asObservable()
83 | .observe(on: ConcurrentMainScheduler.instance)
84 | .subscribe(onNext: { [weak modifiableView] value in
85 | if let view = modifiableView {
86 | handler(ViewBuilderValueContext(view: view, value: value))
87 | }
88 | })
89 | .disposed(by: $0.rxDisposeBag)
90 | }
91 | }
92 |
93 | }
94 |
95 | extension NSObject {
96 |
97 | private static var RxDisposeBagAttributesKey: UInt8 = 0
98 |
99 | public var rxDisposeBag: DisposeBag {
100 | if let disposeBag = objc_getAssociatedObject( self, &UIView.RxDisposeBagAttributesKey ) as? DisposeBag {
101 | return disposeBag
102 | }
103 | let disposeBag = DisposeBag()
104 | objc_setAssociatedObject(self, &UIView.RxDisposeBagAttributesKey, disposeBag, .OBJC_ASSOCIATION_RETAIN)
105 | return disposeBag
106 | }
107 |
108 | }
109 |
110 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Spacer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+View.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/29/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct SpacerView: ModifiableView {
12 |
13 | public var modifiableView = Modified(UIView())
14 |
15 | public init() {
16 | modifiableView.setContentHuggingPriority(.defaultLow, for: .horizontal)
17 | modifiableView.setContentHuggingPriority(.defaultLow, for: .vertical)
18 | }
19 |
20 | public init(_ height: CGFloat = 16) {
21 | modifiableView.heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
22 | modifiableView.setContentCompressionResistancePriority(.required, for: .vertical)
23 | }
24 |
25 | public init(width: CGFloat = 8) {
26 | modifiableView.widthAnchor.constraint(greaterThanOrEqualToConstant: width).isActive = true
27 | modifiableView.setContentCompressionResistancePriority(.required, for: .horizontal)
28 | }
29 |
30 | }
31 |
32 | public struct FixedSpacerView: ModifiableView {
33 |
34 | public var modifiableView = Modified(UIView())
35 |
36 | public init() {
37 | modifiableView.setContentHuggingPriority(.required, for: .horizontal)
38 | modifiableView.setContentHuggingPriority(.required, for: .vertical)
39 | }
40 |
41 | public init(_ height: CGFloat = 16) {
42 | modifiableView.heightAnchor.constraint(equalToConstant: height).isActive = true
43 | modifiableView.setContentCompressionResistancePriority(.required, for: .vertical)
44 | }
45 |
46 | public init(width: CGFloat = 8) {
47 | modifiableView.widthAnchor.constraint(equalToConstant: width).isActive = true
48 | modifiableView.setContentCompressionResistancePriority(.required, for: .horizontal)
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Styles.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/29/21.
6 | //
7 |
8 | import UIKit
9 |
10 |
11 | public protocol BuilderStyle {
12 | associatedtype Base: UIView
13 | func apply(to view: Base)
14 | }
15 |
16 | extension ModifiableView {
17 |
18 | @discardableResult
19 | public func style(_ style: Style) -> ViewModifier where Style.Base == Base {
20 | ViewModifier(modifiableView) { style.apply(to: $0) }
21 | }
22 |
23 | @discardableResult
24 | public func style(_ style: Style) -> ViewModifier where Style.Base == UIView {
25 | ViewModifier(modifiableView) { style.apply(to: $0) }
26 | }
27 |
28 | }
29 |
30 | //func test() {
31 | // LabelView("Some text")
32 | // .style(StyleLabelAccentTitle())
33 | // ButtonView("Some text")
34 | // .style(StyleButtonFilled())
35 | //}
36 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Switch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Switch.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 11/9/21.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 |
11 | // Custom builder fot UILabel
12 | public struct SwitchView: ModifiableView {
13 |
14 | public let modifiableView: UISwitch = Modified(UISwitch()) {
15 | $0.onTintColor = ViewBuilderEnvironment.defaultButtonColor
16 | $0.contentHuggingPriority(.required, for: .horizontal)
17 | }
18 |
19 | // lifecycle
20 | public init(_ isOn: Bool = true) {
21 | modifiableView.isOn = isOn
22 | }
23 |
24 | public init(_ binding: Binding) where Binding.T == Bool {
25 | isOn(bind: binding)
26 | }
27 |
28 | public init(_ binding: Binding) where Binding.T == Bool {
29 | isOn(bidirectionalBind: binding)
30 | }
31 |
32 | }
33 |
34 |
35 | // Custom UILabel modifiers
36 | extension ModifiableView where Base: UISwitch {
37 |
38 | @discardableResult
39 | public func isOn(bind binding: Binding) -> ViewModifier where Binding.T == Bool {
40 | ViewModifier(modifiableView, binding: binding, keyPath: \.isOn)
41 | }
42 |
43 | @discardableResult
44 | public func isOn(bidirectionalBind binding: Binding) -> ViewModifier where Binding.T == Bool {
45 | ViewModifier(modifiableView) { switchView in
46 | let relay = binding.asRelay()
47 | switchView.rxDisposeBag.insert(
48 | relay
49 | .observe(on: ConcurrentMainScheduler.instance)
50 | .subscribe(onNext: { [weak switchView] value in
51 | if let view = switchView, view.isOn != value {
52 | view.isOn = value
53 | }
54 | }),
55 | switchView.rx.isOn
56 | .subscribe(onNext: { [weak relay] value in
57 | if let relay = relay, relay.value != value {
58 | relay.accept(value)
59 | }
60 | })
61 | )
62 | }
63 | }
64 |
65 | @discardableResult
66 | public func onTintColor(_ color: UIColor?) -> ViewModifier {
67 | ViewModifier(modifiableView, keyPath: \.onTintColor, value: color)
68 | }
69 |
70 | @discardableResult
71 | public func onChange(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier {
72 | ViewModifier(modifiableView) {
73 | $0.rx.isOn
74 | .changed
75 | .subscribe(onNext: { [unowned modifiableView] value in
76 | handler(ViewBuilderValueContext(view: modifiableView, value: modifiableView.isOn))
77 | })
78 | .disposed(by: $0.rxDisposeBag)
79 | }
80 | }
81 |
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+Variable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+Variable.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/23/21.
6 | //
7 |
8 | import Foundation
9 | import RxSwift
10 | import RxCocoa
11 |
12 | @propertyWrapper public struct Variable {
13 |
14 | private var relay: BehaviorRelay
15 |
16 | public init(_ relay: BehaviorRelay) {
17 | self.relay = relay
18 | }
19 |
20 | public var wrappedValue: T {
21 | get { return relay.value }
22 | nonmutating set { relay.accept(newValue) }
23 | }
24 |
25 | public var projectedValue: Variable {
26 | get { return self }
27 | }
28 |
29 | }
30 |
31 | extension Variable {
32 |
33 | public init(wrappedValue: T) {
34 | self.relay = BehaviorRelay(value: wrappedValue)
35 | }
36 |
37 | }
38 |
39 | extension Variable where T:Equatable {
40 |
41 | public func onChange(_ observer: @escaping (_ value: T) -> ()) -> Disposable {
42 | relay
43 | .skip(1)
44 | .distinctUntilChanged()
45 | .subscribe { observer($0) }
46 | }
47 |
48 | }
49 |
50 | extension Variable: RxBinding {
51 |
52 | public func asObservable() -> Observable {
53 | return relay.asObservable()
54 | }
55 |
56 | public func observe(on scheduler: ImmediateSchedulerType) -> Observable {
57 | return relay.observe(on: scheduler)
58 | }
59 |
60 | public func bind(_ observable: Observable) -> Disposable {
61 | return observable.bind(to: relay)
62 | }
63 |
64 | }
65 |
66 | extension Variable: RxBidirectionalBinding {
67 | public func asRelay() -> BehaviorRelay {
68 | return relay
69 | }
70 |
71 | }
72 |
73 | //struct A: ViewBuilder {
74 | // @Variable var name = "Michael"
75 | // var body: View {
76 | // B(name: $name)
77 | // }
78 | //}
79 | //
80 | //struct B: ViewBuilder {
81 | // @Variable var name: String
82 | // var body: View {
83 | // LabelView(name)
84 | // }
85 | //}
86 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+ViewController.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 10/4/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIViewController {
12 |
13 | convenience public init(_ view: View, padding: UIEdgeInsets? = nil, safeArea: Bool = false) {
14 | self.init()
15 | if #available(iOS 13, *) {
16 | self.view.backgroundColor = .systemBackground
17 | } else {
18 | self.view.backgroundColor = .white
19 | }
20 | self.view.embed(view(), padding: padding, safeArea: safeArea)
21 | }
22 |
23 | public func transition(to view: View, padding: UIEdgeInsets? = nil, safeArea: Bool = false, delay: Double = 0.2) {
24 | self.view.transition(to: view, padding: padding, safeArea: safeArea, delay: delay)
25 | }
26 |
27 | public func transition(to viewController: UIViewController, delay: Double) {
28 | self.view.transition(to: viewController, delay: delay)
29 | }
30 |
31 | }
32 |
33 | public struct ViewControllerHostView: ModifiableView {
34 |
35 | public var modifiableView = Modified(BuilderInternalViewControllerHostView()) {
36 | $0.backgroundColor = .clear
37 | }
38 |
39 | public init(_ viewController: UIViewController) {
40 | modifiableView.viewController = viewController
41 | }
42 |
43 | }
44 |
45 | public class BuilderInternalViewControllerHostView: UIView {
46 |
47 | var viewController: UIViewController!
48 |
49 | public init() {
50 | super.init(frame: .zero)
51 | }
52 |
53 | required init?(coder: NSCoder) {
54 | fatalError("init(coder:) has not been implemented")
55 | }
56 |
57 | override public func didMoveToWindow() {
58 | if subviews.isEmpty, let parentViewController = parentViewController {
59 | parentViewController.addChild(viewController)
60 | embed(viewController.view)
61 | viewController.didMove(toParent: parentViewController)
62 | }
63 | }
64 |
65 | override public func didMoveToSuperview() {
66 | if superview == nil {
67 | viewController?.willMove(toParent: nil)
68 | viewController?.view.removeFromSuperview()
69 | viewController?.removeFromParent()
70 | }
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+With.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+With.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 7/2/21.
6 | //
7 |
8 | import UIKit
9 |
10 | public typealias With = ViewModifier
11 |
--------------------------------------------------------------------------------
/Sources/Builder/Builder/Builder+ZStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder+ZStack.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 1/30/21.
6 | //
7 |
8 | import UIKit
9 |
10 | public typealias ZStackView = ContainerView
11 |
--------------------------------------------------------------------------------
/Sources/Builder/Extensions/UIColor+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Extensions.swift
3 | // Builder
4 | //
5 | // Created by Michael Long on 11/26/21.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 |
12 | func lighter(by percentage: CGFloat = 30.0) -> UIColor {
13 | return self.adjust(by: abs(percentage))
14 | }
15 |
16 | func darker(by percentage: CGFloat = 30.0) -> UIColor {
17 | return self.adjust(by: -1 * abs(percentage))
18 | }
19 |
20 | private func adjust(by percentage: CGFloat = 30.0) -> UIColor {
21 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0
22 | if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
23 | return UIColor(red: min(red + percentage/100, 1.0),
24 | green: min(green + percentage/100, 1.0),
25 | blue: min(blue + percentage/100, 1.0),
26 | alpha: alpha)
27 | }
28 | return self
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Builder/Extensions/UIImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Extensions.swift
3 | // ViewBuilder
4 | //
5 | // Created by Michael Long on 9/26/20.
6 | // Copyright © 2020 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIImage {
12 |
13 | /**
14 | Initializes and returns the image object of the specified color and size.
15 | */
16 | public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
17 | let rect = CGRect(origin: .zero, size: size)
18 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
19 | color.setFill()
20 | UIRectFill(rect)
21 | let image = UIGraphicsGetImageFromCurrentImageContext()
22 | UIGraphicsEndImageContext()
23 |
24 | guard let cgImage = image?.cgImage else {
25 | return nil
26 | }
27 | self.init(cgImage: cgImage)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/BuilderTests/BuilderTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Builder
3 |
4 | final class BuilderTests: XCTestCase {
5 | func testExample() throws {
6 | }
7 | }
8 |
--------------------------------------------------------------------------------