├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Documentation
└── Resources
│ ├── RxSwiftWidgetsBanner.jpg
│ ├── Widget-Account-Details.png
│ ├── Widget-Login.png
│ ├── Widget-Menu.png
│ ├── Widget-User-Details.png
│ ├── Widget-User-List-2.png
│ └── Widget-User-List.png
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── RxSwiftWidgets.xcodeproj
├── RxCocoaRuntime_Info.plist
├── RxCocoa_Info.plist
├── RxRelay_Info.plist
├── RxSwiftWidgetsTests_Info.plist
├── RxSwiftWidgets_Info.plist
├── RxSwift_Info.plist
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── xcshareddata
│ └── xcschemes
│ ├── RxSwiftWidgets-Demo.xcscheme
│ └── RxSwiftWidgets-Package.xcscheme
├── RxSwiftWidgetsDemo
├── AppDelegate.swift
├── Application
│ ├── Account Details
│ │ ├── AccountDetailsViewModel.swift
│ │ └── AccountDetailsWidget.swift
│ ├── Features
│ │ ├── DemoBindingWidget.swift
│ │ ├── DemoDismissibleWidget.swift
│ │ ├── DemoPositioningWidget.swift
│ │ ├── DemoScrollingWidget.swift
│ │ ├── DemoStaticTableViewWidget.swift
│ │ ├── DoneButtonWidget.swift
│ │ └── FeaturesWidget.swift
│ ├── Login
│ │ └── LoginFormWidget.swift
│ ├── MainMenu
│ │ ├── MainMenuLabelWidget.swift
│ │ ├── MainMenuWidget.swift
│ │ └── SampleWidget.swift
│ └── Users
│ │ ├── UserDetailsWidget.swift
│ │ └── UserListWidget.swift
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── Models
│ ├── AccountInformation.swift
│ ├── GenericError.swift
│ └── User.swift
├── Resources
│ ├── Assets.xcassets
│ │ ├── 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.imageset
│ │ │ ├── Contents.json
│ │ │ └── RxSwiftWidgets-DK.png
│ │ ├── RxSwiftWidgets-Logo-DK.imageset
│ │ │ ├── Contents.json
│ │ │ └── RxSwiftWidgets-Logo-DK.png
│ │ ├── Solutions-Center-logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── Solutions-Center-logo.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
│ │ ├── blur-background11.imageset
│ │ │ ├── Contents.json
│ │ │ └── blur-background11.jpg
│ │ ├── vector1.imageset
│ │ │ ├── Contents.json
│ │ │ └── vector1.jpg
│ │ └── vector2.imageset
│ │ │ ├── Contents.json
│ │ │ └── vector2.jpg
│ └── Launch Screen.storyboard
├── Shared
│ ├── CardWidget.swift
│ └── ErrorMessageWidget.swift
└── Styles
│ ├── Colors.swift
│ ├── Fonts.swift
│ ├── Style.swift
│ └── Styles.swift
├── Sources
└── RxSwiftWidgets
│ ├── Core
│ ├── Widget.swift
│ ├── WidgetAnimation.swift
│ ├── WidgetContext.swift
│ ├── WidgetDismissible.swift
│ ├── WidgetModifier.swift
│ ├── WidgetModifying.swift
│ ├── WidgetNavigator.swift
│ ├── WidgetPadding.swift
│ ├── WidgetPositioning.swift
│ ├── WidgetSwiftUI.swift
│ ├── WidgetTheme.swift
│ ├── WidgetView.swift
│ └── WidgetViewNavigating.swift
│ ├── Extensions
│ ├── UIColor+Widgets.swift
│ ├── UIView+Widgets.swift
│ └── UIWidgetHostController.swift
│ ├── Modifiers
│ ├── WidgetModifyUIControl.swift
│ ├── WidgetModifyUINavigation.swift
│ ├── WidgetModifyUIView.swift
│ ├── WidgetModifyUIViewConstraints.swift
│ ├── WidgetModifyUIViewControllerEvents.swift
│ ├── WidgetModifyUIViewCustom.swift
│ ├── WidgetModifyUIViewGestures.swift
│ ├── WidgetModifyUIViewLayer.swift
│ └── WidgetModifyUIViewRx.swift
│ ├── Rx
│ ├── BindableElement.swift
│ ├── Binding.swift
│ ├── ObservableElement.swift
│ ├── ObservableListBuilder.swift
│ └── State.swift
│ └── Widgets
│ ├── Containers
│ ├── ContainerWidget.swift
│ ├── DisclosableWidget.swift
│ ├── HStackWidget.swift
│ ├── ScrollWidget.swift
│ ├── UIControlWidget.swift
│ ├── UIViewWidget.swift
│ ├── VStackWidget.swift
│ └── ZStackWidget.swift
│ ├── Controls
│ ├── ButtonWidget.swift
│ └── TextFieldWidget.swift
│ ├── General
│ ├── ImageWidget.swift
│ ├── LabelWidget.swift
│ ├── SpacerWidget.swift
│ └── SpinnerWidget.swift
│ ├── Navigation
│ └── AlertWidget.swift
│ └── TableView
│ ├── TableCellWidget.swift
│ ├── TableSectionWidget.swift
│ └── TableWidget.swift
├── Tests
├── LinuxMain.swift
└── RxSwiftWidgetsTests
│ ├── RxSwiftWidgetsTests.swift
│ └── XCTestManifests.swift
└── jazzy.yaml
/.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 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Documentation/Resources/RxSwiftWidgetsBanner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/RxSwiftWidgetsBanner.jpg
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-Account-Details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-Account-Details.png
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-Login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-Login.png
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-Menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-Menu.png
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-User-Details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-User-Details.png
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-User-List-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-User-List-2.png
--------------------------------------------------------------------------------
/Documentation/Resources/Widget-User-List.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/Documentation/Resources/Widget-User-List.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 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 | "object": {
3 | "pins": [
4 | {
5 | "package": "RxSwift",
6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395",
10 | "version": "5.0.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
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: "RxSwiftWidgets",
8 | platforms: [
9 | .iOS(.v11),
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "RxSwiftWidgets",
15 | targets: ["RxSwiftWidgets"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.0.1")
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 which this package depends on.
25 | .target(
26 | name: "RxSwiftWidgets",
27 | dependencies: ["RxSwift", "RxCocoa"]),
28 | .testTarget(
29 | name: "RxSwiftWidgetsTests",
30 | dependencies: ["RxSwiftWidgets","RxSwift", "RxCocoa"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxCocoaRuntime_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxCocoa_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxRelay_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxSwiftWidgetsTests_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | BNDL
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxSwiftWidgets_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/RxSwift_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/xcshareddata/xcschemes/RxSwiftWidgets-Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/RxSwiftWidgets.xcodeproj/xcshareddata/xcschemes/RxSwiftWidgets-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwiftWidgets
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
14 |
15 | var window: UIWindow?
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 |
19 | configureStyles()
20 |
21 | WidgetTheme.defaultTheme
22 | .update {
23 | $0.color.accent = UIColor(red: 1.0, green: 0.0, blue: 0.5, alpha: 1.0)
24 | }
25 |
26 | let widget = MainMenuWidget()
27 |
28 | window = UIWindow(frame: UIScreen.main.bounds)
29 | window?.rootViewController = widget.controller(with: WidgetContext())
30 | window?.makeKeyAndVisible()
31 |
32 | return true
33 | }
34 |
35 | func applicationWillResignActive(_ application: UIApplication) {
36 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
37 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
38 | }
39 |
40 | func applicationDidEnterBackground(_ application: UIApplication) {
41 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
42 | }
43 |
44 | func applicationWillEnterForeground(_ application: UIApplication) {
45 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
46 | }
47 |
48 | func applicationDidBecomeActive(_ application: UIApplication) {
49 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Account Details/AccountDetailsViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccountDetailsViewModel.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 7/31/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxCocoa
11 |
12 | class AccountDetailsViewModel {
13 |
14 | var loading = BehaviorRelay(value: true)
15 |
16 | lazy var title = accountInformation
17 | .map { $0.name }
18 |
19 | lazy var accountDetails = accountInformation
20 | .map { Array($0.details.prefix(2)) }
21 |
22 | lazy var paymentDetails = accountInformation
23 | .map { Array($0.details.dropFirst(2)) }
24 |
25 | lazy var footnotes = accountInformation
26 | .map { $0.footnotes }
27 |
28 | private var accountInformation = PublishSubject()
29 |
30 | func load() {
31 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
32 | self.accountInformation.onNext(AccountInformation.sample)
33 | self.loading.accept(false)
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Account Details/AccountDetailsWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxSwiftWidgets
5 |
6 | struct AccountDetailsWidget: WidgetView {
7 |
8 | let viewModel = AccountDetailsViewModel()
9 |
10 | func widget(_ context: WidgetContext) -> Widget {
11 |
12 | ZStackWidget([
13 |
14 | ImageWidget(named: "vector2")
15 | .contentMode(.scaleAspectFill)
16 | .safeArea(false),
17 |
18 | ScrollWidget(
19 |
20 | VStackWidget([
21 |
22 | ContainerWidget(
23 | SpinnerWidget().color(.gray)
24 | )
25 | .padding(15)
26 | .hidden(viewModel.loading.map { !$0 }),
27 |
28 | VStackWidget([
29 |
30 | LabelWidget(viewModel.title)
31 | .alignment(.center)
32 | .color(context.theme.color.accent)
33 | .font(.title1),
34 |
35 | NameValueSectionWidget(values: viewModel.accountDetails),
36 |
37 | NameValueSectionWidget(values: viewModel.paymentDetails),
38 |
39 | LabelWidget(viewModel.footnotes)
40 | .color(.gray)
41 | .font(.footnote)
42 | .numberOfLines(0)
43 | .padding(h: 15, v: 0),
44 | ])
45 | .spacing(20)
46 | .hidden(viewModel.loading),
47 |
48 | ]) // VStackWidget
49 | .spacing(15)
50 |
51 | ) // ScrollWidget
52 | .padding(h: 30, v: 20)
53 | .safeArea(false)
54 |
55 | ]) // ZStackWidget
56 | .navigationBar(title: "Account Details", hidden: false)
57 | .safeArea(false)
58 | .onViewDidAppear { _ in
59 | self.viewModel.load()
60 | }
61 | }
62 |
63 | }
64 |
65 |
66 | fileprivate struct NameValueSectionWidget: WidgetView {
67 |
68 | let values: Observable<[AccountInformation.AccountDetails]>
69 |
70 | func widget(_ context: WidgetContext) -> Widget {
71 | CardWidget(widget: VStackWidget(values) {
72 | HStackWidget([
73 | LabelWidget($0.name)
74 | .color(.gray),
75 | SpacerWidget(),
76 | LabelWidget($0.value)
77 | .color(.darkText),
78 | ])
79 | })
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DemoBindingWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoBindingWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 7/30/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwiftWidgets
10 |
11 | struct DemoBindingWidget: WidgetView {
12 |
13 | @Binding var title: String
14 |
15 | let desc = """
16 | Demonstrates binding object properties in and across Widgets using @State and @Binding values.
17 | """
18 |
19 | func widget(_ context: WidgetContext) -> Widget {
20 |
21 | ZStackWidget([
22 |
23 | ImageWidget(named: "vector1")
24 | .contentMode(.scaleAspectFill)
25 | .safeArea(false),
26 |
27 | VStackWidget([
28 |
29 | LabelWidget("Binding Sample")
30 | .font(.title1)
31 | .color(.white)
32 | .alignment(.center),
33 |
34 | LabelWidget(desc)
35 | .font(.preferredFont(forTextStyle: .callout))
36 | .color(.white)
37 | .numberOfLines(0)
38 | .padding(h: 0, v: 15),
39 |
40 | LabelWidget($title.map { "Parent page title is '\($0)'."} )
41 | .font(.preferredFont(forTextStyle: .callout))
42 | .alignment(.center)
43 | .color(.white)
44 | .numberOfLines(0)
45 | .padding(h: 0, v: 8),
46 |
47 | ButtonWidget("Update Choice 1")
48 | .color(.orange)
49 | .onTap { context in
50 | self.title = "Features - Choice 1"
51 | },
52 |
53 | ButtonWidget("Update Choice 2")
54 | .color(.orange)
55 | .onTap { context in
56 | self.title = "Features - Choice 2"
57 | },
58 |
59 | SpacerWidget(),
60 |
61 | BackButtonWidget(text: "Done"),
62 |
63 | ]) // VStackWidget
64 | .spacing(15)
65 | .padding(h: 40, v: 50)
66 |
67 | ]) // ZStackWidget
68 | .navigationBar(title: "Binding Demo", hidden: true)
69 | .safeArea(false)
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DemoDismissibleWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwiftWidgets
4 |
5 | struct DemoDismissibleWidget: WidgetView {
6 |
7 | let desc = """
8 | Demonstrates launching a dismissible widget and returning a value or simply dismissing (cancelling) the screen programatically.
9 |
10 | If a value is returned it will be shown using an AlertWidget.
11 |
12 | This screen will also automatically timeout after 8 seconds.
13 | """
14 |
15 | @State var dismissed = false
16 |
17 | func widget(_ context: WidgetContext) -> Widget {
18 |
19 | ZStackWidget([
20 |
21 | ImageWidget(named: "vector1")
22 | .contentMode(.scaleAspectFill)
23 | .safeArea(false),
24 |
25 | VStackWidget([
26 |
27 | LabelWidget("Dismissible Sample")
28 | .font(.title1)
29 | .color(.white)
30 | .alignment(.center),
31 |
32 | LabelWidget(desc)
33 | .font(.preferredFont(forTextStyle: .callout))
34 | .color(.white)
35 | .numberOfLines(0)
36 | .padding(h: 0, v: 15),
37 |
38 | ButtonWidget("Return Random Number")
39 | .color(.orange)
40 | .onTap { context in
41 | self.dismissed = true
42 | context.navigator?.dismiss(returning: "\(Int.random(in: 1..<1000))")
43 | },
44 |
45 | ButtonWidget("Dismiss")
46 | .color(.orange)
47 | .onTap { context in
48 | self.dismissed = true
49 | context.navigator?.dismiss()
50 | },
51 |
52 | SpacerWidget(),
53 |
54 | BackButtonWidget(text: "Done"),
55 |
56 | ]) // VStackWidget
57 | .spacing(15)
58 | .padding(h: 40, v: 50)
59 |
60 | ]) // ZStackWidget
61 | .navigationBar(title: "Dismissible Demo", hidden: true)
62 | .safeArea(false)
63 | .onViewDidAppear { (context) in
64 | DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
65 | if !self.dismissed {
66 | context.navigator?.dismiss()
67 | }
68 | }
69 | }
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DemoPositioningWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwiftWidgets
4 |
5 | struct DemoPositioningWidget: WidgetView {
6 |
7 | let desc = """
8 | Uses a ZStack to demonstrate multiple ways of positioning widgets within a container.
9 | """
10 |
11 | func widget(_ context: WidgetContext) -> Widget {
12 |
13 | let widget = ZStackWidget([
14 |
15 | ImageWidget(named: "vector1")
16 | .contentMode(.scaleAspectFill)
17 | .safeArea(false),
18 |
19 | VStackWidget([
20 |
21 | LabelWidget("Positioning Sample")
22 | .font(.title1)
23 | .color(.white)
24 | .alignment(.center),
25 |
26 | LabelWidget(desc)
27 | .font(.preferredFont(forTextStyle: .callout))
28 | .color(.white)
29 | .numberOfLines(0)
30 | .padding(h: 0, v: 15),
31 |
32 | borderedStack([
33 | LabelWidget.title3("Center Left")
34 | .position(.centerLeft),
35 | LabelWidget.title3("Center Right")
36 | .position(.centerRight),
37 | LabelWidget.title3("Center Top")
38 | .position(.centerTop),
39 | LabelWidget.title3("Center Bottom")
40 | .position(.centerBottom),
41 | ]),
42 |
43 | borderedStack([
44 | LabelWidget.title3("Top Left")
45 | .position(.topLeft),
46 | LabelWidget.title3("Top Right")
47 | .position(.topRight),
48 | LabelWidget.title3("Bottom Left")
49 | .position(.bottomLeft),
50 | LabelWidget.title3("Bottom Right")
51 | .position(.bottomRight),
52 | LabelWidget.title3("Center")
53 | .position(.center),
54 | ]),
55 |
56 | SpacerWidget(),
57 |
58 | BackButtonWidget(text: "Done"),
59 |
60 | ]) // VStackWidget
61 | .spacing(15)
62 | .padding(h: 40, v: 50)
63 |
64 | ]) // ZStackWidget
65 | .navigationBar(title: "Dismissible Demo", hidden: true)
66 | .safeArea(false)
67 |
68 | widget.walk { print($0) }
69 |
70 | return widget
71 | }
72 |
73 | func borderedStack(_ widgets: [Widget]) -> Widget {
74 | ZStackWidget(widgets)
75 | .border(color: .lightGray)
76 | .padding(10)
77 | .height(120)
78 | }
79 |
80 | }
81 |
82 | extension LabelWidget {
83 | static func title3(_ text: String) -> LabelWidget {
84 | LabelWidget(text)
85 | .color(.white)
86 | .font(.preferredFont(forTextStyle: .title3))
87 | .contentHuggingPriority(.defaultHigh, for: .horizontal)
88 | .padding(h: 0, v: 4)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DemoScrollingWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwiftWidgets
4 |
5 | struct DemoScrollingWidget: WidgetView {
6 |
7 | func widget(_ context: WidgetContext) -> Widget {
8 |
9 | let widgets = [Widget].init(repeating: LabelWidget("This is a line.").color(.white), count: 40)
10 |
11 | return ContainerWidget(
12 | ScrollWidget(
13 | VStackWidget(widgets)
14 | .spacing(10)
15 | .padding(h: 30, v: 20)
16 | )
17 | )
18 | .backgroundColor(.black)
19 | .navigationBar(title: "Scrolling", hidden: false)
20 | .safeArea(false)
21 |
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DemoStaticTableViewWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoStaticTableViewWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 8/16/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxSwiftWidgets
11 |
12 | struct DemoStaticTableViewWidget: WidgetView {
13 |
14 | func widget(_ context: WidgetContext) -> Widget {
15 |
16 | TableWidget([
17 | TableSectionWidget([
18 | VStackWidget([
19 | LabelWidget("This is line 1"),
20 | LabelWidget("This is a footnote for line 1")
21 | .font(.footnote)
22 | ])
23 | .spacing(2),
24 | TableCellWidget("This is line 2", subtitle: "This is a subtitle"),
25 | TableCellWidget("Name", value: "Value"),
26 | ]),
27 | TableSectionWidget([
28 | HStackWidget([
29 | LabelWidget("On or Off"),
30 | SpacerWidget(),
31 | UIControlWidget(UISwitch())
32 | .with({ (view, context) in
33 | view.onTintColor = context.theme.color.accent
34 | })
35 | ])
36 | ]),
37 | TableSectionWidget([
38 | LabelWidget("This is label line 1"),
39 | TableCellWidget("This is row line 2"),
40 | ]),
41 | ]) // TableWidget
42 | .navigationBar(title: "Static TableView", hidden: false)
43 | .safeArea(true)
44 | .onSelect { (context, path) in
45 | context.tableView?.deselectRow(at: path, animated: true)
46 | }
47 | .onViewDidAppear { _ in
48 |
49 | }
50 | .theme {
51 | $0.color.text = $0.color.accent
52 | }
53 |
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/DoneButtonWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxCocoa
5 | import RxSwiftWidgets
6 |
7 | struct BackButtonWidget: WidgetView {
8 |
9 | let text: String
10 |
11 | func widget(_ context: WidgetContext) -> Widget {
12 | LabelWidget(text)
13 | .alignment(.center)
14 | .alpha(0.0)
15 | .color(.white)
16 | .font(.title3)
17 | .padding(h: 30, v: 10)
18 | .position(.topRight)
19 | .onTap{ context in
20 | context.navigator?.dismiss()
21 | }
22 | .with { (view, _) in
23 | UIView.animate(withDuration: 1.5) {
24 | view.alpha = 0.8
25 | }
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Features/FeaturesWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxCocoa
5 | import RxSwiftWidgets
6 |
7 | struct FeaturesWidget: WidgetView {
8 |
9 | @State private var title: String = "Features"
10 |
11 | func widget(_ context: WidgetContext) -> Widget {
12 |
13 | ZStackWidget([
14 |
15 | ImageWidget(named: "vector1")
16 | .contentMode(.scaleAspectFill)
17 | .safeArea(false),
18 |
19 | ScrollWidget(
20 |
21 | VStackWidget([
22 |
23 | LabelWidget($title)
24 | .font(.title1)
25 | .color(.white)
26 | .alignment(.center)
27 | .padding(20),
28 |
29 | MainMenuItemWidget(text: "Property Binding", onTap: { (context) in
30 | context.navigator?.push(DemoBindingWidget(title: self.$title))
31 | }),
32 |
33 | MainMenuItemWidget(text: "Dismissible", onTap: { context in
34 | context.navigator?.present(DemoDismissibleWidget(), onDismiss: { (value: String) in
35 | self.showAlert(value, with: context)
36 | })
37 | }),
38 |
39 | MainMenuItemWidget(text: "Positioning", onTap: { context in
40 | context.navigator?.push(DemoPositioningWidget())
41 | }),
42 |
43 | MainMenuItemWidget(text: "Scrolling", onTap: { context in
44 | context.navigator?.push(DemoScrollingWidget())
45 | }),
46 |
47 | MainMenuItemWidget(text: "Static TableView", onTap: { context in
48 | context.navigator?.push(DemoStaticTableViewWidget())
49 | }),
50 |
51 | ButtonWidget("Reset Title")
52 | .color(.orange)
53 | .hidden($title.map { $0 == "Features" })
54 | .onTap { context in
55 | UIView.animate(withDuration: 0.2) {
56 | self.title = "Features"
57 | }
58 | },
59 |
60 | BackButtonWidget(text: "Done"),
61 |
62 | ]) // VStackWidget
63 | .spacing(15)
64 | .padding(h: 30, v: 50)
65 | .onEvent($title, handle: { (value, _) in
66 | print(value)
67 | })
68 | )
69 | .safeArea(false)
70 |
71 | ]) // ZStackWidget
72 | .navigationBar(title: "Features", hidden: true)
73 | .safeArea(false)
74 |
75 | }
76 |
77 | func showAlert(_ value: String, with context: WidgetContext) {
78 | OperationQueue.main.addOperation {
79 | let alert = AlertWidget(title: "Returned", message: value)
80 | .addAction(title: "Okay") { _ in
81 | self.showAlert("Onward!", with: context)
82 | }
83 | .addAction(title: "Cancel", style: .cancel)
84 | context.navigator?.present(alert)
85 | }
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Login/LoginFormWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxSwiftWidgets
5 |
6 | struct LoginFormWidget: WidgetView {
7 |
8 | @State var username: String = "Michael Long"
9 | @State var password: String = ""
10 | @State var authenticated: Bool = false
11 | @State var error: String = ""
12 |
13 | func widget(_ context: WidgetContext) -> Widget {
14 |
15 | ZStackWidget([
16 |
17 | ImageWidget(named: "vector1")
18 | .contentMode(.scaleAspectFill)
19 | .safeArea(false),
20 |
21 | ScrollWidget(
22 | VStackWidget([
23 | logoSection,
24 | ErrorMessageWidget(message: $error),
25 | SpacerWidget(v: 20),
26 | usernameSection,
27 | SpacerWidget(v: 10),
28 | passwordSection,
29 | SpacerWidget(v: 20),
30 | loginButtonSection,
31 | footnoteSection,
32 | ])
33 | .spacing(5)
34 | .padding(h: 0, v: 50)
35 | )
36 | .automaticallyAdjustForKeyboard()
37 | .safeArea(false),
38 |
39 | BackButtonWidget(text: "X"),
40 |
41 |
42 | ]) // ZStackWidget
43 | .navigationBar(title: "Login", hidden: true)
44 | .safeArea(false)
45 | .onEvent($authenticated.filter { $0 }) { (_, context) in
46 | context.navigator?.dismiss()
47 | }
48 |
49 | }
50 |
51 | var logoSection: Widget {
52 | ContainerWidget(
53 | HStackWidget([
54 | ImageWidget(named: "RxSwiftWidgets-Logo-DK")
55 | .height(100)
56 | .width(100)
57 | .contentMode(.scaleAspectFit),
58 | LabelWidget("RxSwiftWidgets")
59 | .font(.title2)
60 | .color(.white)
61 | ])
62 | .padding(10)
63 | .position(.centerHorizontally)
64 | )
65 | }
66 |
67 | var usernameSection: Widget {
68 | ContainerWidget(
69 | VStackWidget([
70 | LabelWidget.footnote("Username"),
71 | TextFieldWidget($username)
72 | .placeholder("Username / Email Address")
73 | .font(.title2)
74 | .color(.black)
75 | .with { textField, _ in
76 | textField.isSecureTextEntry = false
77 | textField.keyboardAppearance = .dark
78 | }
79 | ])
80 | .spacing(0)
81 | )
82 | .backgroundColor(UIColor(white: 1.0, alpha: 0.8))
83 | .padding(h: 30, v: 8)
84 | }
85 |
86 | var passwordSection: Widget {
87 | ContainerWidget(
88 | VStackWidget([
89 | LabelWidget.footnote("Password"),
90 | TextFieldWidget($password)
91 | .font(.title2)
92 | .color(.black)
93 | .with { textField, _ in
94 | textField.isSecureTextEntry = true
95 | textField.keyboardAppearance = .dark
96 | }
97 | .onEditingDidEndOnExit { (_, _) in
98 | self.login()
99 | }
100 | ])
101 | .spacing(0)
102 | )
103 | .backgroundColor(UIColor(white: 1.0, alpha: 0.8))
104 | .padding(h: 30, v: 8)
105 | }
106 |
107 | var loginButtonSection: Widget {
108 | ButtonWidget("Login")
109 | .backgroundColor(UIColor(red: 0.8, green: 0.0, blue: 0.4, alpha: 0.8))
110 | .font(.title2)
111 | .color(.white)
112 | .padding(h: 30, v: 14)
113 | .onTap(handler: { _ in
114 | self.login()
115 | })
116 | }
117 |
118 | var footnoteSection: Widget {
119 | LabelWidget.footnote("RxSwiftWidgets Demo Version 0.7\nCreated by Michael Long")
120 | .alignment(.center)
121 | .padding(h: 20, v: 20)
122 | }
123 |
124 | func login() {
125 | guard !self.username.isEmpty && !self.password.isEmpty else {
126 | error = "Username and password is required."
127 | return
128 | }
129 | error = ""
130 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
131 | self.password = ""
132 | self.authenticated = true
133 | }
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/MainMenu/MainMenuLabelWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxCocoa
5 | import RxSwiftWidgets
6 |
7 | struct MainMenuItemWidget: WidgetView, WidgetViewModifying {
8 |
9 | let text: String
10 | let onTap: (WidgetContext) -> Void
11 |
12 | var modifiers = WidgetModifiers()
13 |
14 | func widget(_ context: WidgetContext) -> Widget {
15 | LabelWidget(text)
16 | .backgroundColor(.init(white: 1.0, alpha: 0.3))
17 | .clipsToBounds(true)
18 | .cornerRadius(10)
19 | .color(.white)
20 | .font(.preferredFont(forTextStyle: .title3))
21 | .padding(h: 20, v: 10)
22 | .onTap(handler: onTap)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/MainMenu/MainMenuWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwiftWidgets
4 |
5 | struct MainMenuWidget: WidgetView, WidgetViewNavigating {
6 |
7 | func widget(_ context: WidgetContext) -> Widget {
8 |
9 | ZStackWidget([
10 |
11 | ImageWidget(named: "vector1")
12 | .contentMode(.scaleAspectFill)
13 | .safeArea(false),
14 |
15 | ScrollWidget(
16 | VStackWidget([
17 | ContainerWidget(
18 | HStackWidget([
19 | ImageWidget(named: "RxSwiftWidgets-Logo-DK")
20 | .height(100)
21 | .width(100)
22 | .contentMode(.scaleAspectFit),
23 | LabelWidget("RxSwiftWidgets")
24 | .font(.title2)
25 | .color(.white)
26 | ])
27 | .position(.centerHorizontally)
28 | ),
29 |
30 | MainMenuItemWidget(text: "Account Details", onTap: { context in
31 | context.navigator?.push(AccountDetailsWidget())
32 | }),
33 |
34 | MainMenuItemWidget(text: "Login Form", onTap: { context in
35 | context.navigator?.push(LoginFormWidget())
36 | }),
37 |
38 | MainMenuItemWidget(text: "User List", onTap: { context in
39 | context.navigator?.push(UserListWidget())
40 | }),
41 |
42 | MainMenuItemWidget(text: "Features", onTap: { context in
43 | context.navigator?.push(FeaturesWidget())
44 | }),
45 |
46 | ]) // VStackWidget
47 | .spacing(15)
48 | .padding(h: 30, v: 50)
49 | .safeArea(true)
50 | ) // ScrollWidget
51 | .safeArea(false),
52 |
53 | LabelWidget.footnote("RxSwiftWidgets Demo Version 0.7\nCreated by Michael Long")
54 | .alignment(.center)
55 | .backgroundColor(UIColor(white: 0.0, alpha: 0.4))
56 | .padding(h: 20, v: 20)
57 | .position(.bottom)
58 | .safeArea(true),
59 |
60 | ]) // ZStackWidget
61 | .navigationBar(title: "Menu", hidden: true)
62 | .safeArea(false)
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/MainMenu/SampleWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxCocoa
5 | import RxSwiftWidgets
6 |
7 | public class SampleWidgetViewModel {
8 |
9 | let titleText = "Michael Long"
10 |
11 | var labelText1 = "Test"
12 | var labelText2 = "Michael Long"
13 |
14 | let labelPublisher = PublishSubject()
15 | let imageNamePublisher = PublishSubject()
16 | let errorPublisher = BehaviorRelay(value: nil)
17 |
18 | func load() {
19 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
20 | self.imageNamePublisher.onNext("User-ML")
21 | }
22 | }
23 | }
24 |
25 | struct SampleWidget: WidgetView {
26 |
27 | public let viewModel = SampleWidgetViewModel()
28 |
29 | func widget(_ context: WidgetContext) -> Widget {
30 |
31 | ContainerWidget(
32 |
33 | VStackWidget([
34 |
35 | HStackWidget([
36 | ImageWidget()
37 | .image(viewModel.imageNamePublisher)
38 | .backgroundColor(.gray)
39 | .cornerRadius(30)
40 | .clipsToBounds(true)
41 | .height(60)
42 | .width(60),
43 |
44 | TitleWidgetView(title: viewModel.labelText2),
45 | ])
46 | .padding(10),
47 |
48 | LabelWidget()
49 | .text(viewModel.labelText1)
50 | .font(.preferredFont(forTextStyle: .title2))
51 | .backgroundColor(.gray)
52 | .padding(4)
53 | .with { (label, _) in
54 | label.adjustsFontSizeToFitWidth = true
55 | },
56 |
57 | LabelWidget()
58 | .text(viewModel.labelText2)
59 | .text(viewModel.labelPublisher)
60 | .text(viewModel.labelPublisher.map { $0 })
61 | .font(.preferredFont(forTextStyle: .title2))
62 | .backgroundColor(.gray)
63 | .padding(4),
64 |
65 | LabelWidget()
66 | .text(viewModel.errorPublisher)
67 | .font(.preferredFont(forTextStyle: .callout))
68 | .color(.white)
69 | .backgroundColor(.red)
70 | .numberOfLines(0)
71 | .padding(5)
72 | .hidden(viewModel.errorPublisher.map { $0 == nil }),
73 |
74 | HStackWidget([
75 | LabelWidget.title3("•"),
76 | LabelWidget.title3("Remember Me"),
77 | SpacerWidget(),
78 | UIViewWidget(UISwitch())
79 | .with { (view, _) in
80 | view.isOn = true
81 | }
82 | ]),
83 |
84 | ButtonWidget("Toggle Error")
85 | .color(.orange)
86 | .font(.footnote)
87 | .onTap { _ in
88 | if self.viewModel.errorPublisher.value == nil {
89 | self.viewModel.errorPublisher.accept("This is an error!")
90 | } else {
91 | self.viewModel.errorPublisher.accept(nil)
92 | }
93 | },
94 |
95 | SpacerWidget(),
96 |
97 | BackButtonWidget(text: "Done"),
98 |
99 | ]) // ColumnWidget
100 | .padding(20)
101 |
102 | ) // ContainerWidget
103 | .context { $0.put(UIFont.preferredFont(forTextStyle: .title1)) }
104 | .onViewWillAppear { _ in self.viewModel.load() }
105 | .navigationBar(title: "Sample", hidden: true)
106 | .backgroundColor(UIColor(white: 0.1, alpha: 1.0))
107 | .safeArea(false)
108 | }
109 | }
110 |
111 | struct TitleWidgetView: WidgetView {
112 | var title: String
113 | func widget(_ context: WidgetContext) -> Widget {
114 | LabelWidget(title)
115 | .color(.white)
116 | .font(context.get()) // note context extraction of font
117 | .contentHuggingPriority(.defaultHigh, for: .horizontal)
118 | }
119 | }
120 |
121 | extension LabelWidget {
122 | static func title3(_ text: String) -> LabelWidget {
123 | LabelWidget(text)
124 | .color(.white)
125 | .font(.preferredFont(forTextStyle: .title3))
126 | .contentHuggingPriority(.defaultHigh, for: .horizontal)
127 | .padding(h: 0, v: 4)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Users/UserDetailsWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDetailsWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 8/16/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxSwiftWidgets
11 |
12 | struct UserDetailsWidget: WidgetView {
13 |
14 | var user: User
15 |
16 | func widget(_ context: WidgetContext) -> Widget {
17 |
18 | ScrollWidget(
19 | VStackWidget([
20 |
21 | ContainerWidget(
22 | HStackWidget([
23 | UserPhotoWidget(initials: user.initials, size: 80),
24 | LabelWidget(user.name)
25 | .font(.title1)
26 | ]) // HStackWidget
27 | .position(.centerHorizontally)
28 | ), // ContainerWidget
29 |
30 | CardWidget(widget:
31 | VStackWidget([
32 | DetailsNameValueWidget(name: "Address", value: user.address),
33 | DetailsNameValueWidget(name: "City", value: user.city),
34 | DetailsNameValueWidget(name: "State", value: user.state),
35 | DetailsNameValueWidget(name: "Zip", value: user.zip),
36 | ])
37 | ),
38 |
39 | CardWidget(widget:
40 | VStackWidget([
41 | DetailsNameValueWidget(name: "Email", value: user.email),
42 | ])
43 | ),
44 |
45 | ]) // VStackWidget
46 | .spacing(20)
47 |
48 | ) // ScrollWidget
49 | .backgroundColor(.semanticSystemBackground)
50 | .safeArea(false)
51 | .padding(20)
52 | .navigationBar(title: "User Information", hidden: false)
53 |
54 | }
55 |
56 | }
57 |
58 | struct UserPhotoWidget: WidgetView {
59 |
60 | var initials: String?
61 | var size: CGFloat
62 |
63 | func widget(_ context: WidgetContext) -> Widget {
64 | ZStackWidget([
65 | LabelWidget(initials)
66 | .font(size > 40 ? .title1 : .body)
67 | .alignment(.center)
68 | .backgroundColor(.gray)
69 | .color(.white),
70 | ImageWidget(named: "User-\(initials ?? "")")
71 | ])
72 | .height(size)
73 | .width(size)
74 | .cornerRadius(size/2)
75 | }
76 |
77 | }
78 |
79 | fileprivate struct DetailsNameValueWidget: WidgetView {
80 |
81 | var name: String?
82 | var value: String?
83 |
84 | func widget(_ context: WidgetContext) -> Widget {
85 | HStackWidget([
86 | LabelWidget(name)
87 | .color(.gray),
88 | SpacerWidget(),
89 | LabelWidget(value)
90 | ])
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Application/Users/UserListWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserListWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 8/16/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxSwiftWidgets
11 |
12 | class UserListViewModel {
13 |
14 | @State var users: [User] = []
15 |
16 | func reload() {
17 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
18 | self.users = User.users
19 | })
20 | }
21 |
22 | }
23 |
24 | struct UserListWidget: WidgetView {
25 |
26 | var viewModel = UserListViewModel()
27 |
28 | func widget(_ context: WidgetContext) -> Widget {
29 |
30 | TableWidget([
31 | DynamicTableSectionWidget(viewModel.$users) {
32 | TableCellWidget(
33 | HStackWidget([
34 | UserPhotoWidget(initials: $0.initials, size: 35),
35 | LabelWidget($0.name)
36 | ])
37 | )
38 | .accessoryType(.disclosureIndicator)
39 | }
40 | .onSelect { (context, path, user) in
41 | context.navigator?.push(UserDetailsWidget(user: user))
42 | context.tableView?.deselectRow(at: path, animated: true)
43 | }
44 | ]) // TableWidget
45 | .onRefresh(initialRefresh: true, handler: { _ in
46 | self.viewModel.reload()
47 | })
48 | .navigationBar(title: "User List", hidden: false)
49 | .safeArea(false)
50 |
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/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 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | Launch Screen
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UIStatusBarStyle
30 | UIStatusBarStyleDarkContent
31 | UIStatusBarTintParameters
32 |
33 | UINavigationBar
34 |
35 | Style
36 | UIBarStyleDefault
37 | Translucent
38 |
39 |
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationPortraitUpsideDown
45 | UIInterfaceOrientationLandscapeLeft
46 | UIInterfaceOrientationLandscapeRight
47 |
48 | UISupportedInterfaceOrientations~ipad
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationPortraitUpsideDown
52 | UIInterfaceOrientationLandscapeLeft
53 | UIInterfaceOrientationLandscapeRight
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Models/AccountInformation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccountInformation.swift
3 | // Widgets
4 | //
5 | // Created by Michael Long on 3/28/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | struct AccountInformation {
13 |
14 | struct AccountDetails {
15 | let name: String
16 | let value: String
17 | }
18 |
19 | let name: String
20 | let footnotes: String
21 | let details: [AccountDetails]
22 |
23 | static let sample = AccountInformation(
24 | name: "Michael's Credit Card",
25 | footnotes: "Account balances are subject to processing delays and may not accurately represent the current balance.",
26 | details: [
27 | AccountDetails(name: "Account Type", value: "Visa Card"),
28 | AccountDetails(name: "Current Balance", value: "$12,346.23"),
29 | AccountDetails(name: "Balance Due", value: "$10,246.39"),
30 | AccountDetails(name: "Minimum Payment Due", value: "$146.23"),
31 | AccountDetails(name: "Last Payment Amount", value: "$5,000.00"),
32 | AccountDetails(name: "Interest Charged", value: "$233.22"),
33 | AccountDetails(name: "Payment Due", value: "03/12/19"),
34 | AccountDetails(name: "APR", value: "17.23%"),
35 | ]
36 | )
37 |
38 | static var dataOrError = true
39 |
40 | static func load() -> Observable {
41 | return .create({ (results) -> Disposable in
42 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
43 | if AccountInformation.dataOrError {
44 | results.onNext(AccountInformation.sample)
45 | results.onCompleted()
46 | } else {
47 | results.onError(GenericError.description("Unable to obtain account information at this time. Please try again later."))
48 | }
49 | AccountInformation.dataOrError.toggle()
50 | })
51 | return Disposables.create()
52 | })
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Models/GenericError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenericError.swift
3 | // Widgets
4 | //
5 | // Created by Michael Long on 3/30/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum GenericError: Error, LocalizedError {
12 |
13 | case description(String)
14 |
15 | public var errorDescription: String? {
16 | if case .description(let description) = self {
17 | return description
18 | }
19 | return "Unknown error occurred. Please try again later."
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // Widgets
4 | //
5 | // Created by Michael Long on 3/13/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import RxSwift
11 |
12 | struct User {
13 | let name: String?
14 | let address: String?
15 | let city: String?
16 | let state: String?
17 | let zip: String?
18 | let email: String?
19 | let initials: String?
20 | }
21 |
22 | extension User {
23 | static var users = [
24 | User(name: "Michael Long", address: "2310 S 178th", city: "Omaha", state: "NE", zip: "68130", email: "hml@gmail.com", initials: "ML"),
25 | User(name: "Frank Hardy", address: "190 W Dixon", city: "Los Angles", state: "CA", zip: "90210", email: "fhardy@gmail.com", initials: "FH"),
26 | User(name: "Joesph Hardy", address: "190 W Dixon", city: "Los Angles", state: "CA", zip: "90210", email: "jhardyverylongemailaddress@gmail.com", initials: "JH"),
27 | User(name: "Tom Swift", address: "33 Appleton Way", city: "New York", state: "NE", zip: "10101", email: "tswift@gmail.com", initials: "TS"),
28 | User(name: "Jonathan Quest", address: "General Delivery", city: "Palm Key", state: "FL", zip: "33480", email: "jquest@hb.com", initials: "JQ")
29 | ]
30 | }
31 |
32 | extension User {
33 |
34 | static var loginOrError = true
35 |
36 | static func login(_ username: String, _ password: String) -> Observable {
37 | return .create({ (results) -> Disposable in
38 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
39 | if User.loginOrError {
40 | results.onNext(User.users[0])
41 | results.onCompleted()
42 | } else {
43 | results.onError(GenericError.description("Unable to login at this time. Please try again later."))
44 | }
45 | User.loginOrError.toggle()
46 | })
47 | return Disposables.create()
48 | })
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "NotificationIcon@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "NotificationIcon@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-Small.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-Small@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-Small@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "57x57",
47 | "idiom" : "iphone",
48 | "filename" : "Icon.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "57x57",
53 | "idiom" : "iphone",
54 | "filename" : "Icon@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "Icon-60@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "60x60",
65 | "idiom" : "iphone",
66 | "filename" : "Icon-60@3x.png",
67 | "scale" : "3x"
68 | },
69 | {
70 | "size" : "20x20",
71 | "idiom" : "ipad",
72 | "filename" : "NotificationIcon~ipad.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "20x20",
77 | "idiom" : "ipad",
78 | "filename" : "NotificationIcon~ipad@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "29x29",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-Small.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "29x29",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-Small@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "40x40",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-40.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "40x40",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-40@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "50x50",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-Small-50.png",
109 | "scale" : "1x"
110 | },
111 | {
112 | "size" : "50x50",
113 | "idiom" : "ipad",
114 | "filename" : "Icon-Small-50@2x.png",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "72x72",
119 | "idiom" : "ipad",
120 | "filename" : "Icon-72.png",
121 | "scale" : "1x"
122 | },
123 | {
124 | "size" : "72x72",
125 | "idiom" : "ipad",
126 | "filename" : "Icon-72@2x.png",
127 | "scale" : "2x"
128 | },
129 | {
130 | "size" : "76x76",
131 | "idiom" : "ipad",
132 | "filename" : "Icon-76.png",
133 | "scale" : "1x"
134 | },
135 | {
136 | "size" : "76x76",
137 | "idiom" : "ipad",
138 | "filename" : "Icon-76@2x.png",
139 | "scale" : "2x"
140 | },
141 | {
142 | "size" : "83.5x83.5",
143 | "idiom" : "ipad",
144 | "filename" : "Icon-83.5@2x.png",
145 | "scale" : "2x"
146 | },
147 | {
148 | "size" : "1024x1024",
149 | "idiom" : "ios-marketing",
150 | "filename" : "ios-marketing.png",
151 | "scale" : "1x"
152 | }
153 | ],
154 | "info" : {
155 | "version" : 1,
156 | "author" : "xcode"
157 | }
158 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "RxSwiftWidgets-DK.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Logo.imageset/RxSwiftWidgets-DK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Logo.imageset/RxSwiftWidgets-DK.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/RxSwiftWidgets-Logo-DK.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "RxSwiftWidgets-Logo-DK.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/RxSwiftWidgets-Logo-DK.imageset/RxSwiftWidgets-Logo-DK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/RxSwiftWidgets-Logo-DK.imageset/RxSwiftWidgets-Logo-DK.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Solutions-Center-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Solutions-Center-logo.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Solutions-Center-logo.imageset/Solutions-Center-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/Solutions-Center-logo.imageset/Solutions-Center-logo.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-JQ.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "User-JQ.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-JQ.imageset/User-JQ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-JQ.imageset/User-JQ.jpg
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-ML.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "HML Animoji-G.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-ML.imageset/HML Animoji-G.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-ML.imageset/HML Animoji-G.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-TS.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "User-TS.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-TS.imageset/User-TS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-TS.imageset/User-TS.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-Unknown.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "unknown.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-Unknown.imageset/unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/User-Unknown.imageset/unknown.png
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/blur-background11.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "blur-background11.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/blur-background11.imageset/blur-background11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/blur-background11.imageset/blur-background11.jpg
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "vector1.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector1.imageset/vector1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector1.imageset/vector1.jpg
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "vector2.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector2.imageset/vector2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmlongco/RxSwiftWidgets/bfc4222771ef0487be605f7bede8adfb2ccbcc91/RxSwiftWidgetsDemo/Resources/Assets.xcassets/vector2.imageset/vector2.jpg
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Resources/Launch Screen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Shared/CardWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 9/1/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwiftWidgets
10 |
11 | struct CardWidget: WidgetView {
12 |
13 | let widget: Widget
14 |
15 | func widget(_ context: WidgetContext) -> Widget {
16 | ContainerWidget(widget)
17 | .padding(h: 20, v: 15)
18 | .cornerRadius(10)
19 | .backgroundColor(UIColor(white: 0.5, alpha: 0.15))
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Shared/ErrorMessageWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorMessageWidget.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 9/7/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwiftWidgets
10 |
11 | struct ErrorMessageWidget: WidgetView {
12 |
13 | @Binding var message: String
14 |
15 | func widget(_ context: WidgetContext) -> Widget {
16 | LabelWidget()
17 | .backgroundColor(UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.8))
18 | .color(.white)
19 | .padding(h:30, v: 20)
20 | .numberOfLines(0)
21 | .hidden(true)
22 | .onEvent($message) { (value, context) in
23 | guard let label = context.view as? UILabel else {
24 | return
25 | }
26 | if label.isHidden && value.isEmpty {
27 | return
28 | }
29 | let baseAnimation = { (_ value: String) in
30 | label.text = value.isEmpty ? nil : value
31 | label.isHidden = value.isEmpty
32 | label.superview?.layoutIfNeeded()
33 | }
34 | if label.isHidden {
35 | UIView.animate(withDuration: 0.3) {
36 | baseAnimation(value)
37 | }
38 | } else {
39 | UIView.animate(withDuration: 0.1, animations: {
40 | baseAnimation("")
41 | }) { (completed) in
42 | if completed && !value.isEmpty {
43 | UIView.animate(withDuration: 0.3) {
44 | baseAnimation(value)
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Styles/Colors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Colors.swift
3 | //
4 | // Created by Michael Long on 4/17/18.
5 | //
6 |
7 | import UIKit
8 |
9 | extension UIColor {
10 |
11 | public static let backgroundGray = UIColor(white: 0.95, alpha: 1.0)
12 |
13 | public static let brand = UIColor.orange
14 |
15 | public static let error = UIColor(red: 0.9, green: 0.2, blue: 0.2, alpha: 1.0)
16 | public static let errorText = UIColor(red: 0.9, green: 0.2, blue: 0.2, alpha: 1.0)
17 |
18 | public static let errorBannerBackground = UIColor(red: 1.0, green: 0.9, blue: 0.9, alpha: 1.0)
19 | public static let errorBannerText = UIColor(red: 0.9, green: 0.2, blue: 0.2, alpha: 1.0)
20 |
21 | static let disabledGray = UIColor(red: 0.556862745, green: 0.556862745, blue: 0.556862745, alpha: 1)
22 | }
23 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Styles/Fonts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fonts.swift
3 | //
4 | // Created by Michael Long on 4/17/18.
5 | //
6 |
7 | import UIKit
8 |
9 | extension UIFont {
10 |
11 | static let body = UIFont.preferredFont(forTextStyle: .body)
12 | static let callout = UIFont.preferredFont(forTextStyle: .callout)
13 | static let caption1 = UIFont.preferredFont(forTextStyle: .caption1)
14 | static let caption2 = UIFont.preferredFont(forTextStyle: .caption2)
15 | static let error = UIFont.preferredFont(forTextStyle: .footnote)
16 | static let footnote = UIFont.preferredFont(forTextStyle: .footnote)
17 | static let headline = UIFont.preferredFont(forTextStyle: .headline)
18 | static let text = UIFont.preferredFont(forTextStyle: .body)
19 | static let title1 = UIFont.preferredFont(forTextStyle: .title1)
20 | static let title2 = UIFont.preferredFont(forTextStyle: .title2)
21 | static let title3 = UIFont.preferredFont(forTextStyle: .title3)
22 |
23 | }
24 |
25 | extension UIFont {
26 |
27 | private func withTraits(traits: UIFontDescriptor.SymbolicTraits...) -> UIFont? {
28 | guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits)) else {
29 | return nil
30 | }
31 |
32 | return UIFont(descriptor: descriptor, size: 0)
33 | }
34 |
35 | func bold() -> UIFont? {
36 | return withTraits(traits: .traitBold)
37 | }
38 |
39 | func italic() -> UIFont? {
40 | return withTraits(traits: .traitItalic)
41 | }
42 |
43 | func boldItalic() -> UIFont? {
44 | return withTraits(traits: .traitBold, .traitItalic)
45 | }
46 |
47 | func condensed() -> UIFont? {
48 | return withTraits(traits: .traitCondensed)
49 | }
50 |
51 | func expanded() -> UIFont? {
52 | return withTraits(traits: .traitExpanded)
53 | }
54 |
55 | func tightLeading() -> UIFont? {
56 | return withTraits(traits: .traitTightLeading)
57 | }
58 |
59 | func looseLeading() -> UIFont? {
60 | return withTraits(traits: .traitLooseLeading)
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Styles/Style.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Defaults.swift
3 | // Widgets
4 | //
5 | // Created by Michael Long on 3/18/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class Style {
12 | static var shared: StyleProviding = DefaultStyle()
13 | }
14 |
15 | protocol StyleProviding {
16 |
17 | var primaryColor: UIColor { get }
18 | var secondaryColor: UIColor { get }
19 |
20 | var navigationColor: UIColor { get }
21 | var backgroundColor: UIColor { get }
22 |
23 | var textColor: UIColor { get }
24 | var textFont: UIFont { get }
25 |
26 | var sectionBackgroundColor: UIColor { get }
27 |
28 | }
29 |
30 | class DefaultStyle: StyleProviding {
31 |
32 | lazy var primaryColor: UIColor = .red
33 | lazy var secondaryColor: UIColor = .gray
34 |
35 | lazy var navigationColor: UIColor = primaryColor
36 | lazy var backgroundColor: UIColor = .white
37 |
38 | lazy var textColor: UIColor = .darkText
39 | lazy var textFont: UIFont = .preferredFont(forTextStyle: .callout)
40 |
41 | lazy var sectionBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 1.0)
42 |
43 | }
44 |
45 | class DarkStyle: StyleProviding {
46 |
47 | lazy var primaryColor: UIColor = .red
48 | lazy var secondaryColor: UIColor = .gray
49 |
50 | lazy var navigationColor: UIColor = primaryColor
51 | lazy var backgroundColor: UIColor = UIColor(white: 0, alpha: 1.0)
52 |
53 | lazy var textColor: UIColor = .lightText
54 | lazy var textFont: UIFont = .preferredFont(forTextStyle: .callout)
55 |
56 | lazy var sectionBackgroundColor: UIColor = UIColor(white: 0.2, alpha: 1.0)
57 |
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/RxSwiftWidgetsDemo/Styles/Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Styles.swift
3 | //
4 | // Created by Michael Long on 4/17/18.
5 | //
6 |
7 | import UIKit
8 |
9 | extension AppDelegate {
10 |
11 | func configureStyles() {
12 | window?.tintColor = .brand
13 | setupControlStyles()
14 | setupLabelStyles()
15 | setupNavigationStyles()
16 | }
17 |
18 | private func setupControlStyles() {
19 | UIImageView.appearance(whenContainedInInstancesOf: [UIButton.self]).tintColor = .brand
20 | UITextField.appearance().textColor = .darkText
21 | UITextField.appearance().tintColor = .gray
22 | UISwitch.appearance().onTintColor = .brand
23 | }
24 |
25 | private func setupLabelStyles() {
26 | // BrandLabel.appearance().textColor = .brand
27 | // DarkBrandLabel.appearance().textColor = .darkBrand
28 | // BrandFootnoteLabel.appearance().font = .footnote
29 | // BrandFootnoteLabel.appearance().textColor = .brand
30 | // DarkBrandFootnoteLabel.appearance().font = .footnote
31 | // DarkBrandFootnoteLabel.appearance().textColor = .darkBrand
32 | // ErrorLabel.appearance().font = .footnote
33 | // ErrorLabel.appearance().textColor = .error
34 | // FootnoteLabel.appearance().font = .footnote
35 | // FootnoteLabel.appearance().textColor = .disabledGrayHex
36 | // DarkFootnoteLabel.appearance().font = .footnote
37 | // DarkFootnoteLabel.appearance().textColor = .disabledGrayHex
38 | }
39 |
40 | private func setupNavigationStyles() {
41 | UINavigationBar.appearance().barStyle = .black
42 | UINavigationBar.appearance().backgroundColor = .black
43 | UINavigationBar.appearance().barTintColor = .black
44 | UINavigationBar.appearance().tintColor = .white
45 | UINavigationBar.appearance().isTranslucent = false
46 | UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
47 | if #available(iOS 11.0, *) {
48 | UINavigationBar.appearance().largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/Widget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Widget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Base type for any type of widget.
12 | public protocol AnyWidget { }
13 |
14 | /// A special-purpose widget capable of building a specific UIViewController when asked. Does not generate UIViews.
15 | public protocol WidgetControllerType: AnyWidget {
16 | /// Builds or returns a UIViewController that wraps the current widget.
17 | func controller(with context: WidgetContext) -> UIViewController
18 | }
19 |
20 | /// Core definition of a widget as an entity capable of building a specific UIView when asked.
21 | public protocol WidgetViewType: WidgetControllerType {
22 | /// Builds or returns a UIView that represents the current widget.
23 | func build(with context: WidgetContext) -> UIView
24 | }
25 |
26 | /// Convenience type as most widgets are WidgetViewTypes.
27 | public typealias Widget = WidgetViewType
28 |
29 | /// Namespace for many RxSwiftWidget Enumerations and Definitions
30 | public struct Widgets { }
31 |
32 | /// A widget that contains or wraps another widget
33 | public protocol WidgetContaining: Widget {
34 | /// Variable containing the wrapped widget
35 | var widget: Widget { get }
36 | }
37 |
38 | /// A widget that holds a list of widgets, usually some form of stack
39 | public protocol WidgetsContaining: Widget {
40 | /// Variable containing an array of the wrapped widgets
41 | var widgets: [Widget] { get }
42 | }
43 |
44 | /// Common functions on WidgetViewType.
45 | extension WidgetViewType {
46 |
47 | /// Walks widget tree of WidgetContaining and WidgetsContaining and calls closure on each node.
48 | public func walk(_ process: (_ widget: Widget) -> Void ) {
49 | func each(_ widget: Widget) {
50 | process(widget)
51 | if let widget = widget as? WidgetContaining {
52 | each(widget)
53 | } else if let widget = widget as? WidgetsContaining {
54 | widget.widgets.forEach { each($0) }
55 | }
56 | }
57 | each(self)
58 | }
59 |
60 | }
61 |
62 | public struct WidgetFactory { }
63 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetAnimation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetAnimation.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 8/2/19.
6 | //
7 |
8 | import UIKit
9 |
10 | public struct WidgetAnimation {
11 |
12 | let duration: TimeInterval
13 |
14 | public init(duration: TimeInterval) {
15 | self.duration = duration
16 | }
17 |
18 | public func perform(_ callback: @escaping () -> Void) {
19 | UIView.animate(withDuration: duration) {
20 | callback()
21 | }
22 | }
23 |
24 | public static func basic(duration: TimeInterval = 0.2) -> WidgetAnimation {
25 | WidgetAnimation(duration: 0.2)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetContext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetContext.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 | // func test1() {
9 | // let context = self.put(MyClass())
10 | // if let myClass: MyClass = context.find() {
11 | // print(myClass.value)
12 | // }
13 | // }
14 | //
15 | // func test2() {
16 | // let context = self
17 | // .set(UIFont.preferredFont(forTextStyle: .title2), for: WidgetContext.Keys.titleFont)
18 | // .set(UIFont.preferredFont(forTextStyle: .callout), for: WidgetContext.Keys.bodyFont)
19 | //
20 | // if let font: UIFont = context.find(WidgetContext.Keys.bodyFont) {
21 | // print(font.familyName)
22 | // }
23 | // }
24 |
25 | import UIKit
26 | import RxSwift
27 | import RxCocoa
28 |
29 | public struct WidgetContext {
30 |
31 | public weak var viewController: UIViewController?
32 |
33 | public weak var parentView: UIView?
34 | public weak var view: UIView?
35 |
36 | public var attributes: [String:Any?] = [:]
37 | public var disposeBag: DisposeBag
38 |
39 | public init() {
40 | self.disposeBag = DisposeBag()
41 | }
42 |
43 | public func set(view: UIView) -> WidgetContext {
44 | var context = self
45 | context.parentView = context.view
46 | context.view = view
47 | return context
48 | }
49 |
50 | public func get(_ key: String) -> T {
51 | // siwftlint:disable force_unwrapping
52 | (attributes[key] as? T)!
53 | // siwftlint:enable force_unwrapping
54 | }
55 |
56 | public func get(_ type: T.Type = T.self) -> T {
57 | // siwftlint:disable force_unwrapping
58 | (attributes[String(describing: type)] as? T)!
59 | // siwftlint:enable force_unwrapping
60 | }
61 |
62 | public func getWeak(_ type: T.Type = T.self) -> T? {
63 | ((attributes[String(describing: type)] as? WeakBox)?.object as? T)
64 | }
65 |
66 | public func find(_ key: String) -> T? {
67 | attributes[key] as? T
68 | }
69 |
70 | public func find(_ type: T.Type = T.self) -> T? {
71 | attributes[String(describing: type)] as? T
72 | }
73 |
74 | public func put(_ object: T) -> WidgetContext {
75 | var context = self
76 | context.attributes[String(describing: T.self)] = object
77 | return context
78 | }
79 |
80 | public func putWeak(_ object: T) -> WidgetContext {
81 | var context = self
82 | context.attributes[String(describing: T.self)] = WeakBox(object: object)
83 | return context
84 | }
85 |
86 | public func set(_ value: T?, for key: String) -> WidgetContext {
87 | var context = self
88 | context.attributes[key] = value
89 | return context
90 | }
91 |
92 | }
93 |
94 | fileprivate struct WeakBox {
95 | weak var object: AnyObject?
96 | }
97 |
98 | public typealias WidgetContextModifier = (WidgetContext) -> WidgetContext
99 |
100 | extension WidgetViewModifying {
101 |
102 | public func context(_ modifier: @escaping WidgetContextModifier) -> Self {
103 | return modified { $0.modifiers.contextModifier = modifier }
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetDismissible.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetDismissible.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/28/19.
6 | //
7 |
8 | import UIKit
9 |
10 | public typealias WidgetDismissibleReturnHandler = (_ value: ReturnType) -> Void
11 |
12 | public protocol WidgetDismissibleType {}
13 |
14 | public struct WidgetDismissibleReturn: WidgetDismissibleType {
15 |
16 | public let handler: WidgetDismissibleReturnHandler
17 |
18 | public init(_ handler: @escaping WidgetDismissibleReturnHandler) {
19 | self.handler = handler
20 | }
21 |
22 | }
23 |
24 | public enum WidgetDismissiblePresentationType {
25 | case alert
26 | case presented
27 | case pushed
28 | case sheet
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | /// A type-erased widget view modifier
14 | public protocol AnyWidgetModifier {
15 | func apply(to view: UIView, with context: WidgetContext)
16 | }
17 |
18 | /// Standard widget modifier that updates the view using keypath and values
19 | public struct WidgetModifier: AnyWidgetModifier {
20 |
21 | public let keyPath: WritableKeyPath
22 | public let value: Value
23 |
24 | public init(keyPath: WritableKeyPath, value: Value) {
25 | self.keyPath = keyPath
26 | self.value = value
27 | }
28 |
29 | public func apply(to view: UIView, with context: WidgetContext) {
30 | if var view = view as? View {
31 | view[keyPath: keyPath] = value
32 | }
33 | }
34 | }
35 |
36 |
37 | public typealias WidgetModifierBlockType = (View, WidgetContext) -> Void
38 |
39 | /// Standard widget modifier that updates the view using a closure block
40 | public struct WidgetModifierBlock: AnyWidgetModifier {
41 |
42 | public let modifier: WidgetModifierBlockType
43 |
44 | public init(_ modifier: @escaping WidgetModifierBlockType) {
45 | self.modifier = modifier
46 | }
47 |
48 | public func apply(to view: UIView, with context: WidgetContext) {
49 | if let view = view as? View {
50 | modifier(view, context)
51 | }
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetModifying.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | /// List of view modifiers
14 | public struct WidgetModifiers {
15 | /// List of additional modifiers
16 | public var list: Array?
17 | /// Support for context modifiers
18 | public var contextModifier: WidgetContextModifier?
19 | /// Modifier for primary view data binding, if any
20 | public var binding: AnyWidgetModifier? = nil
21 | /// Used if WiddgetPadding is supported
22 | public var padding: UIEdgeInsets? = nil
23 | /// Public initialization
24 | public init() {}
25 | }
26 |
27 | extension WidgetModifiers {
28 |
29 | public func apply(to view: UIView, with context: WidgetContext) {
30 | self.binding?.apply(to: view, with: context)
31 | self.list?.forEach { $0.apply(to: view, with: context) }
32 | }
33 |
34 | public func modified(_ context: WidgetContext, for view: UIView) -> WidgetContext {
35 | return (contextModifier?(context) ?? context).set(view: view)
36 | }
37 | }
38 |
39 |
40 | /// Defines a widget capable of supporting view modifiers
41 | public protocol WidgetModifying: Widget {
42 | var modifiers: WidgetModifiers { get set }
43 | }
44 |
45 | extension WidgetModifying {
46 |
47 | /// Returns new widget with view modifier added to modification array.
48 | ///
49 | /// This is the primary function behind widget chaining in struct-based widgets. Same mechanism will work for class-based widgets
50 | /// as widget effectively returns iteself with new list entry.
51 | ///
52 | /// Arrays choosen over modification linked-list as each element in a list would need dynamic allocation whereas Swift arrays expand
53 | /// exponentially and would also have improved performance iterating through the array.
54 | public func modified(_ modifier: AnyWidgetModifier) -> Self {
55 | var widget = self
56 | if widget.modifiers.list == nil {
57 | widget.modifiers.list = [modifier]
58 | widget.modifiers.list?.reserveCapacity(8)
59 | } else {
60 | widget.modifiers.list?.append(modifier)
61 | }
62 | return widget
63 | }
64 |
65 | public func modified(_ modifier: (_ widget: inout Self) -> Void) -> Self {
66 | var widget = self
67 | modifier(&widget)
68 | return widget
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetNavigator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetNavigator.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/23/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | /// WidgetNavigator provides access to the current UINavgiationController and navigation stack.
13 | public struct WidgetNavigator {
14 |
15 | var context: WidgetContext
16 |
17 | public init(_ context: WidgetContext) {
18 | self.context = context
19 | }
20 |
21 | /// Returns the current UINavigation controller for the current context.
22 | public var navigationController: UINavigationController? {
23 | context.viewController?.navigationController
24 | }
25 |
26 | // dismissible functionality
27 |
28 | /// Pushes the widget onto the navigation stack in a new UIWidgetHostController.
29 | public func push(_ widget: WidgetControllerType, animated: Bool = true) {
30 | let context = self.context.set(presentation: .pushed)
31 | let viewController = widget.controller(with: context)
32 | navigationController?.pushViewController(viewController, animated: animated)
33 | }
34 |
35 | /// Pushes the widget onto the navigation stack in a new UIWidgetHostController with a return value handler.
36 | public func push(_ widget: WidgetControllerType, animated: Bool = true,
37 | onDismiss handler: @escaping WidgetDismissibleReturnHandler) {
38 | let dismissible = WidgetDismissibleReturn(handler)
39 | let context = self.context.set(presentation: .pushed).set(dismissible: dismissible)
40 | let viewController = widget.controller(with: context)
41 | navigationController?.pushViewController(viewController, animated: animated)
42 | }
43 |
44 | /// Presents a widget on the navigation stack in a new UIWidgetHostController.
45 | public func present(_ widget: WidgetControllerType, animated: Bool = true) {
46 | let context = self.context.set(presentation: .presented)
47 | let viewController = widget.controller(with: context)
48 | navigationController?.present(viewController, animated: animated, completion: nil)
49 | }
50 |
51 | /// Presents a widget on the navigation stack in a new UIWidgetHostController with a return value handler.
52 | public func present(_ widget: WidgetControllerType, animated: Bool = true,
53 | onDismiss handler: @escaping WidgetDismissibleReturnHandler) {
54 | let dismissible = WidgetDismissibleReturn(handler)
55 | let context = self.context.set(presentation: .presented).set(dismissible: dismissible)
56 | let viewController = widget.controller(with: context)
57 | navigationController?.present(viewController, animated: animated, completion: nil)
58 | }
59 |
60 | /// Pops or dismisses the current view controller, returning a value that will be passed to the onDismiss handler.
61 | public func dismiss(returning value: Value, animated: Bool = true) {
62 | if let dimissible = context.dismissible as? WidgetDismissibleReturn {
63 | dimissible.handler(value)
64 | }
65 | dismiss(animated: animated)
66 | }
67 |
68 | /// Pops or dismisses the current view controller, dependent upon the presentation state.
69 | public func dismiss(animated: Bool = true) {
70 | guard let viewController = context.viewController else {
71 | return
72 | }
73 | switch self.context.presentation {
74 | case .alert, .presented, .sheet:
75 | viewController.dismiss(animated: animated, completion: nil)
76 | case .pushed:
77 | viewController.navigationController?.popToViewController(viewController, animated: false)
78 | viewController.navigationController?.popViewController(animated: animated)
79 | }
80 | }
81 |
82 | // standard functionality
83 |
84 | /// Pushes a non-widget baseed view controller onto the current navigation stack.
85 | public func pushViewController(_ viewController: UIViewController, animated: Bool = true) {
86 | navigationController?.pushViewController(viewController, animated: animated)
87 | }
88 |
89 | /// Pops a view controller from the current navigation stack.
90 | public func popViewController(animated: Bool = true) {
91 | navigationController?.popViewController(animated: animated)
92 | }
93 |
94 | /// Pops to the root view controller on the current navigation stack.
95 | public func popToRootViewController(animated: Bool = true) {
96 | navigationController?.popToRootViewController(animated: animated)
97 | }
98 |
99 | /// Presents a view controller using the current navigation stack.
100 | public func presentViewController(_ viewController: UIViewController, animated: Bool = true) {
101 | context.viewController?.present(viewController, animated: animated, completion: nil)
102 | }
103 |
104 | /// Dismisses a view controller from the current navigation stack.
105 | public func dismissViewController(animated: Bool = true) {
106 | context.viewController?.dismiss(animated: animated, completion: nil)
107 | }
108 |
109 | }
110 |
111 |
112 | extension WidgetContext {
113 |
114 | /// Returns the Navigator for the current context.
115 | public var navigator: WidgetNavigator? {
116 | if viewController != nil {
117 | return WidgetNavigator(self)
118 | }
119 | return nil
120 | }
121 |
122 | /// Returns the current context with a new RxSwift DisposeBag for subscriptions.
123 | public func new() -> WidgetContext {
124 | var context = self
125 | context.disposeBag = DisposeBag()
126 | return context
127 | }
128 |
129 | /// Sets the current view controller to be the passed view controller.
130 | public func set(viewController: UIViewController) -> WidgetContext {
131 | var context = self
132 | context.viewController = viewController
133 | return context
134 | }
135 |
136 | /// Returns the current navigation dismissible, if any.
137 | public var dismissible: WidgetDismissibleType? {
138 | return get(WidgetDismissibleType.self)
139 | }
140 |
141 | /// Sets the current navigation dismissible.
142 | public func set(dismissible: WidgetDismissibleType?) -> WidgetContext {
143 | if let dismissible = dismissible {
144 | return put(dismissible)
145 | }
146 | return self
147 | }
148 |
149 | /// Returns the current navigation presentation type for the current view controller.
150 | public var presentation: WidgetDismissiblePresentationType {
151 | return find(WidgetDismissiblePresentationType.self) ?? .pushed
152 | }
153 |
154 | /// Sets the current navigation presentation type for the current view controller.
155 | public func set(presentation: WidgetDismissiblePresentationType) -> WidgetContext {
156 | return put(presentation)
157 | }
158 |
159 | }
160 |
161 | fileprivate struct UIViewControllerBox {
162 | weak var viewController: UIViewController?
163 | }
164 |
165 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetPadding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetPadding.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | /// Protocol defines view capable of supporting custom padding
14 | public protocol WidgetPadding: WidgetModifying {}
15 |
16 | extension WidgetPadding {
17 |
18 | public func padding(insets: UIEdgeInsets) -> Self {
19 | var widget = self
20 | widget.modifiers.padding = insets
21 | return widget
22 | }
23 |
24 | public func padding(_ padding: CGFloat) -> Self {
25 | return self.padding(insets: UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding))
26 | }
27 |
28 | public func padding(h: CGFloat, v: CGFloat) -> Self {
29 | return self.padding(insets: UIEdgeInsets(top: v, left: h, bottom: v, right: h))
30 | }
31 |
32 | public func padding(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) -> Self {
33 | return self.padding(insets: UIEdgeInsets(top: top, left: left, bottom: bottom, right: right))
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetSwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetSwiftUI.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 8/2/19.
6 | //
7 |
8 | import Foundation
9 |
10 | //public typealias Button = ButtonWidget
11 | //public typealias Container = ContainerWidget
12 | //public typealias Image = ImageWidget
13 | //public typealias HStack = HStackWidget
14 | //public typealias Spacer = SpacerWidget
15 | //public typealias Spinner = SpinnerWidget
16 | //public typealias Text = LabelWidget
17 | //public typealias VStack = VStackWidget
18 | //public typealias ZStack = ZStackWidget
19 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetTheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetTheme.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 8/26/19.
6 | //
7 |
8 | import UIKit
9 |
10 | extension WidgetContext {
11 |
12 | public var theme: WidgetTheme {
13 | if let theme = find(WidgetTheme.self) {
14 | return theme
15 | }
16 | return WidgetTheme.defaultTheme
17 | }
18 |
19 | public func set(theme: WidgetTheme) -> WidgetContext {
20 | return put(theme as WidgetTheme)
21 | }
22 |
23 | }
24 |
25 | public struct WidgetTheme {
26 |
27 | public struct Color {
28 |
29 | public var accent: UIColor
30 | public var link: UIColor
31 |
32 | public var text: UIColor
33 | public var secondaryText: UIColor
34 |
35 | }
36 |
37 | public struct Font {
38 | public var body: UIFont
39 | }
40 |
41 | public var color: Color
42 | public var font: Font
43 |
44 | public mutating func update(_ updater: @escaping (_ theme: inout WidgetTheme) -> Void) {
45 | updater(&self)
46 | }
47 |
48 | }
49 |
50 | extension WidgetTheme {
51 |
52 | public static var defaultTheme: WidgetTheme = {
53 | if #available(iOS 13, *) {
54 | let color = WidgetTheme.Color(
55 | accent: .systemBlue,
56 | link: .systemBlue,
57 | text: .label,
58 | secondaryText: .secondaryLabel
59 | )
60 | let font = WidgetTheme.Font(
61 | body: UIFont.preferredFont(forTextStyle: .body)
62 | )
63 | return WidgetTheme(color: color, font: font)
64 | } else {
65 | let color = WidgetTheme.Color(
66 | accent: .blue,
67 | link: .blue,
68 | text: .darkText,
69 | secondaryText: .gray
70 | )
71 | let font = WidgetTheme.Font(
72 | body: UIFont.preferredFont(forTextStyle: .body)
73 | )
74 | return WidgetTheme(color: color, font: font)
75 | }
76 | }()
77 |
78 | }
79 |
80 | extension WidgetViewModifying {
81 |
82 | public func theme(_ theme: WidgetTheme) -> Self {
83 | let modifier: WidgetContextModifier = { $0.set(theme: theme) }
84 | return modified { $0.modifiers.contextModifier = modifier }
85 | }
86 |
87 | public func theme(_ update: @escaping (_ theme: inout WidgetTheme) -> Void) -> Self {
88 | let modifier: WidgetContextModifier = { context in
89 | var theme = context.theme
90 | update(&theme)
91 | return context.set(theme: theme)
92 | }
93 | return modified { $0.modifiers.contextModifier = modifier }
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetView.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/16/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public protocol WidgetView: Widget {
14 |
15 | func widget(_ context: WidgetContext) -> Widget
16 |
17 | func build(_ widget: Widget, with context: WidgetContext) -> UIView
18 |
19 | }
20 |
21 | extension WidgetView {
22 |
23 | /// Implement this function to hook into the standard build process and perform addtional processing on the passed context
24 | /// before the view is built or to add additional processing onto the generated UIView.
25 | ///
26 | /// Default implementation delegates to build(:Widget:Context) function below.
27 | ///
28 | public func build(with context: WidgetContext) -> UIView {
29 | return build(widget(context), with: context)
30 | }
31 |
32 | /// Performs the actual build on the widget provided by WidgetView.
33 | public func build(_ widget: Widget, with context: WidgetContext) -> UIView {
34 | let view = widget.build(with: context)
35 | if let modifying = self as? WidgetModifying {
36 | let context = modifying.modifiers.modified(context, for: view)
37 | modifying.modifiers.apply(to: view, with: context)
38 | }
39 | return view
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Core/WidgetViewNavigating.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 9/16/19.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Protocol indicates that current widget view should wraps its viewcontroller in a navigation controller.
11 | public protocol WidgetViewNavigating: WidgetView {
12 |
13 | /// Returns a custom UINavigationController that's used to wrap the current widget's view controller.
14 | func navigationController(with context: WidgetContext) -> UINavigationController
15 |
16 | }
17 |
18 | extension WidgetFactory {
19 |
20 | public static var defaultNavigationController: (_ context: WidgetContext) -> UINavigationController = { _ in
21 | return UINavigationController()
22 | }
23 |
24 | }
25 |
26 | extension WidgetViewNavigating {
27 |
28 | /// Returns a custom UINavigationController that's used to wrap the current widget's view controller.
29 | ///
30 | /// Default implementation returns a plain UINavigationController.
31 | ///
32 | public func navigationController(with context: WidgetContext) -> UINavigationController {
33 | WidgetFactory.defaultNavigationController(context)
34 | }
35 |
36 | }
37 |
38 | extension WidgetViewType {
39 |
40 | /// Allows any widget to be pushed or presented on the navigation stack. Calls the widget's build function and then
41 | /// wraps the result in an UIWidgetHostController.
42 | ///
43 | /// If Self is WidgetViewNavigating then a UINavigationController is constructed and wrapped around the host controller.
44 | ///
45 | public func controller(with context: WidgetContext) -> UIViewController {
46 | if let navigating = self as? WidgetViewNavigating {
47 | let navigationController = navigating.navigationController(with: context)
48 | let viewController = UIWidgetHostController(self, with: context)
49 | navigationController.addChild(viewController)
50 | return navigationController
51 | }
52 | return UIWidgetHostController(self, with: context)
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Extensions/UIColor+Widgets.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Widgets.swift
3 | // RxSwiftWidgetsDemo
4 | //
5 | // Created by Michael Long on 9/10/19.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 |
12 | public class var semanticSystemBackground: UIColor {
13 | if #available(iOS 13, *) {
14 | return .systemBackground
15 | }
16 | return .white
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Extensions/UIView+Widgets.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Widgets.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 |
12 | /// Associated data which MAY exist on explicitly positioned subviews
13 | public struct WidgetViewAttributes {
14 |
15 | var position: Widgets.Position = .fill
16 | var safeArea = true
17 |
18 | }
19 |
20 | public protocol WidgetViewExtendable: UIView {
21 |
22 | var widget: WidgetViewAttributes { get set }
23 |
24 | func build(widget: Widget, with context: WidgetContext)
25 | func addConstrainedSubview(_ subview: UIView, with padding: UIEdgeInsets?)
26 |
27 | }
28 |
29 | public protocol WidgetViewCustomConstraints {
30 | func addCustomConstraints()
31 | }
32 |
33 | extension UIView: WidgetViewExtendable {
34 |
35 | private static var WidgetViewAttributesKey: UInt8 = 0
36 |
37 | public func build(widget: Widget, with context: WidgetContext) {
38 | let context = context.set(view: self)
39 | let view = widget.build(with: context)
40 | addConstrainedSubview(view)
41 | }
42 |
43 | public func addConstrainedSubview(_ subview: UIView, with padding: UIEdgeInsets? = nil) {
44 | let attributes = subview.widget
45 | let padding = padding ?? UIEdgeInsets.zero
46 |
47 | self.addSubview(subview)
48 |
49 | attributes.position.apply(to: subview, padding: padding, safeArea: attributes.safeArea)
50 |
51 | if let customView = subview as? WidgetViewCustomConstraints {
52 | customView.addCustomConstraints()
53 | }
54 | }
55 |
56 | public var widget: WidgetViewAttributes {
57 | get {
58 | if let attributes = objc_getAssociatedObject( self, &UIView.WidgetViewAttributesKey ) as? WidgetViewAttributes {
59 | return attributes
60 | }
61 | return WidgetViewAttributes()
62 | }
63 | set {
64 | objc_setAssociatedObject(self, &UIView.WidgetViewAttributesKey, newValue, .OBJC_ASSOCIATION_RETAIN)
65 | }
66 | }
67 |
68 | public func viewWithID(_ id: T) -> V? where T: RawRepresentable, T.RawValue == Int {
69 | return viewWithTag(id.rawValue) as? V
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Extensions/UIWidgetHostController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+Widgets.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/23/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | open class UIWidgetHostController: UIViewController {
14 |
15 | public var widget: Widget!
16 | public var context: WidgetContext!
17 |
18 | // lifecycle
19 |
20 | public init(_ widget: Widget, with context: WidgetContext? = nil) {
21 | super.init(nibName: nil, bundle: nil)
22 | self.widget = widget
23 | self.context = (context?.new() ?? WidgetContext())
24 | .set(viewController: self)
25 | }
26 |
27 | required public init?(coder: NSCoder) {
28 | fatalError("init(coder:) has not been implemented")
29 | }
30 |
31 | deinit {
32 | print("DEINIT UIWidgetViewController")
33 | }
34 |
35 | override public func viewDidLoad() {
36 | super.viewDidLoad()
37 | build()
38 | }
39 |
40 | public func build() {
41 | view.build(widget: widget, with: context)
42 | }
43 |
44 | public func rebuild() {
45 | context = context.new()
46 | build()
47 | }
48 |
49 | }
50 |
51 | extension WidgetContext {
52 |
53 | public func rebuild() {
54 | (self.viewController as? UIWidgetHostController)?.rebuild()
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetControlModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | /// Extends the basic modifification system with modifiers for many of the core UIControl attributes.
15 | public protocol WidgetControlModifying: WidgetViewModifying {
16 |
17 | }
18 |
19 | // MARK:- UIView Standard Properties
20 |
21 | extension WidgetControlModifying {
22 |
23 | /// Sets enabled for control
24 | public func enabled(_ isEnabled: Bool) -> Self {
25 | return modified(WidgetModifier(keyPath: \UIControl.isEnabled, value: isEnabled))
26 | }
27 |
28 | /// Sets selected for control
29 | public func selected(_ isSelected: Bool) -> Self {
30 | return modified(WidgetModifier(keyPath: \UIControl.isSelected, value: isSelected))
31 | }
32 |
33 | public func tintColor(_ tintColor: UIColor) -> Self {
34 | return modified(WidgetModifier(keyPath: \UIControl.tintColor, value: tintColor))
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUINavigation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | extension WidgetModifying {
14 |
15 | public func navigationBar(title: String, preferLargeTitles: Bool? = nil, hidden: Bool? = nil) -> Self {
16 | return modified(WidgetModifierBlock { _, context in
17 | if let vc = context.viewController {
18 | vc.rx.methodInvoked(#selector(UIViewController.viewWillAppear(_:)))
19 | .subscribe(onNext: { _ in
20 | context.viewController?.title = title
21 | guard let nav = context.navigator?.navigationController else { return }
22 | if let largeTitles = preferLargeTitles {
23 | nav.navigationBar.prefersLargeTitles = largeTitles
24 | }
25 | if let hidden = hidden {
26 | nav.isNavigationBarHidden = hidden
27 | }
28 | })
29 | .disposed(by: context.disposeBag)
30 | }
31 | })
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | /// Extends the basic modifification system with modifiers for many of the core UIView attributes.
15 | public protocol WidgetViewModifying: WidgetModifying {
16 |
17 | }
18 |
19 | // MARK:- UIView Standard Properties
20 |
21 | extension WidgetViewModifying {
22 |
23 | public func accessibilityLabel(_ accessibilityLabel: String?) -> Self {
24 | return modified(WidgetModifier(keyPath: \UIView.accessibilityLabel, value: accessibilityLabel))
25 | }
26 |
27 | public func alpha(_ alpha: CGFloat) -> Self {
28 | return modified(WidgetModifier(keyPath: \UIView.alpha, value: alpha))
29 | }
30 |
31 | public func backgroundColor(_ backgroundColor: UIColor) -> Self {
32 | return modified(WidgetModifier(keyPath: \UIView.backgroundColor, value: backgroundColor))
33 | }
34 |
35 | public func clipsToBounds(_ clipsToBounds: Bool) -> Self {
36 | return modified(WidgetModifier(keyPath: \UIView.clipsToBounds, value: clipsToBounds))
37 | }
38 |
39 | public func contentMode(_ contentMode: UIView.ContentMode) -> Self {
40 | return modified(WidgetModifier(keyPath: \UIView.contentMode, value: contentMode))
41 | }
42 |
43 | public func hidden(_ isHidden: Bool) -> Self {
44 | return modified(WidgetModifier(keyPath: \UIView.isHidden, value: isHidden))
45 | }
46 |
47 | public func tag(_ id: T) -> Self where T: RawRepresentable, T.RawValue == Int {
48 | return modified(WidgetModifier(keyPath: \UIView.tag, value: id.rawValue))
49 | }
50 |
51 | public func tag(_ tag: Int) -> Self {
52 | return modified(WidgetModifier(keyPath: \UIView.tag, value: tag))
53 | }
54 |
55 | public func userInteractionEnabled (_ isUserInteractionEnabled: Bool) -> Self {
56 | return modified(WidgetModifier(keyPath: \UIView.isUserInteractionEnabled, value: isUserInteractionEnabled))
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewConstraints.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | // MARK:- UIView Constraints
14 |
15 | extension WidgetViewModifying {
16 |
17 | // compression
18 |
19 | public func contentCompressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self {
20 | return modified(WidgetModifierBlock { view, _ in
21 | view.setContentCompressionResistancePriority(priority, for: axis)
22 | })
23 | }
24 |
25 | // hugging
26 |
27 | public func contentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self {
28 | return modified(WidgetModifierBlock { view, _ in
29 | view.setContentHuggingPriority(priority, for: axis)
30 | })
31 | }
32 |
33 | // height
34 |
35 | public func height(_ height: CGFloat, priority: Float = 999) -> Self {
36 | return modified(WidgetModifierBlock { view, _ in
37 | let height = view.heightAnchor.constraint(equalToConstant: height)
38 | height.priority = UILayoutPriority(priority)
39 | height.isActive = true
40 | })
41 | }
42 |
43 | public func height(min height: CGFloat, priority: Float = 999) -> Self {
44 | return modified(WidgetModifierBlock { view, _ in
45 | let height = view.heightAnchor.constraint(greaterThanOrEqualToConstant: height)
46 | height.priority = UILayoutPriority(priority)
47 | height.isActive = true
48 | })
49 | }
50 |
51 | public func height(max height: CGFloat, priority: Float = 999) -> Self {
52 | return modified(WidgetModifierBlock { view, _ in
53 | let height = view.heightAnchor.constraint(lessThanOrEqualToConstant: height)
54 | height.priority = UILayoutPriority(priority)
55 | height.isActive = true
56 | })
57 | }
58 |
59 | // width
60 |
61 | public func width(_ width: CGFloat, priority: Float = 999) -> Self {
62 | return modified(WidgetModifierBlock { view, _ in
63 | let width = view.widthAnchor.constraint(equalToConstant: width)
64 | width.priority = UILayoutPriority(priority)
65 | width.isActive = true
66 | })
67 | }
68 |
69 | public func width(min width: CGFloat, priority: Float = 999) -> Self {
70 | return modified(WidgetModifierBlock { view, _ in
71 | let width = view.widthAnchor.constraint(greaterThanOrEqualToConstant: width)
72 | width.priority = UILayoutPriority(priority)
73 | width.isActive = true
74 | })
75 | }
76 |
77 | public func width(max width: CGFloat, priority: Float = 999) -> Self {
78 | return modified(WidgetModifierBlock { view, _ in
79 | let width = view.widthAnchor.constraint(lessThanOrEqualToConstant: width)
80 | width.priority = UILayoutPriority(priority)
81 | width.isActive = true
82 | })
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewControllerEvents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifyUIViewControllerEvents.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/27/19.
6 | //
7 |
8 | import UIKit
9 |
10 | extension WidgetViewModifying {
11 |
12 | public func onViewWillAppear(_ handler: @escaping (WidgetContext) -> Void) -> Self {
13 | return modified(WidgetModifierBlock({ (view, context) in
14 | if let vc = context.viewController {
15 | vc.rx.methodInvoked(#selector(UIViewController.viewWillAppear(_:)))
16 | .subscribe(onNext: { (arguments) in
17 | let animated = arguments.first as? Bool ?? true
18 | handler(context.set(animated, for: "animated"))
19 | })
20 | .disposed(by: context.disposeBag)
21 | }
22 | }))
23 | }
24 |
25 | public func onViewDidAppear(_ handler: @escaping (WidgetContext) -> Void) -> Self {
26 | return modified(WidgetModifierBlock({ (view, context) in
27 | if let vc = context.viewController {
28 | vc.rx.methodInvoked(#selector(UIViewController.viewDidAppear(_:)))
29 | .subscribe(onNext: { (arguments) in
30 | let animated = arguments.first as? Bool ?? true
31 | handler(context.set(animated, for: "animated"))
32 | })
33 | .disposed(by: context.disposeBag)
34 | }
35 | }))
36 | }
37 |
38 | public func onViewWillDisappear(_ handler: @escaping (WidgetContext) -> Void) -> Self {
39 | return modified(WidgetModifierBlock({ (view, context) in
40 | if let vc = context.viewController {
41 | vc.rx.methodInvoked(#selector(UIViewController.viewWillDisappear(_:)))
42 | .subscribe(onNext: { (arguments) in
43 | let animated = arguments.first as? Bool ?? true
44 | handler(context.set(animated, for: "animated"))
45 | })
46 | .disposed(by: context.disposeBag)
47 | }
48 | }))
49 | }
50 |
51 | public func onViewDidDisappear(_ handler: @escaping (WidgetContext) -> Void) -> Self {
52 | return modified(WidgetModifierBlock({ (view, context) in
53 | if let vc = context.viewController {
54 | vc.rx.methodInvoked(#selector(UIViewController.viewDidDisappear(_:)))
55 | .subscribe(onNext: { (arguments) in
56 | let animated = arguments.first as? Bool ?? true
57 | handler(context.set(animated, for: "animated"))
58 | })
59 | .disposed(by: context.disposeBag)
60 | }
61 | }))
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewCustom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetUIViewModifiersCustom.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | // MARK:- UIView Widget Properties
15 |
16 | extension WidgetViewModifying {
17 |
18 | public func safeArea(_ value: Bool) -> Self {
19 | return modified(WidgetModifier(keyPath: \UIView.widget.safeArea, value: value))
20 | }
21 |
22 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
23 | return modified(WidgetModifierBlock(block))
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewGestures.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetUIViewModifiersCustom.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | // MARK:- UIView Widget Properties
15 |
16 | extension WidgetViewModifying {
17 |
18 | public func onTap(effect: WidgetTapEffectType? = WidgetTapEffectDim(), handler: @escaping (WidgetContext) -> Void) -> Self {
19 | return modified(WidgetModifierBlock({ (view, context) in
20 | let tapGesture = UITapGestureRecognizer()
21 | view.addGestureRecognizer(tapGesture)
22 | view.isUserInteractionEnabled = true
23 | tapGesture.rx.event
24 | .subscribe(onNext: { (gesture) in
25 | if let effect = effect {
26 | effect.animate(view) {
27 | handler(context)
28 | }
29 | } else {
30 | handler(context)
31 | }
32 | })
33 | .disposed(by: context.disposeBag)
34 | }))
35 | }
36 |
37 | public func onSwipeLeft(_ swiped: @escaping (WidgetContext) -> Void) -> Self {
38 | return modified(WidgetModifierBlock({ (view, context) in
39 | let swipeGesture = UISwipeGestureRecognizer()
40 | swipeGesture.direction = .left
41 | view.addGestureRecognizer(swipeGesture)
42 | view.isUserInteractionEnabled = true
43 | swipeGesture.rx.event
44 | .subscribe(onNext: { (gesture) in
45 | swiped(context)
46 | })
47 | .disposed(by: context.disposeBag)
48 | }))
49 | }
50 |
51 | public func onSwipeRight(_ swiped: @escaping (WidgetContext) -> Void) -> Self {
52 | return modified(WidgetModifierBlock({ (view, context) in
53 | let swipeGesture = UISwipeGestureRecognizer()
54 | swipeGesture.direction = .right
55 | view.addGestureRecognizer(swipeGesture)
56 | view.isUserInteractionEnabled = true
57 | swipeGesture.rx.event
58 | .subscribe(onNext: { (gesture) in
59 | swiped(context)
60 | })
61 | .disposed(by: context.disposeBag)
62 | }))
63 | }
64 |
65 | }
66 |
67 | public protocol WidgetTapEffectType {
68 | func animate(_ view: UIView, _ completion: @escaping () -> Void)
69 | }
70 |
71 | public struct WidgetTapEffectDim: WidgetTapEffectType {
72 | public init() {}
73 | public func animate(_ view: UIView, _ completion: @escaping () -> Void) {
74 | let oldAlpha = view.alpha
75 | UIView.animate(withDuration: 0.05, animations: {
76 | view.alpha = max(view.alpha - 0.4, 0)
77 | }, completion: { (completed) in
78 | UIView.animate(withDuration: 0.05, animations: {
79 | view.alpha = oldAlpha
80 | }, completion: { completed in
81 | completion()
82 | })
83 | })
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 |
14 | // MARK:- UIView Layer Properties
15 |
16 | extension WidgetViewModifying {
17 |
18 | public func border(color: UIColor = .gray, width: CGFloat = 1, radius: CGFloat = 0) -> Self {
19 | return modified(WidgetModifierBlock({ (view, context) in
20 | view.layer.borderColor = color.cgColor
21 | view.layer.borderWidth = width
22 | view.layer.cornerRadius = radius
23 | view.clipsToBounds = true
24 | }))
25 | }
26 |
27 | public func borderColor(_ borderColor: UIColor) -> Self {
28 | return modified(WidgetModifier(keyPath: \UIView.layer.borderColor, value: borderColor.cgColor))
29 | }
30 |
31 | public func borderWidth(_ borderWidth: CGFloat) -> Self {
32 | return modified(WidgetModifier(keyPath: \UIView.layer.borderWidth, value: borderWidth))
33 | }
34 |
35 | public func cornerRadius(_ cornerRadius: CGFloat) -> Self {
36 | return modified(WidgetModifierBlock({ (view, context) in
37 | view.layer.cornerRadius = cornerRadius
38 | view.clipsToBounds = true
39 | }))
40 | }
41 |
42 | public func shadow(offset: CGSize, color: UIColor = .gray, opacity: Float = 0.5, radius: CGFloat = 0) -> Self {
43 | return modified(WidgetModifierBlock({ (view, context) in
44 | view.layer.shadowOffset = offset
45 | view.layer.shadowColor = color.cgColor
46 | view.layer.shadowOpacity = opacity
47 | view.layer.shadowRadius = radius
48 | view.clipsToBounds = false
49 | }))
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Modifiers/WidgetModifyUIViewRx.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetModifying.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | // MARK:- UIView Standard Properties
14 |
15 | extension WidgetViewModifying {
16 |
17 | public func hidden(_ observable: O) -> Self where O.Element == Bool {
18 | return modified(WidgetModifierBlock { view, context in
19 | observable.asObservable().bind(to: view.rx.isHidden).disposed(by: context.disposeBag)
20 | })
21 | }
22 |
23 | public func onEvent(_ observable: O, handle: @escaping (_ value: Value, _ context: WidgetContext) -> Void) -> Self where O.Element == Value {
24 | return modified(WidgetModifierBlock({ (_, context) in
25 | observable.asObservable()
26 | .subscribe(onNext: { (value) in
27 | handle(value, context)
28 | })
29 | .disposed(by: context.disposeBag)
30 | }))
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Rx/BindableElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BindableElement.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | // Define Protocols
14 |
15 | public protocol BindableElement: ObservableElement {
16 | func observe(_ observable: Observable) -> Disposable
17 | }
18 |
19 | extension BehaviorRelay: BindableElement {
20 | public func observe(_ observable: Observable) -> Disposable {
21 | observable.subscribe(onNext: { [weak self] (element) in
22 | self?.accept(element) // only forward onNext events
23 | })
24 | }
25 | }
26 |
27 | extension BehaviorSubject: BindableElement {
28 | public func observe(_ observable: Observable) -> Disposable {
29 | observable.subscribe(onNext: { [weak self] (element) in
30 | self?.onNext(element) // only forward onNext events
31 | })
32 | }
33 | }
34 |
35 | extension PublishRelay: BindableElement {
36 | public func observe(_ observable: Observable) -> Disposable {
37 | observable.subscribe(onNext: { [weak self] (element) in
38 | self?.accept(element) // only forward onNext events
39 | })
40 | }
41 | }
42 |
43 | extension PublishSubject: BindableElement {
44 | public func observe(_ observable: Observable) -> Disposable {
45 | observable.subscribe(onNext: { [weak self] (element) in
46 | self?.onNext(element) // only forward onNext events
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Rx/Binding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Binding.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | @propertyWrapper
14 | public struct Binding: BindableElement {
15 |
16 | private let relay: BehaviorRelay
17 |
18 | init(_ relay: BehaviorRelay) {
19 | self.relay = relay
20 | }
21 |
22 | public var value: Element {
23 | get { relay.value }
24 | nonmutating set { relay.accept(newValue) }
25 | }
26 |
27 | public var wrappedValue: Element {
28 | get { relay.value }
29 | nonmutating set { relay.accept(newValue) }
30 | }
31 |
32 | public var projectedValue: Binding {
33 | Binding(relay)
34 | }
35 |
36 | public func asBinding() -> Binding {
37 | Binding(relay)
38 | }
39 |
40 | public func asObservable() -> Observable {
41 | relay.asObservable()
42 | }
43 |
44 | public func observe(_ observable: Observable) -> Disposable {
45 | observable.subscribe(onNext: { (element) in
46 | self.relay.accept(element) // only forward onNext events
47 | })
48 | }
49 |
50 | public func subscribe(_ observer: Observer) -> Disposable where Element == Observer.Element {
51 | relay.subscribe(observer)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Rx/ObservableElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableElement.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | // Define Protocols
14 |
15 | public protocol ObservableElement: ObservableType {
16 | associatedtype Element
17 | func asObservable() -> Observable
18 | }
19 |
20 | extension Observable: ObservableElement {
21 | // just conformance, observables are already observable
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Rx/ObservableListBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableListBuilder.swift
3 | // Widgets
4 | //
5 | // Created by Michael Long on 3/3/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 |
12 | // Building
13 |
14 | public protocol ObservableListBuilderType {
15 |
16 | associatedtype Item
17 |
18 | var items: Observable<[Item]> { get }
19 |
20 | func widget(for item: Item) -> Widget?
21 |
22 | }
23 |
24 | public struct ObservableListBuilder- : ObservableListBuilderType {
25 |
26 | public let items: Observable<[Item]>
27 |
28 | private let builder: (_ item: Item) -> Widget?
29 |
30 | public init(_ items: O, builder: @escaping (_ item: Item) -> Widget) where O.Element == [Item] {
31 | self.items = items.asObservable()
32 | self.builder = builder
33 | }
34 |
35 | public func widget(for item: Item) -> Widget? {
36 | return builder(item)
37 | }
38 |
39 | }
40 |
41 | public struct AnyObservableListBuilder: ObservableListBuilderType {
42 |
43 | public typealias Item = Any
44 |
45 | public let items: Observable<[Any]>
46 |
47 | private let builder: (_ item: Any) -> Widget?
48 |
49 | public init(_ builder: O) {
50 | self.items = builder.items.map { $0 as [Any] }
51 | self.builder = { item in
52 | if let item = item as? O.Item {
53 | return builder.widget(for: item)
54 | }
55 | return nil
56 | }
57 | }
58 |
59 | public func widget(for item: Any) -> Widget? {
60 | return builder(item)
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Rx/State.swift:
--------------------------------------------------------------------------------
1 | //
2 | // State.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | @propertyWrapper
14 | public struct State: BindableElement {
15 |
16 | private let relay: BehaviorRelay
17 |
18 | public init(wrappedValue: Element) {
19 | relay = BehaviorRelay(value: wrappedValue)
20 | }
21 |
22 | public var value: Element {
23 | get { relay.value }
24 | nonmutating set { relay.accept(newValue) }
25 | }
26 |
27 | public var wrappedValue: Element {
28 | get { relay.value }
29 | nonmutating set { relay.accept(newValue) }
30 | }
31 |
32 | public var projectedValue: Binding {
33 | Binding(relay)
34 | }
35 |
36 | public func asBinding() -> Binding {
37 | Binding(relay)
38 | }
39 |
40 | public func asObservable() -> Observable {
41 | relay.asObservable()
42 | }
43 |
44 | public func observe(_ observable: Observable) -> Disposable {
45 | observable.subscribe(onNext: { (element) in
46 | self.relay.accept(element) // only forward onNext events
47 | })
48 | }
49 |
50 | public func subscribe(_ observer: Observer) -> Disposable where Element == Observer.Element {
51 | relay.subscribe(observer)
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/ContainerWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContainerWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public struct ContainerWidget
14 | : WidgetContaining
15 | , WidgetViewModifying
16 | , WidgetPadding
17 | , CustomDebugStringConvertible {
18 |
19 | public var debugDescription: String { "ContainerWidget()" }
20 |
21 | public var modifiers = WidgetModifiers()
22 | public let widget: Widget
23 |
24 | public init(_ widget: Widget) {
25 | self.widget = widget
26 | }
27 |
28 | public func build(with context: WidgetContext) -> UIView {
29 |
30 | let view = UIView()
31 | let context = modifiers.modified(context, for: view)
32 | let subview = widget.build(with: context)
33 |
34 | view.translatesAutoresizingMaskIntoConstraints = false
35 | view.backgroundColor = .clear
36 | view.addConstrainedSubview(subview, with: modifiers.padding)
37 |
38 | modifiers.apply(to: view, with: context)
39 |
40 | return view
41 | }
42 |
43 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
44 | return modified(WidgetModifierBlock(block))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/DisclosableWidget.swift:
--------------------------------------------------------------------------------
1 |
2 | import UIKit
3 | import RxSwift
4 | import RxCocoa
5 |
6 | public struct DisclosableWidget: Widget, WidgetContaining, WidgetPadding, WidgetViewModifying {
7 |
8 | public var widget: Widget
9 |
10 | public init(_ widget: Widget) {
11 | self.widget = widget
12 | }
13 |
14 | public var modifiers = WidgetModifiers()
15 |
16 | private var color = UIColor.init(white: 0.85, alpha: 1.0)
17 |
18 | public func build(with context: WidgetContext) -> UIView {
19 |
20 | let enclosure = UIView()
21 | let context = modifiers.modified(context, for: enclosure)
22 | let childView = widget.build(with: context)
23 |
24 | enclosure.backgroundColor = .clear
25 | enclosure.addSubview(childView)
26 |
27 | let label = UILabel()
28 | label.translatesAutoresizingMaskIntoConstraints = false
29 | label.text = "\u{203A}"
30 | label.font = UIFont.preferredFont(forTextStyle: .title1)
31 | label.textColor = color
32 | label.backgroundColor = .clear
33 | enclosure.addSubview(label)
34 |
35 | let padding = self.modifiers.padding ?? UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
36 |
37 | childView.topAnchor.constraint(equalTo: enclosure.topAnchor, constant: padding.top).isActive = true
38 | childView.bottomAnchor.constraint(equalTo: enclosure.bottomAnchor, constant: -padding.bottom).isActive = true
39 | childView.leadingAnchor.constraint(equalTo: enclosure.leadingAnchor, constant: padding.left).isActive = true
40 | label.trailingAnchor.constraint(equalTo: enclosure.trailingAnchor, constant: -padding.right).isActive = true
41 |
42 | label.leadingAnchor.constraint(equalTo: childView.trailingAnchor, constant: 10).isActive = true
43 | label.centerYAnchor.constraint(equalTo: enclosure.centerYAnchor).isActive = true
44 |
45 | label.setContentCompressionResistancePriority(.required, for: .horizontal)
46 | label.setContentCompressionResistancePriority(.required, for: .vertical)
47 | label.setContentHuggingPriority(.required, for: .horizontal)
48 | label.setContentHuggingPriority(.required, for: .vertical)
49 |
50 | modifiers.apply(to: enclosure, with: context)
51 |
52 | return enclosure
53 | }
54 |
55 | public func color(_ color: UIColor) -> Self {
56 | return modified { $0.color = color }
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/HStackWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColumnWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 | import RxSwift
12 | import RxCocoa
13 |
14 | public struct HStackWidget
15 | : WidgetsContaining
16 | , WidgetViewModifying
17 | , WidgetPadding
18 | , CustomDebugStringConvertible {
19 |
20 | public var debugDescription: String { "HStackWidget()" }
21 |
22 | public var widgets: [Widget]
23 | public var modifiers = WidgetModifiers()
24 |
25 | public init(_ widgets: [Widget] = []) {
26 | self.widgets = widgets
27 | }
28 |
29 | public init
- (_ items: O, builder: @escaping (_ item: Item) -> Widget) where O.Element == [Item] {
30 | self.modifiers.binding = WidgetModifierBlock { (stack, context) in
31 | let items = items.map { $0.map { builder($0) } }
32 | stack.subscribe(to: items, with: context)
33 | }
34 | self.widgets = []
35 | }
36 |
37 | public func build(with context: WidgetContext) -> UIView {
38 |
39 | let stack = WidgetPrivateStackView()
40 | let context = modifiers.modified(context, for: stack)
41 |
42 | stack.translatesAutoresizingMaskIntoConstraints = false
43 | stack.insetsLayoutMarginsFromSafeArea = false
44 | stack.axis = .horizontal
45 | stack.spacing = UIStackView.spacingUseSystem
46 |
47 | if let insets = modifiers.padding {
48 | stack.isLayoutMarginsRelativeArrangement = true
49 | stack.layoutMargins = insets
50 | }
51 |
52 | for widget in widgets {
53 | stack.addArrangedSubview(widget.build(with: context))
54 | }
55 |
56 | modifiers.apply(to: stack, with: context)
57 |
58 | return stack
59 | }
60 |
61 | public func alignment(_ alignment: UIStackView.Alignment) -> Self {
62 | return modified(WidgetModifier(keyPath: \UIStackView.alignment, value: alignment))
63 | }
64 |
65 | public func distribution(_ distribution: UIStackView.Distribution) -> Self {
66 | return modified(WidgetModifier(keyPath: \UIStackView.distribution, value: distribution))
67 | }
68 |
69 | public func placeholder(_ widgets: [Widget]) -> Self {
70 | return modified { $0.widgets = widgets }
71 | }
72 |
73 | public func spacing(_ spacing: CGFloat) -> Self {
74 | return modified(WidgetModifier(keyPath: \UIStackView.spacing, value: spacing))
75 | }
76 |
77 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
78 | return modified(WidgetModifierBlock(block))
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/ScrollWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | extension Widgets {
14 | public enum ScrollAxis {
15 | case both
16 | case vertical
17 | case horizontal
18 | }
19 | }
20 |
21 | public struct ScrollWidget
22 | : WidgetViewModifying
23 | , WidgetContaining
24 | , WidgetPadding
25 | , CustomDebugStringConvertible {
26 |
27 | public var debugDescription: String { "ScrollWidget()" }
28 |
29 | public var widget: Widget
30 | public var modifiers = WidgetModifiers()
31 | public var axis = Widgets.ScrollAxis.vertical
32 |
33 | public init(_ widget: Widget) {
34 | self.widget = widget
35 | }
36 |
37 | public func build(with context: WidgetContext) -> UIView {
38 |
39 | let view = WidgetScrollView()
40 | let context = modifiers.modified(context, for: view)
41 | let contentView = widget.build(with: context)
42 | let padding = self.modifiers.padding ?? UIEdgeInsets.zero
43 |
44 | view.translatesAutoresizingMaskIntoConstraints = false
45 | view.axis = axis
46 | view.padding = padding
47 | view.addSubview(contentView)
48 |
49 | contentView.topAnchor.constraint(equalTo: view.topAnchor, constant: padding.top).isActive = true
50 | contentView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: padding.left).isActive = true
51 | contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding.bottom).isActive = true
52 | contentView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -padding.right).isActive = true
53 |
54 | modifiers.apply(to: view, with: context)
55 |
56 | return view
57 | }
58 |
59 | public func automaticallyAdjustForKeyboard() -> Self {
60 | return modified(WidgetModifierBlock { scrollView, context in
61 | NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
62 | .asObservable()
63 | .observeOn(MainScheduler.instance)
64 | .subscribe(onNext: { [weak scrollView] (notification) in
65 | guard let scrollView = scrollView, let window = scrollView.window else { return }
66 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
67 | let info: NSDictionary = notification.userInfo! as NSDictionary
68 | // get keyboard frame in window coordinate space
69 | let keyboardFrameInWindow = ((info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue)!
70 | // get scrollview frame in window coordinate space
71 | let scrollViewFrameInWindow = scrollView.convert(scrollView.frame, to: window)
72 | // compute intersection
73 | let intersection = scrollViewFrameInWindow.intersection(keyboardFrameInWindow)
74 | // if intersects, inset scrollview by that amount
75 | if intersection.height > 0 {
76 | let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: intersection.height, right: 0)
77 | scrollView.contentInset = contentInsets
78 | scrollView.scrollIndicatorInsets = contentInsets
79 | }
80 | }
81 | })
82 | .disposed(by: context.disposeBag)
83 |
84 | NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification)
85 | .asObservable()
86 | .observeOn(MainScheduler.instance)
87 | .subscribe(onNext: { [weak scrollView] _ in
88 | if let scrollView = scrollView, scrollView.contentInset.bottom > 0 {
89 | scrollView.contentInset = UIEdgeInsets.zero
90 | scrollView.scrollIndicatorInsets = UIEdgeInsets.zero
91 | }
92 | })
93 | .disposed(by: context.disposeBag)
94 | })
95 | }
96 |
97 | public func axis(_ axis: Widgets.ScrollAxis) -> Self {
98 | return modified { $0.axis = axis }
99 | }
100 |
101 | /// Pass an UIScrollViewDelegate object and it will be assigned as the delegate for the generated UIScrollView.
102 | public func delegate(_ delegate: UIScrollViewDelegate) -> Self {
103 | return modified(WidgetModifierBlock { view, _ in
104 | view.delegateReference = delegate
105 | view.delegate = delegate
106 | })
107 | }
108 |
109 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
110 | return modified(WidgetModifierBlock(block))
111 | }
112 |
113 | }
114 |
115 | internal class WidgetScrollView: UIScrollView, WidgetViewCustomConstraints {
116 |
117 | var axis: Widgets.ScrollAxis = .both
118 | var delegateReference: UIScrollViewDelegate?
119 | var padding: UIEdgeInsets!
120 |
121 | func addCustomConstraints() {
122 | guard let superview = superview, let subview = subviews.first else { return }
123 | switch axis {
124 | case .vertical:
125 | let padding = self.padding.left + self.padding.right
126 | subview.widthAnchor.constraint(equalTo: superview.widthAnchor, constant: -padding).isActive = true
127 | case .horizontal:
128 | let padding = self.padding.top + self.padding.bottom
129 | subview.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -padding).isActive = true
130 | case .both:
131 | break
132 | }
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/UIControlWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIControlWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public struct UIControlWidget
14 | : Widget
15 | , WidgetControlModifying
16 | , CustomDebugStringConvertible {
17 |
18 | public var debugDescription: String { "UIViewWidget()" }
19 |
20 | public var modifiers = WidgetModifiers()
21 | public var view: View
22 |
23 | public init(_ view: View) {
24 | self.view = view
25 | }
26 |
27 | public func build(with context: WidgetContext) -> UIView {
28 | view.translatesAutoresizingMaskIntoConstraints = false
29 | let context = modifiers.modified(context, for: view)
30 | modifiers.apply(to: view, with: context)
31 | return view
32 | }
33 |
34 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
35 | return modified(WidgetModifierBlock(block))
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/UIViewWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public struct UIViewWidget
14 | : Widget
15 | , WidgetViewModifying
16 | , CustomDebugStringConvertible {
17 |
18 | public var debugDescription: String { "UIViewWidget()" }
19 |
20 | public var modifiers = WidgetModifiers()
21 | public var view: View
22 |
23 | public init(_ view: View) {
24 | self.view = view
25 | }
26 |
27 | public func build(with context: WidgetContext) -> UIView {
28 | view.translatesAutoresizingMaskIntoConstraints = false
29 | let context = modifiers.modified(context, for: view)
30 | modifiers.apply(to: view, with: context)
31 | return view
32 | }
33 |
34 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
35 | return modified(WidgetModifierBlock(block))
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/VStackWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VStackWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 | import RxSwift
12 | import RxCocoa
13 |
14 | public struct VStackWidget
15 | : WidgetsContaining
16 | , WidgetViewModifying
17 | , WidgetPadding
18 | , CustomDebugStringConvertible {
19 |
20 | public var debugDescription: String { "VStackWidget()" }
21 |
22 | public var widgets: [Widget]
23 | public var modifiers = WidgetModifiers()
24 |
25 | public init(_ widgets: [Widget] = []) {
26 | self.widgets = widgets
27 | }
28 |
29 | public init
- (_ items: O, builder: @escaping (_ item: Item) -> Widget) where O.Element == [Item] {
30 | self.modifiers.binding = WidgetModifierBlock { (stack, context) in
31 | let items = items.map { $0.map { builder($0) } }
32 | stack.subscribe(to: items, with: context)
33 | }
34 | self.widgets = []
35 | }
36 |
37 | public func build(with context: WidgetContext) -> UIView {
38 |
39 | let stack = WidgetPrivateStackView()
40 | let context = modifiers.modified(context, for: stack)
41 |
42 | stack.translatesAutoresizingMaskIntoConstraints = false
43 | stack.insetsLayoutMarginsFromSafeArea = false
44 | stack.axis = .vertical
45 | stack.spacing = UIStackView.spacingUseSystem
46 |
47 | if let insets = modifiers.padding {
48 | stack.isLayoutMarginsRelativeArrangement = true
49 | stack.layoutMargins = insets
50 | }
51 |
52 | for widget in widgets {
53 | stack.addArrangedSubview(widget.build(with: context))
54 | }
55 |
56 | modifiers.apply(to: stack, with: context)
57 |
58 | return stack
59 | }
60 |
61 | public func alignment(_ alignment: UIStackView.Alignment) -> Self {
62 | return modified(WidgetModifier(keyPath: \UIStackView.alignment, value: alignment))
63 | }
64 |
65 | public func distribution(_ distribution: UIStackView.Distribution) -> Self {
66 | return modified(WidgetModifier(keyPath: \UIStackView.distribution, value: distribution))
67 | }
68 |
69 | public func placeholder(_ widgets: [Widget]) -> Self {
70 | return modified { $0.widgets = widgets }
71 | }
72 |
73 | public func spacing(_ spacing: CGFloat) -> Self {
74 | return modified(WidgetModifier(keyPath: \UIStackView.spacing, value: spacing))
75 | }
76 |
77 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
78 | return modified(WidgetModifierBlock(block))
79 | }
80 |
81 | }
82 |
83 | internal class WidgetPrivateStackView: UIStackView {
84 |
85 | var disposeBag: DisposeBag!
86 |
87 | public func subscribe(to observable: Observable<[Widget]>, with context: WidgetContext) {
88 | observable
89 | .observeOn(MainScheduler.instance)
90 | .subscribe(onNext: { [weak self] (widgets) in
91 | guard let self = self else { return }
92 |
93 | self.disposeBag = DisposeBag()
94 |
95 | self.subviews.forEach {
96 | $0.removeFromSuperview()
97 | }
98 |
99 | var context = context
100 | context.disposeBag = self.disposeBag
101 |
102 | widgets.forEach { widget in
103 | let view = widget.build(with: context)
104 | self.addArrangedSubview(view)
105 | }
106 |
107 | UIView.animate(withDuration: 0.01, animations: {
108 | self.layoutIfNeeded()
109 | })
110 | })
111 | .disposed(by: context.disposeBag)
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Containers/ZStackWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZStackWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 | import RxSwift
12 | import RxCocoa
13 |
14 | public struct ZStackWidget
15 | : WidgetsContaining
16 | , WidgetViewModifying
17 | , WidgetPadding
18 | , CustomDebugStringConvertible {
19 |
20 | public var debugDescription: String { "ZStackWidget()" }
21 |
22 | public let widgets: [Widget]
23 | public var modifiers = WidgetModifiers()
24 |
25 | public init(_ widgets: [Widget]) {
26 | self.widgets = widgets
27 | }
28 |
29 | public func build(with context: WidgetContext) -> UIView {
30 | let view = UIView()
31 | let context = modifiers.modified(context, for: view)
32 |
33 | view.translatesAutoresizingMaskIntoConstraints = false
34 | view.insetsLayoutMarginsFromSafeArea = false
35 | view.backgroundColor = .clear
36 |
37 | for widget in widgets {
38 | let subview = widget.build(with: context)
39 | view.addConstrainedSubview(subview, with: modifiers.padding)
40 | }
41 |
42 | modifiers.apply(to: view, with: context)
43 |
44 | return view
45 | }
46 |
47 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
48 | return modified(WidgetModifierBlock(block))
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Controls/ButtonWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/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 ButtonWidget
15 | : WidgetViewModifying
16 | , WidgetPadding
17 | , CustomDebugStringConvertible {
18 |
19 | public var debugDescription: String { "ButtonWidget()" }
20 |
21 | public var modifiers = WidgetModifiers()
22 |
23 | public init(_ text: String? = nil, for state: UIControl.State = .normal) {
24 | if let text = text {
25 | modifiers.binding = WidgetModifierBlock { button, _ in
26 | button.setTitle(text, for: state)
27 | }
28 | }
29 | }
30 |
31 | public func build(with context: WidgetContext) -> UIView {
32 |
33 | let button = UIButton()
34 | let context = modifiers.modified(context, for: button)
35 |
36 | button.translatesAutoresizingMaskIntoConstraints = false
37 | button.titleLabel?.font = context.theme.font.body
38 | button.setTitleColor(context.theme.color.link, for: .normal)
39 | button.contentEdgeInsets = modifiers.padding ?? button.contentEdgeInsets
40 | button.backgroundColor = .clear
41 | button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
42 | button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
43 |
44 | modifiers.apply(to: button, with: context)
45 |
46 | return button
47 | }
48 |
49 | public func alignment(_ alignment: UIControl.ContentHorizontalAlignment) -> Self {
50 | return modified(WidgetModifier(keyPath: \UIButton.contentHorizontalAlignment, value: alignment))
51 | }
52 |
53 | public func color(_ color: UIColor, for state: UIControl.State = .normal) -> Self {
54 | return modified(WidgetModifierBlock { button, _ in
55 | button.setTitleColor(color, for: state)
56 | })
57 | }
58 |
59 | public func font(_ font: UIFont) -> Self {
60 | return modified(WidgetModifierBlock { button, _ in
61 | button.titleLabel?.font = font
62 | })
63 | }
64 |
65 | public func onTap(effect: WidgetTapEffectType? = WidgetTapEffectDim(), handler: @escaping (WidgetContext) -> Void) -> Self {
66 | return modified(WidgetModifierBlock({ (button, context) in
67 | button.rx.tap
68 | .subscribe(onNext: { _ in
69 | if let effect = effect {
70 | effect.animate(button) {
71 | handler(context)
72 | }
73 | } else {
74 | handler(context)
75 | }
76 | })
77 | .disposed(by: context.disposeBag)
78 | }))
79 | }
80 |
81 | public func text(_ text: String?, for state: UIControl.State = .normal) -> Self {
82 | return modified(WidgetModifierBlock { button, _ in
83 | button.setTitle(text, for: state)
84 | })
85 | }
86 |
87 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
88 | return modified(WidgetModifierBlock(block))
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Controls/TextFieldWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/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 TextFieldWidget
15 | : WidgetControlModifying
16 | , CustomDebugStringConvertible {
17 |
18 | public var debugDescription: String { "TextFieldWidget()" }
19 |
20 | public var modifiers = WidgetModifiers()
21 |
22 | /// Sets field text on initialization
23 | public init(_ bindable: B) where B.Element == String {
24 | modifiers.binding = WidgetModifierBlock { view, context in
25 | context.disposeBag.insert(
26 | bindable.asObservable().bind(to: view.rx.text),
27 | bindable.observe(view.rx.text.orEmpty.asObservable())
28 | )
29 | }
30 | }
31 |
32 | public init(_ bindable: B) where B.Element == String? {
33 | modifiers.binding = WidgetModifierBlock { view, context in
34 | context.disposeBag.insert(
35 | bindable.asObservable().bind(to: view.rx.text),
36 | bindable.observe(view.rx.text.asObservable())
37 | )
38 | }
39 | }
40 |
41 | public func build(with context: WidgetContext) -> UIView {
42 | let textField = WidgetTextField()
43 | let context = modifiers.modified(context, for: textField)
44 |
45 | textField.translatesAutoresizingMaskIntoConstraints = false
46 | textField.font = context.theme.font.body
47 | textField.textColor = context.theme.color.text
48 | textField.backgroundColor = .clear
49 |
50 | modifiers.apply(to: textField, with: context)
51 |
52 | return textField
53 | }
54 |
55 | /// Sets alignment of label text
56 | public func alignment(_ alignment: NSTextAlignment) -> Self {
57 | return modified(WidgetModifier(keyPath: \UITextField.textAlignment, value: alignment))
58 | }
59 |
60 | /// Sets borderStyle for text field
61 | public func borderStyle(_ borderStyle: UITextField.BorderStyle) -> Self {
62 | return modified(WidgetModifier(keyPath: \UITextField.borderStyle, value: borderStyle))
63 | }
64 |
65 | /// Sets color of text field
66 | public func color(_ color: UIColor) -> Self {
67 | return modified(WidgetModifier(keyPath: \UITextField.textColor, value: color))
68 | }
69 |
70 | /// Sets font of text field
71 | public func font(_ font: UIFont) -> Self {
72 | return modified(WidgetModifier(keyPath: \UITextField.font, value: font))
73 | }
74 |
75 | /// Sets placeholder for text field
76 | public func placeholder(_ placeholder: String) -> Self {
77 | return modified(WidgetModifier(keyPath: \UITextField.placeholder, value: placeholder))
78 | }
79 |
80 | public func placeholder(_ placeholder: String?) -> Self {
81 | return modified(WidgetModifier(keyPath: \UITextField.placeholder, value: placeholder))
82 | }
83 |
84 | /// Sets secureTextEntry for text field
85 | public func secureTextEntry(_ isSecureTextEntry: Bool) -> Self {
86 | return modified(WidgetModifier(keyPath: \UITextField.isSecureTextEntry, value: isSecureTextEntry))
87 | }
88 |
89 | /// Allows modification of generated field
90 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
91 | return modified(WidgetModifierBlock(block))
92 | }
93 | }
94 |
95 | extension TextFieldWidget {
96 |
97 | public func onChange(_ handler: @escaping (_ textField: UITextField, _ context: WidgetContext) -> Void) -> Self {
98 | return modified(WidgetModifierBlock({ (view, context) in
99 | view.rx.controlEvent(.editingChanged)
100 | .subscribe(onNext: { () in
101 | handler(view, context)
102 | })
103 | .disposed(by: context.disposeBag)
104 | }))
105 | }
106 |
107 | public func onEditingDidBegin(_ handler: @escaping (_ textField: UITextField, _ context: WidgetContext) -> Void) -> Self {
108 | return modified(WidgetModifierBlock({ (view, context) in
109 | view.rx.controlEvent(.editingDidBegin)
110 | .subscribe(onNext: { () in
111 | handler(view, context)
112 | })
113 | .disposed(by: context.disposeBag)
114 | }))
115 | }
116 |
117 | public func onEditingDidEnd(_ handler: @escaping (_ textField: UITextField, _ context: WidgetContext) -> Void) -> Self {
118 | return modified(WidgetModifierBlock({ (view, context) in
119 | view.rx.controlEvent(.editingDidEnd)
120 | .subscribe(onNext: { () in
121 | handler(view, context)
122 | })
123 | .disposed(by: context.disposeBag)
124 | }))
125 | }
126 |
127 | public func onEditingDidEndOnExit(_ handler: @escaping (_ textField: UITextField, _ context: WidgetContext) -> Void) -> Self {
128 | return modified(WidgetModifierBlock({ (view, context) in
129 | view.rx.controlEvent(.editingDidEndOnExit)
130 | .subscribe(onNext: { () in
131 | handler(view, context)
132 | })
133 | .disposed(by: context.disposeBag)
134 | }))
135 | }
136 |
137 | }
138 |
139 | fileprivate class WidgetTextField: UITextField {
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/General/ImageWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public struct ImageWidget: WidgetViewModifying
14 | , CustomDebugStringConvertible {
15 |
16 | public var debugDescription: String { "ImageWidget()" }
17 |
18 | public var modifiers = WidgetModifiers()
19 |
20 | public init(_ image: UIImage) {
21 | self.modifiers.binding = WidgetModifier(keyPath: \UIImageView.image, value: image)
22 | }
23 |
24 | public init(named name: String) {
25 | self.modifiers.binding = WidgetModifierBlock { view, context in
26 | if let image = UIImage(named: name) {
27 | view.image = image
28 | }
29 | }
30 | }
31 |
32 | public init(_ observable: O) where O.Element == UIImage? {
33 | self.modifiers.binding = WidgetModifierBlock { view, context in
34 | observable.asObservable().bind(to: view.rx.image).disposed(by: context.disposeBag)
35 | }
36 | }
37 |
38 | public init(_ observable: O) where O.Element == String {
39 | self.modifiers.binding = WidgetModifierBlock { view, context in
40 | observable.asObservable().map { UIImage(named: $0) }.bind(to: view.rx.image).disposed(by: context.disposeBag)
41 | }
42 | }
43 |
44 | public func build(with context: WidgetContext) -> UIView {
45 | let view = UIImageView()
46 | let context = modifiers.modified(context, for: view)
47 |
48 | view.translatesAutoresizingMaskIntoConstraints = false
49 | view.clipsToBounds = true // fixes transition animation bug with full screen background images
50 |
51 | modifiers.apply(to: view, with: context)
52 |
53 | return view
54 | }
55 |
56 | public func placeholder(_ image: UIImage) -> Self {
57 | return modified(WidgetModifier(keyPath: \UIImageView.image, value: image))
58 | }
59 |
60 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
61 | return modified(WidgetModifierBlock(block))
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/General/LabelWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabelWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/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 LabelWidget
15 | : WidgetViewModifying
16 | , WidgetPadding
17 | , CustomDebugStringConvertible {
18 |
19 | public var debugDescription: String { "LabelWidget()" }
20 |
21 | public var modifiers = WidgetModifiers()
22 |
23 | /// Sets label text on initialization
24 | public init(_ text: String? = nil) {
25 | modifiers.binding = WidgetModifier(keyPath: \UILabel.text, value: text)
26 | }
27 |
28 | /// Allows initialization of label text with ObservableElement
29 | public init(_ observable: O) where O.Element == String {
30 | modifiers.binding = WidgetModifierBlock { label, context in
31 | observable.asObservable().bind(to: label.rx.text).disposed(by: context.disposeBag)
32 | }
33 | }
34 |
35 | /// Allows initialization of label text with ObservableElement
36 | public init(_ observable: O) where O.Element == String? {
37 | modifiers.binding = WidgetModifierBlock { label, context in
38 | observable.asObservable().bind(to: label.rx.text).disposed(by: context.disposeBag)
39 | }
40 | }
41 |
42 | public func build(with context: WidgetContext) -> UIView {
43 | let label = WidgetLabel()
44 | let context = modifiers.modified(context, for: label)
45 |
46 | label.translatesAutoresizingMaskIntoConstraints = false
47 | label.font = context.theme.font.body
48 | label.textColor = context.theme.color.text
49 | label.textInsets = modifiers.padding
50 | label.backgroundColor = .clear
51 | label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
52 | label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
53 |
54 | modifiers.apply(to: label, with: context)
55 |
56 | return label
57 | }
58 |
59 | /// Sets alignment of label text
60 | public func alignment(_ alignment: NSTextAlignment) -> Self {
61 | return modified(WidgetModifier(keyPath: \UILabel.textAlignment, value: alignment))
62 | }
63 |
64 | /// Sets color of label text
65 | public func color(_ color: UIColor) -> Self {
66 | return modified(WidgetModifier(keyPath: \UILabel.textColor, value: color))
67 | }
68 |
69 | /// Sets font of label text
70 | public func font(_ font: UIFont) -> Self {
71 | return modified(WidgetModifier(keyPath: \UILabel.font, value: font))
72 | }
73 |
74 | /// Sets lineBreakMode for label text
75 | public func lineBreakMode(_ lineBreakMode: NSLineBreakMode) -> Self {
76 | return modified(WidgetModifier(keyPath: \UILabel.lineBreakMode, value: lineBreakMode))
77 | }
78 |
79 | /// Sets number of lines of label text
80 | public func numberOfLines(_ numberOfLines: Int) -> Self {
81 | return modified(WidgetModifier(keyPath: \UILabel.numberOfLines, value: numberOfLines))
82 | }
83 |
84 | /// Placeholder text
85 | public func placeholder(_ text: String) -> Self {
86 | return modified(WidgetModifier(keyPath: \UILabel.text, value: text))
87 | }
88 |
89 | /// Allows modification of generated label
90 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
91 | return modified(WidgetModifierBlock(block))
92 | }
93 | }
94 |
95 | extension LabelWidget {
96 | public static func footnote(_ text: String) -> LabelWidget {
97 | return LabelWidget(text)
98 | .color(.lightGray)
99 | .numberOfLines(0)
100 | .font(.preferredFont(forTextStyle: .footnote))
101 | }
102 | }
103 |
104 | fileprivate class WidgetLabel: UILabel {
105 | var textInsets: UIEdgeInsets? {
106 | didSet { invalidateIntrinsicContentSize() }
107 | }
108 |
109 | override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
110 | guard let textInsets = textInsets else {
111 | return super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines)
112 | }
113 | let insetRect = bounds.inset(by: textInsets)
114 | let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines)
115 | let invertedInsets = UIEdgeInsets(top: -textInsets.top, left: -textInsets.left, bottom: -textInsets.bottom, right: -textInsets.right)
116 | return textRect.inset(by: invertedInsets)
117 | }
118 |
119 | override func drawText(in rect: CGRect) {
120 | guard let textInsets = textInsets else {
121 | super.drawText(in: rect)
122 | return
123 | }
124 | super.drawText(in: rect.inset(by: textInsets))
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/General/SpacerWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpacerWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public typealias FlexibleSpaceWidget = SpacerWidget
12 |
13 | public struct SpacerWidget: Widget
14 | , CustomDebugStringConvertible {
15 |
16 | public var debugDescription: String { "SpacerWidget()" }
17 |
18 | private var h: CGFloat?
19 | private var v: CGFloat?
20 |
21 | public init(h: CGFloat? = nil, v: CGFloat? = nil) {
22 | self.h = h
23 | self.v = v
24 | }
25 |
26 | public func build(with context: WidgetContext) -> UIView {
27 | let view = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
28 | view.translatesAutoresizingMaskIntoConstraints = false
29 | if let h = h {
30 | view.widthAnchor.constraint(equalToConstant: h).isActive = true
31 | } else {
32 | view.setContentHuggingPriority(.defaultLow, for: .horizontal)
33 | }
34 | if let v = v {
35 | view.heightAnchor.constraint(equalToConstant: v).isActive = true
36 | } else {
37 | view.setContentHuggingPriority(.defaultLow, for: .vertical)
38 | }
39 | return view
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/General/SpinnerWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinnerWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/11/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import RxSwift
11 | import RxCocoa
12 |
13 | public struct SpinnerWidget: WidgetViewModifying
14 | , CustomDebugStringConvertible {
15 |
16 | public var debugDescription: String { "SpinnerWidget()" }
17 |
18 | public var modifiers = WidgetModifiers()
19 |
20 | public init() {}
21 |
22 | public func build(with context: WidgetContext) -> UIView {
23 | let view = UIActivityIndicatorView(frame: .zero)
24 | let context = modifiers.modified(context, for: view)
25 | view.translatesAutoresizingMaskIntoConstraints = false
26 | view.startAnimating()
27 | modifiers.apply(to: view, with: context)
28 | return view
29 | }
30 |
31 | public func color(_ color: UIColor) -> Self {
32 | return modified(WidgetModifier(keyPath: \UIActivityIndicatorView.color, value: color))
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/Navigation/AlertWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 9/15/19.
6 | //
7 |
8 | import UIKit
9 |
10 | public struct AlertWidget
11 | : WidgetControllerType
12 | {
13 |
14 | internal let title: String?
15 | internal let message: String?
16 |
17 | internal var actions: [WidgetAlertAction] = []
18 |
19 | public init(title: String?, message: String?) {
20 | self.title = title
21 | self.message = message
22 | }
23 |
24 | public func controller(with context: WidgetContext) -> UIViewController {
25 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
26 | let context = context.set(viewController: alert)
27 | for action in actions {
28 | alert.addAction(UIAlertAction(title: action.title, style: action.style, handler: { (_) in
29 | action.handler?(context)
30 | }))
31 | }
32 | return alert
33 | }
34 |
35 | public func addAction(title: String?, style: UIAlertAction.Style = .default, handler: ((_ context: WidgetContext) -> Void)? = nil) -> Self {
36 | var widget = self
37 | widget.actions.append(WidgetAlertAction(title: title, style: style, handler: handler))
38 | return widget
39 | }
40 |
41 | }
42 |
43 | internal struct WidgetAlertAction {
44 | let title: String?
45 | let style: UIAlertAction.Style
46 | let handler: ((_ context: WidgetContext) -> Void)?
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/TableView/TableCellWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableCellWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 8/18/19.
6 | //
7 |
8 | import UIKit
9 | import RxSwift
10 | import RxCocoa
11 |
12 | public protocol Hideable {
13 |
14 | }
15 |
16 | extension Widgets {
17 | public enum TableCellCaching {
18 | case auto
19 | case cache
20 | case none
21 | }
22 | }
23 |
24 | open class TableCellWidget: Widget
25 | , WidgetTableViewCellProviding
26 | , WidgetViewModifying
27 | , WidgetPadding {
28 |
29 | public var title: String?
30 | public var detail: String?
31 | public var widget: Widget?
32 | public var accessoryType: UITableViewCell.AccessoryType = .none
33 | public var caching = Widgets.TableCellCaching.auto
34 | public var reusableCellID: String
35 | public var cellType: AnyClass
36 | public var modifiers = WidgetModifiers()
37 |
38 | public init(_ title: String?) {
39 | self.title = title
40 | self.reusableCellID = String(describing: RowWidgetTitleCell.self)
41 | self.cellType = RowWidgetTitleCell.self
42 | }
43 |
44 | public init(_ title: String?, subtitle: String?) {
45 | self.title = title
46 | self.detail = subtitle
47 | self.reusableCellID = String(describing: RowWidgetSubtitleCell.self)
48 | self.cellType = RowWidgetSubtitleCell.self
49 | }
50 |
51 | public init(_ title: String?, value: String?) {
52 | self.title = title
53 | self.detail = value
54 | self.reusableCellID = String(describing: RowWidgetValueCell.self)
55 | self.cellType = RowWidgetValueCell.self
56 | }
57 |
58 | public init(_ widget: Widget) {
59 | self.widget = widget
60 | self.reusableCellID = String(describing: RowWidgetCustomCell.self)
61 | self.cellType = RowWidgetCustomCell.self
62 | }
63 |
64 | public func build(with context: WidgetContext) -> UIView {
65 | fatalError("")
66 | }
67 |
68 | public func cell(for tableView: UITableView, with context: WidgetContext) -> UITableViewCell {
69 | tableView.register(cellType, forCellReuseIdentifier: reusableCellID)
70 | if let cell = tableView.dequeueReusableCell(withIdentifier: reusableCellID) {
71 | configure(cell: cell, with: context)
72 | return cell
73 | }
74 | return UITableViewCell()
75 | }
76 |
77 | public func configure(cell: UITableViewCell, with context: WidgetContext) {
78 | if let widget = widget, let cell = cell as? RowWidgetCustomCell {
79 | cell.reset(widget, with: context, padding: modifiers.padding ?? UIEdgeInsets(top: 12, left: 20, bottom: 12, right: 20))
80 | } else {
81 | cell.textLabel?.text = title
82 | cell.textLabel?.textColor = context.theme.color.text
83 | cell.textLabel?.font = context.theme.font.body
84 | cell.detailTextLabel?.text = detail
85 | cell.detailTextLabel?.textColor = context.theme.color.secondaryText
86 | }
87 | cell.accessoryType = accessoryType
88 | modifiers.apply(to: cell, with: context)
89 | }
90 |
91 | public func accessoryType(_ accessoryType: UITableViewCell.AccessoryType) -> Self {
92 | self.accessoryType = accessoryType
93 | return self
94 | }
95 |
96 | public func caching(_ caching: Widgets.TableCellCaching) -> Self {
97 | self.caching = caching
98 | return self
99 | }
100 |
101 | }
102 |
103 | fileprivate class RowWidgetTitleCell: UITableViewCell {
104 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
105 | super.init(style: .default, reuseIdentifier: reuseIdentifier)
106 | }
107 | required init?(coder: NSCoder) {
108 | fatalError("init(coder:) has not been implemented")
109 | }
110 | }
111 |
112 | fileprivate class RowWidgetSubtitleCell: UITableViewCell {
113 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
114 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
115 | }
116 | required init?(coder: NSCoder) {
117 | fatalError("init(coder:) has not been implemented")
118 | }
119 | }
120 |
121 | fileprivate class RowWidgetValueCell: UITableViewCell {
122 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
123 | super.init(style: .value1, reuseIdentifier: reuseIdentifier)
124 | }
125 | required init?(coder: NSCoder) {
126 | fatalError("init(coder:) has not been implemented")
127 | }
128 | }
129 |
130 | open class RowWidgetCustomCell: UITableViewCell {
131 |
132 | var disposeBag = DisposeBag()
133 |
134 | override open func prepareForReuse() {
135 | disposeBag = DisposeBag()
136 | contentView.subviews.forEach { $0.removeFromSuperview() }
137 | }
138 |
139 | open func reset(_ widget: Widget, with context: WidgetContext, padding: UIEdgeInsets) {
140 | var context = context
141 | context.disposeBag = disposeBag
142 | let view = widget.build(with: context)
143 | contentView.addConstrainedSubview(view, with: padding)
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/TableView/TableSectionWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewSectionWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 | import RxSwift
12 | import RxCocoa
13 |
14 | open class BaseTableSection: Widget
15 | , WidgetUpdatable {
16 |
17 | public weak var parent: WidgetUpdatable? = nil
18 |
19 | public var context: WidgetContext!
20 | public var defaultCellPadding = UIEdgeInsets(top: 12, left: 20, bottom: 12, right: 20)
21 |
22 | fileprivate init() {}
23 |
24 | public func build(with context: WidgetContext) -> UIView {
25 | fatalError()
26 | }
27 |
28 | public var count: Int {
29 | return 0
30 | }
31 |
32 | public func cell(for tableView: UITableView, at row: Int) -> UITableViewCell {
33 | guard let widget = getWidget(at: row) else {
34 | return UITableViewCell()
35 | }
36 | if let provider = widget as? WidgetTableViewCellProviding {
37 | return provider.cell(for: tableView, with: context)
38 | } else if let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: RowWidgetCustomCell.self)) as? RowWidgetCustomCell {
39 | cell.reset(widget, with: context, padding: defaultCellPadding)
40 | return cell
41 | } else {
42 | return UITableViewCell()
43 | }
44 | }
45 |
46 | public func getWidget(at row: Int) -> Widget? {
47 | return nil
48 | }
49 |
50 | public func didSelectRowAt(indexPath: IndexPath) -> Bool {
51 | fatalError("abstract class")
52 | }
53 |
54 | public func updated() {
55 | parent?.updated()
56 | }
57 |
58 | }
59 |
60 | open class TableSectionWidget: BaseTableSection {
61 |
62 | public var widgets: [Widget] {
63 | didSet { updated() }
64 | }
65 |
66 | public var cache: [Int:UITableViewCell] = [:]
67 | public var caching = Widgets.TableCellCaching.auto
68 | public var selectionHandler: ((_ context: WidgetContext, _ indexPath: IndexPath) -> Void)?
69 |
70 | public init(_ widgets: [Widget] = []) {
71 | self.widgets = widgets
72 | super.init()
73 | }
74 |
75 | public override var count: Int {
76 | return widgets.count
77 | }
78 |
79 | public override func cell(for tableView: UITableView, at row: Int) -> UITableViewCell {
80 | guard let widget = getWidget(at: row) else {
81 | return UITableViewCell()
82 | }
83 | if let widget = widget as? TableCellWidget {
84 | switch (caching, widget.caching) {
85 | case (_, .none), (.none, _):
86 | return super.cell(for: tableView, at: row)
87 | default:
88 | if let cell = cache[row] {
89 | return cell
90 | }
91 | let cell = super.cell(for: tableView, at: row)
92 | cache[row] = cell
93 | return cell
94 | }
95 | } else {
96 | return super.cell(for: tableView, at: row)
97 | }
98 | }
99 |
100 | public override func getWidget(at row: Int) -> Widget? {
101 | guard widgets.indices.contains(row) else { return nil }
102 | return widgets[row]
103 | }
104 |
105 | public func caching(_ caching: Widgets.TableCellCaching) {
106 | self.caching = caching
107 | }
108 |
109 | public func onSelect(_ selectionHandler: @escaping (_ context: WidgetContext, _ indexPath: IndexPath) -> Void) -> Self {
110 | self.selectionHandler = selectionHandler
111 | return self
112 | }
113 |
114 | public override func didSelectRowAt(indexPath: IndexPath) -> Bool {
115 | if let selectionHandler = selectionHandler {
116 | selectionHandler(context, indexPath)
117 | return true
118 | }
119 | return false
120 | }
121 |
122 | }
123 |
124 | open class DynamicTableSectionWidget
- : BaseTableSection {
125 |
126 | private var builder: (_ item: Item) -> Widget
127 | private var items: [Item] = []
128 | private var selectionHandler: ((_ context: WidgetContext, _ indexPath: IndexPath, _ item: Item) -> Void)?
129 | private var disposeBag = DisposeBag()
130 |
131 | public init(_ items: O, builder: @escaping (_ item: Item) -> Widget) where O.Element == [Item] {
132 | self.builder = builder
133 | super.init()
134 | items
135 | .observeOn(MainScheduler.instance)
136 | .subscribe(onNext: { [weak self] (items) in
137 | self?.items = items
138 | self?.updated()
139 | })
140 | .disposed(by: disposeBag)
141 | }
142 |
143 | public override var count: Int {
144 | return items.count
145 | }
146 |
147 | public override func getWidget(at row: Int) -> Widget? {
148 | guard items.indices.contains(row) else { return nil }
149 | return builder(items[row])
150 | }
151 |
152 | public func onSelect(_ selectionHandler: @escaping (_ context: WidgetContext, _ indexPath: IndexPath, _ item: Item) -> Void) -> Self {
153 | self.selectionHandler = selectionHandler
154 | return self
155 | }
156 |
157 | public override func didSelectRowAt(indexPath: IndexPath) -> Bool {
158 | guard items.indices.contains(indexPath.row) else { return false }
159 | if let selectionHandler = selectionHandler {
160 | selectionHandler(context, indexPath, items[indexPath.row])
161 | return true
162 | }
163 | return false
164 | }
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/Sources/RxSwiftWidgets/Widgets/TableView/TableWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableWidget.swift
3 | // RxSwiftWidgets
4 | //
5 | // Created by Michael Long on 7/10/19.
6 | // Copyright © 2019 Michael Long. All rights reserved.
7 | //
8 |
9 |
10 | import UIKit
11 | import RxSwift
12 | import RxCocoa
13 |
14 | //open class TableViewTest {
15 | //
16 | // @State var items: [String] = []
17 | //
18 | // func builder() -> Widget {
19 | // return TableWidget([
20 | // TableSectionWidget([
21 | // LabelWidget("Section 1 Row 1"),
22 | // LabelWidget("Section 1 Row 2"),
23 | // LabelWidget("Section 1 Row 2")
24 | // ]),
25 | // TableSectionWidget([
26 | // LabelWidget("Section 2 Row 1"),
27 | // LabelWidget("Section 2 Row 2")
28 | // ]),
29 | // DynamicSectionWidget($items) {
30 | // LabelWidget($0)
31 | // }
32 | // ])
33 | //
34 | // }
35 | //}
36 |
37 | public protocol WidgetUpdatable: class {
38 | func updated()
39 | }
40 |
41 | public protocol WidgetTableViewCellProviding {
42 | func cell(for tableView: UITableView, with context: WidgetContext) -> UITableViewCell
43 | }
44 |
45 | //public protocol CollectionViewCellProviding {
46 | // func cell(for collectionView: UICollectionView, with context: WidgetContext) -> UICollectionViewCell
47 | //}
48 |
49 | open class TableWidget
50 | : NSObject
51 | , Widget
52 | , WidgetViewModifying
53 | , WidgetPadding
54 | , WidgetUpdatable {
55 |
56 | public override var debugDescription: String { "TableWidget()" }
57 |
58 | public weak var tableView: UITableView!
59 |
60 | public var sections: [BaseTableSection]
61 |
62 | public var modifiers = WidgetModifiers()
63 | public var context: WidgetContext!
64 |
65 | public var grouped: UITableView.Style?
66 | public var selectionHandler: ((_ context: WidgetContext, _ indexPath: IndexPath) -> Void)?
67 | public var initialRefresh: Bool = false
68 | public var refreshHandler: ((_ context: WidgetContext) -> Void)?
69 |
70 | public init(_ sections: [BaseTableSection] = []) {
71 | self.sections = sections
72 | super.init()
73 | }
74 |
75 | public func build(with context: WidgetContext) -> UIView {
76 |
77 | let grouped = self.grouped ?? (sections.count > 1 ? .grouped : .plain)
78 |
79 | let view = WidgetTableView(frame: .zero, style: grouped)
80 |
81 | self.context = modifiers.modified(context, for: view)
82 | .putWeak(view)
83 |
84 | sections.forEach {
85 | $0.parent = self
86 | $0.context = self.context
87 | }
88 |
89 | view.translatesAutoresizingMaskIntoConstraints = false
90 | view.insetsLayoutMarginsFromSafeArea = false
91 |
92 | view.tableViewWidget = self
93 | view.dataSource = self
94 | view.delegate = self
95 | view.register(RowWidgetCustomCell.self, forCellReuseIdentifier: String(describing: RowWidgetCustomCell.self))
96 |
97 | if #available(iOS 11.0, *) {
98 | view.insetsContentViewsToSafeArea = false
99 | } // kill default behavior and left safearea modifier handle things.
100 |
101 | if let refreshHandler = refreshHandler {
102 | view.enablePullToRefresh(refreshHandler)
103 | if initialRefresh {
104 | DispatchQueue.main.async {
105 | view.setContentOffset(CGPoint(x: 0, y: -20), animated: true)
106 | view.refreshControl?.beginRefreshing()
107 | view.refreshControl?.sendActions(for: .valueChanged)
108 | }
109 | }
110 | }
111 |
112 | modifiers.apply(to: view, with: self.context)
113 |
114 | self.tableView = view
115 |
116 | return view
117 | }
118 |
119 | public func onRefresh(initialRefresh: Bool = false, handler: @escaping ((_ context: WidgetContext) -> Void)) -> Self {
120 | self.initialRefresh = initialRefresh
121 | self.refreshHandler = handler
122 | return self
123 | }
124 |
125 | public func onSelect(_ selectionHandler: @escaping (_ context: WidgetContext, _ indexPath: IndexPath) -> Void) -> Self {
126 | self.selectionHandler = selectionHandler
127 | return self
128 | }
129 |
130 | public func with(_ block: @escaping WidgetModifierBlockType) -> Self {
131 | return modified(WidgetModifierBlock(block))
132 | }
133 |
134 | public func updated() {
135 | tableView?.reloadData()
136 | tableView?.refreshControl?.endRefreshing()
137 | }
138 |
139 | }
140 |
141 | extension TableWidget: UITableViewDataSource {
142 |
143 | public func numberOfSections(in tableView: UITableView) -> Int {
144 | return sections.count
145 | }
146 |
147 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
148 | guard sections.indices.contains(section) else { return 0 }
149 | return sections[section].count
150 | }
151 |
152 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
153 | return sections[indexPath.section].cell(for: tableView, at: indexPath.row)
154 | }
155 |
156 | }
157 |
158 | extension TableWidget: UITableViewDelegate {
159 |
160 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
161 | guard sections.indices.contains(indexPath.section) else { return }
162 | if !sections[indexPath.section].didSelectRowAt(indexPath: indexPath) {
163 | selectionHandler?(context, indexPath)
164 | }
165 | }
166 |
167 | }
168 |
169 | extension WidgetContext {
170 | public var tableView: UITableView? {
171 | return getWeak(WidgetTableView.self)
172 | }
173 | }
174 |
175 |
176 | fileprivate class WidgetTableView: UITableView {
177 |
178 | public var tableViewWidget: TableWidget?
179 | public var refresh: ((_ context: WidgetContext) -> Void)?
180 |
181 | open func enablePullToRefresh(_ refresh: @escaping (_ context: WidgetContext) -> Void) {
182 | self.refresh = refresh
183 | refreshControl = UIRefreshControl()
184 | refreshControl?.addTarget(self, action: #selector(callRefresh(_:)), for: .valueChanged)
185 | }
186 |
187 | @objc private func callRefresh(_ sender: Any) {
188 | if let widget = tableViewWidget {
189 | refresh?(widget.context)
190 | }
191 | }
192 |
193 | }
194 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import RxSwiftWidgetsTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += RxSwiftWidgetsTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/RxSwiftWidgetsTests/RxSwiftWidgetsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import RxSwiftWidgets
3 |
4 | final class RxSwiftWidgetsTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(RxSwiftWidgets().text, "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/RxSwiftWidgetsTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(RxSwiftWidgetsTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/jazzy.yaml:
--------------------------------------------------------------------------------
1 | clean: true
2 | module: RxSwiftWidgets
3 | output: Documentation/API
4 | xcodebuild_arguments:
5 | - "-project"
6 | - "RxSwiftWidgets.xcodeproj"
7 | - "-scheme"
8 | - "RxSwiftWidgets-Package"
9 | - "-destination"
10 | - "platform=iOS Simulator,name=iPhone Xr 13"
11 |
--------------------------------------------------------------------------------