├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── AppIcon1024pt.png │ │ ├── AppIcon60pt@2x.png │ │ ├── AppIcon60pt@3x.png │ │ ├── AppIcon76pt.png │ │ ├── AppIcon76pt@2x.png │ │ ├── AppIcon83.5pt@2x.png │ │ └── Contents.json │ ├── Background.colorset │ │ └── Contents.json │ ├── Bittersweet.colorset │ │ └── Contents.json │ ├── Checkmark.imageset │ │ ├── Checkmark.pdf │ │ └── Contents.json │ ├── Cinder.colorset │ │ └── Contents.json │ ├── CommentText.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── Control Add.imageset │ │ ├── Contents.json │ │ └── Control Add.pdf │ ├── Deep Carmine Pink.colorset │ │ └── Contents.json │ ├── Disclosure Indicator.imageset │ │ ├── Contents.json │ │ └── Disclosure Indicator.pdf │ ├── Eucalyptus.colorset │ │ └── Contents.json │ ├── Flix Icon.imageset │ │ ├── Contents.json │ │ ├── Flix Icon@2x.png │ │ └── Flix Icon@3x.png │ ├── GitHub Logo.imageset │ │ ├── Contents.json │ │ └── GitHub Logo.pdf │ ├── Icon Clear.imageset │ │ ├── Contents.json │ │ └── Icon Clear.pdf │ ├── Icon Current Location.imageset │ │ ├── Contents.json │ │ └── Icon Current Location.pdf │ ├── Icon Location Gray.imageset │ │ ├── Contents.json │ │ └── Icon Location Gray.pdf │ ├── Icon Location Red.imageset │ │ ├── Contents.json │ │ └── Icon Location Red.pdf │ ├── Settings │ │ ├── Airplane Icon.imageset │ │ │ ├── Airplane Icon.pdf │ │ │ └── Contents.json │ │ ├── Bluetooth Icon.imageset │ │ │ ├── Bluetooth Icon.pdf │ │ │ └── Contents.json │ │ ├── Camera App Icon.imageset │ │ │ ├── Camera App Icon.pdf │ │ │ └── Contents.json │ │ ├── Carrier Icon.imageset │ │ │ ├── Carrier Icon.pdf │ │ │ └── Contents.json │ │ ├── Cellular Icon.imageset │ │ │ ├── Cellular Icon.pdf │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Music App Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Music App Icon.pdf │ │ ├── News App Icon.imageset │ │ │ ├── Contents.json │ │ │ └── News App Icon.pdf │ │ ├── Personal Hotspot Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Personal Hotspot Icon.pdf │ │ ├── Photos App Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Photos App Icon.pdf │ │ ├── Safari App Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Safari App Icon.pdf │ │ ├── Wallet App Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Wallet App Icon.pdf │ │ └── Wifi Icon.imageset │ │ │ ├── Contents.json │ │ │ └── Wifi Icon.pdf │ └── Ufo Green.colorset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Example │ ├── CalendarEvent │ │ ├── Event │ │ │ ├── CalendarEventObject.swift │ │ │ ├── DateSelectProvider.swift │ │ │ ├── EventEditViewController.swift │ │ │ ├── EventListViewController.swift │ │ │ ├── SelectedLocationProvider.swift │ │ │ ├── SpacingSectionProvider.swift │ │ │ ├── StartAndEndDateGroupProvider.swift │ │ │ ├── TextFieldProvider.swift │ │ │ └── TextViewProvider.swift │ │ ├── Location │ │ │ ├── CLLocationManager+Rx.swift │ │ │ ├── GeolocationService.swift │ │ │ ├── LocalSearchProvider.swift │ │ │ ├── MKLocalSearch+Rx.swift │ │ │ ├── RxCLLocationManagerDelegateProxy.swift │ │ │ └── SelectLocationViewController.swift │ │ ├── Options │ │ │ ├── EventOptionProvider.swift │ │ │ └── EventOptionsViewController.swift │ │ ├── Repeat │ │ │ ├── EndRepeatProvider.swift │ │ │ ├── EndRepeatSelectViewController.swift │ │ │ └── RepeatGroupProvider.swift │ │ └── TimeZone │ │ │ └── SelectTimeZoneViewController.swift │ ├── ControlCenterCustomizeViewController.swift │ ├── DeleteItemViewController.swift │ ├── DoNotDisturbSettingsViewController.swift │ ├── EmptyViewController.swift │ ├── GitHubSigup │ │ ├── ActivityIndicator.swift │ │ ├── DefaultImplementations.swift │ │ ├── GitHubSignupViewController.swift │ │ ├── GithubSignupViewModel1.swift │ │ ├── Operators.swift │ │ ├── Protocols.swift │ │ ├── String+URL.swift │ │ └── Wireframe.swift │ ├── LoginViewController.swift │ ├── MoveCollectionViewController.swift │ ├── NestFormViewController.swift │ ├── StoryboardViewController.swift │ └── Tutorial │ │ ├── AppsProvider.swift │ │ ├── ProfileProvider.swift │ │ ├── SettingsViewController.swift │ │ └── TableViewProvider.swift ├── ExampleListViewController.swift ├── Info.plist ├── Providers │ ├── RadioProvider.swift │ ├── TextListProvider.swift │ ├── TextSectionProvider.swift │ ├── UniqueButtonTableViewProvider.swift │ ├── UniqueCommentTextProvider.swift │ ├── UniqueMessageTableViewProvider.swift │ ├── UniqueSwitchProvider.swift │ ├── UniqueTextFieldTableViewProvider.swift │ └── UniqueTextProvider.swift └── ViewController.swift ├── Flix.png ├── Flix.podspec ├── Flix.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── Example.xcscheme │ ├── Flix.xcscheme │ └── FlixTests.xcscheme ├── Flix.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── Flix ├── Builder │ ├── AnimatableCollectionViewBuilder.swift │ ├── AnimatableTableViewBuilder.swift │ ├── Builder.swift │ ├── BuilderProxy.swift │ ├── ChangesetInfo.swift │ ├── CollectionViewBuilder.swift │ ├── PerformGroupUpdatesable.swift │ ├── TableViewBuilder.swift │ ├── _CollectionViewBuilder.swift │ └── _TableViewBuilder.swift ├── Cell │ └── UIView+Configure.swift ├── CollectionEdit │ └── CollectionViewMoveable.swift ├── DelegateProxy │ ├── CollectionViewDelegateProxy.swift │ └── TableViewDelegateProxy.swift ├── Flix.h ├── Info.plist ├── Node │ ├── IdentifiableNode.swift │ └── Node.swift ├── Provider │ ├── CollectionViewEvent.swift │ ├── CollectionViewProvider.swift │ ├── CollectionViewSectionPartionProvider.swift │ ├── CollectionViewSectionProvider.swift │ ├── CustomIdentityType.swift │ ├── Group │ │ ├── AnimatableCollectionViewGroupProvider.swift │ │ ├── AnimatableTableViewGroupProvider.swift │ │ ├── CollectionViewGroupProvider.swift │ │ └── TableViewGroupProvider.swift │ ├── ProviderDescription.swift │ ├── ProviderHiddenable.swift │ ├── TableViewEvent.swift │ ├── TableViewProvider.swift │ ├── TableViewSectionPartionProvider.swift │ └── TableViewSectionProvider.swift ├── Storyboard │ ├── FlixStackItemProvider.swift │ └── FlixStackView.swift ├── TableViewEdit │ ├── TableViewDeleteable.swift │ ├── TableViewEditable.swift │ ├── TableViewInsertable.swift │ ├── TableViewMoveable.swift │ └── TableViewSwipeable.swift └── UniqueCustomProvider │ ├── CustomProvider.swift │ ├── NeverHitSelfView.swift │ ├── UniqueCustomCollectionViewProvider.swift │ ├── UniqueCustomCollectionViewSectionProvider.swift │ ├── UniqueCustomTableViewProvider.swift │ └── UniqueCustomTableViewSectionProvider.swift ├── FlixTests ├── CollectionViewBuilderMemoryLeakTests.swift ├── FlixTests.swift ├── Info.plist ├── SingleProviderTests.swift └── TableViewBuilderMemoryLeakTests.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Podfile ├── Podfile.lock ├── README-zh.md ├── README.md ├── block_diagram.png └── screenshots ├── example.png ├── tutorial_0_profile.png ├── tutorial_1_profile_with_section.png ├── tutorial_2_more_sections.png └── tutorial_3_final.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 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 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | git: 2 | depth: 3 3 | 4 | os: osx 5 | language: objective-c 6 | sudo: true 7 | osx_image: xcode11.3 8 | 9 | before_install: 10 | - bundle install 11 | - bundle exec pod install 12 | 13 | notifications: 14 | email: false 15 | 16 | install: true 17 | 18 | env: 19 | - BUILD="xcodebuild -workspace Flix.xcworkspace -scheme Example -configuration Release -destination 'platform=iOS Simulator,name=iPhone 11' build | xcpretty" 20 | - BUILD="xcodebuild test -workspace Flix.xcworkspace -scheme Flix -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 11' build | xcpretty" 21 | - BUILD="bundle exec pod lib lint Flix.podspec --verbose" 22 | 23 | script: eval "${BUILD}" 24 | 25 | after_success: 26 | - sleep 5 # workaround https://github.com/travis-ci/travis-ci/issues/4725 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | --- 4 | 5 | ## [4.0.0](https://github.com/DianQK/Flix/releases/tag/4.0.0) 6 | 7 | - Support RxSwift 5 8 | 9 | ## [3.0.0](https://github.com/DianQK/Flix/releases/tag/3.0.0) 10 | 11 | - Support Swift 5 12 | 13 | ## [1.1.0](https://github.com/DianQK/Flix/releases/tag/1.1.0) 14 | 15 | - Add CollectionViewMoveable 16 | 17 | ## [1.0.0](https://github.com/DianQK/Flix/releases/tag/1.0.0) 18 | 19 | - Add more examples 20 | - Add GroupProvider 21 | 22 | #### Breaking Changes 23 | 24 | - Add `UITableView` / `UICollectionView` argument for `itemHeight`, `sectionHeight`, `itemSize` and `sectionSize` 25 | - Update `tap` type to `ControlEvent` for Unique Custom Provider 26 | - Update `isHidden` type to `Bool` for Unique Custom Provider 27 | - Add `rx` extension for Unique Custom Provider 28 | 29 | 30 | ## [0.7.0](https://github.com/DianQK/Flix/releases/tag/0.7.0) 31 | 32 | - Support `UICollectionView` / `UITableView`. 33 | - Support no reused when you need. 34 | - Support reused for list when you need. 35 | - Support nest form. 36 | - Support add, delete and insert 37 | - Support Storyboard 38 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | 18 | UIView.appearance().tintColor = UIColor(named: "Deep Carmine Pink") 19 | 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon1024pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon1024pt.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon60pt@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon60pt@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon76pt.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon76pt@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/AppIcon83.5pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/AppIcon.appiconset/AppIcon83.5pt@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "AppIcon60pt@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "AppIcon60pt@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "size" : "20x20", 48 | "scale" : "1x" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "size" : "20x20", 53 | "scale" : "2x" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "size" : "29x29", 58 | "scale" : "1x" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "size" : "29x29", 63 | "scale" : "2x" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "size" : "40x40", 68 | "scale" : "1x" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "size" : "40x40", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "76x76", 77 | "idiom" : "ipad", 78 | "filename" : "AppIcon76pt.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "76x76", 83 | "idiom" : "ipad", 84 | "filename" : "AppIcon76pt@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "83.5x83.5", 89 | "idiom" : "ipad", 90 | "filename" : "AppIcon83.5pt@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "1024x1024", 95 | "idiom" : "ios-marketing", 96 | "filename" : "AppIcon1024pt.png", 97 | "scale" : "1x" 98 | } 99 | ], 100 | "info" : { 101 | "version" : 1, 102 | "author" : "xcode" 103 | } 104 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0.937", 13 | "alpha" : "1.000", 14 | "blue" : "0.957", 15 | "green" : "0.937" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Bittersweet.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.439", 15 | "green" : "0.451" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Checkmark.imageset/Checkmark.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Checkmark.imageset/Checkmark.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Checkmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Checkmark.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cinder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "extended-srgb", 11 | "components" : { 12 | "red" : "0x24", 13 | "alpha" : "1.000", 14 | "blue" : "0x2E", 15 | "green" : "0x28" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/CommentText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0x6C", 13 | "alpha" : "1.000", 14 | "blue" : "0x71", 15 | "green" : "0x6C" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Control Add.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Control Add.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Control Add.imageset/Control Add.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Control Add.imageset/Control Add.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Deep Carmine Pink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "1.000", 13 | "alpha" : "1.000", 14 | "blue" : "0.200", 15 | "green" : "0.173" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Disclosure Indicator.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Disclosure Indicator.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Disclosure Indicator.imageset/Disclosure Indicator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Disclosure Indicator.imageset/Disclosure Indicator.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Eucalyptus.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0.157", 13 | "alpha" : "1.000", 14 | "blue" : "0.271", 15 | "green" : "0.655" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Flix Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Flix Icon@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "Flix Icon@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Flix Icon.imageset/Flix Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Flix Icon.imageset/Flix Icon@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/Flix Icon.imageset/Flix Icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Flix Icon.imageset/Flix Icon@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/GitHub Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "GitHub Logo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/GitHub Logo.imageset/GitHub Logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/GitHub Logo.imageset/GitHub Logo.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Clear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon Clear.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Clear.imageset/Icon Clear.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Icon Clear.imageset/Icon Clear.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Current Location.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon Current Location.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Current Location.imageset/Icon Current Location.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Icon Current Location.imageset/Icon Current Location.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Location Gray.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon Location Gray.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Location Gray.imageset/Icon Location Gray.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Icon Location Gray.imageset/Icon Location Gray.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Location Red.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Icon Location Red.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Icon Location Red.imageset/Icon Location Red.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Icon Location Red.imageset/Icon Location Red.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Airplane Icon.imageset/Airplane Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Airplane Icon.imageset/Airplane Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Airplane Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Airplane Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Bluetooth Icon.imageset/Bluetooth Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Bluetooth Icon.imageset/Bluetooth Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Bluetooth Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Bluetooth Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Camera App Icon.imageset/Camera App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Camera App Icon.imageset/Camera App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Camera App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Camera App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Carrier Icon.imageset/Carrier Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Carrier Icon.imageset/Carrier Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Carrier Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Carrier Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Cellular Icon.imageset/Cellular Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Cellular Icon.imageset/Cellular Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Cellular Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Cellular Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Music App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Music App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Music App Icon.imageset/Music App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Music App Icon.imageset/Music App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/News App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "News App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/News App Icon.imageset/News App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/News App Icon.imageset/News App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Personal Hotspot Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Personal Hotspot Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Personal Hotspot Icon.imageset/Personal Hotspot Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Personal Hotspot Icon.imageset/Personal Hotspot Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Photos App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Photos App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Photos App Icon.imageset/Photos App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Photos App Icon.imageset/Photos App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Safari App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Safari App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Safari App Icon.imageset/Safari App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Safari App Icon.imageset/Safari App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Wallet App Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Wallet App Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Wallet App Icon.imageset/Wallet App Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Wallet App Icon.imageset/Wallet App Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Wifi Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Wifi Icon.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Settings/Wifi Icon.imageset/Wifi Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Example/Assets.xcassets/Settings/Wifi Icon.imageset/Wifi Icon.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/Ufo Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0.204", 13 | "alpha" : "1.000", 14 | "blue" : "0.345", 15 | "green" : "0.816" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Event/CalendarEventObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarEventObject.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Flix 11 | 12 | struct CalendarEventObject: Equatable { 13 | 14 | var id: Int 15 | 16 | let title: String // default New Event 17 | let location: EventLocation? 18 | 19 | let isAllDay: Bool 20 | let startsDate: Date 21 | let endsDate: Date 22 | let eventRepeat: RepeatOption 23 | let endRepeatDate: Date? 24 | 25 | let calendar: CalendarOption 26 | 27 | let alert: AlertOption 28 | let secondAlert: AlertOption 29 | 30 | let showAs: ShowAsOption 31 | 32 | let url: String? 33 | let notes: String? 34 | 35 | } 36 | 37 | extension CalendarEventObject: StringIdentifiableType { 38 | 39 | var identity: String { 40 | return self.id.description 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Event/EventListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventListViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 27/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class EventListProvider: AnimatableTableViewProvider { 15 | 16 | typealias Cell = TitleTableViewCell 17 | typealias Value = CalendarEventObject 18 | 19 | let objects = BehaviorRelay<[CalendarEventObject]>(value: []) 20 | 21 | func configureCell(_ tableView: UITableView, cell: TitleTableViewCell, indexPath: IndexPath, value: CalendarEventObject) { 22 | cell.titleLabel.text = value.title 23 | cell.accessoryType = .disclosureIndicator 24 | } 25 | 26 | func createValues() -> Observable<[CalendarEventObject]> { 27 | return self.objects.asObservable() 28 | } 29 | 30 | var addObject: Binder { 31 | return Binder(self, binding: { (provider, object) in 32 | if object.id == 0 { 33 | var object = object 34 | let id = provider.objects.value.map { $0.id }.max() ?? 1 35 | object.id = id 36 | provider.objects.accept(provider.objects.value + [object]) 37 | } else { 38 | provider.objects.accept(provider.objects.value.map { (old) -> CalendarEventObject in 39 | return old.id == object.id ? object : old 40 | }) 41 | } 42 | }) 43 | } 44 | 45 | func itemSelected(_ tableView: UITableView, indexPath: IndexPath, value: CalendarEventObject) { 46 | tableView.deselectRow(at: indexPath, animated: true) 47 | } 48 | 49 | } 50 | 51 | class EventListViewController: TableViewController { 52 | 53 | let provider = EventListProvider() 54 | 55 | override func viewDidLoad() { 56 | super.viewDidLoad() 57 | title = "All Events" 58 | 59 | let addBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: nil, action: nil) 60 | self.navigationItem.rightBarButtonItem = addBarButtonItem 61 | Observable.merge([ 62 | addBarButtonItem.rx.tap.map { nil as CalendarEventObject? }, 63 | provider.event.modelSelected.map { $0 as CalendarEventObject? } 64 | ]) 65 | .flatMapLatest { [weak self] event in 66 | return EventEditViewController.rx.createWithParent(self, calendarEvent: event) 67 | .flatMap({ $0.saved.asObservable() }) 68 | .take(1) 69 | } 70 | .bind(to: provider.addObject) 71 | .disposed(by: disposeBag) 72 | 73 | provider.objects.asObservable().map { $0.isEmpty } 74 | .subscribe(onNext: { [weak self] (isEmpty) in 75 | if isEmpty { 76 | let backgroundImageView = UIImageView(image: #imageLiteral(resourceName: "Flix Icon")) 77 | backgroundImageView.contentMode = .center 78 | backgroundImageView.backgroundColor = UIColor.white 79 | self?.tableView.backgroundView = backgroundImageView 80 | } else { 81 | self?.tableView.backgroundView = nil 82 | } 83 | }) 84 | .disposed(by: disposeBag) 85 | 86 | self.tableView.separatorInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0) 87 | 88 | self.tableView.flix.animatable.build([provider]) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Event/SpacingSectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpacingSectionProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 27/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class SpacingSectionProvider: AnimatableTableViewSectionProvider { 15 | 16 | convenience init(providers: [_AnimatableTableViewMultiNodeProvider], headerHeight: CGFloat, footerHeight: CGFloat) { 17 | let headerProvider = UniqueCustomTableViewSectionProvider(tableElementKindSection: .header) 18 | headerProvider.sectionHeight = { _ in return headerHeight } 19 | let footerProvider = UniqueCustomTableViewSectionProvider(tableElementKindSection: .footer) 20 | footerProvider.sectionHeight = { _ in return footerHeight } 21 | self.init( 22 | providers: providers, 23 | headerProvider: headerProvider, 24 | footerProvider: footerProvider 25 | ) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Event/TextFieldProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 27/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class TextFieldProvider: UITextField, UniqueAnimatableTableViewProvider { 15 | 16 | typealias Cell = UITableViewCell 17 | 18 | func onCreate(_ tableView: UITableView, cell: UITableViewCell, indexPath: IndexPath) { 19 | cell.contentView.addSubview(self) 20 | self.clearButtonMode = .whileEditing 21 | self.translatesAutoresizingMaskIntoConstraints = false 22 | self.topAnchor.constraint(equalTo: cell.contentView.topAnchor).isActive = true 23 | self.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true 24 | self.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 20).isActive = true 25 | self.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -20).isActive = true 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Event/TextViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import Flix 12 | 13 | class TextViewProvider: UITextView, UniqueAnimatableTableViewProvider { 14 | 15 | typealias Cell = UITableViewCell 16 | 17 | let disposeBag = DisposeBag() 18 | private let placeholderTextField = UITextField() 19 | 20 | var placeholder: String? { 21 | get { 22 | return placeholderTextField.placeholder 23 | } 24 | set { 25 | placeholderTextField.placeholder = newValue 26 | } 27 | } 28 | 29 | func onCreate(_ tableView: UITableView, cell: UITableViewCell, indexPath: IndexPath) { 30 | cell.contentView.addSubview(self) 31 | self.font = UIFont.systemFont(ofSize: 17) 32 | self.translatesAutoresizingMaskIntoConstraints = false 33 | self.isScrollEnabled = false 34 | self.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 0).isActive = true 35 | self.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 0).isActive = true 36 | self.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: 0).isActive = true 37 | self.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: 0).isActive = true 38 | self.textContainerInset = UIEdgeInsets(top: 12, left: 20, bottom: 0, right: 20) 39 | self.contentInset = UIEdgeInsets.zero 40 | self.contentInsetAdjustmentBehavior = .never 41 | 42 | cell.contentView.addSubview(placeholderTextField) 43 | placeholderTextField.translatesAutoresizingMaskIntoConstraints = false 44 | placeholderTextField.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 12).isActive = true 45 | placeholderTextField.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 20).isActive = true 46 | placeholderTextField.isUserInteractionEnabled = false 47 | 48 | self.rx.text.orEmpty.changed 49 | .distinctUntilChanged() 50 | .subscribe(onNext: { [weak tableView, weak self] (text) in 51 | self?.placeholderTextField.isHidden = !text.isEmpty 52 | tableView?.performBatchUpdates(nil, completion: nil) 53 | }) 54 | .disposed(by: disposeBag) 55 | } 56 | 57 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath, value: TextViewProvider) -> CGFloat? { 58 | let height = NSAttributedString(string: value.text, attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 17)]).boundingRect(with: CGSize(width: tableView.bounds.width - 40, height: CGFloat.greatestFiniteMagnitude), options: [NSStringDrawingOptions.usesFontLeading, .usesLineFragmentOrigin], context: nil).height + 12 59 | return max(120, height) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Location/GeolocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationService.swift 3 | // RxExample 4 | // 5 | // Created by Carlos García on 19/01/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | #if !RX_NO_MODULE 11 | import RxSwift 12 | import RxCocoa 13 | #endif 14 | 15 | class GeolocationService { 16 | 17 | static let instance = GeolocationService() 18 | private (set) var authorized: Driver 19 | private (set) var location: Driver 20 | 21 | private let locationManager = CLLocationManager() 22 | 23 | private init() { 24 | 25 | locationManager.distanceFilter = kCLDistanceFilterNone 26 | locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation 27 | 28 | authorized = Observable.deferred { [weak locationManager] in 29 | let status = CLLocationManager.authorizationStatus() 30 | guard let locationManager = locationManager else { 31 | return Observable.just(status) 32 | } 33 | return locationManager 34 | .rx.didChangeAuthorizationStatus 35 | .startWith(status) 36 | } 37 | .asDriver(onErrorJustReturn: CLAuthorizationStatus.notDetermined) 38 | .map { 39 | switch $0 { 40 | case .authorizedAlways, .authorizedWhenInUse: 41 | return true 42 | default: 43 | return false 44 | } 45 | } 46 | 47 | location = locationManager.rx.didUpdateLocations 48 | .asDriver(onErrorJustReturn: []) 49 | .map { $0.last } 50 | 51 | locationManager.requestAlwaysAuthorization() 52 | locationManager.startUpdatingLocation() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Location/MKLocalSearch+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKLocalSearch+Rx.swift 3 | // Example 4 | // 5 | // Created by DianQK on 25/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import CoreLocation 11 | import MapKit 12 | 13 | extension MKLocalSearch { 14 | 15 | convenience init(naturalLanguageQuery: String) { 16 | let request = MKLocalSearch.Request() 17 | request.naturalLanguageQuery = naturalLanguageQuery 18 | self.init(request: request) 19 | } 20 | 21 | } 22 | 23 | extension Reactive where Base: MKLocalSearch { 24 | 25 | var start: Observable { 26 | return Observable.create({ [weak search = self.base] (observer) -> Disposable in 27 | guard let search = search else { 28 | observer.onCompleted() 29 | return Disposables.create() 30 | } 31 | search.start { (response, error) in 32 | if let error = error { 33 | observer.onError(error) 34 | } else { 35 | observer.onNext(response) 36 | observer.onCompleted() 37 | } 38 | } 39 | return Disposables.create { 40 | search.cancel() 41 | } 42 | }) 43 | } 44 | 45 | var searchedPlacemarks: Observable<[CLPlacemark]> { 46 | return self.start.map { $0?.mapItems.map { $0.placemark } ?? [] } 47 | } 48 | 49 | static func search(naturalLanguageQuery: String) -> Observable<[CLPlacemark]> { 50 | return Observable.just(MKLocalSearch(naturalLanguageQuery: naturalLanguageQuery)) 51 | .flatMap { $0.rx.searchedPlacemarks } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Location/RxCLLocationManagerDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxCLLocationManagerDelegateProxy.swift 3 | // RxExample 4 | // 5 | // Created by Carlos García on 8/7/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | #if !RX_NO_MODULE 11 | import RxSwift 12 | import RxCocoa 13 | #endif 14 | 15 | extension CLLocationManager: HasDelegate { 16 | public typealias Delegate = CLLocationManagerDelegate 17 | } 18 | 19 | public class RxCLLocationManagerDelegateProxy 20 | : DelegateProxy 21 | , DelegateProxyType 22 | , CLLocationManagerDelegate { 23 | 24 | public init(locationManager: CLLocationManager) { 25 | super.init(parentObject: locationManager, delegateProxy: RxCLLocationManagerDelegateProxy.self) 26 | } 27 | 28 | public static func registerKnownImplementations() { 29 | self.register { RxCLLocationManagerDelegateProxy(locationManager: $0) } 30 | } 31 | 32 | internal lazy var didUpdateLocationsSubject = PublishSubject<[CLLocation]>() 33 | internal lazy var didFailWithErrorSubject = PublishSubject() 34 | 35 | public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 36 | _forwardToDelegate?.locationManager?(manager, didUpdateLocations: locations) 37 | didUpdateLocationsSubject.onNext(locations) 38 | } 39 | 40 | public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 41 | _forwardToDelegate?.locationManager?(manager, didFailWithError: error) 42 | didFailWithErrorSubject.onNext(error) 43 | } 44 | 45 | deinit { 46 | self.didUpdateLocationsSubject.on(.completed) 47 | self.didFailWithErrorSubject.on(.completed) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Options/EventOptionsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventOptionsViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class EventOptionsProvider: AnimatableTableViewProvider { 15 | 16 | func configureCell(_ tableView: UITableView, cell: UITableViewCell, indexPath: IndexPath, value: T) { 17 | if !cell.hasConfigured { 18 | cell.hasConfigured = true 19 | let titleLabel = UILabel() 20 | cell.contentView.addSubview(titleLabel) 21 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 22 | titleLabel.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 20).isActive = true 23 | titleLabel.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor).isActive = true 24 | titleLabel.text = value.name 25 | if let selectedOption = selectedOption, selectedOption == value { 26 | cell.accessoryType = .checkmark 27 | } 28 | } 29 | } 30 | 31 | func createValues() -> Observable<[T]> { 32 | return Observable.just(options) 33 | } 34 | 35 | let options: [T] 36 | let selectedOption: T? 37 | 38 | init(options: [T], selectedOption: T?) { 39 | self.options = options 40 | self.selectedOption = selectedOption 41 | } 42 | 43 | typealias Value = T 44 | typealias Cell = UITableViewCell 45 | 46 | } 47 | 48 | class EventOptionsViewController: TableViewController { 49 | 50 | let selectedOption: T? 51 | let optionSelected = PublishSubject() 52 | 53 | init(selectedOption: T?) { 54 | self.selectedOption = selectedOption 55 | super.init(nibName: nil, bundle: nil) 56 | self.title = T.title 57 | } 58 | 59 | required init?(coder aDecoder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | 66 | let selectedOption = self.selectedOption 67 | 68 | let providers = T.allOptions.map { EventOptionsProvider(options: $0, selectedOption: selectedOption) } 69 | 70 | for provider in providers { 71 | provider.event.modelSelected.asObservable() 72 | .subscribe(onNext: { [weak self] (option) in 73 | guard let `self` = self else { return } 74 | self.optionSelected.onNext(option) 75 | self.optionSelected.onCompleted() 76 | self.navigationController?.popViewController(animated: true) 77 | }) 78 | .disposed(by: disposeBag) 79 | } 80 | 81 | let sectionProviders = providers 82 | .map { SpacingSectionProvider(providers: [$0], headerHeight: 18, footerHeight: 18) } 83 | 84 | self.tableView.flix.build(sectionProviders) 85 | } 86 | 87 | override func viewWillDisappear(_ animated: Bool) { 88 | super.viewDidDisappear(animated) 89 | self.optionSelected.onCompleted() 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Repeat/EndRepeatProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EndRepeatProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class EndRepeatProvider: TitleDescProvider { 15 | 16 | let endRepeatDate: BehaviorRelay 17 | 18 | required init(viewController: UIViewController, minEndDate: Observable, endRepeatDate: Date?) { 19 | self.endRepeatDate = BehaviorRelay(value: endRepeatDate) 20 | super.init() 21 | self.titleLabel.text = "End Repeat" 22 | self.descLabel.textColor = UIColor(named: "CommentText") 23 | 24 | self.endRepeatDate.asObservable() 25 | .map { date -> String in 26 | if let date = date { 27 | let dateformatter = DateFormatter() 28 | dateformatter.dateFormat = "EEE, MMM d, y" 29 | return dateformatter.string(from: date) 30 | } else { 31 | return "Never" 32 | } 33 | } 34 | .bind(to: self.descLabel.rx.text) 35 | .disposed(by: disposeBag) 36 | 37 | self.event.selectedEvent.asObservable() 38 | .subscribe(onNext: { [weak viewController, weak self] in 39 | guard let `self` = self else { return } 40 | let endRepeatSelectViewController = EndRepeatSelectViewController(endRepeatDate: self.endRepeatDate, minEndDate: minEndDate) 41 | viewController?.show(endRepeatSelectViewController, sender: nil) 42 | }) 43 | .disposed(by: disposeBag) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Repeat/EndRepeatSelectViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EndRepeatSelectViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class EndRepeatSelectViewController: TableViewController { 15 | 16 | let endRepeatDate: BehaviorRelay 17 | let minEndDate: Observable 18 | 19 | let neverProvider = TitleDescProvider() 20 | let onDateProvider = TitleDescProvider() 21 | let datePickerProvider: DatePickerProvider 22 | 23 | init(endRepeatDate: BehaviorRelay, minEndDate: Observable) { 24 | self.endRepeatDate = endRepeatDate 25 | self.minEndDate = minEndDate 26 | self.datePickerProvider = DatePickerProvider(date: endRepeatDate.value) 27 | super.init(nibName: nil, bundle: nil) 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | title = "End Repeat" 37 | 38 | neverProvider.titleLabel.text = "Never" 39 | onDateProvider.titleLabel.text = "On Date" 40 | 41 | minEndDate 42 | .subscribe(onNext: { [weak self] (date) in 43 | self?.datePickerProvider.datePicker.minimumDate = date 44 | }) 45 | .disposed(by: disposeBag) 46 | 47 | self.datePickerProvider.datePicker.rx.date.changed 48 | .bind(to: self.endRepeatDate) 49 | .disposed(by: disposeBag) 50 | 51 | let isNever = self.endRepeatDate.asObservable().map { $0 == nil } 52 | 53 | isNever.map { $0 ? UITableViewCell.AccessoryType.checkmark : UITableViewCell.AccessoryType.none } 54 | .subscribe(onNext: { [weak self] (accessoryType) in 55 | self?.neverProvider.accessoryType = accessoryType 56 | }) 57 | .disposed(by: disposeBag) 58 | 59 | isNever 60 | .subscribe(onNext: { [weak self] (isNever) in 61 | self?.onDateProvider.accessoryType = isNever ? UITableViewCell.AccessoryType.none : UITableViewCell.AccessoryType.checkmark 62 | self?.onDateProvider.titleLabel.textColor = isNever ? UIColor.darkText : UIColor(named: "Deep Carmine Pink") 63 | }) 64 | .disposed(by: disposeBag) 65 | 66 | isNever.bind(to: self.datePickerProvider.rx.isHidden).disposed(by: disposeBag) 67 | 68 | self.neverProvider.event.selectedEvent.map { nil as Date? }.bind(to: self.endRepeatDate).disposed(by: disposeBag) 69 | 70 | self.onDateProvider.event.selectedEvent 71 | .withLatestFrom(self.datePickerProvider.datePicker.rx.date.asObservable()) 72 | .bind(to: self.endRepeatDate) 73 | .disposed(by: disposeBag) 74 | 75 | self.tableView.flix.animatable.build([ 76 | SpacingSectionProvider(providers: [neverProvider, onDateProvider, datePickerProvider], headerHeight: 18, footerHeight: 18) 77 | ]) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Example/Example/CalendarEvent/Repeat/RepeatGroupProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepeatGroupProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | enum RepeatOption: String, EventOptionType { 15 | 16 | static var allOptions: [[RepeatOption]] { 17 | return [[.never, .everyDay, .everyWeek, .every2Weeks, .everyMonth, .everyYear]] 18 | } 19 | 20 | static var title: String { 21 | return "Repeat" 22 | } 23 | 24 | case never = "Never" 25 | case everyDay = "Every Day" 26 | case everyWeek = "Every Week" 27 | case every2Weeks = "Every 2 Weeks" 28 | case everyMonth = "Every Month" 29 | case everyYear = "Every Year" 30 | 31 | var name: String { 32 | return self.rawValue 33 | } 34 | 35 | var identity: String { 36 | return self.rawValue 37 | } 38 | 39 | static func createProvider(viewController: UIViewController, selected: RepeatOption) -> EventOptionProvider { 40 | return EventOptionProvider(viewController: viewController, selectedOption: selected) 41 | } 42 | 43 | } 44 | 45 | class RepeatGroupProvider: AnimatableTableViewGroupProvider { 46 | 47 | var providers: [_AnimatableTableViewMultiNodeProvider] { 48 | return [self.repeatProvider, self.endRepeatProvider] 49 | } 50 | 51 | let repeatProvider: EventOptionProvider 52 | let endRepeatProvider: EndRepeatProvider 53 | 54 | init(viewController: UIViewController, minEndDate: Observable, selectedRepeat: RepeatOption?, endRepeatDate: Date?) { 55 | self.repeatProvider = RepeatOption.createProvider(viewController: viewController, selected: selectedRepeat ?? RepeatOption.never) 56 | self.endRepeatProvider = EndRepeatProvider(viewController: viewController, minEndDate: minEndDate, endRepeatDate: endRepeatDate) 57 | } 58 | 59 | func createAnimatableProviders() -> Observable<[_AnimatableTableViewMultiNodeProvider]> { 60 | return self.repeatProvider.selectedOption.asObservable().map { $0 == .never }.distinctUntilChanged() 61 | .map { [weak self] isNever in 62 | guard let `self` = self else { return [] } 63 | return isNever ? [self.repeatProvider] : [self.repeatProvider, self.endRepeatProvider] 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Example/Example/DoNotDisturbSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoNotDisturbSettingsViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | import Flix 14 | 15 | class DoNotDisturbSettingsViewController: CollectionViewController { 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | title = "Do Not Disturb" 21 | 22 | var providers: [_AnimatableCollectionViewMultiNodeProvider] = [] 23 | 24 | let doNotDisturbProvider = UniqueSwitchProvider() 25 | doNotDisturbProvider.titleLabel.text = "Do Not Disturb" 26 | providers.append(doNotDisturbProvider) 27 | 28 | let doNotDisturbCommnetProvider = UniqueCommentTextProvider( 29 | text: "When Do Not Disturb is enabled, calls and alerts that arrive while locked will be silenced, and a moon icon will appear in the status bar." 30 | ) 31 | providers.append(doNotDisturbCommnetProvider) 32 | 33 | let scheduledProvider = UniqueSwitchProvider() 34 | scheduledProvider.titleLabel.text = "Scheduled" 35 | providers.append(scheduledProvider) 36 | 37 | let slienceTitleProvider = UniqueCommentTextProvider( 38 | text: "SLIENCE" 39 | ) 40 | providers.append(slienceTitleProvider) 41 | 42 | enum SlienceMode: String, StringIdentifiableType, Equatable, CustomStringConvertible { 43 | case always 44 | case whileLocked 45 | 46 | var identity: String { 47 | return self.rawValue 48 | } 49 | 50 | var description: String { 51 | switch self { 52 | case .always: 53 | return "Always" 54 | case .whileLocked: 55 | return "While iPhone is locked" 56 | } 57 | } 58 | 59 | var comment: String { 60 | switch self { 61 | case .always: 62 | return "Incoming calls and notifications will be silenced while iPhone is either locked or unlocked." 63 | case .whileLocked: 64 | return "Incoming calls and notifications will be silenced while iPhone is locked." 65 | } 66 | } 67 | } 68 | 69 | let radioProvider = RadioProvider(options: [SlienceMode.always, SlienceMode.whileLocked]) 70 | radioProvider.checkedOption.accept(SlienceMode.always) 71 | providers.append(radioProvider) 72 | 73 | let slienceCommentProvider = UniqueCommentTextProvider( 74 | text: "" 75 | ) 76 | radioProvider.checkedOption.asObservable() 77 | .map { (option) -> String in 78 | return option?.comment ?? "" 79 | } 80 | .bind(to: slienceCommentProvider.text) 81 | .disposed(by: disposeBag) 82 | providers.append(slienceCommentProvider) 83 | 84 | let allowCallsFromTitleProvider = UniqueCommentTextProvider( 85 | text: "PHONE" 86 | ) 87 | providers.append(allowCallsFromTitleProvider) 88 | let allowCallsFromProvider = UniqueTextProvider( 89 | title: "Allow Calls From", 90 | desc: "Everyone" 91 | ) 92 | providers.append(allowCallsFromProvider) 93 | let allowCallsFromCommentProvider = UniqueCommentTextProvider( 94 | text: "When in Do Not Disturb, allow incoming calls from everyone." 95 | ) 96 | providers.append(allowCallsFromCommentProvider) 97 | 98 | self.collectionView.flix.animatable.build(providers) 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 10/18/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | private struct ActivityToken : ObservableConvertibleType, Disposable { 13 | private let _source: Observable 14 | private let _dispose: Cancelable 15 | 16 | init(source: Observable, disposeAction: @escaping () -> ()) { 17 | _source = source 18 | _dispose = Disposables.create(with: disposeAction) 19 | } 20 | 21 | func dispose() { 22 | _dispose.dispose() 23 | } 24 | 25 | func asObservable() -> Observable { 26 | return _source 27 | } 28 | } 29 | 30 | /** 31 | Enables monitoring of sequence computation. 32 | 33 | If there is at least one sequence computation in progress, `true` will be sent. 34 | When all activities complete `false` will be sent. 35 | */ 36 | public class ActivityIndicator : SharedSequenceConvertibleType { 37 | public typealias Element = Bool 38 | public typealias SharingStrategy = DriverSharingStrategy 39 | 40 | private let _lock = NSRecursiveLock() 41 | private let _variable = BehaviorRelay(value: 0) 42 | private let _loading: SharedSequence 43 | 44 | public init() { 45 | _loading = _variable.asDriver() 46 | .map { $0 > 0 } 47 | .distinctUntilChanged() 48 | } 49 | 50 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 51 | return Observable.using({ () -> ActivityToken in 52 | self.increment() 53 | return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) 54 | }) { t in 55 | return t.asObservable() 56 | } 57 | } 58 | 59 | private func increment() { 60 | _lock.lock() 61 | _variable.accept(_variable.value + 1) 62 | _lock.unlock() 63 | } 64 | 65 | private func decrement() { 66 | _lock.lock() 67 | _variable.accept(_variable.value - 1) 68 | _lock.unlock() 69 | } 70 | 71 | public func asSharedSequence() -> SharedSequence { 72 | return _loading 73 | } 74 | } 75 | 76 | extension ObservableConvertibleType { 77 | public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { 78 | return activityIndicator.trackActivityOfObservable(self) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/DefaultImplementations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultImplementations.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 12/6/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if !RX_NO_MODULE 10 | import RxSwift 11 | #endif 12 | 13 | import struct Foundation.CharacterSet 14 | import struct Foundation.URL 15 | import struct Foundation.URLRequest 16 | import struct Foundation.NSRange 17 | import class Foundation.URLSession 18 | import func Foundation.arc4random 19 | 20 | class GitHubDefaultValidationService: GitHubValidationService { 21 | let API: GitHubAPI 22 | 23 | static let sharedValidationService = GitHubDefaultValidationService(API: GitHubDefaultAPI.sharedAPI) 24 | 25 | init (API: GitHubAPI) { 26 | self.API = API 27 | } 28 | 29 | // validation 30 | 31 | let minPasswordCount = 5 32 | 33 | func validateUsername(_ username: String) -> Observable { 34 | if username.count == 0 { 35 | return .just(.empty) 36 | } 37 | 38 | 39 | // this obviously won't be 40 | if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil { 41 | return .just(.failed(message: "Username can only contain numbers or digits")) 42 | } 43 | 44 | let loadingValue = ValidationResult.validating 45 | 46 | return API 47 | .usernameAvailable(username) 48 | .map { available in 49 | if available { 50 | return .ok(message: "Username available") 51 | } 52 | else { 53 | return .failed(message: "Username already taken") 54 | } 55 | } 56 | .startWith(loadingValue) 57 | } 58 | 59 | func validatePassword(_ password: String) -> ValidationResult { 60 | let numberOfCharacters = password.count 61 | if numberOfCharacters == 0 { 62 | return .empty 63 | } 64 | 65 | if numberOfCharacters < minPasswordCount { 66 | return .failed(message: "Password must be at least \(minPasswordCount) characters") 67 | } 68 | 69 | return .ok(message: "Password acceptable") 70 | } 71 | 72 | func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult { 73 | if repeatedPassword.count == 0 { 74 | return .empty 75 | } 76 | 77 | if repeatedPassword == password { 78 | return .ok(message: "Password repeated") 79 | } 80 | else { 81 | return .failed(message: "Password different") 82 | } 83 | } 84 | } 85 | 86 | 87 | class GitHubDefaultAPI : GitHubAPI { 88 | let URLSession: Foundation.URLSession 89 | 90 | static let sharedAPI = GitHubDefaultAPI( 91 | URLSession: Foundation.URLSession.shared 92 | ) 93 | 94 | init(URLSession: Foundation.URLSession) { 95 | self.URLSession = URLSession 96 | } 97 | 98 | func usernameAvailable(_ username: String) -> Observable { 99 | // this is ofc just mock, but good enough 100 | 101 | let url = URL(string: "https://github.com/\(username.URLEscaped)")! 102 | let request = URLRequest(url: url) 103 | return self.URLSession.rx.response(request: request) 104 | .map { pair in 105 | return pair.response.statusCode == 404 106 | } 107 | .catchErrorJustReturn(false) 108 | } 109 | 110 | func signUp(_ username: String, password: String) -> Observable { 111 | // this is also just a mock 112 | let signUpResult = arc4random() % 5 == 0 ? false : true 113 | 114 | return Observable.just(signUpResult) 115 | .delay(.seconds(1), scheduler: MainScheduler.instance) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/GithubSignupViewModel1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubSignupViewModel1.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 12/6/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if !RX_NO_MODULE 10 | import RxSwift 11 | import RxCocoa 12 | #endif 13 | 14 | /** 15 | This is example where view model is mutable. Some consider this to be MVVM, some consider this to be Presenter, 16 | or some other name. 17 | In the end, it doesn't matter. 18 | 19 | If you want to take a look at example using "immutable VMs", take a look at `TableViewWithEditingCommands` example. 20 | 21 | This uses vanilla rx observable sequences. 22 | 23 | Please note that there is no explicit state, outputs are defined using inputs and dependencies. 24 | Please note that there is no dispose bag, because no subscription is being made. 25 | */ 26 | class GithubSignupViewModel1 { 27 | // outputs { 28 | 29 | let validatedUsername: Observable 30 | let validatedPassword: Observable 31 | let validatedPasswordRepeated: Observable 32 | 33 | // Is signUp button enabled 34 | let signUpEnabled: Observable 35 | 36 | // Has user signed in 37 | let signedIn: Observable 38 | 39 | // Is signing process in progress 40 | let signingIn: Observable 41 | 42 | // } 43 | 44 | init(input: ( 45 | username: Observable, 46 | password: Observable, 47 | repeatedPassword: Observable, 48 | loginTaps: Observable 49 | ), 50 | dependency: ( 51 | API: GitHubAPI, 52 | validationService: GitHubValidationService, 53 | wireframe: Wireframe 54 | ) 55 | ) { 56 | let API = dependency.API 57 | let validationService = dependency.validationService 58 | let wireframe = dependency.wireframe 59 | 60 | /** 61 | Notice how no subscribe call is being made. 62 | Everything is just a definition. 63 | 64 | Pure transformation of input sequences to output sequences. 65 | */ 66 | 67 | validatedUsername = input.username 68 | .flatMapLatest { username in 69 | return validationService.validateUsername(username) 70 | .observeOn(MainScheduler.instance) 71 | .catchErrorJustReturn(.failed(message: "Error contacting server")) 72 | } 73 | .share(replay: 1) 74 | 75 | validatedPassword = input.password 76 | .map { password in 77 | return validationService.validatePassword(password) 78 | } 79 | .share(replay: 1) 80 | 81 | validatedPasswordRepeated = Observable.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword) 82 | .share(replay: 1) 83 | 84 | let signingIn = ActivityIndicator() 85 | self.signingIn = signingIn.asObservable() 86 | 87 | let usernameAndPassword = Observable.combineLatest(input.username, input.password) { (username: $0, password: $1) } 88 | 89 | signedIn = input.loginTaps.withLatestFrom(usernameAndPassword) 90 | .flatMapLatest { pair in 91 | return API.signUp(pair.username, password: pair.password) 92 | .observeOn(MainScheduler.instance) 93 | .catchErrorJustReturn(false) 94 | .trackActivity(signingIn) 95 | } 96 | .flatMapLatest { loggedIn -> Observable in 97 | let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed" 98 | return wireframe.promptFor(message, cancelAction: "OK", actions: []) 99 | // propagate original value 100 | .map { _ in 101 | loggedIn 102 | } 103 | } 104 | .share(replay: 1) 105 | 106 | signUpEnabled = Observable.combineLatest( 107 | validatedUsername, 108 | validatedPassword, 109 | validatedPasswordRepeated, 110 | signingIn.asObservable() 111 | ) { username, password, repeatPassword, signingIn in 112 | username.isValid && 113 | password.isValid && 114 | repeatPassword.isValid && 115 | !signingIn 116 | } 117 | .distinctUntilChanged() 118 | .share(replay: 1) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Operators.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 12/6/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if !RX_NO_MODULE 10 | import RxSwift 11 | import RxCocoa 12 | #endif 13 | 14 | import UIKit 15 | 16 | // Two way binding operator between control property and variable, that's all it takes { 17 | 18 | infix operator <-> : DefaultPrecedence 19 | 20 | func nonMarkedText(_ textInput: UITextInput) -> String? { 21 | let start = textInput.beginningOfDocument 22 | let end = textInput.endOfDocument 23 | 24 | guard let rangeAll = textInput.textRange(from: start, to: end), 25 | let text = textInput.text(in: rangeAll) else { 26 | return nil 27 | } 28 | 29 | guard let markedTextRange = textInput.markedTextRange else { 30 | return text 31 | } 32 | 33 | guard let startRange = textInput.textRange(from: start, to: markedTextRange.start), 34 | let endRange = textInput.textRange(from: markedTextRange.end, to: end) else { 35 | return text 36 | } 37 | 38 | return (textInput.text(in: startRange) ?? "") + (textInput.text(in: endRange) ?? "") 39 | } 40 | 41 | func <-> (textInput: TextInput, variable: BehaviorRelay) -> Disposable { 42 | let bindToUIDisposable = variable.asObservable() 43 | .bind(to: textInput.text) 44 | let bindToBehaviorRelay = textInput.text 45 | .subscribe(onNext: { [weak base = textInput.base] n in 46 | guard let base = base else { 47 | return 48 | } 49 | 50 | let nonMarkedTextValue = nonMarkedText(base) 51 | 52 | /** 53 | In some cases `textInput.textRangeFromPosition(start, toPosition: end)` will return nil even though the underlying 54 | value is not nil. This appears to be an Apple bug. If it's not, and we are doing something wrong, please let us know. 55 | The can be reproed easily if replace bottom code with 56 | 57 | if nonMarkedTextValue != variable.value { 58 | variable.value = nonMarkedTextValue ?? "" 59 | } 60 | 61 | and you hit "Done" button on keyboard. 62 | */ 63 | if let nonMarkedTextValue = nonMarkedTextValue, nonMarkedTextValue != variable.value { 64 | variable.accept(nonMarkedTextValue) 65 | } 66 | }, onCompleted: { 67 | bindToUIDisposable.dispose() 68 | }) 69 | 70 | return Disposables.create(bindToUIDisposable, bindToBehaviorRelay) 71 | } 72 | 73 | func <-> (property: ControlProperty, variable: BehaviorRelay) -> Disposable { 74 | if T.self == String.self { 75 | #if DEBUG 76 | fatalError("It is ok to delete this message, but this is here to warn that you are maybe trying to bind to some `rx.text` property directly to variable.\n" + 77 | "That will usually work ok, but for some languages that use IME, that simplistic method could cause unexpected issues because it will return intermediate results while text is being inputed.\n" + 78 | "REMEDY: Just use `textField <-> variable` instead of `textField.rx.text <-> variable`.\n" + 79 | "Find out more here: https://github.com/ReactiveX/RxSwift/issues/649\n" 80 | ) 81 | #endif 82 | } 83 | 84 | let bindToUIDisposable = variable.asObservable() 85 | .bind(to: property) 86 | let bindToBehaviorRelay = property 87 | .subscribe(onNext: { n in 88 | variable.accept(n) 89 | }, onCompleted: { 90 | bindToUIDisposable.dispose() 91 | }) 92 | 93 | return Disposables.create(bindToUIDisposable, bindToBehaviorRelay) 94 | } 95 | 96 | // } 97 | 98 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/Protocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 12/6/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxCocoa 11 | 12 | enum ValidationResult { 13 | case ok(message: String) 14 | case empty 15 | case validating 16 | case failed(message: String) 17 | } 18 | 19 | enum SignupState { 20 | case signedUp(signedUp: Bool) 21 | } 22 | 23 | protocol GitHubAPI { 24 | func usernameAvailable(_ username: String) -> Observable 25 | func signUp(_ username: String, password: String) -> Observable 26 | } 27 | 28 | protocol GitHubValidationService { 29 | func validateUsername(_ username: String) -> Observable 30 | func validatePassword(_ password: String) -> ValidationResult 31 | func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult 32 | } 33 | 34 | extension ValidationResult { 35 | var isValid: Bool { 36 | switch self { 37 | case .ok: 38 | return true 39 | default: 40 | return false 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/String+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+URL.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 12/28/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | 10 | extension String { 11 | var URLEscaped: String { 12 | return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Example/Example/GitHubSigup/Wireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Wireframe.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 4/3/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if !RX_NO_MODULE 10 | import RxSwift 11 | #endif 12 | 13 | #if os(iOS) 14 | import UIKit 15 | #elseif os(macOS) 16 | import Cocoa 17 | #endif 18 | 19 | enum RetryResult { 20 | case retry 21 | case cancel 22 | } 23 | 24 | protocol Wireframe { 25 | func open(url: URL) 26 | func promptFor(_ message: String, cancelAction: Action, actions: [Action]) -> Observable 27 | } 28 | 29 | 30 | class DefaultWireframe: Wireframe { 31 | static let shared = DefaultWireframe() 32 | 33 | func open(url: URL) { 34 | UIApplication.shared.open(url, options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), completionHandler: nil) 35 | } 36 | 37 | #if os(iOS) 38 | private static func rootViewController() -> UIViewController { 39 | // cheating, I know 40 | return UIApplication.shared.keyWindow!.rootViewController! 41 | } 42 | #endif 43 | 44 | static func presentAlert(_ message: String) { 45 | #if os(iOS) 46 | let alertView = UIAlertController(title: "RxExample", message: message, preferredStyle: .alert) 47 | alertView.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in 48 | }) 49 | rootViewController().present(alertView, animated: true, completion: nil) 50 | #endif 51 | } 52 | 53 | func promptFor(_ message: String, cancelAction: Action, actions: [Action]) -> Observable { 54 | #if os(iOS) 55 | return Observable.create { observer in 56 | let alertView = UIAlertController(title: "RxExample", message: message, preferredStyle: .alert) 57 | alertView.addAction(UIAlertAction(title: cancelAction.description, style: .cancel) { _ in 58 | observer.on(.next(cancelAction)) 59 | }) 60 | 61 | for action in actions { 62 | alertView.addAction(UIAlertAction(title: action.description, style: .default) { _ in 63 | observer.on(.next(action)) 64 | }) 65 | } 66 | 67 | DefaultWireframe.rootViewController().present(alertView, animated: true, completion: nil) 68 | 69 | return Disposables.create { 70 | alertView.dismiss(animated:false, completion: nil) 71 | } 72 | } 73 | #elseif os(macOS) 74 | return Observable.error(NSError(domain: "Unimplemented", code: -1, userInfo: nil)) 75 | #endif 76 | } 77 | } 78 | 79 | 80 | extension RetryResult : CustomStringConvertible { 81 | var description: String { 82 | switch self { 83 | case .retry: 84 | return "Retry" 85 | case .cancel: 86 | return "Cancel" 87 | } 88 | } 89 | } 90 | 91 | // Helper function inserted by Swift 4.2 migrator. 92 | fileprivate func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) -> [UIApplication.OpenExternalURLOptionsKey: Any] { 93 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value)}) 94 | } 95 | -------------------------------------------------------------------------------- /Example/Example/MoveCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveCollectionViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 14/11/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | extension UIColor: StringIdentifiableType { 15 | 16 | public var identity: String { 17 | return self.description 18 | } 19 | 20 | } 21 | 22 | class MoveCollectionViewProvider: AnimatableCollectionViewProvider, CollectionViewMoveable { 23 | 24 | func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndex: Int, to destinationIndex: Int, value: UIColor) { 25 | var result = colors.value 26 | result.remove(at: sourceIndex) 27 | result.insert(value, at: destinationIndex) 28 | colors.accept(result) 29 | } 30 | 31 | func configureCell(_ collectionView: UICollectionView, cell: UICollectionViewCell, indexPath: IndexPath, value: UIColor) { 32 | cell.backgroundColor = value 33 | } 34 | 35 | func createValues() -> Observable<[UIColor]> { 36 | return colors.asObservable() 37 | } 38 | 39 | typealias Value = UIColor 40 | typealias Cell = UICollectionViewCell 41 | 42 | let colors = BehaviorRelay(value: [UIColor.black, UIColor.blue, UIColor.red, UIColor.green, UIColor.yellow, UIColor.purple]) 43 | 44 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: UIColor) -> CGSize? { 45 | return CGSize(width: 80, height: 80) 46 | } 47 | 48 | } 49 | 50 | class MoveCollectionViewController: CollectionViewController { 51 | 52 | let moveCollectionViewProvider = MoveCollectionViewProvider() 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | title = "Move" 57 | 58 | let long = UILongPressGestureRecognizer() 59 | long.rx.event 60 | .subscribe(onNext: { [unowned collectionView] gesture in 61 | switch gesture.state { 62 | case .began: 63 | guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) 64 | , let canMoveItemAtIndexPath = collectionView.dataSource?.collectionView?(collectionView, canMoveItemAt: selectedIndexPath) 65 | , canMoveItemAtIndexPath else { 66 | return 67 | } 68 | collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) 69 | case .changed: 70 | collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view)) 71 | case .ended: 72 | collectionView.endInteractiveMovement() 73 | case .cancelled, .failed, .possible: 74 | collectionView.cancelInteractiveMovement() 75 | @unknown default: 76 | break; 77 | } 78 | }) 79 | .disposed(by: disposeBag) 80 | self.collectionView.addGestureRecognizer(long) 81 | 82 | self.collectionView.flix.animatable.build([moveCollectionViewProvider]) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Example/Example/StoryboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 23/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Flix 11 | 12 | class StoryboardViewController: UIViewController { 13 | 14 | @IBOutlet weak var logoProvider: FlixStackItemProvider! 15 | 16 | @IBAction func flixLogoClicked(_ sender: FlixStackItemProvider) { 17 | let alert = UIAlertController(title: nil, message: "Logo Clicked", preferredStyle: .alert) 18 | alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) 19 | self.present(alert, animated: true, completion: nil) 20 | } 21 | 22 | @IBAction func flixContentClicked(_ sender: FlixStackItemProvider) { 23 | let alert = UIAlertController(title: nil, message: "Content Clicked", preferredStyle: .alert) 24 | alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) 25 | self.present(alert, animated: true, completion: nil) 26 | } 27 | 28 | @IBAction func contentSwitchChanged(_ sender: UISwitch) { 29 | logoProvider.isHidden = !sender.isOn 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Example/Example/Tutorial/AppsProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppsProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 04/11/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import Flix 12 | 13 | class AppTableViewCell: UITableViewCell { 14 | 15 | lazy var titleLabel: UILabel = { 16 | let label = UILabel() 17 | label.font = UIFont.systemFont(ofSize: 17) 18 | return label 19 | }() 20 | 21 | lazy var iconImageView = UIImageView() 22 | 23 | private let leftStackView: UIStackView = { 24 | let stackView = UIStackView() 25 | stackView.axis = .horizontal 26 | stackView.spacing = 15 27 | stackView.alignment = .center 28 | return stackView 29 | }() 30 | 31 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 32 | super.init(style: style, reuseIdentifier: reuseIdentifier) 33 | leftStackView.addArrangedSubview(iconImageView) 34 | leftStackView.addArrangedSubview(titleLabel) 35 | 36 | self.accessoryType = .disclosureIndicator 37 | 38 | self.separatorInset = UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 0) 39 | self.contentView.addSubview(leftStackView) 40 | leftStackView.translatesAutoresizingMaskIntoConstraints = false 41 | leftStackView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 16).isActive = true 42 | leftStackView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | } 50 | 51 | struct App: StringIdentifiableType, Equatable { 52 | 53 | var identity: String { 54 | return self.title 55 | } 56 | 57 | static func ==(lhs: App, rhs: App) -> Bool { 58 | return lhs.title == rhs.title 59 | } 60 | 61 | let icon: UIImage 62 | let title: String 63 | 64 | } 65 | 66 | class AppsProvider: AnimatableTableViewProvider { 67 | 68 | typealias Cell = AppTableViewCell 69 | typealias Value = App 70 | 71 | let apps: [App] 72 | 73 | init(apps: [App]) { 74 | self.apps = apps 75 | } 76 | 77 | func configureCell(_ tableView: UITableView, cell: AppTableViewCell, indexPath: IndexPath, value: App) { 78 | cell.iconImageView.image = value.icon 79 | cell.titleLabel.text = value.title 80 | } 81 | 82 | func createValues() -> Observable<[App]> { 83 | return Observable.just(apps) 84 | } 85 | 86 | func itemSelected(_ tableView: UITableView, indexPath: IndexPath, value: App) { 87 | tableView.deselectRow(at: indexPath, animated: true) 88 | } 89 | 90 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath, value: App) -> CGFloat? { 91 | return 44 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Example/Example/Tutorial/ProfileProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 04/11/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Flix 11 | 12 | class ProfileProvider: SingleUITableViewCellProvider { 13 | 14 | let avatarImageView = UIImageView() 15 | let nameLabel = UILabel() 16 | let subTitleLabel = UILabel() 17 | 18 | init(avatar: UIImage, name: String) { 19 | super.init() 20 | avatarImageView.image = avatar 21 | nameLabel.text = name 22 | 23 | nameLabel.font = UIFont.systemFont(ofSize: 22) 24 | 25 | subTitleLabel.font = UIFont.systemFont(ofSize: 13) 26 | subTitleLabel.text = "Apple ID, iCloud, iTunes & App Store" 27 | 28 | self.accessoryType = .disclosureIndicator 29 | self.contentView.addSubview(avatarImageView) 30 | avatarImageView.translatesAutoresizingMaskIntoConstraints = false 31 | avatarImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 15).isActive = true 32 | avatarImageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true 33 | avatarImageView.widthAnchor.constraint(equalToConstant: 60).isActive = true 34 | avatarImageView.heightAnchor.constraint(equalToConstant: 60).isActive = true 35 | 36 | self.contentView.addSubview(nameLabel) 37 | nameLabel.translatesAutoresizingMaskIntoConstraints = false 38 | nameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 15).isActive = true 39 | nameLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 14).isActive = true 40 | 41 | self.contentView.addSubview(subTitleLabel) 42 | subTitleLabel.translatesAutoresizingMaskIntoConstraints = false 43 | subTitleLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true 44 | subTitleLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -17).isActive = true 45 | 46 | itemHeight = { _ in 80 } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Example/Example/Tutorial/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 02/11/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class SettingsViewController: TableViewController { 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | navigationItem.largeTitleDisplayMode = .automatic 20 | 21 | title = "Settings" 22 | 23 | let profileProvider = ProfileProvider(avatar: #imageLiteral(resourceName: "Flix Icon"), name: "Flix") 24 | let profileSectionProvider = SpacingSectionProvider(providers: [profileProvider], headerHeight: 35, footerHeight: 0) 25 | 26 | let airplaneModeProvider = SwitchTableViewCellProvider(title: "Airplane Mode", icon: #imageLiteral(resourceName: "Airplane Icon"), isOn: false) 27 | let wifiProvider = DescriptionTableViewCellProvider(title: "Wi-Fi", icon: #imageLiteral(resourceName: "Wifi Icon"), description: "Flix_5G") 28 | let bluetoothProvider = DescriptionTableViewCellProvider(title: "Bluetooth", icon: #imageLiteral(resourceName: "Bluetooth Icon"), description: "On") 29 | let cellularProvider = DescriptionTableViewCellProvider(title: "Cellular", icon: #imageLiteral(resourceName: "Cellular Icon")) 30 | let personalHotspotProvider = DescriptionTableViewCellProvider(title: "Personal Hotspot", icon: #imageLiteral(resourceName: "Personal Hotspot Icon"), description: "Off") 31 | let carrierProvider = DescriptionTableViewCellProvider(title: "Carrier", icon: #imageLiteral(resourceName: "Carrier Icon"), description: "AT&T") 32 | 33 | let networkSectionProvider = SpacingSectionProvider( 34 | providers: [ 35 | airplaneModeProvider, 36 | wifiProvider, 37 | bluetoothProvider, 38 | cellularProvider, 39 | personalHotspotProvider, 40 | carrierProvider 41 | ], 42 | headerHeight: 35, 43 | footerHeight: 0 44 | ) 45 | 46 | let appSectionProvider = SpacingSectionProvider( 47 | providers: [AppsProvider(apps: [ 48 | App(icon: #imageLiteral(resourceName: "Wallet App Icon"), title: "Wallet"), 49 | App(icon: #imageLiteral(resourceName: "Music App Icon"), title: "Music"), 50 | App(icon: #imageLiteral(resourceName: "Safari App Icon"), title: "Safari"), 51 | App(icon: #imageLiteral(resourceName: "News App Icon"), title: "News"), 52 | App(icon: #imageLiteral(resourceName: "Camera App Icon"), title: "Camera"), 53 | App(icon: #imageLiteral(resourceName: "Photos App Icon"), title: "Photo") 54 | ])], 55 | headerHeight: 35, 56 | footerHeight: 35 57 | ) 58 | 59 | self.tableView.flix.build([profileSectionProvider, networkSectionProvider, appSectionProvider]) 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Example/ExampleListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleListViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | import Flix 14 | 15 | class ExampleListViewController: CollectionViewController { 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | navigationItem.largeTitleDisplayMode = .always 21 | 22 | typealias UIViewControllerCreater = () -> UIViewController 23 | typealias Model = TextListProviderModel 24 | 25 | let iconProvider = SingleUICollectionViewCellProvider() 26 | let iconImageView = UIImageView(image: #imageLiteral(resourceName: "Flix Icon")) 27 | iconProvider.backgroundView = UIView() 28 | iconProvider.backgroundView?.backgroundColor = UIColor.white 29 | iconProvider.selectedBackgroundView = UIView() 30 | iconProvider.selectedBackgroundView?.backgroundColor = UIColor.lightGray.withAlphaComponent(0.6) 31 | iconProvider.itemSize = { [unowned self] in 32 | return CGSize(width: self.collectionView.bounds.width, height: 180) 33 | } 34 | iconProvider.contentView.addSubview(iconImageView) 35 | iconImageView.translatesAutoresizingMaskIntoConstraints = false 36 | iconImageView.centerXAnchor.constraint(equalTo: iconProvider.contentView.centerXAnchor).isActive = true 37 | iconImageView.centerYAnchor.constraint(equalTo: iconProvider.contentView.centerYAnchor).isActive = true 38 | 39 | iconProvider.event.itemSelected 40 | .subscribe(onNext: { _ in 41 | UIApplication.shared.open(URL(string: "https://github.com/DianQK/Flix")!, options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), completionHandler: nil) 42 | }) 43 | .disposed(by: disposeBag) 44 | 45 | let textListProvider = TextListProvider( 46 | items: [ 47 | Model(title: "Settings", desc: "", value: { return SettingsViewController() }), 48 | Model(title: "All Events", desc: "", value: { return EventListViewController() }), 49 | Model(title: "Do Not Disturb", desc: "", value: { return DoNotDisturbSettingsViewController() }), 50 | Model(title: "Login", desc: "", value: { return LoginViewController() }), 51 | Model(title: "GitHub Signup", desc: "", value: { return GitHubSignupViewController() }), 52 | Model(title: "Nest Form", desc: "", value: { return NestFormViewController() }), 53 | Model(title: "Delete", desc: "", value: { return DeleteItemViewController() }), 54 | Model(title: "Control Center", desc: "", value: { return ControlCenterCustomizeViewController() }), 55 | Model(title: "Storyboard", desc: "", value: { return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "StoryboardViewController") }), 56 | Model(title: "Move", desc: "", value: { return MoveCollectionViewController() }), 57 | Model(title: "Empty", desc: "", value: { return EmptyViewController() }) 58 | ] 59 | ) 60 | textListProvider.event.itemSelected 61 | .subscribe(onNext: { [unowned self] (_, _, model) in 62 | self.show(model.value(), sender: nil) 63 | }) 64 | .disposed(by: disposeBag) 65 | 66 | self.collectionView.flix.animatable.build([iconProvider, textListProvider]) 67 | 68 | } 69 | 70 | } 71 | 72 | // Helper function inserted by Swift 4.2 migrator. 73 | fileprivate func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) -> [UIApplication.OpenExternalURLOptionsKey: Any] { 74 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value)}) 75 | } 76 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flix Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | NSLocationAlwaysAndWhenInUseUsageDescription 30 | We need location 31 | NSLocationWhenInUseUsageDescription 32 | We need location 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Example/Providers/RadioProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 02/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class RadioCollectionViewCell: UICollectionViewCell { 15 | 16 | let titleLabel = UILabel() 17 | let checkImageView = UIImageView(image: #imageLiteral(resourceName: "Checkmark")) 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | self.contentView.addSubview(titleLabel) 22 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 23 | titleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 15).isActive = true 24 | titleLabel.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: 0).isActive = true 25 | 26 | self.contentView.addSubview(checkImageView) 27 | checkImageView.translatesAutoresizingMaskIntoConstraints = false 28 | checkImageView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -15).isActive = true 29 | checkImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor, constant: 0).isActive = true 30 | checkImageView.isHidden = true 31 | 32 | self.backgroundColor = UIColor.white 33 | self.selectedBackgroundView = UIView() 34 | self.selectedBackgroundView?.backgroundColor = UIColor.lightGray.withAlphaComponent(0.6) 35 | } 36 | 37 | var reuseBag = DisposeBag() 38 | 39 | override func prepareForReuse() { 40 | super.prepareForReuse() 41 | reuseBag = DisposeBag() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | var isChecked: Binder { 49 | return Binder(self, binding: { (cell, isChecked) in 50 | cell.checkImageView.isHidden = !isChecked 51 | }) 52 | } 53 | 54 | } 55 | 56 | class RadioProvider: AnimatableCollectionViewProvider { 57 | 58 | let options: [Option] 59 | let checkedOption = BehaviorRelay(value: nil) 60 | let disposeBag = DisposeBag() 61 | 62 | typealias Cell = RadioCollectionViewCell 63 | typealias Value = Option 64 | 65 | init(options: [Option]) { 66 | self.options = options 67 | } 68 | 69 | func configureCell(_ collectionView: UICollectionView, cell: RadioCollectionViewCell, indexPath: IndexPath, value: Option) { 70 | cell.titleLabel.text = String(describing: value) 71 | checkedOption.asObservable() 72 | .map { $0 == value } 73 | .bind(to: cell.isChecked) 74 | .disposed(by: cell.reuseBag) 75 | } 76 | 77 | func itemSelected(_ collectionView: UICollectionView, indexPath: IndexPath, value: Option) { 78 | collectionView.deselectItem(at: indexPath, animated: true) 79 | checkedOption.accept(value) 80 | } 81 | 82 | func createValues() -> Observable<[Option]> { 83 | return Observable.just(options) 84 | } 85 | 86 | // workaround: Segmentation fault: 11 While emitting IR SIL function "@$s7Example13RadioProviderCyqd__G4Flix033AnimatableCollectionViewMultiNodeC0AaeFP06createE5Nodes7RxSwift10ObservableCySayAE012IdentifiableI0VGGyFTW". for 'createAnimatableNodes()' (in module 'Flix') 87 | func createAnimatableNodes() -> Observable<[IdentifiableNode]> { 88 | let providerIdentity = self._flix_identity 89 | return createValues() 90 | .map { $0.map { IdentifiableNode(providerIdentity: providerIdentity, valueNode: $0) } } 91 | } 92 | 93 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: Option) -> CGSize? { 94 | return CGSize(width: collectionView.bounds.width, height: 44) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Example/Providers/TextListProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextListProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 01/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class TextCollectionViewCell: UICollectionViewCell { 15 | 16 | let titleLabel = UILabel() 17 | let descLabel = UILabel() 18 | let disclosureIndicatorImageView = UIImageView(image: #imageLiteral(resourceName: "Disclosure Indicator")) 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | self.contentView.addSubview(titleLabel) 23 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 24 | titleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 20).isActive = true 25 | titleLabel.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: 0).isActive = true 26 | descLabel.textColor = UIColor(named: "CommentText") 27 | 28 | let stackView = UIStackView(arrangedSubviews: [descLabel, disclosureIndicatorImageView]) 29 | stackView.axis = .horizontal 30 | stackView.alignment = .center 31 | stackView.distribution = .fill 32 | stackView.spacing = 11 33 | 34 | self.contentView.addSubview(stackView) 35 | stackView.translatesAutoresizingMaskIntoConstraints = false 36 | stackView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -15).isActive = true 37 | stackView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 0).isActive = true 38 | stackView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: 0).isActive = true 39 | 40 | self.backgroundColor = UIColor.white 41 | self.selectedBackgroundView = UIView() 42 | self.selectedBackgroundView?.backgroundColor = UIColor.lightGray.withAlphaComponent(0.6) 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | } 50 | 51 | struct TextListProviderModel: Equatable, StringIdentifiableType { 52 | let title: String 53 | let desc: String 54 | let value: Value 55 | 56 | var identity: String { 57 | return self.title 58 | } 59 | 60 | static func ==(lhs: TextListProviderModel, rhs: TextListProviderModel) -> Bool { 61 | return lhs.desc == rhs.desc 62 | } 63 | } 64 | 65 | class TextListProvider: AnimatableCollectionViewProvider { 66 | 67 | typealias Model = TextListProviderModel 68 | 69 | let items: [Model] 70 | let isHidden = BehaviorRelay(value: false) 71 | 72 | typealias Cell = TextCollectionViewCell 73 | typealias NodeType = Model 74 | 75 | init(items: [Model]) { 76 | self.items = items 77 | } 78 | 79 | func configureCell(_ collectionView: UICollectionView, cell: TextCollectionViewCell, indexPath: IndexPath, value: NodeType) { 80 | cell.titleLabel.text = value.title 81 | cell.descLabel.text = value.desc 82 | } 83 | 84 | func itemSelected(_ collectionView: UICollectionView, indexPath: IndexPath, value: NodeType) { 85 | collectionView.deselectItem(at: indexPath, animated: true) 86 | } 87 | 88 | func createValues() -> Observable<[Model]> { 89 | let items = self.items 90 | return isHidden.asObservable().distinctUntilChanged().map { isHidden in isHidden ? [] : items } 91 | } 92 | 93 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: NodeType) -> CGSize? { 94 | return CGSize(width: collectionView.bounds.width, height: 44) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Example/Providers/TextSectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextSectionProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 01/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class TextCollectionReusableView: UICollectionReusableView { 15 | 16 | let textLabel = UILabel() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | textLabel.font = UIFont.systemFont(ofSize: 12) 21 | textLabel.numberOfLines = 0 22 | textLabel.textColor = UIColor(named: "CommentText") 23 | self.addSubview(textLabel) 24 | textLabel.translatesAutoresizingMaskIntoConstraints = false 25 | textLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15).isActive = true 26 | textLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true 27 | textLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15).isActive = true 28 | } 29 | 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | } 35 | 36 | class TextSectionProvider: AnimatableSectionPartionCollectionViewProvider, StringIdentifiableType, Equatable { 37 | 38 | static func ==(lhs: TextSectionProvider, rhs: TextSectionProvider) -> Bool { 39 | return true 40 | } 41 | 42 | func configureSupplementaryView(_ collectionView: UICollectionView, sectionView: TextCollectionReusableView, indexPath: IndexPath, value: TextSectionProvider) { 43 | if !sectionView.hasConfigured { 44 | sectionView.hasConfigured = true 45 | } 46 | value.text.asObservable() 47 | .bind(to: sectionView.textLabel.rx.text) 48 | .disposed(by: disposeBag) 49 | 50 | value.text.asObservable().distinctUntilChanged() 51 | .subscribe(onNext: { [weak collectionView] (text) in 52 | collectionView?.performBatchUpdates(nil, completion: nil) 53 | }) 54 | .disposed(by: disposeBag) 55 | } 56 | 57 | func createSectionPartion() -> Observable { 58 | return Observable.just(self) 59 | } 60 | 61 | typealias Cell = TextCollectionReusableView 62 | typealias NodeType = TextSectionProvider 63 | 64 | let collectionElementKindSection: UICollectionElementKindSection 65 | let text: BehaviorRelay 66 | let disposeBag = DisposeBag() 67 | 68 | init(collectionElementKindSection: UICollectionElementKindSection, text: String) { 69 | self.collectionElementKindSection = collectionElementKindSection 70 | self.text = BehaviorRelay(value: text) 71 | } 72 | 73 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, value: TextSectionProvider) -> CGSize? { 74 | let height = NSAttributedString(string: value.text.value, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)]) 75 | .boundingRect(with: CGSize(width: collectionView.bounds.width - 30, height: CGFloat.greatestFiniteMagnitude), options: [NSStringDrawingOptions.usesFontLeading, NSStringDrawingOptions.usesLineFragmentOrigin], context: nil).height 76 | return CGSize(width: collectionView.bounds.width, height: height + 20) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/Providers/UniqueButtonTableViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueButtonTableViewProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | open class UniqueButtonTableViewProvider: SingleUITableViewCellProvider { 15 | 16 | let textLabel = UILabel() 17 | let activityIndicatorView = UIActivityIndicatorView() 18 | 19 | public override init() { 20 | super.init() 21 | textLabel.textAlignment = .center 22 | 23 | backgroundView = UIView() 24 | selectedBackgroundView = UIView() 25 | contentView.addSubview(textLabel) 26 | textLabel.translatesAutoresizingMaskIntoConstraints = false 27 | textLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true 28 | textLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 29 | textLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true 30 | textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true 31 | 32 | contentView.addSubview(activityIndicatorView) 33 | activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false 34 | activityIndicatorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15).isActive = true 35 | activityIndicatorView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Example/Providers/UniqueCommentTextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueCommentTextProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 02/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | class UniqueCommentTextProvider: SingleUICollectionViewCellProvider { 15 | 16 | let textLabel = UILabel() 17 | 18 | let text: BehaviorRelay 19 | let disposeBag = DisposeBag() 20 | 21 | required init(text: String) { 22 | self.text = BehaviorRelay(value: text) 23 | super.init() 24 | self.text.asObservable().bind(to: textLabel.rx.text).disposed(by: disposeBag) 25 | 26 | self.contentView.addSubview(textLabel) 27 | textLabel.font = UIFont.systemFont(ofSize: 12) 28 | textLabel.numberOfLines = 0 29 | textLabel.textColor = UIColor(named: "CommentText") 30 | textLabel.translatesAutoresizingMaskIntoConstraints = false 31 | textLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 15).isActive = true 32 | textLabel.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor, constant: 0).isActive = true 33 | textLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -15).isActive = true 34 | self.text.asObservable() 35 | .subscribe(onNext: { [weak self] (_) in 36 | self?.collectionView?.performBatchUpdates(nil, completion: nil) 37 | }) 38 | .disposed(by: disposeBag) 39 | } 40 | 41 | override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: SingleUICollectionViewCellProvider) -> CGSize? { 42 | let height = NSAttributedString(string: self.text.value, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)]) 43 | .boundingRect(with: CGSize(width: collectionView.bounds.width - 30, height: CGFloat.greatestFiniteMagnitude), options: [NSStringDrawingOptions.usesFontLeading, NSStringDrawingOptions.usesLineFragmentOrigin], context: nil).height 44 | return CGSize(width: collectionView.bounds.width, height: height + 20) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Example/Providers/UniqueMessageTableViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueMessageTableViewProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | struct ValidationColors { 15 | static let okColor = UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0) 16 | static let errorColor = UIColor.red 17 | } 18 | 19 | extension ValidationResult { 20 | var textColor: UIColor { 21 | switch self { 22 | case .ok: 23 | return ValidationColors.okColor 24 | case .empty: 25 | return UIColor.black 26 | case .validating: 27 | return UIColor.black 28 | case .failed: 29 | return ValidationColors.errorColor 30 | } 31 | } 32 | } 33 | 34 | extension ValidationResult: CustomStringConvertible { 35 | var description: String { 36 | switch self { 37 | case let .ok(message): 38 | return message 39 | case .empty: 40 | return "" 41 | case .validating: 42 | return "validating ..." 43 | case let .failed(message): 44 | return message 45 | } 46 | } 47 | } 48 | 49 | open class ValidationTableViewProvider: AnimatableTableViewGroupProvider { 50 | 51 | public var providers: [_AnimatableTableViewMultiNodeProvider] { 52 | return [self.valueProvider, self.uniqueMessageTableViewProvider] 53 | } 54 | 55 | public func createAnimatableProviders() -> Observable<[_AnimatableTableViewMultiNodeProvider]> { 56 | return Observable.just([self.valueProvider, self.uniqueMessageTableViewProvider]) 57 | } 58 | 59 | public let valueProvider: ValueProvider 60 | public let uniqueMessageTableViewProvider = UniqueMessageTableViewProvider() 61 | 62 | public init(valueProvider: ValueProvider) { 63 | self.valueProvider = valueProvider 64 | } 65 | 66 | var validationResult: Binder { 67 | return self.uniqueMessageTableViewProvider.validationResult 68 | } 69 | 70 | } 71 | 72 | open class UniqueMessageTableViewProvider: SingleUITableViewCellProvider { 73 | 74 | public let messageLabel = UILabel() 75 | 76 | let disposeBag = DisposeBag() 77 | 78 | public override init() { 79 | super.init() 80 | self.messageLabel.font = UIFont.systemFont(ofSize: 12) 81 | self.messageLabel.textColor = UIColor.white 82 | 83 | self.selectionStyle = .none 84 | self.contentView.addSubview(messageLabel) 85 | self.messageLabel.translatesAutoresizingMaskIntoConstraints = false 86 | self.messageLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true 87 | self.messageLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 15).isActive = true 88 | self.messageLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -15).isActive = true 89 | self.messageLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true 90 | 91 | self.backgroundView = UIView() 92 | 93 | itemHeight = { _ in 30 } 94 | } 95 | 96 | var validationResult: Binder { 97 | return Binder(self) { provider, result in 98 | provider.backgroundView?.backgroundColor = result.textColor 99 | provider.messageLabel.text = result.description 100 | provider.isHidden = result.description.isEmpty 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Example/Providers/UniqueSwitchProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueSwitchProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 01/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import Flix 12 | 13 | class UniqueSwitchProvider: UniqueAnimatableCollectionViewProvider { 14 | 15 | typealias Cell = UICollectionViewCell 16 | 17 | let uiSwitch = UISwitch() 18 | let titleLabel = UILabel() 19 | 20 | func onCreate(_ collectionView: UICollectionView, cell: UICollectionViewCell, indexPath: IndexPath) { 21 | cell.contentView.addSubview(uiSwitch) 22 | uiSwitch.translatesAutoresizingMaskIntoConstraints = false 23 | uiSwitch.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -15).isActive = true 24 | uiSwitch.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor, constant: 0).isActive = true 25 | 26 | cell.contentView.addSubview(titleLabel) 27 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 28 | titleLabel.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 15).isActive = true 29 | titleLabel.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor, constant: 0).isActive = true 30 | cell.backgroundColor = UIColor.white 31 | } 32 | 33 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: UniqueSwitchProvider) -> CGSize? { 34 | return CGSize(width: collectionView.bounds.width, height: 44) 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Example/Providers/UniqueTextFieldTableViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueTextFieldTableViewProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import Flix 13 | 14 | open class UniqueTextFieldTableViewProvider: SingleUITableViewCellProvider { 15 | 16 | public let textField = UITextField() 17 | 18 | public override init() { 19 | super.init() 20 | self.selectionStyle = .none 21 | self.contentView.addSubview(textField) 22 | textField.translatesAutoresizingMaskIntoConstraints = false 23 | textField.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 15).isActive = true 24 | textField.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true 25 | textField.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -15).isActive = true 26 | textField.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor).isActive = true 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Example/Providers/UniqueTextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueTextProvider.swift 3 | // Example 4 | // 5 | // Created by DianQK on 01/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import Flix 12 | 13 | class UniqueTextProvider: UniqueAnimatableCollectionViewProvider { 14 | 15 | typealias Cell = UICollectionViewCell 16 | 17 | let title: String 18 | let desc: String 19 | 20 | init(title: String, desc: String) { 21 | self.title = title 22 | self.desc = desc 23 | } 24 | 25 | let titleLabel = UILabel() 26 | let descLabel = UILabel() 27 | let disclosureIndicatorImageView = UIImageView(image: #imageLiteral(resourceName: "Disclosure Indicator")) 28 | 29 | func onCreate(_ collectionView: UICollectionView, cell: Cell, indexPath: IndexPath) { 30 | cell.contentView.addSubview(titleLabel) 31 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 32 | titleLabel.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 20).isActive = true 33 | titleLabel.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor, constant: 0).isActive = true 34 | titleLabel.text = title 35 | 36 | descLabel.textColor = UIColor(named: "CommentText") 37 | descLabel.text = desc 38 | 39 | let stackView = UIStackView(arrangedSubviews: [descLabel, disclosureIndicatorImageView]) 40 | stackView.axis = .horizontal 41 | stackView.alignment = .center 42 | stackView.distribution = .fill 43 | stackView.spacing = 11 44 | 45 | cell.contentView.addSubview(stackView) 46 | stackView.translatesAutoresizingMaskIntoConstraints = false 47 | stackView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -15).isActive = true 48 | stackView.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 0).isActive = true 49 | stackView.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: 0).isActive = true 50 | 51 | cell.backgroundColor = UIColor.white 52 | } 53 | 54 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: UniqueTextProvider) -> CGSize? { 55 | return CGSize(width: collectionView.bounds.width, height: 44) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | import Flix 14 | import RxKeyboard 15 | 16 | class CollectionViewController: UIViewController { 17 | 18 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout()) 19 | let disposeBag = DisposeBag() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | navigationItem.largeTitleDisplayMode = .never 25 | 26 | self.view.backgroundColor = UIColor.white 27 | self.view.addSubview(collectionView) 28 | collectionView.backgroundColor = UIColor.lightGray 29 | collectionView.translatesAutoresizingMaskIntoConstraints = false 30 | collectionView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true 31 | collectionView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true 32 | collectionView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true 33 | collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true 34 | 35 | collectionView.backgroundColor = UIColor(named: "Background") 36 | 37 | let viewLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout 38 | viewLayout.minimumLineSpacing = 0.5 39 | viewLayout.estimatedItemSize = CGSize.zero 40 | 41 | collectionView.alwaysBounceVertical = true 42 | 43 | } 44 | } 45 | 46 | class TableViewController: UIViewController { 47 | 48 | let tableView = UITableView(frame: .zero, style: .grouped) 49 | let disposeBag = DisposeBag() 50 | 51 | override func viewDidLoad() { 52 | super.viewDidLoad() 53 | 54 | navigationItem.largeTitleDisplayMode = .never 55 | 56 | self.view.backgroundColor = UIColor.white 57 | self.view.addSubview(tableView) 58 | tableView.translatesAutoresizingMaskIntoConstraints = false 59 | tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true 60 | tableView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true 61 | tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true 62 | tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true 63 | tableView.backgroundColor = UIColor(named: "Background") 64 | tableView.separatorColor = UIColor(named: "Background") 65 | tableView.rowHeight = 44 66 | tableView.sectionFooterHeight = CGFloat.leastNonzeroMagnitude 67 | tableView.sectionHeaderHeight = CGFloat.leastNonzeroMagnitude 68 | 69 | tableView.estimatedRowHeight = 0 70 | tableView.estimatedSectionFooterHeight = 0 71 | tableView.estimatedSectionHeaderHeight = 0 72 | 73 | RxKeyboard.instance.visibleHeight 74 | .drive(onNext: { [unowned self] keyboardVisibleHeight in 75 | let height: CGFloat = max(0, keyboardVisibleHeight - self.tableView.safeAreaInsets.bottom) 76 | self.tableView.contentInset.bottom = height 77 | self.tableView.scrollIndicatorInsets.bottom = height 78 | }) 79 | .disposed(by: disposeBag) 80 | 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Flix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/Flix.png -------------------------------------------------------------------------------- /Flix.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Flix' 3 | s.version = '4.0.0' 4 | s.summary = 'iOS form builder in Swift' 5 | s.homepage = 'https://github.com/DianQK/Flix' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'DianQK' => 'dianqk@icloud.com' } 8 | s.source = { :git => 'https://github.com/DianQK/Flix.git', 9 | :tag => s.version.to_s } 10 | s.source_files = 'Flix/**/*.swift' 11 | s.frameworks = 'UIKit', 'Foundation' 12 | s.requires_arc = true 13 | 14 | s.dependency 'RxSwift', '~> 5.0' 15 | s.dependency 'RxCocoa', '~> 5.0' 16 | s.dependency 'RxDataSources', '~> 4.0' 17 | 18 | s.ios.deployment_target = '9.0' 19 | 20 | s.swift_version = '5.0' 21 | end 22 | -------------------------------------------------------------------------------- /Flix.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Flix.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Flix.xcodeproj/xcshareddata/xcschemes/Flix.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Flix.xcodeproj/xcshareddata/xcschemes/FlixTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Flix.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Flix.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Flix.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Flix/Builder/Builder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Builder.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/15. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import RxDataSources 10 | 11 | public protocol Builder: class { 12 | 13 | } 14 | 15 | 16 | protocol CombineSectionModelType { 17 | 18 | associatedtype Section 19 | associatedtype Item 20 | 21 | init(model: Section, items: [Item]) 22 | 23 | } 24 | 25 | extension SectionModel: CombineSectionModelType { } 26 | extension AnimatableSectionModel: CombineSectionModelType { } 27 | 28 | struct BuilderTool { 29 | 30 | static func combineSections(_ value: [(section: S, nodes: [N])?]) -> [FlixSectionModel] 31 | where FlixSectionModel.Item == N, FlixSectionModel.Section == S { 32 | return value.compactMap { $0 }.enumerated() 33 | .map { (offset, section) -> FlixSectionModel in 34 | let items = section.nodes.map { (node) -> N in 35 | var node = node 36 | node.providerStartIndexPath.section = offset 37 | node.providerEndIndexPath.section = offset 38 | return node 39 | } 40 | return FlixSectionModel.init(model: section.section, items: items) 41 | } 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Flix/Builder/ChangesetInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangesetInfo.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 06/03/2018. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxDataSources 11 | 12 | public protocol ChangesetInfo { 13 | 14 | var reloadData: Bool { get } 15 | 16 | var insertedSections: [Int] { get } 17 | var deletedSections: [Int] { get } 18 | var movedSections: [(from: Int, to: Int)] { get } 19 | var updatedSections: [Int] { get } 20 | 21 | var insertedItems: [ItemPath] { get } 22 | var deletedItems: [ItemPath] { get } 23 | var movedItems: [(from: ItemPath, to: ItemPath)] { get } 24 | var updatedItems: [ItemPath] { get } 25 | 26 | } 27 | 28 | extension Changeset: ChangesetInfo { } 29 | -------------------------------------------------------------------------------- /Flix/Builder/PerformGroupUpdatesable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformGroupUpdatesable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/16. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol PerformGroupUpdatesable { 14 | 15 | func beginGroupUpdates() 16 | 17 | func endGroupUpdates() 18 | 19 | } 20 | 21 | extension PerformGroupUpdatesable { 22 | 23 | public func performGroupUpdates(_ updates: (() -> Void)) { 24 | self.beginGroupUpdates(); defer { self.endGroupUpdates() } 25 | updates() 26 | } 27 | 28 | } 29 | 30 | private var performGroupUpdatesBehaviorRelayKey: Void? 31 | 32 | extension PerformGroupUpdatesable where Self: Builder { 33 | 34 | var performGroupUpdatesBehaviorRelay: BehaviorRelay { 35 | if let behaviorRelay = objc_getAssociatedObject(self, &performGroupUpdatesBehaviorRelayKey) as? BehaviorRelay { 36 | return behaviorRelay 37 | } else { 38 | let behaviorRelay = BehaviorRelay(value: true) 39 | objc_setAssociatedObject(self, &performGroupUpdatesBehaviorRelayKey, behaviorRelay, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 40 | return behaviorRelay 41 | } 42 | } 43 | 44 | public func beginGroupUpdates() { 45 | self.performGroupUpdatesBehaviorRelay.accept(false) 46 | } 47 | 48 | public func endGroupUpdates() { 49 | self.performGroupUpdatesBehaviorRelay.accept(true) 50 | } 51 | 52 | } 53 | 54 | extension ObservableConvertibleType { 55 | 56 | func sendLatest(when: T) -> Observable where T.Element == Bool { 57 | return Observable.combineLatest(self.asObservable(), when.asObservable()) 58 | .flatMap { (value, send) -> Observable in 59 | return send ? Observable.just(value) : Observable.empty() 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Flix/Builder/TableViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewBuilder.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 08/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | 14 | public class TableViewBuilder: _TableViewBuilder, PerformGroupUpdatesable { 15 | 16 | typealias SectionModel = RxDataSources.SectionModel 17 | 18 | let disposeBag = DisposeBag() 19 | let delegateProxy = TableViewDelegateProxy() 20 | 21 | public let sectionProviders: BehaviorRelay<[TableViewSectionProvider]> 22 | 23 | var nodeProviders: [String: _TableViewMultiNodeProvider] = [:] { 24 | didSet { 25 | nodeProviders.forEach { (_, provider) in 26 | provider._register(tableView) 27 | } 28 | } 29 | } 30 | var footerSectionProviders: [String: _SectionPartionTableViewProvider] = [:] { 31 | didSet { 32 | footerSectionProviders.forEach { (_, provider) in 33 | provider.register(tableView) 34 | } 35 | } 36 | } 37 | var headerSectionProviders: [String: _SectionPartionTableViewProvider] = [:] { 38 | didSet { 39 | headerSectionProviders.forEach { (_, provider) in 40 | provider.register(tableView) 41 | } 42 | } 43 | } 44 | 45 | weak var _tableView: UITableView? 46 | 47 | var tableView: UITableView { 48 | return _tableView! 49 | } 50 | 51 | public init(tableView: UITableView, sectionProviders: [TableViewSectionProvider]) { 52 | 53 | self._tableView = tableView 54 | 55 | self.sectionProviders = BehaviorRelay(value: sectionProviders) 56 | 57 | let dataSource = RxTableViewSectionedReloadDataSource(configureCell: { [weak self] dataSource, tableView, indexPath, node in 58 | guard let provider = self?.nodeProviders[node.providerIdentity] else { return UITableViewCell() } 59 | return provider._configureCell(tableView, indexPath: indexPath, node: node) 60 | }) 61 | 62 | self.build(dataSource: dataSource) 63 | 64 | self.sectionProviders.asObservable() 65 | .do(onNext: { [weak self] (sectionProviders) in 66 | self?.nodeProviders = Dictionary( 67 | uniqueKeysWithValues: sectionProviders 68 | .flatMap { $0.providers.flatMap { $0.__providers.map { (key: $0._flix_identity, value: $0) } } 69 | }) 70 | self?.footerSectionProviders = Dictionary( 71 | uniqueKeysWithValues: sectionProviders.compactMap { $0.footerProvider.map { (key: $0._flix_identity, value: $0) } }) 72 | self?.headerSectionProviders = Dictionary( 73 | uniqueKeysWithValues: sectionProviders.compactMap { $0.headerProvider.map { (key: $0._flix_identity, value: $0) } }) 74 | }) 75 | .flatMapLatest { (providers) -> Observable<[SectionModel]> in 76 | let sections = providers.map { $0.createSectionModel() } 77 | return Observable.combineLatest(sections) 78 | .ifEmpty(default: []) 79 | .map { value -> [SectionModel] in 80 | return BuilderTool.combineSections(value) 81 | } 82 | } 83 | .sendLatest(when: performGroupUpdatesBehaviorRelay) 84 | .debounce(.seconds(0), scheduler: MainScheduler.instance) 85 | // UITableViewAlertForLayoutOutsideViewHierarchy https://github.com/ReactiveX/RxSwift/pull/2076 86 | .bind(to: tableView.rx.items(dataSource: dataSource)) 87 | .disposed(by: disposeBag) 88 | } 89 | 90 | public convenience init(tableView: UITableView, providers: [_TableViewMultiNodeProvider]) { 91 | let sectionProviderTableViewBuilder = TableViewSectionProvider( 92 | providers: providers, 93 | headerProvider: nil, 94 | footerProvider: nil 95 | ) 96 | self.init(tableView: tableView, sectionProviders: [sectionProviderTableViewBuilder]) 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Flix/Cell/UIView+Configure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Configure.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var key: Void? 12 | 13 | extension UIView { 14 | public var hasConfigured: Bool { 15 | get { 16 | return objc_getAssociatedObject(self, &key) as? Bool ?? false 17 | } 18 | set { 19 | objc_setAssociatedObject(self, 20 | &key, newValue, 21 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Flix/CollectionEdit/CollectionViewMoveable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewMoveable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 14/11/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | 14 | public protocol _CollectionViewMoveable { 15 | 16 | func _collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath, node: _Node) -> Bool 17 | 18 | func _collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndex: Int, to destinationIndex: Int, node: _Node) 19 | 20 | } 21 | 22 | public protocol CollectionViewMoveable: _CollectionViewMoveable { 23 | 24 | associatedtype Value 25 | 26 | func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath, value: Value) -> Bool 27 | 28 | func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndex: Int, to destinationIndex: Int, value: Value) 29 | 30 | } 31 | 32 | extension CollectionViewMoveable where Self: CollectionViewMultiNodeProvider { 33 | 34 | public func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath, value: Self.Value) -> Bool { 35 | return true 36 | } 37 | 38 | public func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndex: Int, to destinationIndex: Int, value: Self.Value) { } 39 | 40 | public func _collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath, node: _Node) -> Bool { 41 | return self.collectionView(collectionView, canMoveItemAt: indexPath, value: node._unwarp()) 42 | } 43 | 44 | public func _collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndex: Int, to destinationIndex: Int, node: _Node) { 45 | self.collectionView(collectionView, moveItemAt: sourceIndex, to: destinationIndex, value: node._unwarp()) 46 | self.event._moveItem.onNext(( 47 | collectionView: collectionView, 48 | sourceIndex: sourceIndex, 49 | destinationIndex: destinationIndex, 50 | value: node._unwarp()) 51 | ) 52 | } 53 | 54 | } 55 | 56 | extension CollectionViewEvent where Provider: CollectionViewMoveable { 57 | 58 | public var moveItem: ControlEvent { 59 | return ControlEvent(events: self._moveItem) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Flix/DelegateProxy/CollectionViewDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewDelegateProxy.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 22/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewDelegateProxy: NSObject, UICollectionViewDelegateFlowLayout { 12 | 13 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 14 | return self.shouldSelectItemAt?(collectionView, indexPath) ?? true 15 | } 16 | 17 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 18 | let collectionViewLayout = collectionViewLayout as! UICollectionViewFlowLayout 19 | return sizeForItem?(collectionView, collectionViewLayout, indexPath) ?? collectionViewLayout.itemSize 20 | } 21 | 22 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 23 | let collectionViewLayout = collectionViewLayout as! UICollectionViewFlowLayout 24 | return referenceSizeForFooterInSection?(collectionView, collectionViewLayout, section) ?? collectionViewLayout.footerReferenceSize 25 | } 26 | 27 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 28 | let collectionViewLayout = collectionViewLayout as! UICollectionViewFlowLayout 29 | return referenceSizeForHeaderInSection?(collectionView, collectionViewLayout, section) ?? collectionViewLayout.headerReferenceSize 30 | } 31 | 32 | func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { 33 | return self.targetIndexPathForMoveFromItemAt?(collectionView, originalIndexPath, proposedIndexPath) ?? proposedIndexPath 34 | } 35 | 36 | var sizeForItem: ((_ collectionView: UICollectionView, _ collectionViewLayout: UICollectionViewLayout, _ indexPath: IndexPath) -> CGSize?)? 37 | var referenceSizeForFooterInSection: ((_ collectionView: UICollectionView, _ collectionViewLayout: UICollectionViewLayout, _ section: Int) -> CGSize?)? 38 | var referenceSizeForHeaderInSection: ((_ collectionView: UICollectionView, _ collectionViewLayout: UICollectionViewLayout, _ section: Int) -> CGSize?)? 39 | var targetIndexPathForMoveFromItemAt: ((_ collectionView: UICollectionView, _ originalIndexPath: IndexPath, _ proposedIndexPath: IndexPath) -> IndexPath)? 40 | var shouldSelectItemAt: ((_ collectionView: UICollectionView, _ indexPath: IndexPath) -> Bool)? 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flix/DelegateProxy/TableViewDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDelegateProxy.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 22/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewDelegateProxy: NSObject, UITableViewDelegate { 12 | 13 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 14 | return self.editActionsForRowAt?(tableView, indexPath) 15 | } 16 | 17 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 18 | return self.heightForRowAt?(tableView, indexPath) ?? tableView.rowHeight 19 | } 20 | 21 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 22 | return self.heightForHeaderInSection?(tableView, section) ?? tableView.sectionHeaderHeight 23 | } 24 | 25 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 26 | return self.viewForHeaderInSection?(tableView, section) 27 | } 28 | 29 | func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 30 | return self.heightForFooterInSection?(tableView, section) ?? tableView.sectionFooterHeight 31 | } 32 | 33 | func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 34 | return self.viewForFooterInSection?(tableView, section) 35 | } 36 | 37 | func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { 38 | return self.titleForDeleteConfirmationButtonForRowAt?(tableView, indexPath) ?? "Delete" 39 | } 40 | 41 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { 42 | return self.editingStyleForRowAt?(tableView, indexPath) ?? UITableViewCell.EditingStyle.delete 43 | } 44 | 45 | func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { 46 | return self.targetIndexPathForMoveFromRowAt?(tableView, sourceIndexPath, proposedDestinationIndexPath) ?? proposedDestinationIndexPath 47 | } 48 | 49 | @available(iOS 11.0, *) 50 | func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 51 | return self.leadingSwipeActionsConfigurationForRowAt?(tableView, indexPath) as? UISwipeActionsConfiguration 52 | } 53 | 54 | @available(iOS 11.0, *) 55 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 56 | return self.trailingSwipeActionsConfigurationForRowAt?(tableView, indexPath) as? UISwipeActionsConfiguration 57 | } 58 | 59 | var heightForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> CGFloat?)? 60 | var heightForFooterInSection: ((_ tableView: UITableView, _ section: Int) -> CGFloat?)? 61 | var heightForHeaderInSection: ((_ tableView: UITableView, _ section: Int) -> CGFloat?)? 62 | var viewForHeaderInSection: ((_ tableView: UITableView, _ section: Int) -> UIView?)? 63 | var viewForFooterInSection: ((_ tableView: UITableView, _ section: Int) -> UIView?)? 64 | var editActionsForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> [UITableViewRowAction]?)? 65 | var targetIndexPathForMoveFromRowAt: ((_ tableView: UITableView, _ sourceIndexPath: IndexPath, _ proposedDestinationIndexPath: IndexPath) -> IndexPath)? 66 | var titleForDeleteConfirmationButtonForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> String?)? 67 | var editingStyleForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> UITableViewCell.EditingStyle)? 68 | 69 | var leadingSwipeActionsConfigurationForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> NSObject?)? 70 | var trailingSwipeActionsConfigurationForRowAt: ((_ tableView: UITableView, _ indexPath: IndexPath) -> NSObject?)? 71 | 72 | } 73 | 74 | -------------------------------------------------------------------------------- /Flix/Flix.h: -------------------------------------------------------------------------------- 1 | // 2 | // Flix.h 3 | // Flix 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | #import 10 | //! Project version number for Flix. 11 | FOUNDATION_EXPORT double FlixVersionNumber; 12 | 13 | //! Project version string for Flix. 14 | FOUNDATION_EXPORT const unsigned char FlixVersionString[]; 15 | -------------------------------------------------------------------------------- /Flix/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Flix/Node/IdentifiableNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifiableNode.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 22/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxDataSources 11 | 12 | public protocol StringIdentifiableType { 13 | 14 | var identity: String { get } 15 | 16 | } 17 | 18 | extension Equatable { 19 | 20 | fileprivate func isEqual(value: Self) -> Bool { 21 | return self == value 22 | } 23 | 24 | } 25 | 26 | public struct IdentifiableNode: _Node, IdentifiableType, Equatable { 27 | 28 | public static func ==(lhs: IdentifiableNode, rhs: IdentifiableNode) -> Bool { 29 | return lhs.isEqual(rhs.value) 30 | } 31 | 32 | public var identity: String { 33 | return providerIdentity + value.identity 34 | } 35 | 36 | public let providerIdentity: String 37 | 38 | public typealias Identity = String 39 | 40 | public let value: StringIdentifiableType 41 | public let isEqual: (StringIdentifiableType) -> (Bool) 42 | 43 | public var providerStartIndexPath = IndexPath(row: 0, section: 0) 44 | public var providerEndIndexPath = IndexPath(row: 0, section: 0) 45 | 46 | public init(providerIdentity: String, valueNode: T) { 47 | self.providerIdentity = providerIdentity 48 | self.value = valueNode 49 | let isEqual = valueNode.isEqual 50 | self.isEqual = { value in 51 | isEqual(value as! T) 52 | } 53 | } 54 | 55 | public func _unwarp() -> Value { 56 | return self.value as! Value 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /Flix/Node/Node.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Node.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | 14 | public protocol _Node { 15 | 16 | var providerIdentity: String { get } 17 | 18 | func _unwarp() -> Value 19 | 20 | var providerStartIndexPath: IndexPath { get set } 21 | var providerEndIndexPath: IndexPath { get set } 22 | 23 | } 24 | 25 | public struct Node: _Node { 26 | 27 | public let providerIdentity: String 28 | public let value: Any 29 | 30 | public var providerStartIndexPath = IndexPath(row: 0, section: 0) 31 | public var providerEndIndexPath = IndexPath(row: 0, section: 0) 32 | 33 | init(providerIdentity: String, value: Any) { 34 | self.providerIdentity = providerIdentity 35 | self.value = value 36 | } 37 | 38 | public func _unwarp() -> Value { 39 | return self.value as! Value 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flix/Provider/CollectionViewEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewEvent.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/17. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public class CollectionViewEvent { 14 | 15 | public typealias Value = Provider.Value 16 | 17 | public typealias EventValue = (collectionView: UICollectionView, indexPath: IndexPath, value: Value) 18 | 19 | public var selectedEvent: ControlEvent<()> { return ControlEvent(events: self._itemSelected.map { _ in }) } 20 | 21 | public var modelSelected: ControlEvent { return ControlEvent(events: self.itemSelected.map { $0.value }) } 22 | 23 | public var itemSelected: ControlEvent { return ControlEvent(events: self._itemSelected) } 24 | private(set) lazy var _itemSelected = PublishSubject() 25 | 26 | public var modelDeselected: ControlEvent { return ControlEvent(events: self.itemDeselected.map { $0.value }) } 27 | 28 | public var itemDeselected: ControlEvent { return ControlEvent(events: self._itemDeselected) } 29 | private(set) lazy var _itemDeselected = PublishSubject() 30 | 31 | public typealias MoveEventValue = (collectionView: UICollectionView, sourceIndex: Int, destinationIndex: Int, value: Value) 32 | private(set) lazy var _moveItem = PublishSubject() 33 | 34 | init() { } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Flix/Provider/CollectionViewSectionPartionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewSectionPartionProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 03/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | 14 | public enum UICollectionElementKindSection: String { 15 | 16 | case header = "UICollectionElementKindSectionHeader" 17 | case footer = "UICollectionElementKindSectionFooter" 18 | 19 | } 20 | 21 | public protocol _SectionPartionCollectionViewProvider: FlixCustomStringConvertible { 22 | 23 | var cellType: UICollectionReusableView.Type { get } 24 | var collectionElementKindSection: UICollectionElementKindSection { get } 25 | 26 | func _configureSupplementaryView(_ collectionView: UICollectionView, sectionView: UICollectionReusableView, indexPath: IndexPath, node: _Node) 27 | 28 | func _createSectionPartion() -> Observable<_Node?> 29 | 30 | func _collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, node: _Node) -> CGSize? 31 | 32 | } 33 | 34 | extension _SectionPartionCollectionViewProvider { 35 | 36 | public func register(_ collectionView: UICollectionView) { 37 | collectionView.register(self.cellType, forSupplementaryViewOfKind: self.collectionElementKindSection.rawValue, withReuseIdentifier: self._flix_identity) 38 | } 39 | 40 | } 41 | 42 | public protocol SectionPartionCollectionViewProvider: _SectionPartionCollectionViewProvider, ReactiveCompatible { 43 | 44 | associatedtype Cell: UICollectionReusableView 45 | associatedtype Value 46 | 47 | func configureSupplementaryView(_ collectionView: UICollectionView, sectionView: Cell, indexPath: IndexPath, value: Value) 48 | 49 | func createSectionPartion() -> Observable 50 | 51 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, value: Value) -> CGSize? 52 | 53 | } 54 | 55 | extension SectionPartionCollectionViewProvider { 56 | 57 | public var cellType: UICollectionReusableView.Type { return Cell.self } 58 | 59 | public func _configureSupplementaryView(_ collectionView: UICollectionView, sectionView: UICollectionReusableView, indexPath: IndexPath, node: _Node) { 60 | return configureSupplementaryView(collectionView, sectionView: sectionView as! Cell, indexPath: indexPath, value: node._unwarp()) 61 | } 62 | 63 | public func _createSectionPartion() -> Observable<_Node?> { 64 | let providerIdentity = self._flix_identity 65 | return createSectionPartion().map { $0.map { Node(providerIdentity: providerIdentity, value: $0) } } 66 | } 67 | 68 | public func _collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, node: _Node) -> CGSize? { 69 | return self.collectionView(collectionView, layout: collectionViewLayout, referenceSizeInSection: section, value: node._unwarp()) 70 | } 71 | 72 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, value: Value) -> CGSize? { 73 | return nil 74 | } 75 | 76 | } 77 | 78 | public typealias _AnimatableSectionPartionCollectionViewProvider = _AnimatableSectionPartionProviderable & _SectionPartionCollectionViewProvider 79 | 80 | public protocol AnimatableSectionPartionCollectionViewProvider: SectionPartionCollectionViewProvider, _AnimatableSectionPartionProviderable where Value: Equatable, Value: StringIdentifiableType { 81 | 82 | func createAnimatableSection() -> Observable 83 | 84 | } 85 | 86 | extension AnimatableSectionPartionCollectionViewProvider { 87 | 88 | public func _createAnimatableSectionPartion() -> Observable { 89 | return createAnimatableSection() 90 | } 91 | 92 | public var identity: String { 93 | return self._flix_identity 94 | } 95 | 96 | } 97 | 98 | extension AnimatableSectionPartionCollectionViewProvider { 99 | 100 | public func createAnimatableSection() -> Observable { 101 | let providerIdentity = self._flix_identity 102 | return createSectionPartion().map { $0.map { IdentifiableNode(providerIdentity: providerIdentity, valueNode: $0) } } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Flix/Provider/CustomIdentityType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomIdentityType.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 24/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol CustomIdentityType { 12 | 13 | var customIdentity: String { get } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Flix/Provider/Group/AnimatableCollectionViewGroupProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableCollectionViewGroupProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 30/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol AnimatableCollectionViewGroupProvider: _AnimatableCollectionViewMultiNodeProvider, _CollectionViewGroupProvider { 14 | 15 | var providers: [_AnimatableCollectionViewMultiNodeProvider] { get } 16 | 17 | func createAnimatableProviders() -> Observable<[_AnimatableCollectionViewMultiNodeProvider]> 18 | 19 | } 20 | 21 | extension AnimatableCollectionViewGroupProvider { 22 | 23 | public var _providers: [_CollectionViewMultiNodeProvider] { 24 | return self.providers.flatMap { (provider) -> [_CollectionViewMultiNodeProvider] in 25 | if let provider = provider as? _CollectionViewGroupProvider { 26 | return provider._providers 27 | } else { 28 | return [provider] 29 | } 30 | } 31 | } 32 | 33 | public func createProviders() -> Observable<[_CollectionViewMultiNodeProvider]> { 34 | return self.createAnimatableProviders().map { $0 as [_CollectionViewMultiNodeProvider] } 35 | } 36 | 37 | public func _createAnimatableNodes() -> Observable<[IdentifiableNode]> { 38 | return createAnimatableProviders().map { $0.map { $0._createAnimatableNodes() } } 39 | .flatMapLatest { Observable.combineLatest($0) { $0.flatMap { $0 } } } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flix/Provider/Group/AnimatableTableViewGroupProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableTableViewGroupProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 29/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol AnimatableTableViewGroupProvider: _AnimatableTableViewMultiNodeProvider, _TableViewGroupProvider { 14 | 15 | var providers: [_AnimatableTableViewMultiNodeProvider] { get } 16 | 17 | func createAnimatableProviders() -> Observable<[_AnimatableTableViewMultiNodeProvider]> 18 | 19 | } 20 | 21 | extension AnimatableTableViewGroupProvider { 22 | 23 | public var _providers: [_TableViewMultiNodeProvider] { 24 | return self.providers.flatMap { (provider) -> [_TableViewMultiNodeProvider] in 25 | if let provider = provider as? _TableViewGroupProvider { 26 | return provider._providers 27 | } else { 28 | return [provider] 29 | } 30 | } 31 | } 32 | 33 | public func createProviders() -> Observable<[_TableViewMultiNodeProvider]> { 34 | return self.createAnimatableProviders().map { $0 as [_TableViewMultiNodeProvider] } 35 | } 36 | 37 | public func _createAnimatableNodes() -> Observable<[IdentifiableNode]> { 38 | return createAnimatableProviders().map { $0.map { $0._createAnimatableNodes() } } 39 | .flatMapLatest { Observable.combineLatest($0) { $0.flatMap { $0 } } } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flix/Provider/Group/CollectionViewGroupProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewGroupProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 30/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol _CollectionViewGroupProvider { 14 | 15 | var _providers: [_CollectionViewMultiNodeProvider] { get } 16 | 17 | func createProviders() -> Observable<[_CollectionViewMultiNodeProvider]> 18 | 19 | } 20 | 21 | extension _CollectionViewGroupProvider where Self: _CollectionViewMultiNodeProvider { 22 | 23 | public func _configureCell(_ collectionView: UICollectionView, indexPath: IndexPath, node: _Node) -> UICollectionViewCell { 24 | fatalError("group provider is abstract provider, you should never call this methods.") 25 | } 26 | 27 | public func _itemSelected(_ collectionView: UICollectionView, indexPath: IndexPath, node: _Node) { 28 | fatalError("group provider is abstract provider, you should never call this methods.") 29 | } 30 | 31 | public func _itemDeselected(_ collectionView: UICollectionView, indexPath: IndexPath, node: _Node) { 32 | fatalError("group provider is abstract provider, you should never call this methods.") 33 | } 34 | 35 | public func _collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, node: _Node) -> CGSize? { 36 | fatalError("group provider is abstract provider, you should never call this methods.") 37 | } 38 | 39 | public func register(_ collectionView: UICollectionView) { 40 | for provider in __providers { 41 | provider._register(collectionView) 42 | } 43 | } 44 | 45 | public func _createNodes() -> Observable<[Node]> { 46 | return createProviders().map { $0.map { $0._createNodes() } } 47 | .flatMapLatest { Observable.combineLatest($0) { $0.flatMap { $0 } } } 48 | } 49 | 50 | } 51 | 52 | public protocol CollectionViewGroupProvider: _CollectionViewMultiNodeProvider, _CollectionViewGroupProvider { 53 | 54 | var providers: [_CollectionViewMultiNodeProvider] { get } 55 | 56 | } 57 | 58 | extension CollectionViewGroupProvider { 59 | 60 | public var _providers: [_CollectionViewMultiNodeProvider] { 61 | return self.providers.flatMap { (provider) -> [_CollectionViewMultiNodeProvider] in 62 | if let provider = provider as? _CollectionViewGroupProvider { 63 | return provider._providers 64 | } else { 65 | return [provider] 66 | } 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Flix/Provider/Group/TableViewGroupProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewGroupProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 30/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol _TableViewGroupProvider { 14 | 15 | var _providers: [_TableViewMultiNodeProvider] { get } 16 | 17 | func createProviders() -> Observable<[_TableViewMultiNodeProvider]> 18 | 19 | } 20 | 21 | extension _TableViewGroupProvider where Self: _TableViewMultiNodeProvider { 22 | 23 | public func _itemSelected(_ tableView: UITableView, indexPath: IndexPath, node: _Node) { 24 | fatalError("group provider is abstract provider, you should never call this methods.") 25 | } 26 | 27 | public func _itemDeselected(_ tableView: UITableView, indexPath: IndexPath, node: _Node) { 28 | fatalError("group provider is abstract provider, you should never call this methods.") 29 | } 30 | 31 | public func _tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath, node: _Node) -> CGFloat? { 32 | fatalError("group provider is abstract provider, you should never call this methods.") 33 | } 34 | 35 | public func _configureCell(_ tableView: UITableView, indexPath: IndexPath, node: _Node) -> UITableViewCell { 36 | fatalError("group provider is abstract provider, you should never call this methods.") 37 | } 38 | 39 | public func register(_ tableView: UITableView) { 40 | for provider in __providers { 41 | provider._register(tableView) 42 | } 43 | } 44 | 45 | public func _createNodes() -> Observable<[Node]> { 46 | return createProviders().map { $0.map { $0._createNodes() } } 47 | .flatMapLatest { Observable.combineLatest($0) { $0.flatMap { $0 } } } 48 | } 49 | 50 | } 51 | 52 | public protocol TableViewGroupProvider: _TableViewMultiNodeProvider, _TableViewGroupProvider { 53 | 54 | var providers: [_TableViewMultiNodeProvider] { get } 55 | 56 | } 57 | 58 | extension TableViewGroupProvider { 59 | 60 | public var _providers: [_TableViewMultiNodeProvider] { 61 | return self.providers.flatMap { (provider) -> [_TableViewMultiNodeProvider] in 62 | if let provider = provider as? _TableViewGroupProvider { 63 | return provider._providers 64 | } else { 65 | return [provider] 66 | } 67 | } 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /Flix/Provider/ProviderDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProviderDescription.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 24/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol FlixCustomStringConvertible: class, CustomStringConvertible { } 12 | 13 | extension FlixCustomStringConvertible { 14 | 15 | public var description: String { 16 | return _flix_description 17 | } 18 | 19 | var _flix_description: String { 20 | return "<\(type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())>" 21 | } 22 | 23 | public var _flix_identity: String { 24 | if let customIdentity = (self as? CustomIdentityType)?.customIdentity { 25 | return "\(self._flix_description)-\(customIdentity)" 26 | } else { 27 | return self._flix_description 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Flix/Provider/ProviderHiddenable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProviderHiddenable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/24. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol ProviderHiddenable: class, ReactiveCompatible { 14 | 15 | var isHidden: Bool { get set } 16 | 17 | } 18 | 19 | extension Reactive where Base: ProviderHiddenable { 20 | 21 | public var isHidden: Binder { 22 | return Binder(self.base, binding: { (provider, isHidden) in 23 | provider.isHidden = isHidden 24 | }) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Flix/Provider/TableViewEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewEvent.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/18. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public class TableViewEvent { 14 | 15 | public typealias Value = Provider.Value 16 | 17 | public typealias EventValue = (tableView: UITableView, indexPath: IndexPath, value: Value) 18 | 19 | public var selectedEvent: ControlEvent<()> { return ControlEvent(events: self._itemSelected.map { _ in }) } 20 | 21 | public var modelSelected: ControlEvent { return ControlEvent(events: self.itemSelected.map { $0.value }) } 22 | 23 | public var itemSelected: ControlEvent { return ControlEvent(events: self._itemSelected) } 24 | private(set) lazy var _itemSelected = PublishSubject() 25 | 26 | public var modelDeselected: ControlEvent { return ControlEvent(events: self.itemDeselected.map { $0.value }) } 27 | 28 | public var itemDeselected: ControlEvent { return ControlEvent(events: self._itemDeselected) } 29 | private(set) lazy var _itemDeselected = PublishSubject() 30 | 31 | public typealias MoveEventValue = (tableView: UITableView, sourceIndex: Int, destinationIndex: Int, value: Value) 32 | private(set) lazy var _moveItem = PublishSubject() 33 | 34 | private(set) lazy var _itemDeleted = PublishSubject() 35 | 36 | private(set) lazy var _itemInserted = PublishSubject() 37 | 38 | init() { } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Flix/Provider/TableViewSectionPartionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewSectionProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | 14 | public enum UITableElementKindSection { 15 | case header 16 | case footer 17 | } 18 | 19 | public protocol _SectionPartionTableViewProvider: FlixCustomStringConvertible { 20 | 21 | var cellType: UITableViewHeaderFooterView.Type { get } 22 | var tableElementKindSection: UITableElementKindSection { get } 23 | 24 | func _tableView(_ tableView: UITableView, heightInSection section: Int, node: _Node) -> CGFloat? 25 | func _configureSection(_ tableView: UITableView, view: UITableViewHeaderFooterView, viewInSection section: Int, node: _Node) 26 | 27 | func _createSectionPartion() -> Observable<_Node?> 28 | 29 | } 30 | 31 | extension _SectionPartionTableViewProvider { 32 | 33 | public func register(_ tableView: UITableView) { 34 | tableView.register(self.cellType, forHeaderFooterViewReuseIdentifier: self._flix_identity) 35 | } 36 | 37 | } 38 | 39 | public protocol SectionPartionTableViewProvider: _SectionPartionTableViewProvider, ReactiveCompatible { 40 | 41 | associatedtype Cell: UITableViewHeaderFooterView 42 | associatedtype Value 43 | 44 | func tableView(_ tableView: UITableView, heightInSection section: Int, value: Value) -> CGFloat? 45 | func configureSection(_ tableView: UITableView, view: UITableViewHeaderFooterView, viewInSection section: Int, value: Value) 46 | 47 | func createSection() -> Observable 48 | 49 | } 50 | 51 | extension SectionPartionTableViewProvider { 52 | 53 | public var cellType: UITableViewHeaderFooterView.Type { return Cell.self } 54 | 55 | public func _configureSection(_ tableView: UITableView, view: UITableViewHeaderFooterView, viewInSection section: Int, node: _Node) { 56 | self.configureSection(tableView, view: view as! Cell, viewInSection: section, value: node._unwarp()) 57 | } 58 | 59 | public func _createSectionPartion() -> Observable<_Node?> { 60 | let providerIdentity = self._flix_identity 61 | return createSection().map { $0.map { Node(providerIdentity: providerIdentity, value: $0) } } 62 | } 63 | 64 | public func _tableView(_ tableView: UITableView, heightInSection section: Int, node: _Node) -> CGFloat? { 65 | return self.tableView(tableView, heightInSection: section, value: node._unwarp()) 66 | } 67 | 68 | public func tableView(_ tableView: UITableView, heightInSection section: Int, node: _Node) -> CGFloat? { 69 | return nil 70 | } 71 | 72 | } 73 | 74 | public protocol _AnimatableSectionPartionProviderable { 75 | 76 | func _createAnimatableSectionPartion() -> Observable 77 | 78 | } 79 | 80 | public typealias _AnimatableSectionPartionTableViewProvider = _AnimatableSectionPartionProviderable & _SectionPartionTableViewProvider 81 | 82 | public protocol AnimatablePartionSectionTableViewProvider: SectionPartionTableViewProvider, _AnimatableSectionPartionProviderable where Value: Equatable, Value: StringIdentifiableType { 83 | 84 | func createAnimatableSectionPartion() -> Observable 85 | 86 | } 87 | 88 | extension AnimatablePartionSectionTableViewProvider { 89 | 90 | public func _createAnimatableSectionPartion() -> Observable { 91 | return createAnimatableSectionPartion() 92 | } 93 | 94 | public var identity: String { 95 | return self._flix_identity 96 | } 97 | 98 | } 99 | 100 | extension AnimatablePartionSectionTableViewProvider { 101 | 102 | public func createAnimatableSectionPartion() -> Observable { 103 | let providerIdentity = self._flix_identity 104 | return createSection().map { $0.map { IdentifiableNode(providerIdentity: providerIdentity, valueNode: $0) } } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Flix/Storyboard/FlixStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlixStackView.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 24/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public class FlixStackView: UIStackView { 14 | 15 | public private(set) var providers: [FlixStackItemProvider] = [] 16 | 17 | public let tableView = UITableView(frame: .zero, style: .grouped) 18 | 19 | private let disposeBag = DisposeBag() 20 | 21 | override public func didMoveToSuperview() { 22 | super.didMoveToSuperview() 23 | tableView.sectionFooterHeight = 0.1 24 | tableView.sectionHeaderHeight = 0.1 25 | tableView.estimatedRowHeight = 0 26 | tableView.estimatedSectionFooterHeight = 0 27 | tableView.estimatedSectionHeaderHeight = 0 28 | 29 | for view in self.arrangedSubviews { 30 | self.removeArrangedSubview(view) 31 | if let provider = view as? FlixStackItemProvider { 32 | let height = provider.bounds.height 33 | provider.itemHeight = { return height } 34 | providers.append(provider) 35 | } 36 | } 37 | 38 | self.addArrangedSubview(tableView) 39 | tableView.flix.animatable.build(providers) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Flix/TableViewEdit/TableViewDeleteable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDeleteable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 22/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol _TableViewDeleteable { 14 | 15 | func _tableView(_ tableView: UITableView, itemDeletedForRowAt indexPath: IndexPath, node: _Node) 16 | func _tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath, node: _Node) -> String? 17 | 18 | } 19 | 20 | public protocol TableViewDeleteable: TableViewEditable, _TableViewDeleteable { 21 | 22 | func tableView(_ tableView: UITableView, itemDeletedForRowAt indexPath: IndexPath, value: Value) 23 | func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath, value: Value) -> String? 24 | 25 | } 26 | 27 | extension TableViewDeleteable where Self: TableViewMultiNodeProvider { 28 | 29 | public func tableView(_ tableView: UITableView, itemDeletedForRowAt indexPath: IndexPath, value: Self.Value) { } 30 | 31 | public func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath, value: Self.Value) -> String? { 32 | return nil 33 | } 34 | 35 | public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, value: Self.Value) -> UITableViewCell.EditingStyle { 36 | return .delete 37 | } 38 | 39 | public func _tableView(_ tableView: UITableView, itemDeletedForRowAt indexPath: IndexPath, node: _Node) { 40 | self.tableView(tableView, itemDeletedForRowAt: indexPath, value: node._unwarp()) 41 | self.event._itemDeleted.onNext((tableView: tableView, indexPath: indexPath, value: node._unwarp())) 42 | } 43 | 44 | public func _tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath, node: _Node) -> String? { 45 | return self.tableView(tableView, titleForDeleteConfirmationButtonForRowAt: indexPath, value: node._unwarp()) 46 | } 47 | 48 | } 49 | 50 | extension TableViewEvent where Provider: TableViewDeleteable { 51 | 52 | public var modelDeleted: ControlEvent { 53 | return ControlEvent(events: self._itemDeleted.map { $0.value }) 54 | } 55 | 56 | public var itemDeleted: ControlEvent { 57 | return ControlEvent(events: self._itemDeleted) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Flix/TableViewEdit/TableViewEditable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewEditable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 07/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol _TableViewEditable { 12 | 13 | func _tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, node: _Node) -> [UITableViewRowAction]? 14 | func _tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath, node: _Node) -> Bool 15 | func _tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, node: _Node) -> UITableViewCell.EditingStyle 16 | 17 | } 18 | 19 | public protocol TableViewEditable: _TableViewEditable { 20 | 21 | associatedtype Value 22 | 23 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, value: Value) -> [UITableViewRowAction]? 24 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath, value: Value) -> Bool 25 | func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, value: Value) -> UITableViewCell.EditingStyle 26 | 27 | } 28 | 29 | extension TableViewEditable where Self: TableViewMultiNodeProvider { 30 | 31 | public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath, value: Self.Value) -> Bool { 32 | return true 33 | } 34 | 35 | public func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, value: Self.Value) -> [UITableViewRowAction]? { 36 | return nil 37 | } 38 | 39 | public func _tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, node: _Node) -> [UITableViewRowAction]? { 40 | return self.tableView(tableView, editActionsForRowAt: indexPath, value: node._unwarp()) 41 | } 42 | 43 | public func _tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath, node: _Node) -> Bool { 44 | return self.tableView(tableView, canEditRowAt: indexPath, value: node._unwarp()) 45 | } 46 | 47 | public func _tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, node: _Node) -> UITableViewCell.EditingStyle { 48 | return self.tableView(tableView, editingStyleForRowAt: indexPath, value: node._unwarp()) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Flix/TableViewEdit/TableViewInsertable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewInsertable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 21/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol _TableViewInsertable { 14 | 15 | func _tableView(_ tableView: UITableView, itemInsertedForRowAt indexPath: IndexPath, node: _Node) 16 | 17 | } 18 | 19 | public protocol TableViewInsertable: _TableViewInsertable, TableViewEditable { 20 | 21 | func tableView(_ tableView: UITableView, itemInsertedForRowAt indexPath: IndexPath, value: Value) 22 | 23 | } 24 | 25 | extension TableViewInsertable where Self: TableViewMultiNodeProvider { 26 | 27 | public func tableView(_ tableView: UITableView, itemInsertedForRowAt indexPath: IndexPath, value: Self.Value) { } 28 | 29 | public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, value: Self.Value) -> UITableViewCell.EditingStyle { 30 | return .insert 31 | } 32 | 33 | public func _tableView(_ tableView: UITableView, itemInsertedForRowAt indexPath: IndexPath, node: _Node) { 34 | self.tableView(tableView, itemInsertedForRowAt: indexPath, value: node._unwarp()) 35 | self.event._itemInserted.onNext((tableView: tableView, indexPath: indexPath, value: node._unwarp())) 36 | } 37 | 38 | } 39 | 40 | extension TableViewEvent where Provider: TableViewInsertable { 41 | 42 | public var modelInserted: ControlEvent { return ControlEvent(events: self._itemInserted.map { $0.value }) } 43 | 44 | public var itemInserted: ControlEvent { 45 | return ControlEvent(events: self._itemInserted) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Flix/TableViewEdit/TableViewMoveable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewMoveable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 20/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public protocol _TableViewMoveable: _TableViewEditable { 14 | 15 | func _tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath, node: _Node) -> Bool 16 | 17 | func _tableView(_ tableView: UITableView, moveRowAt sourceIndex: Int, to destinationIndex: Int, node: _Node) 18 | 19 | } 20 | 21 | public protocol TableViewMoveable: TableViewEditable, _TableViewMoveable { 22 | 23 | func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath, value: Value) -> Bool 24 | 25 | func tableView(_ tableView: UITableView, moveRowAt sourceIndex: Int, to destinationIndex: Int, value: Value) 26 | 27 | } 28 | 29 | extension TableViewMoveable where Self: TableViewMultiNodeProvider { 30 | 31 | public func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath, value: Self.Value) -> Bool { 32 | return true 33 | } 34 | 35 | public func tableView(_ tableView: UITableView, moveRowAt sourceIndex: Int, to destinationIndex: Int, value: Self.Value) { } 36 | 37 | public func _tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath, node: _Node) -> Bool { 38 | return self.tableView(tableView, canMoveRowAt: indexPath, value: node._unwarp()) 39 | } 40 | 41 | public func _tableView(_ tableView: UITableView, moveRowAt sourceIndex: Int, to destinationIndex: Int, node: _Node) { 42 | self.tableView(tableView, moveRowAt: sourceIndex, to: destinationIndex, value: node._unwarp()) 43 | self.event._moveItem.onNext((tableView: tableView, sourceIndex: sourceIndex, destinationIndex: destinationIndex, value: node._unwarp())) 44 | } 45 | 46 | } 47 | 48 | extension TableViewEvent where Provider: TableViewMoveable { 49 | 50 | public var moveItem: ControlEvent { 51 | return ControlEvent(events: self._moveItem) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Flix/TableViewEdit/TableViewSwipeable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewSwipeable.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/5/12. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | @available(iOS 11.0, *) 14 | public protocol _TableViewSwipeable { 15 | 16 | func _tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath, node: _Node) -> UISwipeActionsConfiguration? 17 | func _tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath, node: _Node) -> UISwipeActionsConfiguration? 18 | 19 | } 20 | 21 | @available(iOS 11.0, *) 22 | public protocol TableViewSwipeable: TableViewEditable, _TableViewSwipeable { 23 | 24 | func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath, value: Value) -> UISwipeActionsConfiguration? 25 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath, value: Value) -> UISwipeActionsConfiguration? 26 | 27 | } 28 | 29 | @available(iOS 11.0, *) 30 | extension TableViewSwipeable where Self: TableViewMultiNodeProvider { 31 | 32 | public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath, value: Self.Value) -> UITableViewCell.EditingStyle { 33 | return .none 34 | } 35 | 36 | public func _tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath, node: _Node) -> UISwipeActionsConfiguration? { 37 | return self.tableView(tableView, leadingSwipeActionsConfigurationForRowAt: indexPath, value: node._unwarp()) 38 | } 39 | 40 | public func _tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath, node: _Node) -> UISwipeActionsConfiguration? { 41 | return self.tableView(tableView, trailingSwipeActionsConfigurationForRowAt: indexPath, value: node._unwarp()) 42 | } 43 | 44 | public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath, value: Self.Value) -> UISwipeActionsConfiguration? { 45 | return nil 46 | } 47 | 48 | public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath, value: Self.Value) -> UISwipeActionsConfiguration? { 49 | return nil 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Flix/UniqueCustomProvider/CustomProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 2018/4/13. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CustomProvider: class { 12 | 13 | associatedtype Cell: NSObject 14 | 15 | func whenGetCell(_ cellConfig: @escaping (Cell) -> ()) 16 | 17 | } 18 | 19 | private var cellQueuesKey: Void? 20 | private var cellKey: Void? 21 | 22 | extension CustomProvider { 23 | 24 | public var cell: Cell? { 25 | get { 26 | return objc_getAssociatedObject(self, &cellKey) as? Cell 27 | } 28 | set { 29 | objc_setAssociatedObject(self, &cellKey, newValue, .OBJC_ASSOCIATION_ASSIGN) 30 | } 31 | } 32 | 33 | private var configCellQueues: [(Cell) -> ()] { 34 | get { 35 | return (objc_getAssociatedObject(self, &cellQueuesKey) as? [(Cell) -> ()]) ?? [] 36 | } 37 | set { 38 | objc_setAssociatedObject(self, &cellQueuesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 39 | } 40 | } 41 | 42 | public func whenGetCell(_ cellConfig: @escaping (Cell) -> ()) { 43 | if let cell = self.cell { 44 | cellConfig(cell) 45 | } else { 46 | configCellQueues.append(cellConfig) 47 | } 48 | } 49 | 50 | func onGetCell(_ cell: Cell) { 51 | self.cell = cell 52 | for config in configCellQueues { 53 | config(cell) 54 | } 55 | configCellQueues.removeAll() 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Flix/UniqueCustomProvider/NeverHitSelfView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeverHitSelfView.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 25/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NeverHitSelfView: UIView { 12 | 13 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 14 | guard let result = super.hitTest(point, with: event), result !== self else { return nil } 15 | return result 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Flix/UniqueCustomProvider/UniqueCustomCollectionViewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueCustomCollectionViewProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | public typealias SingleUICollectionViewCellProvider = SingleCollectionViewProvider 14 | @available(*, deprecated, renamed: "SingleUICollectionViewCellProvider") 15 | public typealias UniqueCustomCollectionViewProvider = SingleUICollectionViewCellProvider 16 | 17 | open class SingleCollectionViewProvider: CustomProvider, UniqueAnimatableCollectionViewProvider, ProviderHiddenable, CustomIdentityType { 18 | 19 | public typealias Cell = UICollectionViewCell 20 | 21 | public let customIdentity: String 22 | 23 | public let contentView: UIView = NeverHitSelfView() 24 | 25 | open var selectedBackgroundView: UIView? { 26 | didSet { 27 | whenGetCell { [weak self] (cell) in 28 | guard let `self` = self else { return } 29 | cell.selectedBackgroundView = self.selectedBackgroundView 30 | } 31 | } 32 | } 33 | 34 | open var backgroundView: UIView? { 35 | didSet { 36 | whenGetCell { [weak self] (cell) in 37 | guard let `self` = self else { return } 38 | cell.backgroundView = self.backgroundView 39 | } 40 | } 41 | } 42 | 43 | @available(*, deprecated, renamed: "event.selectedEvent") 44 | public var tap: ControlEvent<()> { return self.event.selectedEvent } 45 | 46 | open var itemSize: (() -> CGSize?)? 47 | 48 | open var isHidden: Bool { 49 | get { 50 | return _isHidden.value 51 | } 52 | set { 53 | _isHidden.accept(newValue) 54 | } 55 | } 56 | private let _isHidden = BehaviorRelay(value: false) 57 | 58 | public init(customIdentity: String) { 59 | self.customIdentity = customIdentity 60 | } 61 | 62 | public init() { 63 | self.customIdentity = "" 64 | } 65 | 66 | open func onCreate(_ collectionView: UICollectionView, cell: UICollectionViewCell, indexPath: IndexPath) { 67 | self.onGetCell(cell) 68 | cell.selectedBackgroundView = self.selectedBackgroundView 69 | cell.backgroundView = self.backgroundView 70 | cell.contentView.addSubview(contentView) 71 | contentView.translatesAutoresizingMaskIntoConstraints = false 72 | contentView.topAnchor.constraint(equalTo: cell.contentView.topAnchor).isActive = true 73 | contentView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor).isActive = true 74 | contentView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor).isActive = true 75 | contentView.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true 76 | } 77 | 78 | open func itemSelected(_ collectionView: UICollectionView, indexPath: IndexPath, value: SingleCollectionViewProvider) { 79 | collectionView.deselectItem(at: indexPath, animated: true) 80 | } 81 | 82 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath, value: SingleCollectionViewProvider) -> CGSize? { 83 | return self.itemSize?() 84 | } 85 | 86 | open func createValues() -> Observable<[SingleCollectionViewProvider]> { 87 | return self._isHidden.asObservable() 88 | .map { [weak self] isHidden in 89 | guard let `self` = self, !isHidden else { return [] } 90 | return [self] 91 | } 92 | } 93 | 94 | open func register(_ collectionView: UICollectionView) { 95 | collectionView.register(Cell.self, forCellWithReuseIdentifier: self._flix_identity) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Flix/UniqueCustomProvider/UniqueCustomCollectionViewSectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueCustomCollectionViewSectionProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 06/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | open class UniqueCustomCollectionViewSectionProvider: AnimatableSectionPartionCollectionViewProvider, StringIdentifiableType, ProviderHiddenable, Equatable { 14 | 15 | public var identity: String { 16 | return self._flix_identity 17 | } 18 | 19 | public let customIdentity: String 20 | public let collectionElementKindSection: UICollectionElementKindSection 21 | 22 | public typealias Cell = UICollectionReusableView 23 | public typealias Value = UniqueCustomCollectionViewSectionProvider 24 | 25 | open var isHidden: Bool { 26 | get { 27 | return _isHidden.value 28 | } 29 | set { 30 | _isHidden.accept(newValue) 31 | } 32 | } 33 | private let _isHidden = BehaviorRelay(value: false) 34 | 35 | open var sectionSize: ((UICollectionView) -> CGSize)? 36 | 37 | public let contentView: UIView = NeverHitSelfView() 38 | 39 | public init(customIdentity: String, collectionElementKindSection: UICollectionElementKindSection) { 40 | self.customIdentity = customIdentity 41 | self.collectionElementKindSection = collectionElementKindSection 42 | } 43 | 44 | public init(collectionElementKindSection: UICollectionElementKindSection) { 45 | self.customIdentity = "" 46 | self.collectionElementKindSection = collectionElementKindSection 47 | } 48 | 49 | public static func ==(lhs: UniqueCustomCollectionViewSectionProvider, rhs: UniqueCustomCollectionViewSectionProvider) -> Bool { 50 | return true 51 | } 52 | 53 | open func configureSupplementaryView(_ collectionView: UICollectionView, sectionView: Cell, indexPath: IndexPath, value: Value) { 54 | sectionView.addSubview(contentView) 55 | contentView.translatesAutoresizingMaskIntoConstraints = false 56 | contentView.leadingAnchor.constraint(equalTo: sectionView.leadingAnchor).isActive = true 57 | contentView.topAnchor.constraint(equalTo: sectionView.topAnchor).isActive = true 58 | contentView.bottomAnchor.constraint(equalTo: sectionView.bottomAnchor).isActive = true 59 | contentView.trailingAnchor.constraint(equalTo: sectionView.trailingAnchor).isActive = true 60 | } 61 | 62 | open func createSectionPartion() -> Observable { 63 | return self._isHidden.asObservable() 64 | .map { [weak self] isHidden in 65 | return isHidden ? nil : self 66 | } 67 | } 68 | 69 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeInSection section: Int, value: Value) -> CGSize? { 70 | return sectionSize?(collectionView) 71 | } 72 | 73 | } 74 | 75 | extension Reactive where Base: UniqueCustomCollectionViewSectionProvider { 76 | 77 | public var isHidden: Binder { 78 | return Binder(self.base) { provider, hidden in 79 | provider.isHidden = hidden 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Flix/UniqueCustomProvider/UniqueCustomTableViewSectionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueCustomTableViewSectionProvider.swift 3 | // Flix 4 | // 5 | // Created by DianQK on 04/10/2017. 6 | // Copyright © 2017 DianQK. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | open class UniqueCustomTableViewSectionProvider: AnimatablePartionSectionTableViewProvider, StringIdentifiableType, ProviderHiddenable, Equatable { 14 | 15 | public var identity: String { 16 | return self._flix_identity 17 | } 18 | 19 | public static func ==(lhs: UniqueCustomTableViewSectionProvider, rhs: UniqueCustomTableViewSectionProvider) -> Bool { 20 | return true 21 | } 22 | 23 | public typealias Cell = UITableViewHeaderFooterView 24 | public typealias Value = UniqueCustomTableViewSectionProvider 25 | 26 | public let customIdentity: String 27 | public let tableElementKindSection: UITableElementKindSection 28 | 29 | open var isHidden: Bool { 30 | get { 31 | return _isHidden.value 32 | } 33 | set { 34 | _isHidden.accept(newValue) 35 | } 36 | } 37 | private let _isHidden = BehaviorRelay(value: false) 38 | 39 | open var sectionHeight: ((UITableView) -> CGFloat)? 40 | 41 | public let contentView: UIView = NeverHitSelfView() 42 | open var backgroundView: UIView? { 43 | didSet { 44 | _view?.backgroundView = backgroundView 45 | } 46 | } 47 | 48 | private weak var _view: UITableViewHeaderFooterView? 49 | 50 | public init(customIdentity: String, tableElementKindSection: UITableElementKindSection) { 51 | self.customIdentity = customIdentity 52 | self.tableElementKindSection = tableElementKindSection 53 | } 54 | 55 | public init(tableElementKindSection: UITableElementKindSection) { 56 | self.customIdentity = "" 57 | self.tableElementKindSection = tableElementKindSection 58 | } 59 | 60 | open func tableView(_ tableView: UITableView, heightInSection section: Int, value: UniqueCustomTableViewSectionProvider) -> CGFloat? { 61 | return self.sectionHeight?(tableView) 62 | } 63 | 64 | open func configureSection(_ tableView: UITableView, view: UITableViewHeaderFooterView, viewInSection section: Int, value: UniqueCustomTableViewSectionProvider) { 65 | if !view.hasConfigured { 66 | _view = view 67 | view.hasConfigured = true 68 | view.backgroundView = self.backgroundView 69 | view.contentView.addSubview(contentView) 70 | contentView.translatesAutoresizingMaskIntoConstraints = false 71 | contentView.topAnchor.constraint(equalTo: view.contentView.topAnchor).isActive = true 72 | contentView.leadingAnchor.constraint(equalTo: view.contentView.leadingAnchor).isActive = true 73 | contentView.trailingAnchor.constraint(equalTo: view.contentView.trailingAnchor).isActive = true 74 | contentView.bottomAnchor.constraint(equalTo: view.contentView.bottomAnchor).isActive = true 75 | } 76 | } 77 | 78 | open func createSection() -> Observable { 79 | return self._isHidden.asObservable() 80 | .map { [weak self] isHidden in 81 | return isHidden ? nil : self 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /FlixTests/CollectionViewBuilderMemoryLeakTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewBuilderMemoryLeakTests.swift 3 | // FlixTests 4 | // 5 | // Created by DianQK on 2018/4/13. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Flix 11 | 12 | class CollectionViewBuilderMemoryLeakTests: XCTestCase { 13 | 14 | weak var collectionViewBuilder: CollectionViewBuilder? 15 | weak var animatableCollectionViewBuilder: AnimatableCollectionViewBuilder? 16 | 17 | override func setUp() { 18 | collectionViewBuilder = nil 19 | animatableCollectionViewBuilder = nil 20 | } 21 | 22 | func testCollectionViewBuilderMemoryLeak() { 23 | var collectionView: UICollectionView? = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 24 | var builder: CollectionViewBuilder? = CollectionViewBuilder(collectionView: collectionView!, providers: [SingleUICollectionViewCellProvider()]) 25 | collectionViewBuilder = builder 26 | builder = nil 27 | collectionView = nil 28 | XCTAssertNil(collectionView) 29 | XCTAssertNil(collectionViewBuilder) 30 | } 31 | 32 | func testAnimatableCollectionViewBuilderMemoryLeak() { 33 | var collectionView: UICollectionView? = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 34 | var builder: AnimatableCollectionViewBuilder? = AnimatableCollectionViewBuilder(collectionView: collectionView!, providers: [SingleUICollectionViewCellProvider()]) 35 | animatableCollectionViewBuilder = builder 36 | builder = nil 37 | collectionView = nil 38 | XCTAssertNil(collectionView) 39 | XCTAssertNil(animatableCollectionViewBuilder) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /FlixTests/FlixTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlixTests.swift 3 | // FlixTests 4 | // 5 | // Created by DianQK on 2018/4/13. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Flix 11 | 12 | class FlixTests: XCTestCase { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /FlixTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FlixTests/SingleProviderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleProviderTests.swift 3 | // FlixTests 4 | // 5 | // Created by DianQK on 2018/4/13. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Flix 11 | 12 | class SingleProviderTests: XCTestCase { 13 | 14 | func testTableViewWhenGetCellMemoryLeak() { 15 | var tableView: UITableView? = UITableView(frame: .zero, style: .grouped) 16 | var provider: SingleUITableViewCellProvider? = SingleUITableViewCellProvider() 17 | weak var weakProvider = provider 18 | provider!.selectionStyle = .none 19 | provider!.accessoryType = .checkmark 20 | provider!.accessoryView = UIView() 21 | provider!.backgroundView = UIView() 22 | provider!.editingAccessoryType = .checkmark 23 | provider!.editingAccessoryView = UIView() 24 | provider!.whenGetCell { (cell) in 25 | cell.textLabel?.text = "Flix" 26 | } 27 | weak var builder: AnimatableTableViewBuilder? = AnimatableTableViewBuilder(tableView: tableView!, providers: [provider!]) 28 | tableView = nil 29 | provider = nil 30 | let expectation = self.expectation(description: "wait for provider disposed") 31 | DispatchQueue.main.async { 32 | XCTAssertNil(tableView) 33 | XCTAssertNil(builder) 34 | XCTAssertNil(provider) 35 | XCTAssertNil(weakProvider) 36 | expectation.fulfill() 37 | } 38 | self.waitForExpectations(timeout: 1, handler: nil) 39 | } 40 | 41 | func testCollectionViewWhenGetCellMemoryLeak() { 42 | var collectionView: UICollectionView? = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 43 | var provider: SingleUICollectionViewCellProvider? = SingleUICollectionViewCellProvider() 44 | weak var weakProvider = provider 45 | weak var builder: AnimatableCollectionViewBuilder? = AnimatableCollectionViewBuilder(collectionView: collectionView!, providers: [provider!]) 46 | provider!.backgroundView = UIView() 47 | provider!.selectedBackgroundView = UIView() 48 | collectionView = nil 49 | provider = nil 50 | let expectation = self.expectation(description: "wait for provider disposed") 51 | DispatchQueue.main.async { 52 | XCTAssertNil(collectionView) 53 | XCTAssertNil(builder) 54 | XCTAssertNil(provider) 55 | XCTAssertNil(weakProvider) 56 | expectation.fulfill() 57 | } 58 | self.waitForExpectations(timeout: 1, handler: nil) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /FlixTests/TableViewBuilderMemoryLeakTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewBuilderMemoryLeakTests.swift 3 | // FlixTests 4 | // 5 | // Created by DianQK on 2018/4/13. 6 | // Copyright © 2018 DianQK. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Flix 11 | 12 | class TableViewBuilderMemoryLeakTests: XCTestCase { 13 | 14 | func testTableBuilderMemoryLeak() { 15 | var tableView: UITableView? = UITableView(frame: .zero, style: .grouped) 16 | weak var builder: TableViewBuilder? = TableViewBuilder(tableView: tableView!, providers: [SingleUITableViewCellProvider()]) 17 | tableView = nil 18 | XCTAssertNil(tableView) 19 | XCTAssertNil(builder) 20 | } 21 | 22 | func testAnimatableTableViewBuilderMemoryLeak() { 23 | var tableView: UITableView? = UITableView(frame: .zero, style: .grouped) 24 | weak var builder: AnimatableTableViewBuilder? = AnimatableTableViewBuilder(tableView: tableView!, providers: [SingleUITableViewCellProvider()]) 25 | tableView = nil 26 | XCTAssertNil(tableView) 27 | XCTAssertNil(builder) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "cocoapods", '~> 1.9' 6 | gem "xcpretty" 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | algoliasearch (1.27.1) 11 | httpclient (~> 2.8, >= 2.8.3) 12 | json (>= 1.5.1) 13 | atomos (0.1.3) 14 | claide (1.0.3) 15 | cocoapods (1.9.1) 16 | activesupport (>= 4.0.2, < 5) 17 | claide (>= 1.0.2, < 2.0) 18 | cocoapods-core (= 1.9.1) 19 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 20 | cocoapods-downloader (>= 1.2.2, < 2.0) 21 | cocoapods-plugins (>= 1.0.0, < 2.0) 22 | cocoapods-search (>= 1.0.0, < 2.0) 23 | cocoapods-stats (>= 1.0.0, < 2.0) 24 | cocoapods-trunk (>= 1.4.0, < 2.0) 25 | cocoapods-try (>= 1.1.0, < 2.0) 26 | colored2 (~> 3.1) 27 | escape (~> 0.0.4) 28 | fourflusher (>= 2.3.0, < 3.0) 29 | gh_inspector (~> 1.0) 30 | molinillo (~> 0.6.6) 31 | nap (~> 1.0) 32 | ruby-macho (~> 1.4) 33 | xcodeproj (>= 1.14.0, < 2.0) 34 | cocoapods-core (1.9.1) 35 | activesupport (>= 4.0.2, < 6) 36 | algoliasearch (~> 1.0) 37 | concurrent-ruby (~> 1.1) 38 | fuzzy_match (~> 2.0.4) 39 | nap (~> 1.0) 40 | netrc (~> 0.11) 41 | typhoeus (~> 1.0) 42 | cocoapods-deintegrate (1.0.4) 43 | cocoapods-downloader (1.3.0) 44 | cocoapods-plugins (1.0.0) 45 | nap 46 | cocoapods-search (1.0.0) 47 | cocoapods-stats (1.1.0) 48 | cocoapods-trunk (1.4.1) 49 | nap (>= 0.8, < 2.0) 50 | netrc (~> 0.11) 51 | cocoapods-try (1.1.0) 52 | colored2 (3.1.2) 53 | concurrent-ruby (1.1.6) 54 | escape (0.0.4) 55 | ethon (0.12.0) 56 | ffi (>= 1.3.0) 57 | ffi (1.12.2) 58 | fourflusher (2.3.1) 59 | fuzzy_match (2.0.4) 60 | gh_inspector (1.1.3) 61 | httpclient (2.8.3) 62 | i18n (0.9.5) 63 | concurrent-ruby (~> 1.0) 64 | json (2.3.0) 65 | minitest (5.14.0) 66 | molinillo (0.6.6) 67 | nanaimo (0.2.6) 68 | nap (1.1.0) 69 | netrc (0.11.0) 70 | rouge (2.0.7) 71 | ruby-macho (1.4.0) 72 | thread_safe (0.3.6) 73 | typhoeus (1.3.1) 74 | ethon (>= 0.9.0) 75 | tzinfo (1.2.6) 76 | thread_safe (~> 0.1) 77 | xcodeproj (1.15.0) 78 | CFPropertyList (>= 2.3.3, < 4.0) 79 | atomos (~> 0.1.3) 80 | claide (>= 1.0.2, < 2.0) 81 | colored2 (~> 3.1) 82 | nanaimo (~> 0.2.6) 83 | xcpretty (0.3.0) 84 | rouge (~> 2.0.7) 85 | 86 | PLATFORMS 87 | ruby 88 | 89 | DEPENDENCIES 90 | cocoapods (~> 1.9) 91 | xcpretty 92 | 93 | BUNDLED WITH 94 | 1.17.3 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 DianQK 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 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | 3 | use_frameworks! 4 | inhibit_all_warnings! 5 | 6 | def source 7 | pod 'RxSwift', '~> 5.0' 8 | pod 'RxCocoa', '~> 5.0' 9 | pod 'RxDataSources', '~> 4.0' 10 | end 11 | 12 | target 'Flix' do 13 | source 14 | 15 | target 'FlixTests' do 16 | end 17 | end 18 | 19 | target 'Example' do 20 | source 21 | pod 'RxKeyboard', '~> 1.0' 22 | end 23 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Differentiator (4.0.1) 3 | - RxCocoa (5.1.0): 4 | - RxRelay (~> 5) 5 | - RxSwift (~> 5) 6 | - RxDataSources (4.0.1): 7 | - Differentiator (~> 4.0) 8 | - RxCocoa (~> 5.0) 9 | - RxSwift (~> 5.0) 10 | - RxKeyboard (1.0.0): 11 | - RxCocoa (~> 5.0) 12 | - RxSwift (~> 5.0) 13 | - RxRelay (5.1.0): 14 | - RxSwift (~> 5) 15 | - RxSwift (5.1.0) 16 | 17 | DEPENDENCIES: 18 | - RxCocoa (~> 5.0) 19 | - RxDataSources (~> 4.0) 20 | - RxKeyboard (~> 1.0) 21 | - RxSwift (~> 5.0) 22 | 23 | SPEC REPOS: 24 | trunk: 25 | - Differentiator 26 | - RxCocoa 27 | - RxDataSources 28 | - RxKeyboard 29 | - RxRelay 30 | - RxSwift 31 | 32 | SPEC CHECKSUMS: 33 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602 34 | RxCocoa: 13d2a4d7546a34b8ececae8c281e4ea1dbb94f2b 35 | RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 36 | RxKeyboard: 6683c4344304a00f943c158bd8a43ce5469c82a7 37 | RxRelay: a168bd6caf712d00c676ac344e9295afc93b418e 38 | RxSwift: ad5874f24bb0dbffd1e9bb8443604e3578796c7a 39 | 40 | PODFILE CHECKSUM: 070def0c901e4b5f7392309599ebea6374c10d00 41 | 42 | COCOAPODS: 1.9.1 43 | -------------------------------------------------------------------------------- /block_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/block_diagram.png -------------------------------------------------------------------------------- /screenshots/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/screenshots/example.png -------------------------------------------------------------------------------- /screenshots/tutorial_0_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/screenshots/tutorial_0_profile.png -------------------------------------------------------------------------------- /screenshots/tutorial_1_profile_with_section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/screenshots/tutorial_1_profile_with_section.png -------------------------------------------------------------------------------- /screenshots/tutorial_2_more_sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/screenshots/tutorial_2_more_sections.png -------------------------------------------------------------------------------- /screenshots/tutorial_3_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianqk/Flix/00ce474b210aa0e5df60af6f2880707bf5624321/screenshots/tutorial_3_final.png --------------------------------------------------------------------------------