├── .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 | --------------------------------------------------------------------------------