├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── .travis.yml ├── CHANGELOG.md ├── Cartfile ├── Cartfile.resolved ├── Differentiator.podspec ├── Examples ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── RxDataSources.xcscmblueprint │ └── xcshareddata │ │ └── xcschemes │ │ ├── Example.xcscheme │ │ └── ExampleUITests.xcscheme ├── Example │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── settings.imageset │ │ │ ├── Contents.json │ │ │ └── settings@2x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Example1_CustomizationUsingTableViewDelegate.swift │ ├── Example2_RandomizedSectionsAnimation.swift │ ├── Example3_TableViewEditing.swift │ ├── Example4_DifferentSectionAndItemTypes.swift │ ├── Example5_UIPickerView.swift │ ├── Info.plist │ ├── Support │ │ ├── AppDelegate.swift │ │ ├── NumberSection.swift │ │ └── Randomizer.swift │ └── Views │ │ ├── ImageTitleTableViewCell.swift │ │ ├── TitleSteperTableViewCell.swift │ │ ├── TitleSwitchTableViewCell.swift │ │ └── UIKitExtensions.swift ├── ExampleUITests │ ├── ExampleUITests.swift │ └── Info.plist ├── RxSwift └── Sources ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── RxDataSources.podspec ├── RxDataSources.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── RxDataSources.xcscmblueprint └── xcshareddata │ └── xcschemes │ ├── Differentiator.xcscheme │ ├── RxDataSources.xcscheme │ └── Tests.xcscheme ├── Sources ├── Differentiator │ ├── AnimatableSectionModel.swift │ ├── AnimatableSectionModelType+ItemPath.swift │ ├── AnimatableSectionModelType.swift │ ├── Changeset.swift │ ├── Diff.swift │ ├── Differentiator.h │ ├── IdentifiableType.swift │ ├── IdentifiableValue.swift │ ├── Info.plist │ ├── ItemPath.swift │ ├── Optional+Extensions.swift │ ├── SectionModel.swift │ ├── SectionModelType.swift │ └── Utilities.swift └── RxDataSources │ ├── AnimationConfiguration.swift │ ├── Array+Extensions.swift │ ├── CollectionViewSectionedDataSource.swift │ ├── DataSources.swift │ ├── Deprecated.swift │ ├── FloatingPointType+IdentifiableType.swift │ ├── Info.plist │ ├── IntegerType+IdentifiableType.swift │ ├── RxCollectionViewSectionedAnimatedDataSource.swift │ ├── RxCollectionViewSectionedReloadDataSource.swift │ ├── RxDataSources.h │ ├── RxPickerViewAdapter.swift │ ├── RxTableViewSectionedAnimatedDataSource.swift │ ├── RxTableViewSectionedReloadDataSource.swift │ ├── String+IdentifiableType.swift │ ├── TableViewSectionedDataSource.swift │ ├── UI+SectionedViewType.swift │ └── ViewTransition.swift └── Tests └── RxDataSourcesTests ├── AlgorithmTests.swift ├── Array+Extensions.swift ├── ChangeSet+TestExtensions.swift ├── Info.plist ├── NumberSection.swift ├── Randomizer.swift ├── RxCollectionViewSectionedDataSource+Test.swift ├── XCTest+Extensions.swift ├── i.swift └── s.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | Build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | 21 | timeline.xctimeline 22 | 23 | # CocoaPods 24 | # 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | Pods 30 | 31 | # Carthage 32 | # 33 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 34 | Carthage/Checkouts 35 | Carthage/Build 36 | 37 | 38 | # Various 39 | 40 | .DS_Store 41 | 42 | 43 | # Linux 44 | 45 | *.swp 46 | *.swo 47 | 48 | # Swift Package Manager 49 | 50 | .build/ 51 | Packages/ 52 | .swiftpm/ 53 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/RxSwift"] 2 | url = https://github.com/ReactiveX/RxSwift.git 3 | path = Carthage/Checkouts/RxSwift 4 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | opt_in_rules: 5 | - overridden_super_call 6 | - private_outlet 7 | - prohibited_super_call 8 | - first_where 9 | - closure_spacing 10 | - unneeded_parentheses_in_closure_argument 11 | - redundant_nil_coalescing 12 | - pattern_matching_keywords 13 | - explicit_init 14 | - contains_over_first_not_nil 15 | disabled_rules: 16 | - line_length 17 | - trailing_whitespace 18 | - type_name 19 | - identifier_name 20 | - vertical_whitespace 21 | - trailing_newline 22 | - opening_brace 23 | - large_tuple 24 | - file_length 25 | - comma 26 | - colon 27 | - private_over_fileprivate 28 | - force_cast 29 | - force_try 30 | - function_parameter_count 31 | - statement_position 32 | - legacy_hashing 33 | - todo 34 | - operator_whitespace 35 | - type_body_length 36 | - function_body_length 37 | - cyclomatic_complexity 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | osx_image: xcode11.5 4 | 5 | notifications: 6 | slack: rxswift:3ykt2Z61f8GkdvhCZTYPduOL 7 | email: false 8 | 9 | install: true 10 | 11 | env: 12 | - BUILD="brew install swiftlint" 13 | - BUILD="carthage update --platform iOS && pushd Examples && set -o pipefail && (xcodebuild -project Example.xcodeproj -scheme ExampleUITests -configuration Release -destination 'platform=iOS Simulator,name=iPhone 8' test && xcodebuild -project Example.xcodeproj -scheme Example -configuration Release -destination 'platform=iOS Simulator,name=iPhone 8' build) | xcpretty" 14 | - BUILD="carthage update --platform iOS && set -o pipefail && (xcodebuild -project RxDataSources.xcodeproj -scheme Tests -configuration Release -destination 'platform=iOS Simulator,name=iPhone 8' test) | xcpretty" 15 | - BUILD="gem install cocoapods --pre --no-document --quiet; pod repo update && pod lib lint RxDataSources.podspec --verbose && pod lib lint Differentiator.podspec --verbose " 16 | - BUILD="carthage update --platform iOS && carthage build --no-skip-current --platform iOS" 17 | - BUILD="carthage update --platform tvOS && carthage build --no-skip-current --platform tvOS" 18 | - BUILD="swift test" 19 | 20 | script: eval "${BUILD}" 21 | 22 | after_success: 23 | - sleep 5 # workaround https://github.com/travis-ci/travis-ci/issues/4725 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | --- 5 | 6 | #### Unreleased 7 | 8 | * Fixes xcodeproj and submodule structure to avoid duplicate symbols and properly build for Carthage #392 9 | * Adds support of mutable CellViewModels. 10 | * Changes `TableViewSectionedDataSource` default parameters `canEditRowAtIndexPath` and `canMoveRowAtIndexPath` to align with iOS default behavior #383 11 | 12 | ## [4.0.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/4.0.1) 13 | 14 | * Fixes Carthage integration and reverts static frameworks to dynamic frameworks. 15 | 16 | ## [4.0.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/4.0.0) 17 | 18 | * Swift 5.0 19 | * Fixes problems with `UICollectionView` animation crashes. 20 | * Improves readability by renaming short generic names to more descriptive names. 21 | * Changes frameworks to be static libs. (Carthage integration) 22 | 23 | ## [3.1.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/3.1.0) 24 | 25 | * Xcode 10.0 compatibility. 26 | 27 | ## [3.0.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/3.0.2) 28 | 29 | * Makes `configureSupplementaryView` optional for reload data source. #186 30 | 31 | ## [3.0.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/3.0.1) 32 | 33 | * Adds custom logic to control should perform animated updates. 34 | * Fixes SPM integration. 35 | 36 | ## [3.0.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/3.0.0) 37 | 38 | * Adapted for RxSwift 4.0 39 | 40 | ## [3.0.0-rc.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/3.0.0-rc.0) 41 | 42 | * Cleans up public interface to use initializers vs nillable properties and deprecates nillable properties in favor of passing parameters 43 | through init. 44 | 45 | ## [2.0.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/2.0.2) 46 | 47 | * Adds Swift Package Manager support 48 | 49 | ## [2.0.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/2.0.1) 50 | 51 | * Fixes issue with CocoaPods and Carthage integration. 52 | 53 | ## [2.0.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/2.0.0) 54 | 55 | * Adds `UIPickerView` extensions. 56 | * Separates `Differentiator` from `RxDataSources`. 57 | 58 | ## [1.0.4](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.4) 59 | 60 | #### Anomalies 61 | * Fixed crash that happened when using a combination of `estimatedHeightForRow` and `tableFooterView`. #129 62 | 63 | ## [1.0.3](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.3) 64 | 65 | #### Anomalies 66 | 67 | * #84 Set data source sections even if view is not in view hierarchy. 68 | * #93 Silence optional debug print warning in swift 3.1 69 | * #96 Adds additional call to `invalidateLayout` after reloading data. 70 | 71 | ## [1.0.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.2) 72 | 73 | * Fixes issue with performing batch updates on view that is not in view hierarchy. 74 | 75 | ## [1.0.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.1) 76 | 77 | * Fixes invalid version in bundle id. 78 | * Update CFBundleShortVersionString to current release version number. 79 | 80 | ## [1.0.0](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.0) 81 | 82 | * Small polish of public interface. 83 | 84 | ## [1.0.0-rc.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.0-rc.2) 85 | 86 | #### Features 87 | 88 | * Makes rest of data source classes and methods open. 89 | * Small polish for UI. 90 | * Removes part of deprecated extensions. 91 | 92 | ## [1.0.0-rc.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.0-rc.1) 93 | 94 | #### Features 95 | 96 | * Makes data sources open. 97 | * Adaptations for RxSwift 3.0.0-rc.1 98 | 99 | ## [1.0.0-beta.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.0-beta.2) 100 | 101 | #### Features 102 | 103 | * Adaptations for Swift 3.0 104 | 105 | #### Fixes 106 | 107 | * Improves collection view animated updates behavior. 108 | 109 | ## [1.0.0.beta.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/1.0.0.beta.1) 110 | 111 | #### Features 112 | 113 | * Adaptations for Swift 3.0 114 | 115 | #### Fixes 116 | 117 | * Fixes `moveItem` 118 | 119 | ## [0.9](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.8.1) 120 | 121 | #### Possibly breaking changes 122 | 123 | * Adds default IdentifiableType extensions for: 124 | * String 125 | * Int 126 | * Float 127 | 128 | This can break your code if you've implemented those extensions locally. This can be easily solved by just removing local extensions. 129 | 130 | #### Features 131 | 132 | * Swift 2.3 compatible 133 | * Improves mutability checks. If data source is being mutated after binding, warning assert is triggered. 134 | * Deprecates `cellFactory` in favor of `configureCell`. 135 | * Improves runtime checks in DEBUG mode for correct `SectionModelType.init` implementation. 136 | 137 | #### Fixes 138 | 139 | * Fixes default value for `canEditRowAtIndexPath` and sets it to `false`. 140 | * Changes DEBUG asserting behavior in case multiple items with same identity are found to printing warning message to terminal. Fallbacks as before to `reloadData`. 141 | 142 | ## [0.8.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.8.1) 143 | 144 | #### Anomalies 145 | 146 | * Fixes problem with `SectionModel.init`. 147 | 148 | ## [0.8](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.8) 149 | 150 | #### Features 151 | 152 | * Adds new example of how to present heterogeneous sections. 153 | 154 | #### Anomalies 155 | 156 | * Fixes old `AnimatableSectionModel` definition. 157 | * Fixes problem with `UICollectionView` iOS 9 reordering features. 158 | 159 | ## [0.7](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.7) 160 | 161 | #### Interface changes 162 | 163 | * Adds required initializer to `SectionModelType.init(original: Self, items: [Item])` to support moving of table rows with animation. 164 | * `rx_itemsAnimatedWithDataSource` deprecated in favor of just using `rx_itemsWithDataSource`. 165 | 166 | #### Features 167 | 168 | * Adds new example how to use delegates and reactive data sources to customize look. 169 | 170 | #### Anomalies 171 | 172 | * Fixes problems with moving rows and animated data source. 173 | 174 | ## [0.6.2](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.6.2) 175 | 176 | #### Features 177 | 178 | * Xcode 7.3 / Swift 2.2 support 179 | 180 | ## [0.6.1](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.6.1) 181 | 182 | #### Anomalies 183 | 184 | * Fixes compilation issues when `DEBUG` is defined. 185 | 186 | ## [0.6](https://github.com/RxSwiftCommunity/RxDataSources/releases/tag/0.6) 187 | 188 | #### Features 189 | 190 | * Adds `self` data source as first parameter to all closures. (**breaking change**) 191 | * Adds `AnimationConfiguration` to enable configuring animation. 192 | * Replaces binding error handling logic with `UIBindingObserver`. 193 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" ~> 6.0 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" "6.0.0" 2 | -------------------------------------------------------------------------------- /Differentiator.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Differentiator" 3 | s.version = "5.0.0" 4 | s.summary = "Diff algorithm for UITableView and UICollectionView." 5 | s.description = <<-DESC 6 | Diff algorithm for UITableView and UICollectionView. 7 | RxDataSources is powered by Differentiator. 8 | DESC 9 | 10 | s.homepage = "https://github.com/RxSwiftCommunity/RxDataSources" 11 | s.license = 'MIT' 12 | s.author = { "Krunoslav Zaher" => "krunoslav.zaher@gmail.com" } 13 | s.source = { :git => "https://github.com/RxSwiftCommunity/RxDataSources.git", :tag => s.version.to_s } 14 | 15 | s.requires_arc = true 16 | s.swift_version = '5.0' 17 | 18 | s.source_files = 'Sources/Differentiator/**/*.swift' 19 | 20 | s.ios.deployment_target = '9.0' 21 | s.tvos.deployment_target = '9.0' 22 | 23 | end 24 | -------------------------------------------------------------------------------- /Examples/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Example.xcodeproj/project.xcworkspace/xcshareddata/RxDataSources.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "4DD6810907F5B741470171C4B7D7EC023CD6437A", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "4DD6810907F5B741470171C4B7D7EC023CD6437A" : 9223372036854775807, 8 | "8B123162C394A0A0A138779108E4C59DD771865A" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "799ED22B-FD39-44D8-8A0A-FB9B3854EA6B", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "4DD6810907F5B741470171C4B7D7EC023CD6437A" : "RxDataSources\/", 13 | "8B123162C394A0A0A138779108E4C59DD771865A" : "RxDataSources\/RxSwift\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "RxDataSources", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "RxDataSources.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:RxSwiftCommunity\/RxDataSources.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4DD6810907F5B741470171C4B7D7EC023CD6437A" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/ReactiveX\/RxSwift.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8B123162C394A0A0A138779108E4C59DD771865A" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Examples/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Examples/Example.xcodeproj/xcshareddata/xcschemes/ExampleUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Examples/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 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Examples/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Example/Assets.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "settings@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Example/Assets.xcassets/settings.imageset/settings@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxSwiftCommunity/RxDataSources/bbfea3869f5492580563c676acda729c64fa489e/Examples/Example/Assets.xcassets/settings.imageset/settings@2x.png -------------------------------------------------------------------------------- /Examples/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Examples/Example/Example1_CustomizationUsingTableViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomizationUsingTableViewDelegate.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 4/19/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | import RxDataSources 14 | import Differentiator 15 | 16 | struct MySection { 17 | var header: String 18 | var items: [Item] 19 | } 20 | 21 | extension MySection : AnimatableSectionModelType { 22 | typealias Item = Int 23 | 24 | var identity: String { 25 | return header 26 | } 27 | 28 | init(original: MySection, items: [Item]) { 29 | self = original 30 | self.items = items 31 | } 32 | } 33 | 34 | class CustomizationUsingTableViewDelegate : UIViewController { 35 | @IBOutlet private var tableView: UITableView! 36 | 37 | let disposeBag = DisposeBag() 38 | 39 | var dataSource: RxTableViewSectionedAnimatedDataSource? 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 45 | 46 | let dataSource = RxTableViewSectionedAnimatedDataSource( 47 | configureCell: { ds, tv, _, item in 48 | let cell = tv.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell") 49 | cell.textLabel?.text = "Item \(item)" 50 | 51 | return cell 52 | }, 53 | titleForHeaderInSection: { ds, index in 54 | return ds.sectionModels[index].header 55 | } 56 | ) 57 | 58 | self.dataSource = dataSource 59 | 60 | let sections = [ 61 | MySection(header: "First section", items: [ 62 | 1, 63 | 2 64 | ]), 65 | MySection(header: "Second section", items: [ 66 | 3, 67 | 4 68 | ]) 69 | ] 70 | 71 | Observable.just(sections) 72 | .bind(to: tableView.rx.items(dataSource: dataSource)) 73 | .disposed(by: disposeBag) 74 | 75 | tableView.rx.setDelegate(self) 76 | .disposed(by: disposeBag) 77 | } 78 | } 79 | 80 | extension CustomizationUsingTableViewDelegate : UITableViewDelegate { 81 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 82 | 83 | // you can also fetch item 84 | guard let item = dataSource?[indexPath], 85 | // .. or section and customize what you like 86 | dataSource?[indexPath.section] != nil 87 | else { 88 | return 0.0 89 | } 90 | 91 | return CGFloat(40 + item * 10) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Examples/Example/Example2_RandomizedSectionsAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Krunoslav Zaher on 1/1/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxDataSources 11 | import RxSwift 12 | import RxCocoa 13 | import CoreLocation 14 | 15 | class NumberCell : UICollectionViewCell { 16 | @IBOutlet private var value: UILabel? 17 | 18 | func configure(with value: String) { 19 | self.value?.text = value 20 | } 21 | } 22 | 23 | class NumberSectionView : UICollectionReusableView { 24 | @IBOutlet private weak var value: UILabel? 25 | 26 | func configure(value: String) { 27 | self.value?.text = value 28 | } 29 | } 30 | 31 | class PartialUpdatesViewController: UIViewController { 32 | 33 | @IBOutlet private weak var animatedTableView: UITableView! 34 | @IBOutlet private weak var tableView: UITableView! 35 | @IBOutlet private weak var animatedCollectionView: UICollectionView! 36 | @IBOutlet private weak var refreshButton: UIButton! 37 | 38 | let disposeBag = DisposeBag() 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | let initialRandomizedSections = Randomizer(rng: PseudoRandomGenerator(4, 3), sections: initialValue()) 44 | 45 | let ticks = Observable.interval(.seconds(1), scheduler: MainScheduler.instance).map { _ in () } 46 | let randomSections = Observable.of(ticks, refreshButton.rx.tap.asObservable()) 47 | .merge() 48 | .scan(initialRandomizedSections) { a, _ in 49 | return a.randomize() 50 | } 51 | .map { a in 52 | return a.sections 53 | } 54 | .share(replay: 1) 55 | 56 | let (configureCell, titleForSection) = PartialUpdatesViewController.tableViewDataSourceUI() 57 | let tvAnimatedDataSource = RxTableViewSectionedAnimatedDataSource( 58 | configureCell: configureCell, 59 | titleForHeaderInSection: titleForSection 60 | ) 61 | let reloadDataSource = RxTableViewSectionedReloadDataSource( 62 | configureCell: configureCell, 63 | titleForHeaderInSection: titleForSection 64 | ) 65 | 66 | randomSections 67 | .bind(to: animatedTableView.rx.items(dataSource: tvAnimatedDataSource)) 68 | .disposed(by: disposeBag) 69 | 70 | randomSections 71 | .bind(to: tableView.rx.items(dataSource: reloadDataSource)) 72 | .disposed(by: disposeBag) 73 | 74 | let (configureCollectionViewCell, configureSupplementaryView) = PartialUpdatesViewController.collectionViewDataSourceUI() 75 | let cvAnimatedDataSource = RxCollectionViewSectionedAnimatedDataSource( 76 | configureCell: configureCollectionViewCell, 77 | configureSupplementaryView: configureSupplementaryView 78 | ) 79 | 80 | randomSections 81 | .bind(to: animatedCollectionView.rx.items(dataSource: cvAnimatedDataSource)) 82 | .disposed(by: disposeBag) 83 | 84 | // touches 85 | 86 | Observable.of( 87 | tableView.rx.modelSelected(IntItem.self), 88 | animatedTableView.rx.modelSelected(IntItem.self), 89 | animatedCollectionView.rx.modelSelected(IntItem.self) 90 | ) 91 | .merge() 92 | .subscribe(onNext: { item in 93 | print("Let me guess, it's .... It's \(item), isn't it? Yeah, I've got it.") 94 | }) 95 | .disposed(by: disposeBag) 96 | } 97 | } 98 | 99 | // MARK: Skinning 100 | extension PartialUpdatesViewController { 101 | 102 | static func tableViewDataSourceUI() -> ( 103 | TableViewSectionedDataSource.ConfigureCell, 104 | TableViewSectionedDataSource.TitleForHeaderInSection 105 | ) { 106 | return ( 107 | { _, tv, ip, i in 108 | let cell = tv.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style:.default, reuseIdentifier: "Cell") 109 | cell.textLabel!.text = "\(i)" 110 | return cell 111 | }, 112 | { ds, section -> String? in 113 | return ds[section].header 114 | } 115 | ) 116 | } 117 | 118 | static func collectionViewDataSourceUI() -> ( 119 | CollectionViewSectionedDataSource.ConfigureCell, 120 | CollectionViewSectionedDataSource.ConfigureSupplementaryView 121 | ) { 122 | return ( 123 | { _, cv, ip, i in 124 | let cell = cv.dequeueReusableCell(withReuseIdentifier: "Cell", for: ip) as! NumberCell 125 | cell.configure(with: "\(i)") 126 | return cell 127 | 128 | }, 129 | { ds ,cv, kind, ip in 130 | let section = cv.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Section", for: ip) as! NumberSectionView 131 | section.configure(value: "\(ds[ip.section].header)") 132 | return section 133 | } 134 | ) 135 | } 136 | 137 | // MARK: Initial value 138 | 139 | func initialValue() -> [NumberSection] { 140 | #if true 141 | let nSections = 10 142 | let nItems = 100 143 | 144 | 145 | /* 146 | let nSections = 10 147 | let nItems = 2 148 | */ 149 | 150 | return (0 ..< nSections).map { (i: Int) in 151 | NumberSection(header: "Section \(i + 1)", numbers: `$`(Array(i * nItems ..< (i + 1) * nItems)), updated: Date()) 152 | } 153 | #else 154 | return _initialValue 155 | #endif 156 | } 157 | 158 | 159 | } 160 | 161 | let _initialValue: [NumberSection] = [ 162 | NumberSection(header: "section 1", numbers: `$`([1, 2, 3]), updated: Date()), 163 | NumberSection(header: "section 2", numbers: `$`([4, 5, 6]), updated: Date()), 164 | NumberSection(header: "section 3", numbers: `$`([7, 8, 9]), updated: Date()), 165 | NumberSection(header: "section 4", numbers: `$`([10, 11, 12]), updated: Date()), 166 | NumberSection(header: "section 5", numbers: `$`([13, 14, 15]), updated: Date()), 167 | NumberSection(header: "section 6", numbers: `$`([16, 17, 18]), updated: Date()), 168 | NumberSection(header: "section 7", numbers: `$`([19, 20, 21]), updated: Date()), 169 | NumberSection(header: "section 8", numbers: `$`([22, 23, 24]), updated: Date()), 170 | NumberSection(header: "section 9", numbers: `$`([25, 26, 27]), updated: Date()), 171 | NumberSection(header: "section 10", numbers: `$`([28, 29, 30]), updated: Date()) 172 | ] 173 | 174 | func `$`(_ numbers: [Int]) -> [IntItem] { 175 | return numbers.map { IntItem(number: $0, date: Date()) } 176 | } 177 | 178 | -------------------------------------------------------------------------------- /Examples/Example/Example3_TableViewEditing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditingExampleTableViewController.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 3/24/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxDataSources 11 | import RxSwift 12 | import RxCocoa 13 | 14 | // redux like editing example 15 | class EditingExampleViewController: UIViewController { 16 | 17 | @IBOutlet private weak var addButton: UIBarButtonItem! 18 | 19 | @IBOutlet private weak var tableView: UITableView! 20 | let disposeBag = DisposeBag() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | let dataSource = EditingExampleViewController.dataSource() 26 | 27 | let sections: [NumberSection] = [NumberSection(header: "Section 1", numbers: [], updated: Date()), 28 | NumberSection(header: "Section 2", numbers: [], updated: Date()), 29 | NumberSection(header: "Section 3", numbers: [], updated: Date())] 30 | 31 | let initialState = SectionedTableViewState(sections: sections) 32 | let add3ItemsAddStart = Observable.of((), (), ()) 33 | let addCommand = Observable.of(addButton.rx.tap.asObservable(), add3ItemsAddStart) 34 | .merge() 35 | .map(TableViewEditingCommand.addRandomItem) 36 | 37 | let deleteCommand = tableView.rx.itemDeleted.asObservable() 38 | .map(TableViewEditingCommand.DeleteItem) 39 | 40 | let movedCommand = tableView.rx.itemMoved 41 | .map(TableViewEditingCommand.MoveItem) 42 | 43 | Observable.of(addCommand, deleteCommand, movedCommand) 44 | .merge() 45 | .scan(initialState) { (state: SectionedTableViewState, command: TableViewEditingCommand) -> SectionedTableViewState in 46 | return state.execute(command: command) 47 | } 48 | .startWith(initialState) 49 | .map { 50 | $0.sections 51 | } 52 | .share(replay: 1) 53 | .bind(to: tableView.rx.items(dataSource: dataSource)) 54 | .disposed(by: disposeBag) 55 | } 56 | 57 | override func viewDidAppear(_ animated: Bool) { 58 | super.viewDidAppear(animated) 59 | tableView.setEditing(true, animated: true) 60 | } 61 | } 62 | 63 | extension EditingExampleViewController { 64 | static func dataSource() -> RxTableViewSectionedAnimatedDataSource { 65 | return RxTableViewSectionedAnimatedDataSource( 66 | animationConfiguration: AnimationConfiguration(insertAnimation: .top, 67 | reloadAnimation: .fade, 68 | deleteAnimation: .left), 69 | configureCell: { _, table, idxPath, item in 70 | let cell = table.dequeueReusableCell(withIdentifier: "Cell", for: idxPath) 71 | cell.textLabel?.text = "\(item)" 72 | return cell 73 | }, 74 | titleForHeaderInSection: { ds, section -> String? in 75 | return ds[section].header 76 | }, 77 | canEditRowAtIndexPath: { _, _ in 78 | return true 79 | }, 80 | canMoveRowAtIndexPath: { _, _ in 81 | return true 82 | } 83 | ) 84 | } 85 | } 86 | 87 | enum TableViewEditingCommand { 88 | case AppendItem(item: IntItem, section: Int) 89 | case MoveItem(sourceIndex: IndexPath, destinationIndex: IndexPath) 90 | case DeleteItem(IndexPath) 91 | } 92 | 93 | // This is the part 94 | 95 | struct SectionedTableViewState { 96 | fileprivate var sections: [NumberSection] 97 | 98 | init(sections: [NumberSection]) { 99 | self.sections = sections 100 | } 101 | 102 | func execute(command: TableViewEditingCommand) -> SectionedTableViewState { 103 | switch command { 104 | case .AppendItem(let appendEvent): 105 | var sections = self.sections 106 | let items = sections[appendEvent.section].items + appendEvent.item 107 | sections[appendEvent.section] = NumberSection(original: sections[appendEvent.section], items: items) 108 | return SectionedTableViewState(sections: sections) 109 | case .DeleteItem(let indexPath): 110 | var sections = self.sections 111 | var items = sections[indexPath.section].items 112 | items.remove(at: indexPath.row) 113 | sections[indexPath.section] = NumberSection(original: sections[indexPath.section], items: items) 114 | return SectionedTableViewState(sections: sections) 115 | case .MoveItem(let moveEvent): 116 | var sections = self.sections 117 | var sourceItems = sections[moveEvent.sourceIndex.section].items 118 | var destinationItems = sections[moveEvent.destinationIndex.section].items 119 | 120 | if moveEvent.sourceIndex.section == moveEvent.destinationIndex.section { 121 | destinationItems.insert(destinationItems.remove(at: moveEvent.sourceIndex.row), 122 | at: moveEvent.destinationIndex.row) 123 | let destinationSection = NumberSection(original: sections[moveEvent.destinationIndex.section], items: destinationItems) 124 | sections[moveEvent.sourceIndex.section] = destinationSection 125 | 126 | return SectionedTableViewState(sections: sections) 127 | } else { 128 | let item = sourceItems.remove(at: moveEvent.sourceIndex.row) 129 | destinationItems.insert(item, at: moveEvent.destinationIndex.row) 130 | let sourceSection = NumberSection(original: sections[moveEvent.sourceIndex.section], items: sourceItems) 131 | let destinationSection = NumberSection(original: sections[moveEvent.destinationIndex.section], items: destinationItems) 132 | sections[moveEvent.sourceIndex.section] = sourceSection 133 | sections[moveEvent.destinationIndex.section] = destinationSection 134 | 135 | return SectionedTableViewState(sections: sections) 136 | } 137 | } 138 | } 139 | } 140 | 141 | extension TableViewEditingCommand { 142 | static var nextNumber = 0 143 | static func addRandomItem() -> TableViewEditingCommand { 144 | let randSection = Int.random(in: 0...2) 145 | let number = nextNumber 146 | defer { nextNumber = nextNumber + 1 } 147 | let item = IntItem(number: number, date: Date()) 148 | return TableViewEditingCommand.AppendItem(item: item, section: randSection) 149 | } 150 | } 151 | 152 | func + (lhs: [T], rhs: T) -> [T] { 153 | var copy = lhs 154 | copy.append(rhs) 155 | return copy 156 | } 157 | -------------------------------------------------------------------------------- /Examples/Example/Example4_DifferentSectionAndItemTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleSectionModelViewController.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 4/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxDataSources 11 | import RxCocoa 12 | import RxSwift 13 | import Differentiator 14 | 15 | // the trick is to just use enum for different section types 16 | class MultipleSectionModelViewController: UIViewController { 17 | 18 | @IBOutlet private weak var tableView: UITableView! 19 | let disposeBag = DisposeBag() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | let sections: [MultipleSectionModel] = [ 25 | .ImageProvidableSection(title: "Section 1", 26 | items: [.ImageSectionItem(image: UIImage(named: "settings")!, title: "General")]), 27 | .ToggleableSection(title: "Section 2", 28 | items: [.ToggleableSectionItem(title: "On", enabled: true)]), 29 | .StepperableSection(title: "Section 3", 30 | items: [.StepperSectionItem(title: "1")]) 31 | ] 32 | 33 | let dataSource = MultipleSectionModelViewController.dataSource() 34 | 35 | Observable.just(sections) 36 | .bind(to: tableView.rx.items(dataSource: dataSource)) 37 | .disposed(by: disposeBag) 38 | } 39 | } 40 | 41 | extension MultipleSectionModelViewController { 42 | static func dataSource() -> RxTableViewSectionedReloadDataSource { 43 | return RxTableViewSectionedReloadDataSource( 44 | configureCell: { dataSource, table, idxPath, _ in 45 | switch dataSource[idxPath] { 46 | case let .ImageSectionItem(image, title): 47 | let cell: ImageTitleTableViewCell = table.dequeueReusableCell(forIndexPath: idxPath) 48 | cell.configure(image: image, title: title) 49 | return cell 50 | case let .StepperSectionItem(title): 51 | let cell: TitleSteperTableViewCell = table.dequeueReusableCell(forIndexPath: idxPath) 52 | cell.configure(title: title) 53 | return cell 54 | case let .ToggleableSectionItem(title, enabled): 55 | let cell: TitleSwitchTableViewCell = table.dequeueReusableCell(forIndexPath: idxPath) 56 | cell.configure(title: title, isEnabled: enabled) 57 | return cell 58 | } 59 | }, 60 | titleForHeaderInSection: { dataSource, index in 61 | let section = dataSource[index] 62 | return section.title 63 | } 64 | ) 65 | } 66 | } 67 | 68 | enum MultipleSectionModel { 69 | case ImageProvidableSection(title: String, items: [SectionItem]) 70 | case ToggleableSection(title: String, items: [SectionItem]) 71 | case StepperableSection(title: String, items: [SectionItem]) 72 | } 73 | 74 | enum SectionItem { 75 | case ImageSectionItem(image: UIImage, title: String) 76 | case ToggleableSectionItem(title: String, enabled: Bool) 77 | case StepperSectionItem(title: String) 78 | } 79 | 80 | extension MultipleSectionModel: SectionModelType { 81 | typealias Item = SectionItem 82 | 83 | var items: [SectionItem] { 84 | switch self { 85 | case .ImageProvidableSection(title: _, items: let items): 86 | return items.map { $0 } 87 | case .StepperableSection(title: _, items: let items): 88 | return items.map { $0 } 89 | case .ToggleableSection(title: _, items: let items): 90 | return items.map { $0 } 91 | } 92 | } 93 | 94 | init(original: MultipleSectionModel, items: [Item]) { 95 | switch original { 96 | case let .ImageProvidableSection(title: title, items: _): 97 | self = .ImageProvidableSection(title: title, items: items) 98 | case let .StepperableSection(title, _): 99 | self = .StepperableSection(title: title, items: items) 100 | case let .ToggleableSection(title, _): 101 | self = .ToggleableSection(title: title, items: items) 102 | } 103 | } 104 | } 105 | 106 | extension MultipleSectionModel { 107 | var title: String { 108 | switch self { 109 | case .ImageProvidableSection(title: let title, items: _): 110 | return title 111 | case .StepperableSection(title: let title, items: _): 112 | return title 113 | case .ToggleableSection(title: let title, items: _): 114 | return title 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Examples/Example/Example5_UIPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Example5_UIPickerView.swift 3 | // RxDataSources 4 | // 5 | // Created by Sergey Shulga on 04/07/2017. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxCocoa 11 | import RxSwift 12 | import RxDataSources 13 | 14 | final class ReactivePickerViewControllerExample: UIViewController { 15 | 16 | @IBOutlet private weak var firstPickerView: UIPickerView! 17 | @IBOutlet private weak var secondPickerView: UIPickerView! 18 | @IBOutlet private weak var thirdPickerView: UIPickerView! 19 | 20 | let disposeBag = DisposeBag() 21 | 22 | private let stringPickerAdapter = RxPickerViewStringAdapter<[String]>(components: [], 23 | numberOfComponents: { _,_,_ in 1 }, 24 | numberOfRowsInComponent: { _, _, items, _ -> Int in 25 | return items.count 26 | }, 27 | titleForRow: { _, _, items, row, _ -> String? in 28 | return items[row] 29 | }) 30 | private let attributedStringPickerAdapter = RxPickerViewAttributedStringAdapter<[String]>( 31 | components: [], 32 | numberOfComponents: { _,_,_ in 1 }, 33 | numberOfRowsInComponent: { _, _, items, _ -> Int in 34 | return items.count 35 | }, 36 | attributedTitleForRow: { _, _, items, row, _ -> NSAttributedString? in 37 | NSAttributedString( 38 | string: items[row], 39 | attributes: [ 40 | .foregroundColor: UIColor.purple, 41 | .underlineStyle: NSUnderlineStyle.double.rawValue, 42 | .textEffect: NSAttributedString.TextEffectStyle.letterpressStyle 43 | ] 44 | ) 45 | } 46 | ) 47 | private let viewPickerAdapter = RxPickerViewViewAdapter<[String]>( 48 | components: [], 49 | numberOfComponents: { _,_,_ in 1 }, 50 | numberOfRowsInComponent: { _, _, items, _ -> Int in 51 | return items.count 52 | }, 53 | viewForRow: { _, _, _, row, _, view -> UIView in 54 | let componentView = view ?? UIView() 55 | componentView.backgroundColor = row % 2 == 0 ? UIColor.red : UIColor.blue 56 | return componentView 57 | } 58 | ) 59 | 60 | override func viewDidLoad() { 61 | super.viewDidLoad() 62 | 63 | Observable.just(["One", "Two", "Tree"]) 64 | .bind(to: firstPickerView.rx.items(adapter: stringPickerAdapter)) 65 | .disposed(by: disposeBag) 66 | 67 | Observable.just(["One", "Two", "Tree"]) 68 | .bind(to: secondPickerView.rx.items(adapter: attributedStringPickerAdapter)) 69 | .disposed(by: disposeBag) 70 | 71 | Observable.just(["One", "Two", "Tree"]) 72 | .bind(to: thirdPickerView.rx.items(adapter: viewPickerAdapter)) 73 | .disposed(by: disposeBag) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Examples/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Examples/Example/Support/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Krunoslav Zaher on 1/1/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | return true 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Examples/Example/Support/NumberSection.swift: -------------------------------------------------------------------------------- 1 | ../../../Tests/RxDataSourcesTests/NumberSection.swift -------------------------------------------------------------------------------- /Examples/Example/Support/Randomizer.swift: -------------------------------------------------------------------------------- 1 | ../../../Tests/RxDataSourcesTests/Randomizer.swift -------------------------------------------------------------------------------- /Examples/Example/Views/ImageTitleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTitleTableViewCell.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 4/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageTitleTableViewCell: UITableViewCell { 12 | 13 | @IBOutlet private weak var cellImageView: UIImageView! 14 | @IBOutlet private weak var titleLabel: UILabel! 15 | 16 | func configure(image: UIImage, title: String) { 17 | cellImageView.image = image 18 | titleLabel.text = title 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/Example/Views/TitleSteperTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleSteperTableViewCell.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 4/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TitleSteperTableViewCell: UITableViewCell { 12 | 13 | @IBOutlet private weak var stepper: UIStepper! 14 | @IBOutlet private weak var titleLabel: UILabel! 15 | 16 | func configure(title: String) { 17 | titleLabel.text = title 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Examples/Example/Views/TitleSwitchTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleSwitchTableViewCell.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 4/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TitleSwitchTableViewCell: UITableViewCell { 12 | 13 | 14 | @IBOutlet private weak var switchControl: UISwitch! 15 | @IBOutlet private weak var titleLabel: UILabel! 16 | 17 | func configure(title: String, isEnabled: Bool) { 18 | switchControl.isOn = isEnabled 19 | titleLabel.text = title 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Examples/Example/Views/UIKitExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitExtensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Segii Shulga on 4/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import class UIKit.UITableViewCell 10 | import class UIKit.UITableView 11 | import struct Foundation.IndexPath 12 | 13 | protocol ReusableView: class { 14 | static var reuseIdentifier: String { get } 15 | } 16 | 17 | extension ReusableView { 18 | static var reuseIdentifier: String { 19 | return String(describing: self) 20 | } 21 | } 22 | 23 | extension UITableViewCell: ReusableView { 24 | } 25 | 26 | extension UITableView { 27 | 28 | func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T { 29 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 30 | fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 31 | } 32 | 33 | return cell 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Examples/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // ExampleUITests 4 | // 5 | // Created by Krunoslav Zaher on 1/1/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | import XCTest 9 | import CoreLocation 10 | 11 | class ExampleUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | continueAfterFailure = false 17 | XCUIApplication().launch() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | XCUIApplication().tables.element(boundBy: 0).cells.staticTexts["Randomize Example"].tap() 26 | 27 | let time: TimeInterval = 120.0 28 | 29 | RunLoop.current.run(until: Date().addingTimeInterval(time)) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Examples/ExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/RxSwift: -------------------------------------------------------------------------------- 1 | ../Carthage/Checkouts/RxSwift -------------------------------------------------------------------------------- /Examples/Sources: -------------------------------------------------------------------------------- 1 | ../Sources -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 RxSwift Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxSwift", 6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c8742ed97fc2f0c015a5ea5eddefb064cd7532d2", 10 | "version": "6.0.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "RxDataSources", 7 | platforms: [ 8 | .iOS(.v9), .tvOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "RxDataSources", targets: ["RxDataSources"]), 12 | .library(name: "RxDataSources-Dynamic", type: .dynamic, targets: ["RxDataSources"]), 13 | .library(name: "Differentiator", targets: ["Differentiator"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")) 17 | ], 18 | targets: [ 19 | .target(name: "RxDataSources", dependencies: ["Differentiator", "RxSwift", "RxCocoa"]), 20 | .target(name: "Differentiator"), 21 | .testTarget(name: "RxDataSourcesTests", dependencies: ["RxDataSources"]) 22 | ], 23 | swiftLanguageVersions: [.v5] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis CI](https://travis-ci.org/RxSwiftCommunity/RxDataSources.svg?branch=main)](https://travis-ci.org/RxSwiftCommunity/RxDataSources) 2 | 3 | Table and Collection view data sources 4 | ====================================== 5 | 6 | ## Features 7 | 8 | - [x] **O(N)** algorithm for calculating differences 9 | - the algorithm has the assumption that all sections and items are unique so there is no ambiguity 10 | - in case there is ambiguity, fallbacks automagically on non animated refresh 11 | - [x] it applies additional heuristics to send the least number of commands to sectioned view 12 | - even though the running time is linear, preferred number of sent commands is usually a lot less than linear 13 | - it is preferred (and possible) to cap the number of changes to some small number, and in case the number of changes grows towards linear, just do normal reload 14 | - [x] Supports **extending your item and section structures** 15 | - just extend your item with `IdentifiableType` and `Equatable`, and your section with `AnimatableSectionModelType` 16 | - [x] Supports all combinations of two level hierarchical animations for **both sections and items** 17 | - Section animations: Insert, Delete, Move 18 | - Item animations: Insert, Delete, Move, Reload (if old value is not equal to new value) 19 | - [x] Configurable animation types for `Insert`, `Reload` and `Delete` (Automatic, Fade, ...) 20 | - [x] Example app 21 | - [x] Randomized stress tests (example app) 22 | - [x] Supports editing out of the box (example app) 23 | - [x] Works with `UITableView` and `UICollectionView` 24 | 25 | ## Why 26 | Writing table and collection view data sources is tedious. There is a large number of delegate methods that need to be implemented for the simplest case possible. 27 | 28 | RxSwift helps alleviate some of the burden with a simple data binding mechanism: 29 | 1) Turn your data into an Observable sequence 30 | 2) Bind the data to the tableView/collectionView using one of: 31 | - `rx.items(dataSource:protocol)` 32 | - `rx.items(cellIdentifier:String)` 33 | - `rx.items(cellIdentifier:String:Cell.Type:_:)` 34 | - `rx.items(_:_:)` 35 | 36 | ```swift 37 | let data = Observable<[String]>.just(["first element", "second element", "third element"]) 38 | 39 | data.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in 40 | cell.textLabel?.text = model 41 | } 42 | .disposed(by: disposeBag) 43 | ``` 44 | 45 | This works well with simple data sets but does not handle cases where you need to bind complex data sets with multiples sections, or when you need to perform animations when adding/modifying/deleting items. 46 | 47 | These are precisely the use cases that RxDataSources helps solve. 48 | 49 | With RxDataSources, it is super easy to just write 50 | 51 | ```swift 52 | let dataSource = RxTableViewSectionedReloadDataSource>(configureCell: configureCell) 53 | Observable.just([SectionModel(model: "title", items: [1, 2, 3])]) 54 | .bind(to: tableView.rx.items(dataSource: dataSource)) 55 | .disposed(by: disposeBag) 56 | ``` 57 | ![RxDataSources example app](https://raw.githubusercontent.com/kzaher/rxswiftcontent/rxdatasources/RxDataSources.gif) 58 | 59 | ## How 60 | Given the following custom data structure: 61 | ```swift 62 | struct CustomData { 63 | var anInt: Int 64 | var aString: String 65 | var aCGPoint: CGPoint 66 | } 67 | ``` 68 | 69 | 1) Start by defining your sections with a struct that conforms to the `SectionModelType` protocol: 70 | - define the `Item` typealias: equal to the type of items that the section will contain 71 | - declare an `items` property: of type array of `Item` 72 | 73 | ```swift 74 | struct SectionOfCustomData { 75 | var header: String 76 | var items: [Item] 77 | } 78 | extension SectionOfCustomData: SectionModelType { 79 | typealias Item = CustomData 80 | 81 | init(original: SectionOfCustomData, items: [Item]) { 82 | self = original 83 | self.items = items 84 | } 85 | } 86 | ``` 87 | 88 | 2) Create a dataSource object and pass it your `SectionOfCustomData` type: 89 | ```swift 90 | let dataSource = RxTableViewSectionedReloadDataSource( 91 | configureCell: { dataSource, tableView, indexPath, item in 92 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 93 | cell.textLabel?.text = "Item \(item.anInt): \(item.aString) - \(item.aCGPoint.x):\(item.aCGPoint.y)" 94 | return cell 95 | }) 96 | ``` 97 | 98 | 3) Customize closures on the dataSource as needed: 99 | - `titleForHeaderInSection` 100 | - `titleForFooterInSection` 101 | - etc 102 | 103 | ```swift 104 | dataSource.titleForHeaderInSection = { dataSource, index in 105 | return dataSource.sectionModels[index].header 106 | } 107 | 108 | dataSource.titleForFooterInSection = { dataSource, index in 109 | return dataSource.sectionModels[index].footer 110 | } 111 | 112 | dataSource.canEditRowAtIndexPath = { dataSource, indexPath in 113 | return true 114 | } 115 | 116 | dataSource.canMoveRowAtIndexPath = { dataSource, indexPath in 117 | return true 118 | } 119 | ``` 120 | 121 | 4) Define the actual data as an Observable sequence of CustomData objects and bind it to the tableView 122 | ```swift 123 | let sections = [ 124 | SectionOfCustomData(header: "First section", items: [CustomData(anInt: 0, aString: "zero", aCGPoint: CGPoint.zero), CustomData(anInt: 1, aString: "one", aCGPoint: CGPoint(x: 1, y: 1)) ]), 125 | SectionOfCustomData(header: "Second section", items: [CustomData(anInt: 2, aString: "two", aCGPoint: CGPoint(x: 2, y: 2)), CustomData(anInt: 3, aString: "three", aCGPoint: CGPoint(x: 3, y: 3)) ]) 126 | ] 127 | 128 | Observable.just(sections) 129 | .bind(to: tableView.rx.items(dataSource: dataSource)) 130 | .disposed(by: disposeBag) 131 | ``` 132 | 133 | ### Animated Data Sources 134 | 135 | RxDataSources provides two special data source types that automatically take care of animating changes in the bound data source: `RxTableViewSectionedAnimatedDataSource` and `RxCollectionViewSectionedAnimatedDataSource`. 136 | 137 | To use one of the two animated data sources, you must take a few extra steps on top of those outlined above: 138 | 139 | - SectionOfCustomData needs to conform to `AnimatableSectionModelType` 140 | - Your data model must conform to 141 | * `IdentifiableType`: The `identity` provided by the `IdentifiableType` protocol must be an **immutable identifier representing an instance of the model**. For example, in case of a `Car` model, you might want to use the car's `plateNumber` as its identity. 142 | * `Equatable`: Conforming to `Equatable` helps `RxDataSources` determine which cells have changed so it can animate only these specific cells. Meaning, changing **any** of the `Car` model's properties will trigger an animated reload of that cell. 143 | 144 | ## Requirements 145 | 146 | Xcode 10.2 147 | 148 | Swift 5.0 149 | 150 | For Swift 4.x version please use versions `3.0.0 ... 3.1.0` 151 | For Swift 3.x version please use versions `1.0 ... 2.0.2` 152 | For Swift 2.3 version please use versions `0.1 ... 0.9` 153 | 154 | ## Installation 155 | 156 | **We'll try to keep the API as stable as possible, but breaking API changes can occur.** 157 | 158 | ### CocoaPods 159 | 160 | Podfile 161 | ``` 162 | pod 'RxDataSources', '~> 5.0' 163 | ``` 164 | 165 | ### Carthage 166 | 167 | Cartfile 168 | ``` 169 | github "RxSwiftCommunity/RxDataSources" ~> 5.0 170 | ``` 171 | 172 | ### Swift Package Manager 173 | 174 | Create a `Package.swift` file. 175 | 176 | ```swift 177 | import PackageDescription 178 | 179 | let package = Package( 180 | name: "SampleProject", 181 | dependencies: [ 182 | .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", from: "5.0.0") 183 | ] 184 | ) 185 | ``` 186 | 187 | If you are using Xcode 11 or higher, go to **File / Swift Packages / Add Package Dependency...** and enter package repository URL **https://github.com/RxSwiftCommunity/RxDataSources.git**, then follow the instructions. 188 | -------------------------------------------------------------------------------- /RxDataSources.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "RxDataSources" 3 | s.version = "5.0.0" 4 | s.summary = "This is a collection of reactive data sources for UITableView and UICollectionView." 5 | s.description = <<-DESC 6 | This is a collection of reactive data sources for UITableView and UICollectionView. 7 | 8 | It enables creation of animated data sources for table an collection views in just a couple of lines of code. 9 | 10 | ```swift 11 | let data: Observable
= ... 12 | 13 | let dataSource = RxTableViewSectionedAnimatedDataSource
() 14 | dataSource.cellFactory = { (tv, ip, i) in 15 | let cell = tv.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style:.Default, reuseIdentifier: "Cell") 16 | cell.textLabel!.text = "\(i)" 17 | return cell 18 | } 19 | 20 | // animated 21 | data 22 | .bind(to: animatedTableView.rx.items(dataSource: dataSource)) 23 | .disposed(by: disposeBag) 24 | 25 | // normal reload 26 | data 27 | .bind(to: tableView.rx.items(dataSource: dataSource)) 28 | .disposed(by: disposeBag) 29 | ``` 30 | DESC 31 | s.homepage = "https://github.com/RxSwiftCommunity/RxDataSources" 32 | s.license = 'MIT' 33 | s.author = { "Krunoslav Zaher" => "krunoslav.zaher@gmail.com" } 34 | s.source = { :git => "https://github.com/RxSwiftCommunity/RxDataSources.git", :tag => s.version.to_s } 35 | 36 | s.requires_arc = true 37 | s.swift_version = '5.0' 38 | 39 | s.source_files = 'Sources/RxDataSources/**/*.swift' 40 | s.dependency 'Differentiator', '~> 5.0' 41 | s.dependency 'RxSwift', '~> 6.0' 42 | s.dependency 'RxCocoa', '~> 6.0' 43 | 44 | s.ios.deployment_target = '9.0' 45 | s.tvos.deployment_target = '9.0' 46 | 47 | end 48 | -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/project.xcworkspace/xcshareddata/RxDataSources.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "4DD6810907F5B741470171C4B7D7EC023CD6437A", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "4DD6810907F5B741470171C4B7D7EC023CD6437A" : 9223372036854775807, 8 | "8B123162C394A0A0A138779108E4C59DD771865A" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "799ED22B-FD39-44D8-8A0A-FB9B3854EA6B", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "4DD6810907F5B741470171C4B7D7EC023CD6437A" : "RxDataSources\/", 13 | "8B123162C394A0A0A138779108E4C59DD771865A" : "RxDataSources\/RxSwift\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "RxDataSources", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "RxDataSources.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:RxSwiftCommunity\/RxDataSources.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4DD6810907F5B741470171C4B7D7EC023CD6437A" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/ReactiveX\/RxSwift.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "8B123162C394A0A0A138779108E4C59DD771865A" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/xcshareddata/xcschemes/Differentiator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/xcshareddata/xcschemes/RxDataSources.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /RxDataSources.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Sources/Differentiator/AnimatableSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableSectionModel.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/10/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct AnimatableSectionModel { 12 | public var model: Section 13 | public var items: [Item] 14 | 15 | public init(model: Section, items: [ItemType]) { 16 | self.model = model 17 | self.items = items 18 | } 19 | 20 | } 21 | 22 | extension AnimatableSectionModel 23 | : AnimatableSectionModelType { 24 | public typealias Item = ItemType 25 | public typealias Identity = Section.Identity 26 | 27 | public var identity: Section.Identity { 28 | return model.identity 29 | } 30 | 31 | public init(original: AnimatableSectionModel, items: [Item]) { 32 | self.model = original.model 33 | self.items = items 34 | } 35 | 36 | public var hashValue: Int { 37 | return self.model.identity.hashValue 38 | } 39 | } 40 | 41 | 42 | extension AnimatableSectionModel 43 | : CustomStringConvertible { 44 | 45 | public var description: String { 46 | return "HashableSectionModel(model: \"\(self.model)\", items: \(items))" 47 | } 48 | 49 | } 50 | 51 | extension AnimatableSectionModel 52 | : Equatable where Section: Equatable { 53 | 54 | public static func == (lhs: AnimatableSectionModel, rhs: AnimatableSectionModel) -> Bool { 55 | return lhs.model == rhs.model 56 | && lhs.items == rhs.items 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Differentiator/AnimatableSectionModelType+ItemPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableSectionModelType+ItemPath.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/9/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element: AnimatableSectionModelType { 12 | subscript(index: ItemPath) -> Element.Item { 13 | return self[index.sectionIndex].items[index.itemIndex] 14 | } 15 | } -------------------------------------------------------------------------------- /Sources/Differentiator/AnimatableSectionModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableSectionModelType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/6/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AnimatableSectionModelType 12 | : SectionModelType 13 | , IdentifiableType where Item: IdentifiableType, Item: Equatable { 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Differentiator/Changeset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Changeset.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 5/30/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Changeset { 12 | public typealias Item = Section.Item 13 | 14 | public let reloadData: Bool 15 | 16 | public let originalSections: [Section] 17 | public let finalSections: [Section] 18 | 19 | public let insertedSections: [Int] 20 | public let deletedSections: [Int] 21 | public let movedSections: [(from: Int, to: Int)] 22 | public let updatedSections: [Int] 23 | 24 | public let insertedItems: [ItemPath] 25 | public let deletedItems: [ItemPath] 26 | public let movedItems: [(from: ItemPath, to: ItemPath)] 27 | public let updatedItems: [ItemPath] 28 | 29 | init(reloadData: Bool = false, 30 | originalSections: [Section] = [], 31 | finalSections: [Section] = [], 32 | insertedSections: [Int] = [], 33 | deletedSections: [Int] = [], 34 | movedSections: [(from: Int, to: Int)] = [], 35 | updatedSections: [Int] = [], 36 | insertedItems: [ItemPath] = [], 37 | deletedItems: [ItemPath] = [], 38 | movedItems: [(from: ItemPath, to: ItemPath)] = [], 39 | updatedItems: [ItemPath] = []) { 40 | self.reloadData = reloadData 41 | 42 | self.originalSections = originalSections 43 | self.finalSections = finalSections 44 | 45 | self.insertedSections = insertedSections 46 | self.deletedSections = deletedSections 47 | self.movedSections = movedSections 48 | self.updatedSections = updatedSections 49 | 50 | self.insertedItems = insertedItems 51 | self.deletedItems = deletedItems 52 | self.movedItems = movedItems 53 | self.updatedItems = updatedItems 54 | } 55 | 56 | public static func initialValue(_ sections: [Section]) -> Changeset
{ 57 | return Changeset
( 58 | reloadData: true, 59 | finalSections: sections, 60 | insertedSections: Array(0 ..< sections.count) as [Int] 61 | ) 62 | } 63 | } 64 | 65 | extension ItemPath 66 | : CustomDebugStringConvertible { 67 | public var debugDescription : String { 68 | return "(\(sectionIndex), \(itemIndex))" 69 | } 70 | } 71 | 72 | extension Changeset 73 | : CustomDebugStringConvertible { 74 | 75 | public var debugDescription : String { 76 | let serializedSections = "[\n" + finalSections.map { "\($0)" }.joined(separator: ",\n") + "\n]\n" 77 | return " >> Final sections" 78 | + " \n\(serializedSections)" 79 | + (!insertedSections.isEmpty || !deletedSections.isEmpty || !movedSections.isEmpty || !updatedSections.isEmpty ? "\nSections:" : "") 80 | + (!insertedSections.isEmpty ? "\ninsertedSections:\n\t\(insertedSections)" : "") 81 | + (!deletedSections.isEmpty ? "\ndeletedSections:\n\t\(deletedSections)" : "") 82 | + (!movedSections.isEmpty ? "\nmovedSections:\n\t\(movedSections)" : "") 83 | + (!updatedSections.isEmpty ? "\nupdatesSections:\n\t\(updatedSections)" : "") 84 | + (!insertedItems.isEmpty || !deletedItems.isEmpty || !movedItems.isEmpty || !updatedItems.isEmpty ? "\nItems:" : "") 85 | + (!insertedItems.isEmpty ? "\ninsertedItems:\n\t\(insertedItems)" : "") 86 | + (!deletedItems.isEmpty ? "\ndeletedItems:\n\t\(deletedItems)" : "") 87 | + (!movedItems.isEmpty ? "\nmovedItems:\n\t\(movedItems)" : "") 88 | + (!updatedItems.isEmpty ? "\nupdatedItems:\n\t\(updatedItems)" : "") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Differentiator/Differentiator.h: -------------------------------------------------------------------------------- 1 | // 2 | // Differentiator.h 3 | // Differentiator 4 | // 5 | // Created by muukii on 7/26/17. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Differentiator. 12 | FOUNDATION_EXPORT double DifferentiatorVersionNumber; 13 | 14 | //! Project version string for Differentiator. 15 | FOUNDATION_EXPORT const unsigned char DifferentiatorVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Differentiator/IdentifiableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifiableType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/6/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol IdentifiableType { 12 | associatedtype Identity: Hashable 13 | 14 | var identity : Identity { get } 15 | } -------------------------------------------------------------------------------- /Sources/Differentiator/IdentifiableValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifiableValue.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/7/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct IdentifiableValue { 12 | public let value: Value 13 | } 14 | 15 | extension IdentifiableValue 16 | : IdentifiableType { 17 | 18 | public typealias Identity = Value 19 | 20 | public var identity : Identity { 21 | return value 22 | } 23 | } 24 | 25 | extension IdentifiableValue 26 | : Equatable 27 | , CustomStringConvertible 28 | , CustomDebugStringConvertible { 29 | 30 | public var description: String { 31 | return "\(value)" 32 | } 33 | 34 | public var debugDescription: String { 35 | return "\(value)" 36 | } 37 | } 38 | 39 | public func == (lhs: IdentifiableValue, rhs: IdentifiableValue) -> Bool { 40 | return lhs.value == rhs.value 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Differentiator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.0.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Differentiator/ItemPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemPath.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/9/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ItemPath { 12 | public let sectionIndex: Int 13 | public let itemIndex: Int 14 | 15 | public init(sectionIndex: Int, itemIndex: Int) { 16 | self.sectionIndex = sectionIndex 17 | self.itemIndex = itemIndex 18 | } 19 | } 20 | 21 | extension ItemPath : Equatable { 22 | 23 | } 24 | 25 | public func == (lhs: ItemPath, rhs: ItemPath) -> Bool { 26 | return lhs.sectionIndex == rhs.sectionIndex && lhs.itemIndex == rhs.itemIndex 27 | } 28 | 29 | extension ItemPath: Hashable { 30 | 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(sectionIndex.byteSwapped.hashValue) 33 | hasher.combine(itemIndex.hashValue) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Differentiator/Optional+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Extensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/8/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Optional { 12 | func unwrap() throws -> Wrapped { 13 | if let unwrapped = self { 14 | return unwrapped 15 | } 16 | else { 17 | debugFatalError("Error during unwrapping optional") 18 | throw DifferentiatorError.unwrappingOptional 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Differentiator/SectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionModel.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 6/16/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SectionModel { 12 | public var model: Section 13 | public var items: [Item] 14 | 15 | public init(model: Section, items: [Item]) { 16 | self.model = model 17 | self.items = items 18 | } 19 | } 20 | 21 | extension SectionModel 22 | : SectionModelType { 23 | public typealias Identity = Section 24 | public typealias Item = ItemType 25 | 26 | public var identity: Section { 27 | return model 28 | } 29 | } 30 | 31 | extension SectionModel 32 | : CustomStringConvertible { 33 | 34 | public var description: String { 35 | return "\(self.model) > \(items)" 36 | } 37 | } 38 | 39 | extension SectionModel { 40 | public init(original: SectionModel, items: [Item]) { 41 | self.model = original.model 42 | self.items = items 43 | } 44 | } 45 | 46 | extension SectionModel 47 | : Equatable where Section: Equatable, ItemType: Equatable { 48 | 49 | public static func == (lhs: SectionModel, rhs: SectionModel) -> Bool { 50 | return lhs.model == rhs.model 51 | && lhs.items == rhs.items 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Differentiator/SectionModelType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionModelType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 6/28/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SectionModelType { 12 | associatedtype Item 13 | 14 | var items: [Item] { get } 15 | 16 | init(original: Self, items: [Item]) 17 | } -------------------------------------------------------------------------------- /Sources/Differentiator/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // RxDataSources 4 | // 5 | // Created by muukii on 8/2/17. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum DifferentiatorError : Error { 12 | case unwrappingOptional 13 | case preconditionFailed(message: String) 14 | } 15 | 16 | func precondition(_ condition: Bool, _ message: @autoclosure() -> String) throws { 17 | if condition { 18 | return 19 | } 20 | debugFatalError("Precondition failed") 21 | 22 | throw DifferentiatorError.preconditionFailed(message: message()) 23 | } 24 | 25 | func debugFatalError(_ error: Error) { 26 | debugFatalError("\(error)") 27 | } 28 | 29 | func debugFatalError(_ message: String) { 30 | #if DEBUG 31 | fatalError(message) 32 | #else 33 | print(message) 34 | #endif 35 | } 36 | -------------------------------------------------------------------------------- /Sources/RxDataSources/AnimationConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationConfiguration.swift 3 | // RxDataSources 4 | // 5 | // Created by Esteban Torres on 5/2/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | 13 | /** 14 | Exposes custom animation styles for insertion, deletion and reloading behavior. 15 | */ 16 | public struct AnimationConfiguration { 17 | public let insertAnimation: UITableView.RowAnimation 18 | public let reloadAnimation: UITableView.RowAnimation 19 | public let deleteAnimation: UITableView.RowAnimation 20 | 21 | public init(insertAnimation: UITableView.RowAnimation = .automatic, 22 | reloadAnimation: UITableView.RowAnimation = .automatic, 23 | deleteAnimation: UITableView.RowAnimation = .automatic) { 24 | self.insertAnimation = insertAnimation 25 | self.reloadAnimation = reloadAnimation 26 | self.deleteAnimation = deleteAnimation 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /Sources/RxDataSources/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 4/26/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | 12 | extension Array where Element: SectionModelType { 13 | mutating func moveFromSourceIndexPath(_ sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) { 14 | let sourceSection = self[sourceIndexPath.section] 15 | var sourceItems = sourceSection.items 16 | 17 | let sourceItem = sourceItems.remove(at: sourceIndexPath.item) 18 | 19 | let sourceSectionNew = Element(original: sourceSection, items: sourceItems) 20 | self[sourceIndexPath.section] = sourceSectionNew 21 | 22 | let destinationSection = self[destinationIndexPath.section] 23 | var destinationItems = destinationSection.items 24 | destinationItems.insert(sourceItem, at: destinationIndexPath.item) 25 | 26 | self[destinationIndexPath.section] = Element(original: destinationSection, items: destinationItems) 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /Sources/RxDataSources/CollectionViewSectionedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewSectionedDataSource.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 7/2/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class CollectionViewSectionedDataSource 18 | : NSObject 19 | , UICollectionViewDataSource 20 | , SectionedViewDataSourceType { 21 | public typealias Item = Section.Item 22 | public typealias Section = Section 23 | public typealias ConfigureCell = (CollectionViewSectionedDataSource
, UICollectionView, IndexPath, Item) -> UICollectionViewCell 24 | public typealias ConfigureSupplementaryView = (CollectionViewSectionedDataSource
, UICollectionView, String, IndexPath) -> UICollectionReusableView 25 | public typealias MoveItem = (CollectionViewSectionedDataSource
, _ sourceIndexPath:IndexPath, _ destinationIndexPath:IndexPath) -> Void 26 | public typealias CanMoveItemAtIndexPath = (CollectionViewSectionedDataSource
, IndexPath) -> Bool 27 | 28 | 29 | public init( 30 | configureCell: @escaping ConfigureCell, 31 | configureSupplementaryView: ConfigureSupplementaryView? = nil, 32 | moveItem: @escaping MoveItem = { _, _, _ in () }, 33 | canMoveItemAtIndexPath: @escaping CanMoveItemAtIndexPath = { _, _ in false } 34 | ) { 35 | self.configureCell = configureCell 36 | self.configureSupplementaryView = configureSupplementaryView 37 | self.moveItem = moveItem 38 | self.canMoveItemAtIndexPath = canMoveItemAtIndexPath 39 | } 40 | 41 | #if DEBUG 42 | // If data source has already been bound, then mutating it 43 | // afterwards isn't something desired. 44 | // This simulates immutability after binding 45 | var _dataSourceBound: Bool = false 46 | 47 | private func ensureNotMutatedAfterBinding() { 48 | assert(!_dataSourceBound, "Data source is already bound. Please write this line before binding call (`bindTo`, `drive`). Data source must first be completely configured, and then bound after that, otherwise there could be runtime bugs, glitches, or partial malfunctions.") 49 | } 50 | 51 | #endif 52 | 53 | // This structure exists because model can be mutable 54 | // In that case current state value should be preserved. 55 | // The state that needs to be preserved is ordering of items in section 56 | // and their relationship with section. 57 | // If particular item is mutable, that is irrelevant for this logic to function 58 | // properly. 59 | public typealias SectionModelSnapshot = SectionModel 60 | 61 | private var _sectionModels: [SectionModelSnapshot] = [] 62 | 63 | open var sectionModels: [Section] { 64 | return _sectionModels.map { Section(original: $0.model, items: $0.items) } 65 | } 66 | 67 | open subscript(section: Int) -> Section { 68 | let sectionModel = self._sectionModels[section] 69 | return Section(original: sectionModel.model, items: sectionModel.items) 70 | } 71 | 72 | open subscript(indexPath: IndexPath) -> Item { 73 | get { 74 | return self._sectionModels[indexPath.section].items[indexPath.item] 75 | } 76 | set(item) { 77 | var section = self._sectionModels[indexPath.section] 78 | section.items[indexPath.item] = item 79 | self._sectionModels[indexPath.section] = section 80 | } 81 | } 82 | 83 | open func model(at indexPath: IndexPath) throws -> Any { 84 | guard indexPath.section < self._sectionModels.count, 85 | indexPath.item < self._sectionModels[indexPath.section].items.count else { 86 | throw RxDataSourceError.outOfBounds(indexPath: indexPath) 87 | } 88 | 89 | return self[indexPath] 90 | } 91 | 92 | open func setSections(_ sections: [Section]) { 93 | self._sectionModels = sections.map { SectionModelSnapshot(model: $0, items: $0.items) } 94 | } 95 | 96 | open var configureCell: ConfigureCell { 97 | didSet { 98 | #if DEBUG 99 | ensureNotMutatedAfterBinding() 100 | #endif 101 | } 102 | } 103 | 104 | open var configureSupplementaryView: ConfigureSupplementaryView? { 105 | didSet { 106 | #if DEBUG 107 | ensureNotMutatedAfterBinding() 108 | #endif 109 | } 110 | } 111 | 112 | open var moveItem: MoveItem { 113 | didSet { 114 | #if DEBUG 115 | ensureNotMutatedAfterBinding() 116 | #endif 117 | } 118 | } 119 | open var canMoveItemAtIndexPath: ((CollectionViewSectionedDataSource
, IndexPath) -> Bool)? { 120 | didSet { 121 | #if DEBUG 122 | ensureNotMutatedAfterBinding() 123 | #endif 124 | } 125 | } 126 | 127 | // UICollectionViewDataSource 128 | 129 | open func numberOfSections(in collectionView: UICollectionView) -> Int { 130 | return _sectionModels.count 131 | } 132 | 133 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 134 | return _sectionModels[section].items.count 135 | } 136 | 137 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 138 | precondition(indexPath.item < _sectionModels[indexPath.section].items.count) 139 | 140 | return configureCell(self, collectionView, indexPath, self[indexPath]) 141 | } 142 | 143 | open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 144 | return configureSupplementaryView!(self, collectionView, kind, indexPath) 145 | } 146 | 147 | open func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 148 | guard let canMoveItem = canMoveItemAtIndexPath?(self, indexPath) else { 149 | return false 150 | } 151 | 152 | return canMoveItem 153 | } 154 | 155 | open func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 156 | self._sectionModels.moveFromSourceIndexPath(sourceIndexPath, destinationIndexPath: destinationIndexPath) 157 | self.moveItem(self, sourceIndexPath, destinationIndexPath) 158 | } 159 | 160 | override open func responds(to aSelector: Selector!) -> Bool { 161 | if aSelector == #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)) { 162 | return configureSupplementaryView != nil 163 | } 164 | else { 165 | return super.responds(to: aSelector) 166 | } 167 | } 168 | } 169 | #endif 170 | -------------------------------------------------------------------------------- /Sources/RxDataSources/DataSources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSources.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/8/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @_exported import Differentiator 12 | 13 | enum RxDataSourceError: Error { 14 | case preconditionFailed(message: String) 15 | case outOfBounds(indexPath: IndexPath) 16 | } 17 | 18 | func rxPrecondition(_ condition: Bool, _ message: @autoclosure() -> String) throws { 19 | if condition { 20 | return 21 | } 22 | rxDebugFatalError("Precondition failed") 23 | 24 | throw RxDataSourceError.preconditionFailed(message: message()) 25 | } 26 | 27 | func rxDebugFatalError(_ error: Error) { 28 | rxDebugFatalError("\(error)") 29 | } 30 | 31 | func rxDebugFatalError(_ message: String) { 32 | #if DEBUG 33 | fatalError(message) 34 | #else 35 | print(message) 36 | #endif 37 | } 38 | -------------------------------------------------------------------------------- /Sources/RxDataSources/Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecated.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 10/8/17. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | extension CollectionViewSectionedDataSource { 11 | @available(*, deprecated, renamed: "configureSupplementaryView") 12 | public var supplementaryViewFactory: ConfigureSupplementaryView? { 13 | get { 14 | return self.configureSupplementaryView 15 | } 16 | set { 17 | self.configureSupplementaryView = newValue 18 | } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/RxDataSources/FloatingPointType+IdentifiableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingPointType+IdentifiableType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 7/4/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FloatingPoint { 12 | typealias identity = Self 13 | 14 | public var identity: Self { 15 | return self 16 | } 17 | } 18 | 19 | extension Float : IdentifiableType { 20 | 21 | } 22 | 23 | extension Double : IdentifiableType { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/RxDataSources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/RxDataSources/IntegerType+IdentifiableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntegerType+IdentifiableType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 7/4/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension BinaryInteger { 12 | typealias identity = Self 13 | 14 | public var identity: Self { 15 | return self 16 | } 17 | } 18 | 19 | extension Int : IdentifiableType { 20 | 21 | } 22 | 23 | extension Int8 : IdentifiableType { 24 | 25 | } 26 | 27 | extension Int16 : IdentifiableType { 28 | 29 | } 30 | 31 | extension Int32 : IdentifiableType { 32 | 33 | } 34 | 35 | extension Int64 : IdentifiableType { 36 | 37 | } 38 | 39 | 40 | extension UInt : IdentifiableType { 41 | 42 | } 43 | 44 | extension UInt8 : IdentifiableType { 45 | 46 | } 47 | 48 | extension UInt16 : IdentifiableType { 49 | 50 | } 51 | 52 | extension UInt32 : IdentifiableType { 53 | 54 | } 55 | 56 | extension UInt64 : IdentifiableType { 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxCollectionViewSectionedAnimatedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxCollectionViewSectionedAnimatedDataSource.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 7/2/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxSwift 14 | import RxCocoa 15 | #endif 16 | import Differentiator 17 | 18 | open class RxCollectionViewSectionedAnimatedDataSource 19 | : CollectionViewSectionedDataSource
20 | , RxCollectionViewDataSourceType { 21 | public typealias Element = [Section] 22 | public typealias DecideViewTransition = (CollectionViewSectionedDataSource
, UICollectionView, [Changeset
]) -> ViewTransition 23 | 24 | // animation configuration 25 | public var animationConfiguration: AnimationConfiguration 26 | 27 | /// Calculates view transition depending on type of changes 28 | public var decideViewTransition: DecideViewTransition 29 | 30 | public init( 31 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 32 | decideViewTransition: @escaping DecideViewTransition = { _, _, _ in .animated }, 33 | configureCell: @escaping ConfigureCell, 34 | configureSupplementaryView: ConfigureSupplementaryView? = nil, 35 | moveItem: @escaping MoveItem = { _, _, _ in () }, 36 | canMoveItemAtIndexPath: @escaping CanMoveItemAtIndexPath = { _, _ in false } 37 | ) { 38 | self.animationConfiguration = animationConfiguration 39 | self.decideViewTransition = decideViewTransition 40 | super.init( 41 | configureCell: configureCell, 42 | configureSupplementaryView: configureSupplementaryView, 43 | moveItem: moveItem, 44 | canMoveItemAtIndexPath: canMoveItemAtIndexPath 45 | ) 46 | } 47 | 48 | // there is no longer limitation to load initial sections with reloadData 49 | // but it is kept as a feature everyone got used to 50 | var dataSet = false 51 | 52 | open func collectionView(_ collectionView: UICollectionView, observedEvent: Event) { 53 | Binder(self) { dataSource, newSections in 54 | #if DEBUG 55 | dataSource._dataSourceBound = true 56 | #endif 57 | if !dataSource.dataSet { 58 | dataSource.dataSet = true 59 | dataSource.setSections(newSections) 60 | collectionView.reloadData() 61 | } 62 | else { 63 | // if view is not in view hierarchy, performing batch updates will crash the app 64 | if collectionView.window == nil { 65 | dataSource.setSections(newSections) 66 | collectionView.reloadData() 67 | return 68 | } 69 | let oldSections = dataSource.sectionModels 70 | do { 71 | let differences = try Diff.differencesForSectionedView(initialSections: oldSections, finalSections: newSections) 72 | 73 | switch dataSource.decideViewTransition(dataSource, collectionView, differences) { 74 | case .animated: 75 | // each difference must be run in a separate 'performBatchUpdates', otherwise it crashes. 76 | // this is a limitation of Diff tool 77 | for difference in differences { 78 | let updateBlock = { 79 | // sections must be set within updateBlock in 'performBatchUpdates' 80 | dataSource.setSections(difference.finalSections) 81 | collectionView.batchUpdates(difference, animationConfiguration: dataSource.animationConfiguration) 82 | } 83 | collectionView.performBatchUpdates(updateBlock, completion: nil) 84 | } 85 | 86 | case .reload: 87 | dataSource.setSections(newSections) 88 | collectionView.reloadData() 89 | return 90 | } 91 | } 92 | catch let e { 93 | rxDebugFatalError(e) 94 | dataSource.setSections(newSections) 95 | collectionView.reloadData() 96 | } 97 | } 98 | }.on(observedEvent) 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxCollectionViewSectionedReloadDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxCollectionViewSectionedReloadDataSource.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 7/2/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxSwift 14 | import RxCocoa 15 | #endif 16 | import Differentiator 17 | 18 | open class RxCollectionViewSectionedReloadDataSource 19 | : CollectionViewSectionedDataSource
20 | , RxCollectionViewDataSourceType { 21 | 22 | public typealias Element = [Section] 23 | 24 | open func collectionView(_ collectionView: UICollectionView, observedEvent: Event) { 25 | Binder(self) { dataSource, element in 26 | #if DEBUG 27 | dataSource._dataSourceBound = true 28 | #endif 29 | dataSource.setSections(element) 30 | collectionView.reloadData() 31 | collectionView.collectionViewLayout.invalidateLayout() 32 | }.on(observedEvent) 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxDataSources.h: -------------------------------------------------------------------------------- 1 | // 2 | // RxDataSources.h 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/1/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for RxDataSources. 12 | FOUNDATION_EXPORT double RxDataSourcesVersionNumber; 13 | 14 | //! Project version string for RxDataSources. 15 | FOUNDATION_EXPORT const unsigned char RxDataSourcesVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxPickerViewAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxPickerViewAdapter.swift 3 | // RxDataSources 4 | // 5 | // Created by Sergey Shulga on 04/07/2017. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | 11 | import Foundation 12 | import UIKit 13 | #if !RX_NO_MODULE 14 | import RxSwift 15 | import RxCocoa 16 | #endif 17 | 18 | /// A reactive UIPickerView adapter which uses `func pickerView(UIPickerView, titleForRow: Int, forComponent: Int)` to display the content 19 | /** 20 | Example: 21 | 22 | let adapter = RxPickerViewStringAdapter<[T]>(...) 23 | 24 | items 25 | .bind(to: firstPickerView.rx.items(adapter: adapter)) 26 | .disposed(by: disposeBag) 27 | 28 | */ 29 | open class RxPickerViewStringAdapter: RxPickerViewDataSource, UIPickerViewDelegate { 30 | /** 31 | - parameter dataSource 32 | - parameter pickerView 33 | - parameter components 34 | - parameter row 35 | - parameter component 36 | */ 37 | public typealias TitleForRow = ( 38 | _ dataSource: RxPickerViewStringAdapter, 39 | _ pickerView: UIPickerView, 40 | _ components: Components, 41 | _ row: Int, 42 | _ component: Int 43 | ) -> String? 44 | 45 | private let titleForRow: TitleForRow 46 | 47 | /** 48 | - parameter components: Initial content value. 49 | - parameter numberOfComponents: Implementation of corresponding delegate method. 50 | - parameter numberOfRowsInComponent: Implementation of corresponding delegate method. 51 | - parameter titleForRow: Implementation of corresponding adapter method that converts component to `String`. 52 | */ 53 | public init(components: Components, 54 | numberOfComponents: @escaping NumberOfComponents, 55 | numberOfRowsInComponent: @escaping NumberOfRowsInComponent, 56 | titleForRow: @escaping TitleForRow) { 57 | self.titleForRow = titleForRow 58 | super.init(components: components, 59 | numberOfComponents: numberOfComponents, 60 | numberOfRowsInComponent: numberOfRowsInComponent) 61 | } 62 | 63 | open func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 64 | return titleForRow(self, pickerView, components, row, component) 65 | } 66 | } 67 | 68 | /// A reactive UIPickerView adapter which uses `func pickerView(UIPickerView, viewForRow: Int, forComponent: Int, reusing: UIView?)` to display the content 69 | /** 70 | Example: 71 | 72 | let adapter = RxPickerViewAttributedStringAdapter<[T]>(...) 73 | 74 | items 75 | .bind(to: firstPickerView.rx.items(adapter: adapter)) 76 | .disposed(by: disposeBag) 77 | 78 | */ 79 | open class RxPickerViewAttributedStringAdapter: RxPickerViewDataSource, UIPickerViewDelegate { 80 | /** 81 | - parameter dataSource 82 | - parameter pickerView 83 | - parameter components 84 | - parameter row 85 | - parameter component 86 | */ 87 | public typealias AttributedTitleForRow = ( 88 | _ dataSource: RxPickerViewAttributedStringAdapter, 89 | _ pickerView: UIPickerView, 90 | _ components: Components, 91 | _ row: Int, 92 | _ component: Int 93 | ) -> NSAttributedString? 94 | 95 | private let attributedTitleForRow: AttributedTitleForRow 96 | 97 | /** 98 | - parameter components: Initial content value. 99 | - parameter numberOfComponents: Implementation of corresponding delegate method. 100 | - parameter numberOfRowsInComponent: Implementation of corresponding delegate method. 101 | - parameter attributedTitleForRow: Implementation of corresponding adapter method that converts component to `NSAttributedString`. 102 | */ 103 | public init(components: Components, 104 | numberOfComponents: @escaping NumberOfComponents, 105 | numberOfRowsInComponent: @escaping NumberOfRowsInComponent, 106 | attributedTitleForRow: @escaping AttributedTitleForRow) { 107 | self.attributedTitleForRow = attributedTitleForRow 108 | super.init(components: components, 109 | numberOfComponents: numberOfComponents, 110 | numberOfRowsInComponent: numberOfRowsInComponent) 111 | } 112 | 113 | open func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { 114 | return attributedTitleForRow(self, pickerView, components, row, component) 115 | } 116 | } 117 | 118 | /// A reactive UIPickerView adapter which uses `func pickerView(pickerView:, viewForRow row:, forComponent component:)` to display the content 119 | /** 120 | Example: 121 | 122 | let adapter = RxPickerViewViewAdapter<[T]>(...) 123 | 124 | items 125 | .bind(to: firstPickerView.rx.items(adapter: adapter)) 126 | .disposed(by: disposeBag) 127 | 128 | */ 129 | open class RxPickerViewViewAdapter: RxPickerViewDataSource, UIPickerViewDelegate { 130 | /** 131 | - parameter dataSource 132 | - parameter pickerView 133 | - parameter components 134 | - parameter row 135 | - parameter component 136 | - parameter view 137 | */ 138 | public typealias ViewForRow = ( 139 | _ dataSource: RxPickerViewViewAdapter, 140 | _ pickerView: UIPickerView, 141 | _ components: Components, 142 | _ row: Int, 143 | _ component: Int, 144 | _ view: UIView? 145 | ) -> UIView 146 | 147 | private let viewForRow: ViewForRow 148 | 149 | /** 150 | - parameter components: Initial content value. 151 | - parameter numberOfComponents: Implementation of corresponding delegate method. 152 | - parameter numberOfRowsInComponent: Implementation of corresponding delegate method. 153 | - parameter attributedTitleForRow: Implementation of corresponding adapter method that converts component to `UIView`. 154 | */ 155 | public init(components: Components, 156 | numberOfComponents: @escaping NumberOfComponents, 157 | numberOfRowsInComponent: @escaping NumberOfRowsInComponent, 158 | viewForRow: @escaping ViewForRow) { 159 | self.viewForRow = viewForRow 160 | super.init(components: components, 161 | numberOfComponents: numberOfComponents, 162 | numberOfRowsInComponent: numberOfRowsInComponent) 163 | } 164 | 165 | open func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { 166 | return viewForRow(self, pickerView, components, row, component, view) 167 | } 168 | } 169 | 170 | /// A reactive UIPickerView data source 171 | open class RxPickerViewDataSource: NSObject, UIPickerViewDataSource { 172 | /** 173 | - parameter dataSource 174 | - parameter pickerView 175 | - parameter components 176 | */ 177 | public typealias NumberOfComponents = ( 178 | _ dataSource: RxPickerViewDataSource, 179 | _ pickerView: UIPickerView, 180 | _ components: Components) -> Int 181 | /** 182 | - parameter dataSource 183 | - parameter pickerView 184 | - parameter components 185 | - parameter component 186 | */ 187 | public typealias NumberOfRowsInComponent = ( 188 | _ dataSource: RxPickerViewDataSource, 189 | _ pickerView: UIPickerView, 190 | _ components: Components, 191 | _ component: Int 192 | ) -> Int 193 | 194 | fileprivate var components: Components 195 | 196 | /** 197 | - parameter components: Initial content value. 198 | - parameter numberOfComponents: Implementation of corresponding delegate method. 199 | - parameter numberOfRowsInComponent: Implementation of corresponding delegate method. 200 | */ 201 | init(components: Components, 202 | numberOfComponents: @escaping NumberOfComponents, 203 | numberOfRowsInComponent: @escaping NumberOfRowsInComponent) { 204 | self.components = components 205 | self.numberOfComponents = numberOfComponents 206 | self.numberOfRowsInComponent = numberOfRowsInComponent 207 | super.init() 208 | } 209 | 210 | private let numberOfComponents: NumberOfComponents 211 | private let numberOfRowsInComponent: NumberOfRowsInComponent 212 | 213 | // MARK: UIPickerViewDataSource 214 | 215 | public func numberOfComponents(in pickerView: UIPickerView) -> Int { 216 | return numberOfComponents(self, pickerView, components) 217 | } 218 | 219 | public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 220 | return numberOfRowsInComponent(self, pickerView, components, component) 221 | } 222 | } 223 | 224 | extension RxPickerViewDataSource: RxPickerViewDataSourceType { 225 | public func pickerView(_ pickerView: UIPickerView, observedEvent: Event) { 226 | Binder(self) { dataSource, components in 227 | dataSource.components = components 228 | pickerView.reloadAllComponents() 229 | }.on(observedEvent) 230 | } 231 | } 232 | 233 | #endif 234 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxTableViewSectionedAnimatedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxTableViewSectionedAnimatedDataSource.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 6/27/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxSwift 14 | import RxCocoa 15 | #endif 16 | import Differentiator 17 | 18 | open class RxTableViewSectionedAnimatedDataSource 19 | : TableViewSectionedDataSource
20 | , RxTableViewDataSourceType { 21 | public typealias Element = [Section] 22 | public typealias DecideViewTransition = (TableViewSectionedDataSource
, UITableView, [Changeset
]) -> ViewTransition 23 | 24 | /// Animation configuration for data source 25 | public var animationConfiguration: AnimationConfiguration 26 | 27 | /// Calculates view transition depending on type of changes 28 | public var decideViewTransition: DecideViewTransition 29 | 30 | #if os(iOS) 31 | public init( 32 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 33 | decideViewTransition: @escaping DecideViewTransition = { _, _, _ in .animated }, 34 | configureCell: @escaping ConfigureCell, 35 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 36 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 37 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 38 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false }, 39 | sectionIndexTitles: @escaping SectionIndexTitles = { _ in nil }, 40 | sectionForSectionIndexTitle: @escaping SectionForSectionIndexTitle = { _, _, index in index } 41 | ) { 42 | self.animationConfiguration = animationConfiguration 43 | self.decideViewTransition = decideViewTransition 44 | super.init( 45 | configureCell: configureCell, 46 | titleForHeaderInSection: titleForHeaderInSection, 47 | titleForFooterInSection: titleForFooterInSection, 48 | canEditRowAtIndexPath: canEditRowAtIndexPath, 49 | canMoveRowAtIndexPath: canMoveRowAtIndexPath, 50 | sectionIndexTitles: sectionIndexTitles, 51 | sectionForSectionIndexTitle: sectionForSectionIndexTitle 52 | ) 53 | } 54 | #else 55 | public init( 56 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 57 | decideViewTransition: @escaping DecideViewTransition = { _, _, _ in .animated }, 58 | configureCell: @escaping ConfigureCell, 59 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 60 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 61 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 62 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false } 63 | ) { 64 | self.animationConfiguration = animationConfiguration 65 | self.decideViewTransition = decideViewTransition 66 | super.init( 67 | configureCell: configureCell, 68 | titleForHeaderInSection: titleForHeaderInSection, 69 | titleForFooterInSection: titleForFooterInSection, 70 | canEditRowAtIndexPath: canEditRowAtIndexPath, 71 | canMoveRowAtIndexPath: canMoveRowAtIndexPath 72 | ) 73 | } 74 | #endif 75 | 76 | var dataSet = false 77 | 78 | open func tableView(_ tableView: UITableView, observedEvent: Event) { 79 | Binder(self) { dataSource, newSections in 80 | #if DEBUG 81 | dataSource._dataSourceBound = true 82 | #endif 83 | if !dataSource.dataSet { 84 | dataSource.dataSet = true 85 | dataSource.setSections(newSections) 86 | tableView.reloadData() 87 | } 88 | else { 89 | // if view is not in view hierarchy, performing batch updates will crash the app 90 | if tableView.window == nil { 91 | dataSource.setSections(newSections) 92 | tableView.reloadData() 93 | return 94 | } 95 | let oldSections = dataSource.sectionModels 96 | do { 97 | let differences = try Diff.differencesForSectionedView(initialSections: oldSections, finalSections: newSections) 98 | 99 | switch dataSource.decideViewTransition(dataSource, tableView, differences) { 100 | case .animated: 101 | // each difference must be run in a separate 'performBatchUpdates', otherwise it crashes. 102 | // this is a limitation of Diff tool 103 | for difference in differences { 104 | let updateBlock = { 105 | // sections must be set within updateBlock in 'performBatchUpdates' 106 | dataSource.setSections(difference.finalSections) 107 | tableView.batchUpdates(difference, animationConfiguration: dataSource.animationConfiguration) 108 | } 109 | if #available(iOS 11, tvOS 11, *) { 110 | tableView.performBatchUpdates(updateBlock, completion: nil) 111 | } else { 112 | tableView.beginUpdates() 113 | updateBlock() 114 | tableView.endUpdates() 115 | } 116 | } 117 | 118 | case .reload: 119 | dataSource.setSections(newSections) 120 | tableView.reloadData() 121 | return 122 | } 123 | } 124 | catch let e { 125 | rxDebugFatalError(e) 126 | dataSource.setSections(newSections) 127 | tableView.reloadData() 128 | } 129 | } 130 | }.on(observedEvent) 131 | } 132 | } 133 | #endif 134 | -------------------------------------------------------------------------------- /Sources/RxDataSources/RxTableViewSectionedReloadDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxTableViewSectionedReloadDataSource.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 6/27/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxSwift 14 | import RxCocoa 15 | #endif 16 | import Differentiator 17 | 18 | open class RxTableViewSectionedReloadDataSource 19 | : TableViewSectionedDataSource
20 | , RxTableViewDataSourceType { 21 | public typealias Element = [Section] 22 | 23 | open func tableView(_ tableView: UITableView, observedEvent: Event) { 24 | Binder(self) { dataSource, element in 25 | #if DEBUG 26 | dataSource._dataSourceBound = true 27 | #endif 28 | dataSource.setSections(element) 29 | tableView.reloadData() 30 | }.on(observedEvent) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/RxDataSources/String+IdentifiableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+IdentifiableType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 7/4/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String : IdentifiableType { 12 | public typealias Identity = String 13 | 14 | public var identity: String { 15 | return self 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/RxDataSources/TableViewSectionedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewSectionedDataSource.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 6/15/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | #if !RX_NO_MODULE 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class TableViewSectionedDataSource 18 | : NSObject 19 | , UITableViewDataSource 20 | , SectionedViewDataSourceType { 21 | 22 | public typealias Item = Section.Item 23 | 24 | public typealias ConfigureCell = (TableViewSectionedDataSource
, UITableView, IndexPath, Item) -> UITableViewCell 25 | public typealias TitleForHeaderInSection = (TableViewSectionedDataSource
, Int) -> String? 26 | public typealias TitleForFooterInSection = (TableViewSectionedDataSource
, Int) -> String? 27 | public typealias CanEditRowAtIndexPath = (TableViewSectionedDataSource
, IndexPath) -> Bool 28 | public typealias CanMoveRowAtIndexPath = (TableViewSectionedDataSource
, IndexPath) -> Bool 29 | 30 | #if os(iOS) 31 | public typealias SectionIndexTitles = (TableViewSectionedDataSource
) -> [String]? 32 | public typealias SectionForSectionIndexTitle = (TableViewSectionedDataSource
, _ title: String, _ index: Int) -> Int 33 | #endif 34 | 35 | #if os(iOS) 36 | public init( 37 | configureCell: @escaping ConfigureCell, 38 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 39 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 40 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in true }, 41 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in true }, 42 | sectionIndexTitles: @escaping SectionIndexTitles = { _ in nil }, 43 | sectionForSectionIndexTitle: @escaping SectionForSectionIndexTitle = { _, _, index in index } 44 | ) { 45 | self.configureCell = configureCell 46 | self.titleForHeaderInSection = titleForHeaderInSection 47 | self.titleForFooterInSection = titleForFooterInSection 48 | self.canEditRowAtIndexPath = canEditRowAtIndexPath 49 | self.canMoveRowAtIndexPath = canMoveRowAtIndexPath 50 | self.sectionIndexTitles = sectionIndexTitles 51 | self.sectionForSectionIndexTitle = sectionForSectionIndexTitle 52 | } 53 | #else 54 | public init( 55 | configureCell: @escaping ConfigureCell, 56 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 57 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 58 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in true }, 59 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in true } 60 | ) { 61 | self.configureCell = configureCell 62 | self.titleForHeaderInSection = titleForHeaderInSection 63 | self.titleForFooterInSection = titleForFooterInSection 64 | self.canEditRowAtIndexPath = canEditRowAtIndexPath 65 | self.canMoveRowAtIndexPath = canMoveRowAtIndexPath 66 | } 67 | #endif 68 | 69 | #if DEBUG 70 | // If data source has already been bound, then mutating it 71 | // afterwards isn't something desired. 72 | // This simulates immutability after binding 73 | var _dataSourceBound: Bool = false 74 | 75 | private func ensureNotMutatedAfterBinding() { 76 | assert(!_dataSourceBound, "Data source is already bound. Please write this line before binding call (`bindTo`, `drive`). Data source must first be completely configured, and then bound after that, otherwise there could be runtime bugs, glitches, or partial malfunctions.") 77 | } 78 | 79 | #endif 80 | 81 | // This structure exists because model can be mutable 82 | // In that case current state value should be preserved. 83 | // The state that needs to be preserved is ordering of items in section 84 | // and their relationship with section. 85 | // If particular item is mutable, that is irrelevant for this logic to function 86 | // properly. 87 | public typealias SectionModelSnapshot = SectionModel 88 | 89 | private var _sectionModels: [SectionModelSnapshot] = [] 90 | 91 | open var sectionModels: [Section] { 92 | return _sectionModels.map { Section(original: $0.model, items: $0.items) } 93 | } 94 | 95 | open subscript(section: Int) -> Section { 96 | let sectionModel = self._sectionModels[section] 97 | return Section(original: sectionModel.model, items: sectionModel.items) 98 | } 99 | 100 | open subscript(indexPath: IndexPath) -> Item { 101 | get { 102 | return self._sectionModels[indexPath.section].items[indexPath.item] 103 | } 104 | set(item) { 105 | var section = self._sectionModels[indexPath.section] 106 | section.items[indexPath.item] = item 107 | self._sectionModels[indexPath.section] = section 108 | } 109 | } 110 | 111 | open func model(at indexPath: IndexPath) throws -> Any { 112 | guard indexPath.section < self._sectionModels.count, 113 | indexPath.item < self._sectionModels[indexPath.section].items.count else { 114 | throw RxDataSourceError.outOfBounds(indexPath: indexPath) 115 | } 116 | 117 | return self[indexPath] 118 | } 119 | 120 | open func setSections(_ sections: [Section]) { 121 | self._sectionModels = sections.map { SectionModelSnapshot(model: $0, items: $0.items) } 122 | } 123 | 124 | open var configureCell: ConfigureCell { 125 | didSet { 126 | #if DEBUG 127 | ensureNotMutatedAfterBinding() 128 | #endif 129 | } 130 | } 131 | 132 | open var titleForHeaderInSection: TitleForHeaderInSection { 133 | didSet { 134 | #if DEBUG 135 | ensureNotMutatedAfterBinding() 136 | #endif 137 | } 138 | } 139 | open var titleForFooterInSection: TitleForFooterInSection { 140 | didSet { 141 | #if DEBUG 142 | ensureNotMutatedAfterBinding() 143 | #endif 144 | } 145 | } 146 | 147 | open var canEditRowAtIndexPath: CanEditRowAtIndexPath { 148 | didSet { 149 | #if DEBUG 150 | ensureNotMutatedAfterBinding() 151 | #endif 152 | } 153 | } 154 | open var canMoveRowAtIndexPath: CanMoveRowAtIndexPath { 155 | didSet { 156 | #if DEBUG 157 | ensureNotMutatedAfterBinding() 158 | #endif 159 | } 160 | } 161 | 162 | #if os(iOS) 163 | open var sectionIndexTitles: SectionIndexTitles { 164 | didSet { 165 | #if DEBUG 166 | ensureNotMutatedAfterBinding() 167 | #endif 168 | } 169 | } 170 | open var sectionForSectionIndexTitle: SectionForSectionIndexTitle { 171 | didSet { 172 | #if DEBUG 173 | ensureNotMutatedAfterBinding() 174 | #endif 175 | } 176 | } 177 | #endif 178 | 179 | 180 | // UITableViewDataSource 181 | 182 | open func numberOfSections(in tableView: UITableView) -> Int { 183 | return _sectionModels.count 184 | } 185 | 186 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 187 | guard _sectionModels.count > section else { return 0 } 188 | return _sectionModels[section].items.count 189 | } 190 | 191 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 192 | precondition(indexPath.item < _sectionModels[indexPath.section].items.count) 193 | 194 | return configureCell(self, tableView, indexPath, self[indexPath]) 195 | } 196 | 197 | open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 198 | return titleForHeaderInSection(self, section) 199 | } 200 | 201 | open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 202 | return titleForFooterInSection(self, section) 203 | } 204 | 205 | open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 206 | return canEditRowAtIndexPath(self, indexPath) 207 | } 208 | 209 | open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 210 | return canMoveRowAtIndexPath(self, indexPath) 211 | } 212 | 213 | open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 214 | self._sectionModels.moveFromSourceIndexPath(sourceIndexPath, destinationIndexPath: destinationIndexPath) 215 | } 216 | 217 | #if os(iOS) 218 | open func sectionIndexTitles(for tableView: UITableView) -> [String]? { 219 | return sectionIndexTitles(self) 220 | } 221 | 222 | open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { 223 | return sectionForSectionIndexTitle(self, title, index) 224 | } 225 | #endif 226 | } 227 | #endif 228 | -------------------------------------------------------------------------------- /Sources/RxDataSources/UI+SectionedViewType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UI+SectionedViewType.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 6/27/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | import Foundation 11 | import UIKit 12 | import Differentiator 13 | 14 | func indexSet(_ values: [Int]) -> IndexSet { 15 | let indexSet = NSMutableIndexSet() 16 | for i in values { 17 | indexSet.add(i) 18 | } 19 | return indexSet as IndexSet 20 | } 21 | 22 | extension UITableView : SectionedViewType { 23 | 24 | public func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 25 | self.insertRows(at: paths, with: animationStyle) 26 | } 27 | 28 | public func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 29 | self.deleteRows(at: paths, with: animationStyle) 30 | } 31 | 32 | public func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) { 33 | self.moveRow(at: from, to: to) 34 | } 35 | 36 | public func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 37 | self.reloadRows(at: paths, with: animationStyle) 38 | } 39 | 40 | public func insertSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 41 | self.insertSections(indexSet(sections), with: animationStyle) 42 | } 43 | 44 | public func deleteSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 45 | self.deleteSections(indexSet(sections), with: animationStyle) 46 | } 47 | 48 | public func moveSection(_ from: Int, to: Int) { 49 | self.moveSection(from, toSection: to) 50 | } 51 | 52 | public func reloadSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 53 | self.reloadSections(indexSet(sections), with: animationStyle) 54 | } 55 | } 56 | 57 | extension UICollectionView : SectionedViewType { 58 | public func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 59 | self.insertItems(at: paths) 60 | } 61 | 62 | public func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 63 | self.deleteItems(at: paths) 64 | } 65 | 66 | public func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) { 67 | self.moveItem(at: from, to: to) 68 | } 69 | 70 | public func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) { 71 | self.reloadItems(at: paths) 72 | } 73 | 74 | public func insertSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 75 | self.insertSections(indexSet(sections)) 76 | } 77 | 78 | public func deleteSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 79 | self.deleteSections(indexSet(sections)) 80 | } 81 | 82 | public func moveSection(_ from: Int, to: Int) { 83 | self.moveSection(from, toSection: to) 84 | } 85 | 86 | public func reloadSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) { 87 | self.reloadSections(indexSet(sections)) 88 | } 89 | } 90 | 91 | public protocol SectionedViewType { 92 | func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) 93 | func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) 94 | func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) 95 | func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableView.RowAnimation) 96 | 97 | func insertSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) 98 | func deleteSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) 99 | func moveSection(_ from: Int, to: Int) 100 | func reloadSections(_ sections: [Int], animationStyle: UITableView.RowAnimation) 101 | } 102 | 103 | extension SectionedViewType { 104 | public func batchUpdates
(_ changes: Changeset
, animationConfiguration: AnimationConfiguration) { 105 | // swiftlint:disable:next nesting 106 | typealias Item = Section.Item 107 | 108 | deleteSections(changes.deletedSections, animationStyle: animationConfiguration.deleteAnimation) 109 | 110 | insertSections(changes.insertedSections, animationStyle: animationConfiguration.insertAnimation) 111 | for (from, to) in changes.movedSections { 112 | moveSection(from, to: to) 113 | } 114 | 115 | deleteItemsAtIndexPaths( 116 | changes.deletedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 117 | animationStyle: animationConfiguration.deleteAnimation 118 | ) 119 | insertItemsAtIndexPaths( 120 | changes.insertedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 121 | animationStyle: animationConfiguration.insertAnimation 122 | ) 123 | reloadItemsAtIndexPaths( 124 | changes.updatedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 125 | animationStyle: animationConfiguration.reloadAnimation 126 | ) 127 | 128 | for (from, to) in changes.movedItems { 129 | moveItemAtIndexPath( 130 | IndexPath(item: from.itemIndex, section: from.sectionIndex), 131 | to: IndexPath(item: to.itemIndex, section: to.sectionIndex) 132 | ) 133 | } 134 | } 135 | } 136 | #endif 137 | -------------------------------------------------------------------------------- /Sources/RxDataSources/ViewTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewTransition.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 10/22/17. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | /// Transition between two view states 10 | public enum ViewTransition { 11 | /// animated transition 12 | case animated 13 | /// refresh view without animations 14 | case reload 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/AlgorithmTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlgorithmTests.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Differentiator 12 | import RxDataSources 13 | 14 | class AlgorithmTests: XCTestCase { 15 | 16 | } 17 | 18 | // single section simple 19 | extension AlgorithmTests { 20 | func testItemInsert() { 21 | let initial: [s] = [ 22 | s(1, [ 23 | i(0, ""), 24 | i(1, ""), 25 | i(2, "") 26 | ]) 27 | ] 28 | 29 | let final: [s] = [ 30 | s(1, [ 31 | i(0, ""), 32 | i(1, ""), 33 | i(2, ""), 34 | i(3, "") 35 | ]) 36 | ] 37 | 38 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 39 | 40 | XCTAssertTrue(differences.count == 1) 41 | XCTAssertTrue(differences.first!.onlyContains(insertedItems: 1)) 42 | 43 | XCTAssertEqual(initial.apply(differences), final) 44 | } 45 | 46 | func testItemDelete() { 47 | let initial: [s] = [ 48 | s(1, [ 49 | i(0, ""), 50 | i(1, ""), 51 | i(2, "") 52 | ]) 53 | ] 54 | 55 | let final: [s] = [ 56 | s(1, [ 57 | i(0, ""), 58 | i(2, "") 59 | ]) 60 | ] 61 | 62 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 63 | 64 | XCTAssertTrue(differences.count == 1) 65 | XCTAssertTrue(differences.first!.onlyContains(deletedItems: 1)) 66 | 67 | XCTAssertEqual(initial.apply(differences), final) 68 | } 69 | 70 | func testItemMove1() { 71 | let initial: [s] = [ 72 | s(1, [ 73 | i(0, ""), 74 | i(1, ""), 75 | i(2, "") 76 | ]) 77 | ] 78 | 79 | let final: [s] = [ 80 | s(1, [ 81 | i(1, ""), 82 | i(2, ""), 83 | i(0, "") 84 | ]) 85 | ] 86 | 87 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 88 | 89 | XCTAssertTrue(differences.count == 1) 90 | XCTAssertTrue(differences.first!.onlyContains(movedItems: 2)) 91 | 92 | XCTAssertEqual(initial.apply(differences), final) 93 | } 94 | 95 | func testItemMove2() { 96 | let initial: [s] = [ 97 | s(1, [ 98 | i(0, ""), 99 | i(1, ""), 100 | i(2, "") 101 | ]) 102 | ] 103 | 104 | let final: [s] = [ 105 | s(1, [ 106 | i(2, ""), 107 | i(0, ""), 108 | i(1, "") 109 | ]) 110 | ] 111 | 112 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 113 | 114 | XCTAssertTrue(differences.count == 1) 115 | XCTAssertTrue(differences.first!.onlyContains(movedItems: 1)) 116 | 117 | XCTAssertEqual(initial.apply(differences), final) 118 | } 119 | 120 | func testItemUpdated() { 121 | let initial: [s] = [ 122 | s(1, [ 123 | i(0, ""), 124 | i(1, ""), 125 | i(2, "") 126 | ]) 127 | ] 128 | 129 | let final: [s] = [ 130 | s(1, [ 131 | i(0, ""), 132 | i(1, "u"), 133 | i(2, "") 134 | ]) 135 | ] 136 | 137 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 138 | 139 | XCTAssertTrue(differences.count == 1) 140 | XCTAssertTrue(differences.first!.onlyContains(updatedItems: 1)) 141 | 142 | XCTAssertEqual(initial.apply(differences), final) 143 | } 144 | 145 | func testItemUpdatedAndMoved() { 146 | let initial: [s] = [ 147 | s(1, [ 148 | i(0, ""), 149 | i(1, ""), 150 | i(2, "") 151 | ]) 152 | ] 153 | 154 | let final: [s] = [ 155 | s(1, [ 156 | i(1, "u"), 157 | i(0, ""), 158 | i(2, "") 159 | ]) 160 | ] 161 | 162 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 163 | 164 | XCTAssertTrue(differences.count == 2) 165 | 166 | // updates ok 167 | XCTAssertTrue(differences[0].onlyContains(updatedItems: 1)) 168 | XCTAssertTrue(differences[0].updatedItems[0] == ItemPath(sectionIndex: 0, itemIndex: 1)) 169 | 170 | // moves ok 171 | XCTAssertTrue(differences[1].onlyContains(movedItems: 1)) 172 | 173 | XCTAssertEqual(initial.apply(differences), final) 174 | } 175 | } 176 | 177 | // multiple sections simple 178 | extension AlgorithmTests { 179 | func testInsertSection() { 180 | let initial: [s] = [ 181 | s(1, [ 182 | i(0, ""), 183 | i(1, ""), 184 | i(2, "") 185 | ]) 186 | ] 187 | 188 | let final: [s] = [ 189 | s(1, [ 190 | i(0, ""), 191 | i(1, ""), 192 | i(2, "") 193 | ]), 194 | s(2, [ 195 | i(3, ""), 196 | i(4, ""), 197 | i(5, "") 198 | ]) 199 | ] 200 | 201 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 202 | 203 | XCTAssertTrue(differences.count == 1) 204 | XCTAssertTrue(differences.first!.onlyContains(insertedSections: 1)) 205 | 206 | XCTAssertEqual(initial.apply(differences), final) 207 | } 208 | 209 | func testDeleteSection() { 210 | let initial: [s] = [ 211 | s(1, [ 212 | i(0, ""), 213 | i(1, ""), 214 | i(2, "") 215 | ]) 216 | ] 217 | 218 | let final: [s] = [ 219 | 220 | ] 221 | 222 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 223 | 224 | XCTAssertTrue(differences.count == 1) 225 | XCTAssertTrue(differences.first!.onlyContains(deletedSections: 1)) 226 | 227 | XCTAssertEqual(initial.apply(differences), final) 228 | } 229 | 230 | func testMovedSection1() { 231 | let initial: [s] = [ 232 | s(1, [ 233 | i(0, ""), 234 | i(1, ""), 235 | i(2, "") 236 | ]), 237 | s(2, [ 238 | i(3, ""), 239 | i(4, ""), 240 | i(5, "") 241 | ]), 242 | s(3, [ 243 | i(6, ""), 244 | i(7, ""), 245 | i(8, "") 246 | ]) 247 | ] 248 | 249 | let final: [s] = [ 250 | s(2, [ 251 | i(3, ""), 252 | i(4, ""), 253 | i(5, "") 254 | ]), 255 | s(3, [ 256 | i(6, ""), 257 | i(7, ""), 258 | i(8, "") 259 | ]), 260 | s(1, [ 261 | i(0, ""), 262 | i(1, ""), 263 | i(2, "") 264 | ]) 265 | ] 266 | 267 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 268 | 269 | XCTAssertTrue(differences.count == 1) 270 | XCTAssertTrue(differences.first!.onlyContains(movedSections: 2)) 271 | 272 | XCTAssertEqual(initial.apply(differences), final) 273 | } 274 | 275 | func testMovedSection2() { 276 | let initial: [s] = [ 277 | s(1, [ 278 | i(0, ""), 279 | i(1, ""), 280 | i(2, "") 281 | ]), 282 | s(2, [ 283 | i(3, ""), 284 | i(4, ""), 285 | i(5, "") 286 | ]), 287 | s(3, [ 288 | i(6, ""), 289 | i(7, ""), 290 | i(8, "") 291 | ]) 292 | ] 293 | 294 | let final: [s] = [ 295 | s(3, [ 296 | i(6, ""), 297 | i(7, ""), 298 | i(8, "") 299 | ]), 300 | s(1, [ 301 | i(0, ""), 302 | i(1, ""), 303 | i(2, "") 304 | ]), 305 | s(2, [ 306 | i(3, ""), 307 | i(4, ""), 308 | i(5, "") 309 | ]) 310 | ] 311 | 312 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 313 | 314 | XCTAssertTrue(differences.count == 1) 315 | XCTAssertTrue(differences.first!.onlyContains(movedSections: 1)) 316 | 317 | XCTAssertEqual(initial.apply(differences), final) 318 | } 319 | } 320 | 321 | // errors 322 | extension AlgorithmTests { 323 | func testThrowsErrorOnDuplicateItem() { 324 | let initial: [s] = [ 325 | s(1, [ 326 | i(1111, "") 327 | ]), 328 | s(2, [ 329 | i(1111, "") 330 | ]) 331 | 332 | ] 333 | 334 | do { 335 | _ = try Diff.differencesForSectionedView(initialSections: initial, finalSections: initial) 336 | XCTFail("Should throw exception") 337 | } 338 | catch let exception { 339 | guard case let .duplicateItem(item) = exception as! Diff.Error else { 340 | XCTFail("Not required error") 341 | return 342 | } 343 | 344 | XCTAssertEqual(item as! i, i(1111, "")) 345 | } 346 | } 347 | 348 | func testThrowsErrorOnDuplicateSection() { 349 | let initial: [s] = [ 350 | s(1, [ 351 | i(1111, "") 352 | ]), 353 | s(1, [ 354 | i(1112, "") 355 | ]) 356 | 357 | ] 358 | 359 | do { 360 | _ = try Diff.differencesForSectionedView(initialSections: initial, finalSections: initial) 361 | XCTFail("Should throw exception") 362 | } 363 | catch let exception { 364 | guard case let .duplicateSection(section) = exception as! Diff.Error else { 365 | XCTFail("Not required error") 366 | return 367 | } 368 | 369 | XCTAssertEqual(section as! s, s(1, [ 370 | i(1112, "") 371 | ])) 372 | } 373 | } 374 | 375 | func testThrowsErrorOnInvalidInitializerImplementation1() { 376 | let initial: [sInvalidInitializerImplementation1] = [ 377 | sInvalidInitializerImplementation1(1, [ 378 | i(1111, "") 379 | ]) 380 | ] 381 | 382 | do { 383 | _ = try Diff.differencesForSectionedView(initialSections: initial, finalSections: initial) 384 | XCTFail("Should throw exception") 385 | } 386 | catch let exception { 387 | guard case let .invalidInitializerImplementation(section, expectedItems, identifier) = exception as! Diff.Error else { 388 | XCTFail("Not required error") 389 | return 390 | } 391 | 392 | XCTAssertEqual(section as! sInvalidInitializerImplementation1, sInvalidInitializerImplementation1(1, [ 393 | i(1111, ""), 394 | i(1111, "") 395 | ])) 396 | 397 | XCTAssertEqual(expectedItems as! [i], [i(1111, "")]) 398 | XCTAssertEqual(identifier as! Int, 1) 399 | } 400 | } 401 | 402 | func testThrowsErrorOnInvalidInitializerImplementation2() { 403 | let initial: [sInvalidInitializerImplementation2] = [ 404 | sInvalidInitializerImplementation2(1, [ 405 | i(1111, "") 406 | ]) 407 | ] 408 | 409 | do { 410 | _ = try Diff.differencesForSectionedView(initialSections: initial, finalSections: initial) 411 | XCTFail("Should throw exception") 412 | } 413 | catch let exception { 414 | guard case let .invalidInitializerImplementation(section, expectedItems, identifier) = exception as! Diff.Error else { 415 | XCTFail("Not required error") 416 | return 417 | } 418 | 419 | XCTAssertEqual(section as! sInvalidInitializerImplementation2, sInvalidInitializerImplementation2(-1, [ 420 | i(1111, "") 421 | ])) 422 | 423 | XCTAssertEqual(expectedItems as! [i], [i(1111, "")]) 424 | XCTAssertEqual(identifier as! Int, 1) 425 | } 426 | } 427 | } 428 | 429 | // edge cases 430 | extension AlgorithmTests { 431 | func testCase1() { 432 | let initial: [s] = [ 433 | s(1, [ 434 | i(1111, "") 435 | ]), 436 | s(2, [ 437 | i(2222, "") 438 | ]) 439 | 440 | ] 441 | 442 | let final: [s] = [ 443 | s(2, [ 444 | i(0, "1") 445 | ]), 446 | s(1, [ 447 | ]) 448 | ] 449 | 450 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 451 | 452 | XCTAssertEqual(initial.apply(differences), final) 453 | } 454 | 455 | func testCase2() { 456 | let initial: [s] = [ 457 | s(4, [ 458 | i(10, ""), 459 | i(11, ""), 460 | i(12, "") 461 | ]), 462 | s(9, [ 463 | i(25, ""), 464 | i(26, ""), 465 | i(27, "") 466 | ]) 467 | 468 | ] 469 | 470 | let final: [s] = [ 471 | s(9, [ 472 | i(11, "u"), 473 | i(26, ""), 474 | i(27, "u") 475 | ]), 476 | s(4, [ 477 | i(10, "u"), 478 | i(12, "") 479 | ]) 480 | ] 481 | 482 | 483 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 484 | 485 | XCTAssertEqual(initial.apply(differences), final) 486 | } 487 | 488 | func testCase3() { 489 | let initial: [s] = [ 490 | s(4, [ 491 | i(5, "") 492 | ]), 493 | s(6, [ 494 | i(20, ""), 495 | i(14, "") 496 | ]), 497 | s(9, [ 498 | ]), 499 | s(2, [ 500 | i(2, ""), 501 | i(26, "") 502 | ]), 503 | s(8, [ 504 | i(23, "") 505 | ]), 506 | s(10, [ 507 | i(8, ""), 508 | i(18, ""), 509 | i(13, "") 510 | ]), 511 | s(1, [ 512 | i(28, ""), 513 | i(25, ""), 514 | i(6, ""), 515 | i(11, ""), 516 | i(10, ""), 517 | i(29, ""), 518 | i(24, ""), 519 | i(7, ""), 520 | i(19, "") 521 | ]) 522 | ] 523 | 524 | let final: [s] = [ 525 | s(4, [ 526 | i(5, "") 527 | ]), 528 | s(6, [ 529 | i(20, "u"), 530 | i(14, "") 531 | ]), 532 | s(9, [ 533 | i(16, "u") 534 | ]), 535 | s(7, [ 536 | i(17, ""), 537 | i(15, ""), 538 | i(4, "u") 539 | ]), 540 | s(2, [ 541 | i(2, ""), 542 | i(26, "u"), 543 | i(23, "u") 544 | ]), 545 | s(8, [ 546 | ]), 547 | s(10, [ 548 | i(8, "u"), 549 | i(18, "u"), 550 | i(13, "u") 551 | ]), 552 | s(1, [ 553 | i(28, "u"), 554 | i(25, "u"), 555 | i(6, "u"), 556 | i(11, "u"), 557 | i(10, "u"), 558 | i(29, "u"), 559 | i(24, "u"), 560 | i(7, "u"), 561 | i(19, "u") 562 | ]) 563 | 564 | ] 565 | 566 | 567 | let differences = try! Diff.differencesForSectionedView(initialSections: initial, finalSections: final) 568 | 569 | XCTAssertEqual(initial.apply(differences), final) 570 | } 571 | } 572 | 573 | // stress test 574 | extension AlgorithmTests { 575 | 576 | func testStress() { 577 | func initialValue() -> [NumberSection] { 578 | let nSections = 100 579 | let nItems = 100 580 | 581 | /* 582 | let nSections = 10 583 | let nItems = 2 584 | */ 585 | 586 | return (0 ..< nSections).map { (i: Int) in 587 | let items = Array(i * nItems ..< (i + 1) * nItems).map { IntItem(number: $0, date: Date.distantPast) } 588 | return NumberSection(header: "Section \(i + 1)", numbers: items, updated: Date.distantPast) 589 | } 590 | } 591 | 592 | let initialRandomizedSections = Randomizer(rng: PseudoRandomGenerator(4, 3), sections: initialValue()) 593 | 594 | var sections = initialRandomizedSections 595 | for i in 0 ..< 1000 { 596 | if i % 100 == 0 { 597 | print(i) 598 | } 599 | let newSections = sections.randomize() 600 | let differences = try! Diff.differencesForSectionedView(initialSections: sections.sections, finalSections: newSections.sections) 601 | 602 | XCTAssertEqual(sections.sections.apply(differences), newSections.sections) 603 | sections = newSections 604 | } 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Dictionary { 13 | init(elements: [T], keySelector: (T) -> Key, valueSelector: (T) -> Value) { 14 | var result: [Key: Value] = [:] 15 | for element in elements { 16 | result[keySelector(element)] = valueSelector(element) 17 | } 18 | 19 | self = result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/ChangeSet+TestExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangeSet+TestExtensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Differentiator 11 | import RxDataSources 12 | 13 | fileprivate class ItemModelTypeWrapper { 14 | let item: I 15 | 16 | var deleted: Bool = false 17 | var updated: Bool = false 18 | var moved: IndexPath? 19 | 20 | init(item: I) { 21 | self.item = item 22 | } 23 | } 24 | 25 | fileprivate class SectionModelTypeWrapper { 26 | var updated: Bool = false 27 | var deleted: Bool = false 28 | var moved: Int? 29 | 30 | var items: [ItemModelTypeWrapper] 31 | 32 | let section: S 33 | 34 | init(section: S) { 35 | self.section = section 36 | self.items = section.items.map { ItemModelTypeWrapper(item: $0) } 37 | } 38 | } 39 | 40 | extension Changeset { 41 | func onlyContains( 42 | insertedSections: Int = 0, 43 | deletedSections: Int = 0, 44 | movedSections: Int = 0, 45 | updatedSections: Int = 0, 46 | insertedItems: Int = 0, 47 | deletedItems: Int = 0, 48 | movedItems: Int = 0, 49 | updatedItems: Int = 0 50 | ) -> Bool { 51 | if self.insertedSections.count != insertedSections { 52 | return false 53 | } 54 | 55 | if self.deletedSections.count != deletedSections { 56 | return false 57 | } 58 | 59 | if self.movedSections.count != movedSections { 60 | return false 61 | } 62 | 63 | if self.updatedSections.count != updatedSections { 64 | return false 65 | } 66 | 67 | if self.insertedItems.count != insertedItems { 68 | return false 69 | } 70 | 71 | if self.deletedItems.count != deletedItems { 72 | return false 73 | } 74 | 75 | if self.movedItems.count != movedItems { 76 | return false 77 | } 78 | 79 | if self.updatedItems.count != updatedItems { 80 | return false 81 | } 82 | 83 | return true 84 | } 85 | } 86 | 87 | extension Changeset { 88 | 89 | fileprivate func apply(original: [Section]) -> [Section] { 90 | 91 | let afterDeletesAndUpdates = applyDeletesAndUpdates(original: original) 92 | let afterSectionMovesAndInserts = applySectionMovesAndInserts(original: afterDeletesAndUpdates) 93 | let afterItemInsertsAndMoves = applyItemInsertsAndMoves(original: afterSectionMovesAndInserts) 94 | 95 | return afterItemInsertsAndMoves 96 | } 97 | 98 | private func applyDeletesAndUpdates(original: [Section]) -> [Section] { 99 | var resultAfterDeletesAndUpdates: [SectionModelTypeWrapper
] = SectionModelTypeWrapper.wrap(original) 100 | 101 | for index in updatedItems { 102 | resultAfterDeletesAndUpdates[index.sectionIndex].items[index.itemIndex].updated = true 103 | } 104 | 105 | for index in deletedItems { 106 | resultAfterDeletesAndUpdates[index.sectionIndex].items[index.itemIndex].deleted = true 107 | } 108 | 109 | for section in deletedSections { 110 | resultAfterDeletesAndUpdates[section].deleted = true 111 | } 112 | 113 | resultAfterDeletesAndUpdates = resultAfterDeletesAndUpdates.filter { !$0.deleted } 114 | 115 | for (sectionIndex, section) in resultAfterDeletesAndUpdates.enumerated() { 116 | section.items = section.items.filter { !$0.deleted } 117 | for (itemIndex, item) in section.items.enumerated() where item.updated { 118 | section.items[itemIndex] = ItemModelTypeWrapper(item: finalSections[sectionIndex].items[itemIndex]) 119 | } 120 | } 121 | 122 | return SectionModelTypeWrapper.unwrap(resultAfterDeletesAndUpdates) 123 | } 124 | 125 | private func applySectionMovesAndInserts(original: [Section]) -> [Section] { 126 | if !updatedSections.isEmpty { 127 | fatalError("Section updates aren't supported") 128 | } 129 | 130 | let sourceSectionIndexes = Set(movedSections.map { $0.from }) 131 | let destinationToSourceMapping = Dictionary( 132 | elements: movedSections, 133 | keySelector: { $0.to }, 134 | valueSelector: { $0.from } 135 | ) 136 | let insertedSectionsIndexes = Set(insertedSections) 137 | 138 | var nextUntouchedSourceSectionIndex = -1 139 | func findNextUntouchedSourceSection() -> Bool { 140 | nextUntouchedSourceSectionIndex += 1 141 | while nextUntouchedSourceSectionIndex < original.count && sourceSectionIndexes.contains(nextUntouchedSourceSectionIndex) { 142 | nextUntouchedSourceSectionIndex += 1 143 | } 144 | 145 | return nextUntouchedSourceSectionIndex < original.count 146 | } 147 | 148 | let totalCount = original.count + insertedSections.count 149 | 150 | var results: [Section] = [] 151 | 152 | for index in 0 ..< totalCount { 153 | if insertedSectionsIndexes.contains(index) { 154 | results.append(finalSections[index]) 155 | } 156 | else if let sourceIndex = destinationToSourceMapping[index] { 157 | results.append(original[sourceIndex]) 158 | } 159 | else { 160 | guard findNextUntouchedSourceSection() else { 161 | fatalError("Oooops, wrong commands.") 162 | } 163 | 164 | results.append(original[nextUntouchedSourceSectionIndex]) 165 | } 166 | } 167 | 168 | return results 169 | } 170 | 171 | private func applyItemInsertsAndMoves(original: [Section]) -> [Section] { 172 | var resultAfterInsertsAndMoves: [Section] = original 173 | 174 | let sourceIndexesThatShouldBeMoved = Set(movedItems.map { $0.from }) 175 | let destinationToSourceMapping = Dictionary(elements: self.movedItems, keySelector: { $0.to }, valueSelector: { $0.from }) 176 | let insertedItemPaths = Set(self.insertedItems) 177 | 178 | var insertedPerSection: [Int] = Array(repeating: 0, count: original.count) 179 | var movedInSection: [Int] = Array(repeating: 0, count: original.count) 180 | var movedOutSection: [Int] = Array(repeating: 0, count: original.count) 181 | 182 | for insertedItemPath in insertedItems { 183 | insertedPerSection[insertedItemPath.sectionIndex] += 1 184 | } 185 | 186 | for moveItem in movedItems { 187 | movedInSection[moveItem.to.sectionIndex] += 1 188 | movedOutSection[moveItem.from.sectionIndex] += 1 189 | } 190 | 191 | for (sectionIndex, section) in resultAfterInsertsAndMoves.enumerated() { 192 | 193 | let originalItems = section.items 194 | 195 | var nextUntouchedSourceItemIndex = -1 196 | func findNextUntouchedSourceItem() -> Bool { 197 | nextUntouchedSourceItemIndex += 1 198 | while nextUntouchedSourceItemIndex < section.items.count 199 | && sourceIndexesThatShouldBeMoved.contains(ItemPath(sectionIndex: sectionIndex, itemIndex: nextUntouchedSourceItemIndex)) { 200 | nextUntouchedSourceItemIndex += 1 201 | } 202 | 203 | return nextUntouchedSourceItemIndex < section.items.count 204 | } 205 | 206 | let totalCount = section.items.count 207 | + insertedPerSection[sectionIndex] 208 | + movedInSection[sectionIndex] 209 | - movedOutSection[sectionIndex] 210 | 211 | var resultItems: [Section.Item] = [] 212 | 213 | for index in 0 ..< totalCount { 214 | let itemPath = ItemPath(sectionIndex: sectionIndex, itemIndex: index) 215 | if insertedItemPaths.contains(itemPath) { 216 | resultItems.append(finalSections[itemPath.sectionIndex].items[itemPath.itemIndex]) 217 | } 218 | else if let sourceIndex = destinationToSourceMapping[itemPath] { 219 | resultItems.append(original[sourceIndex.sectionIndex].items[sourceIndex.itemIndex]) 220 | } 221 | else { 222 | guard findNextUntouchedSourceItem() else { 223 | fatalError("Oooops, wrong commands.") 224 | } 225 | 226 | resultItems.append(originalItems[nextUntouchedSourceItemIndex]) 227 | } 228 | } 229 | 230 | resultAfterInsertsAndMoves[sectionIndex] = Section(original: section, items: resultItems) 231 | } 232 | 233 | return resultAfterInsertsAndMoves 234 | } 235 | } 236 | 237 | extension SectionModelTypeWrapper { 238 | static func wrap(_ sections: [S]) -> [SectionModelTypeWrapper] { 239 | return sections.map { SectionModelTypeWrapper(section: $0) } 240 | } 241 | 242 | static func unwrap(_ sections: [SectionModelTypeWrapper]) -> [S] { 243 | return sections.map { sectionWrapper in 244 | let items = sectionWrapper.items.map { $0.item } 245 | return S(original: sectionWrapper.section, items: items) 246 | } 247 | } 248 | } 249 | 250 | extension Array where Element: AnimatableSectionModelType, Element: Equatable { 251 | 252 | func apply(_ changes: [Changeset]) -> [Element] { 253 | return changes.reduce(self) { sections, changes in 254 | let newSections = changes.apply(original: sections) 255 | XCAssertEqual(newSections, changes.finalSections) 256 | return newSections 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/NumberSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberSection.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 1/7/16. 6 | // Copyright © 2016 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Differentiator 11 | import RxDataSources 12 | 13 | // MARK: Data 14 | 15 | struct NumberSection { 16 | var header: String 17 | 18 | var numbers: [IntItem] 19 | 20 | var updated: Date 21 | 22 | init(header: String, numbers: [Item], updated: Date) { 23 | self.header = header 24 | self.numbers = numbers 25 | self.updated = updated 26 | } 27 | } 28 | 29 | struct IntItem { 30 | let number: Int 31 | let date: Date 32 | } 33 | 34 | // MARK: Just extensions to say how to determine identity and how to determine is entity updated 35 | 36 | extension NumberSection 37 | : AnimatableSectionModelType { 38 | typealias Item = IntItem 39 | typealias Identity = String 40 | 41 | var identity: String { 42 | return header 43 | } 44 | 45 | var items: [IntItem] { 46 | return numbers 47 | } 48 | 49 | init(original: NumberSection, items: [Item]) { 50 | self = original 51 | self.numbers = items 52 | } 53 | } 54 | 55 | extension NumberSection 56 | : CustomDebugStringConvertible { 57 | var debugDescription: String { 58 | let interval = updated.timeIntervalSince1970 59 | let numbersDescription = numbers.map { "\n\($0.debugDescription)" }.joined(separator: "") 60 | return "NumberSection(header: \"\(self.header)\", numbers: \(numbersDescription)\n, updated: \(interval))" 61 | } 62 | } 63 | 64 | extension IntItem 65 | : IdentifiableType 66 | , Equatable { 67 | typealias Identity = Int 68 | 69 | var identity: Int { 70 | return number 71 | } 72 | } 73 | 74 | // equatable, this is needed to detect changes 75 | func == (lhs: IntItem, rhs: IntItem) -> Bool { 76 | return lhs.number == rhs.number && lhs.date == rhs.date 77 | } 78 | 79 | // MARK: Some nice extensions 80 | extension IntItem 81 | : CustomDebugStringConvertible { 82 | var debugDescription: String { 83 | return "IntItem(number: \(number), date: \(date.timeIntervalSince1970))" 84 | } 85 | } 86 | 87 | extension IntItem 88 | : CustomStringConvertible { 89 | 90 | var description: String { 91 | return "\(number)" 92 | } 93 | } 94 | 95 | extension NumberSection: Equatable { 96 | 97 | } 98 | 99 | func == (lhs: NumberSection, rhs: NumberSection) -> Bool { 100 | return lhs.header == rhs.header && lhs.items == rhs.items && lhs.updated == rhs.updated 101 | } 102 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/Randomizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Randomizer.swift 3 | // RxExample 4 | // 5 | // Created by Krunoslav Zaher on 6/28/15. 6 | // Copyright © 2015 Krunoslav Zaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Differentiator 11 | import RxDataSources 12 | 13 | // https://en.wikipedia.org/wiki/Random_number_generation 14 | struct PseudoRandomGenerator { 15 | var m_w: UInt32 /* must not be zero, nor 0x464fffff */ 16 | var m_z: UInt32 /* must not be zero, nor 0x9068ffff */ 17 | 18 | init(_ m_w: UInt32, _ m_z: UInt32) { 19 | self.m_w = m_w 20 | self.m_z = m_z 21 | } 22 | 23 | func get_random() -> (PseudoRandomGenerator, Int) { 24 | let m_z = 36969 &* (self.m_z & 65535) &+ (self.m_z >> 16) 25 | let m_w = 18000 &* (self.m_w & 65535) &+ (self.m_w >> 16) 26 | let val = ((m_z << 16) &+ m_w) 27 | return (PseudoRandomGenerator(m_w, m_z), Int(val % (1 << 30))) /* 32-bit result */ 28 | } 29 | } 30 | 31 | let insertItems = true 32 | let deleteItems = true 33 | let moveItems = true 34 | let reloadItems = true 35 | 36 | let deleteSections = true 37 | let insertSections = true 38 | let explicitlyMoveSections = true 39 | let reloadSections = true 40 | 41 | struct Randomizer { 42 | let sections: [NumberSection] 43 | 44 | let rng: PseudoRandomGenerator 45 | 46 | let unusedItems: [IntItem] 47 | let unusedSections: [String] 48 | let dateCounter: Int 49 | 50 | init(rng: PseudoRandomGenerator, sections: [NumberSection], unusedItems: [IntItem] = [], unusedSections: [String] = [], dateCounter: Int = 0) { 51 | self.rng = rng 52 | self.sections = sections 53 | 54 | self.unusedSections = unusedSections 55 | self.unusedItems = unusedItems 56 | self.dateCounter = dateCounter 57 | } 58 | 59 | func countTotalItems(sections: [NumberSection]) -> Int { 60 | return sections.reduce(0) { p, s in 61 | return p + s.numbers.count 62 | } 63 | } 64 | 65 | func randomize() -> Randomizer { 66 | 67 | var nextUnusedSections = [String]() 68 | var nextUnusedItems = [IntItem]() 69 | 70 | var (nextRng, randomValue) = rng.get_random() 71 | 72 | let updateDates = randomValue % 3 == 1 && reloadItems 73 | 74 | (nextRng, randomValue) = nextRng.get_random() 75 | 76 | let date = Date(timeIntervalSince1970: TimeInterval(dateCounter)) 77 | 78 | // update updates in current items if needed 79 | var sections = self.sections.map { 80 | updateDates ? NumberSection(header: $0.header, numbers: $0.numbers.map { x in IntItem(number: x.number, date: date) }, updated: Date.distantPast) : $0 81 | } 82 | 83 | let currentUnusedItems = self.unusedItems.map { 84 | updateDates ? IntItem(number: $0.number, date: date) : $0 85 | } 86 | 87 | let sectionCount = sections.count 88 | let itemCount = countTotalItems(sections: sections) 89 | 90 | let startItemCount = itemCount + unusedItems.count 91 | let startSectionCount = self.sections.count + unusedSections.count 92 | 93 | 94 | // insert sections 95 | for section in self.unusedSections { 96 | (nextRng, randomValue) = nextRng.get_random() 97 | let index = randomValue % (sections.count + 1) 98 | if insertSections { 99 | sections.insert(NumberSection(header: section, numbers: [], updated: Date.distantPast), at: index) 100 | } 101 | else { 102 | nextUnusedSections.append(section) 103 | } 104 | } 105 | 106 | // insert/reload items 107 | for unusedValue in currentUnusedItems { 108 | (nextRng, randomValue) = nextRng.get_random() 109 | 110 | let sectionIndex = randomValue % sections.count 111 | let section = sections[sectionIndex] 112 | let itemCount = section.numbers.count 113 | 114 | // insert 115 | (nextRng, randomValue) = nextRng.get_random() 116 | if randomValue % 2 == 0 { 117 | (nextRng, randomValue) = nextRng.get_random() 118 | let itemIndex = randomValue % (itemCount + 1) 119 | 120 | if insertItems { 121 | sections[sectionIndex].numbers.insert(unusedValue, at: itemIndex) 122 | } 123 | else { 124 | nextUnusedItems.append(unusedValue) 125 | } 126 | } 127 | // update 128 | else { 129 | if itemCount == 0 { 130 | sections[sectionIndex].numbers.insert(unusedValue, at: 0) 131 | continue 132 | } 133 | 134 | (nextRng, randomValue) = nextRng.get_random() 135 | let itemIndex = itemCount 136 | if reloadItems { 137 | nextUnusedItems.append(sections[sectionIndex].numbers.remove(at: itemIndex % itemCount)) 138 | sections[sectionIndex].numbers.insert(unusedValue, at: itemIndex % itemCount) 139 | 140 | } 141 | else { 142 | nextUnusedItems.append(unusedValue) 143 | } 144 | } 145 | } 146 | 147 | assert(countTotalItems(sections: sections) + nextUnusedItems.count == startItemCount) 148 | assert(sections.count + nextUnusedSections.count == startSectionCount) 149 | 150 | let itemActionCount = itemCount / 7 151 | let sectionActionCount = sectionCount / 3 152 | 153 | // move items 154 | for _ in 0 ..< itemActionCount { 155 | if sections.isEmpty { 156 | continue 157 | } 158 | 159 | (nextRng, randomValue) = nextRng.get_random() 160 | let sourceSectionIndex = randomValue % sections.count 161 | 162 | (nextRng, randomValue) = nextRng.get_random() 163 | let destinationSectionIndex = randomValue % sections.count 164 | 165 | if sections[sourceSectionIndex].numbers.isEmpty { 166 | continue 167 | } 168 | 169 | (nextRng, randomValue) = nextRng.get_random() 170 | let sourceItemIndex = randomValue % sections[sourceSectionIndex].numbers.count 171 | 172 | (nextRng, randomValue) = nextRng.get_random() 173 | 174 | if moveItems { 175 | let item = sections[sourceSectionIndex].numbers.remove(at: sourceItemIndex) 176 | let targetItemIndex = randomValue % (sections[destinationSectionIndex].numbers.count + 1) 177 | sections[destinationSectionIndex].numbers.insert(item, at: targetItemIndex) 178 | } 179 | } 180 | 181 | assert(countTotalItems(sections: sections) + nextUnusedItems.count == startItemCount) 182 | assert(sections.count + nextUnusedSections.count == startSectionCount) 183 | 184 | // delete items 185 | for _ in 0 ..< itemActionCount { 186 | if sections.isEmpty { 187 | continue 188 | } 189 | 190 | (nextRng, randomValue) = nextRng.get_random() 191 | let sourceSectionIndex = randomValue % sections.count 192 | 193 | if sections[sourceSectionIndex].numbers.isEmpty { 194 | continue 195 | } 196 | 197 | (nextRng, randomValue) = nextRng.get_random() 198 | let sourceItemIndex = randomValue % sections[sourceSectionIndex].numbers.count 199 | 200 | if deleteItems { 201 | nextUnusedItems.append(sections[sourceSectionIndex].numbers.remove(at: sourceItemIndex)) 202 | } 203 | } 204 | 205 | assert(countTotalItems(sections: sections) + nextUnusedItems.count == startItemCount) 206 | assert(sections.count + nextUnusedSections.count == startSectionCount) 207 | 208 | // move sections 209 | for _ in 0 ..< sectionActionCount { 210 | if sections.isEmpty { 211 | continue 212 | } 213 | 214 | (nextRng, randomValue) = nextRng.get_random() 215 | let sectionIndex = randomValue % sections.count 216 | (nextRng, randomValue) = nextRng.get_random() 217 | let targetIndex = randomValue % sections.count 218 | 219 | if explicitlyMoveSections { 220 | let section = sections.remove(at: sectionIndex) 221 | sections.insert(section, at: targetIndex) 222 | } 223 | } 224 | 225 | assert(countTotalItems(sections: sections) + nextUnusedItems.count == startItemCount) 226 | assert(sections.count + nextUnusedSections.count == startSectionCount) 227 | 228 | // delete sections 229 | for _ in 0 ..< sectionActionCount { 230 | if sections.isEmpty { 231 | continue 232 | } 233 | 234 | (nextRng, randomValue) = nextRng.get_random() 235 | let sectionIndex = randomValue % sections.count 236 | 237 | if deleteSections { 238 | let section = sections.remove(at: sectionIndex) 239 | 240 | for item in section.numbers { 241 | nextUnusedItems.append(item) 242 | } 243 | 244 | nextUnusedSections.append(section.identity) 245 | } 246 | } 247 | 248 | assert(countTotalItems(sections: sections) + nextUnusedItems.count == startItemCount) 249 | assert(sections.count + nextUnusedSections.count == startSectionCount) 250 | 251 | return Randomizer( 252 | rng: nextRng, 253 | sections: sections, 254 | unusedItems: nextUnusedItems, 255 | unusedSections: nextUnusedSections, 256 | dateCounter: dateCounter + 1) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/RxCollectionViewSectionedDataSource+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxCollectionViewSectionedDataSource+Test.swift 3 | // Tests 4 | // 5 | // Created by Krunoslav Zaher on 11/4/17. 6 | // Copyright © 2017 kzaher. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | 11 | import Foundation 12 | import RxDataSources 13 | import XCTest 14 | import UIKit 15 | 16 | class RxCollectionViewSectionedDataSourceTest: XCTestCase { 17 | } 18 | 19 | // configureSupplementaryView not passed through init 20 | extension RxCollectionViewSectionedDataSourceTest { 21 | func testCollectionViewSectionedReloadDataSource_optionalConfigureSupplementaryView() { 22 | let dataSource = RxCollectionViewSectionedReloadDataSource>(configureCell: { _, _, _, _ in UICollectionViewCell() }) 23 | let layout = UICollectionViewFlowLayout() 24 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 25 | 26 | XCTAssertFalse(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 27 | 28 | let sentinel = UICollectionReusableView() 29 | dataSource.configureSupplementaryView = { _, _, _, _ in return sentinel } 30 | 31 | let returnValue = dataSource.collectionView( 32 | collectionView, 33 | viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, 34 | at: IndexPath(item: 0, section: 0) 35 | ) 36 | XCTAssertEqual(returnValue, sentinel) 37 | XCTAssertTrue(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 38 | } 39 | 40 | func testCollectionViewSectionedDataSource_optionalConfigureSupplementaryView() { 41 | let dataSource = CollectionViewSectionedDataSource>(configureCell: { _, _, _, _ in UICollectionViewCell() }) 42 | let layout = UICollectionViewFlowLayout() 43 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 44 | 45 | XCTAssertFalse(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 46 | 47 | let sentinel = UICollectionReusableView() 48 | dataSource.configureSupplementaryView = { _, _, _, _ in return sentinel } 49 | 50 | let returnValue = dataSource.collectionView( 51 | collectionView, 52 | viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, 53 | at: IndexPath(item: 0, section: 0) 54 | ) 55 | XCTAssertEqual(returnValue, sentinel) 56 | XCTAssertTrue(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 57 | } 58 | } 59 | 60 | // configureSupplementaryView passed through init 61 | extension RxCollectionViewSectionedDataSourceTest { 62 | func testCollectionViewSectionedAnimatedDataSource_optionalConfigureSupplementaryView_initializer() { 63 | let sentinel = UICollectionReusableView() 64 | let dataSource = RxCollectionViewSectionedAnimatedDataSource>( 65 | configureCell: { _, _, _, _ in UICollectionViewCell() }, 66 | configureSupplementaryView: { _, _, _, _ in return sentinel } 67 | ) 68 | let layout = UICollectionViewFlowLayout() 69 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 70 | 71 | let returnValue = dataSource.collectionView( 72 | collectionView, 73 | viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, 74 | at: IndexPath(item: 0, section: 0) 75 | ) 76 | XCTAssertEqual(returnValue, sentinel) 77 | XCTAssertTrue(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 78 | } 79 | 80 | func testCollectionViewSectionedReloadDataSource_optionalConfigureSupplementaryView_initializer() { 81 | let sentinel = UICollectionReusableView() 82 | let dataSource = RxCollectionViewSectionedReloadDataSource>( 83 | configureCell: { _, _, _, _ in UICollectionViewCell() }, 84 | configureSupplementaryView: { _, _, _, _ in return sentinel } 85 | ) 86 | let layout = UICollectionViewFlowLayout() 87 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 88 | 89 | let returnValue = dataSource.collectionView( 90 | collectionView, 91 | viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, 92 | at: IndexPath(item: 0, section: 0) 93 | ) 94 | XCTAssertEqual(returnValue, sentinel) 95 | XCTAssertTrue(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 96 | } 97 | 98 | func testCollectionViewSectionedDataSource_optionalConfigureSupplementaryView_initializer() { 99 | let sentinel = UICollectionReusableView() 100 | let dataSource = CollectionViewSectionedDataSource>( 101 | configureCell: { _, _, _, _ in UICollectionViewCell() }, 102 | configureSupplementaryView: { _, _, _, _ in return sentinel } 103 | ) 104 | let layout = UICollectionViewFlowLayout() 105 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 106 | 107 | let returnValue = dataSource.collectionView( 108 | collectionView, 109 | viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, 110 | at: IndexPath(item: 0, section: 0) 111 | ) 112 | XCTAssertEqual(returnValue, sentinel) 113 | XCTAssertTrue(dataSource.responds(to: #selector(UICollectionViewDataSource.collectionView(_:viewForSupplementaryElementOfKind:at:)))) 114 | } 115 | } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/XCTest+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTest+Extensions.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Differentiator 12 | import RxDataSources 13 | 14 | func XCAssertEqual(_ lhs: [S], _ rhs: [S], file: StaticString = #file, line: UInt = #line) 15 | where S: Equatable { 16 | let areEqual = lhs == rhs 17 | if !areEqual { 18 | printSectionModelDifferences(lhs, rhs) 19 | } 20 | 21 | XCTAssertTrue(areEqual, file: file, line: line) 22 | } 23 | 24 | fileprivate struct EquatableArray : Equatable { 25 | let elements: [Element] 26 | init(_ elements: [Element]) { 27 | self.elements = elements 28 | } 29 | } 30 | 31 | fileprivate func == (lhs: EquatableArray, rhs: EquatableArray) -> Bool { 32 | return lhs.elements == rhs.elements 33 | } 34 | 35 | fileprivate func printSequenceDifferences(_ lhs: [E], _ rhs: [E], _ equal: (E, E) -> Bool) { 36 | print("Differences in sequence:") 37 | for (index, elements) in zip(lhs, rhs).enumerated() { 38 | let l = elements.0 39 | let r = elements.1 40 | if !equal(l, r) { 41 | print("lhs[\(index)]:\n \(l)") 42 | print("rhs[\(index)]:\n \(r)") 43 | } 44 | } 45 | 46 | let shortest = min(lhs.count, rhs.count) 47 | for (index, element) in lhs[shortest ..< lhs.count].enumerated() { 48 | print("lhs[\(index + shortest)]:\n \(element)") 49 | } 50 | for (index, element) in rhs[shortest ..< rhs.count].enumerated() { 51 | print("rhs[\(index + shortest)]:\n \(element)") 52 | } 53 | } 54 | 55 | fileprivate func printSectionModelDifferences(_ lhs: [S], _ rhs: [S]) 56 | where S: Equatable { 57 | print("Differences in sections:") 58 | for (index, elements) in zip(lhs, rhs).enumerated() { 59 | let l = elements.0 60 | let r = elements.1 61 | if l != r { 62 | if l.identity != r.identity { 63 | print("lhs.identity[\(index)] (\(l.identity)) != rhs.identity[\(index)] (\(r.identity))\n") 64 | } 65 | if l.items != r.items { 66 | print("Difference in items for \(l.identity) and \(r.identity)") 67 | printSequenceDifferences(l.items, r.items, { $0 == $1 }) 68 | } 69 | } 70 | } 71 | 72 | let shortest = min(lhs.count, rhs.count) 73 | for (index, element) in lhs[shortest ..< lhs.count].enumerated() { 74 | print("missing lhs[\(index + shortest)]:\n \(element)") 75 | } 76 | for (index, element) in rhs[shortest ..< rhs.count].enumerated() { 77 | print("missing rhs[\(index + shortest)]:\n \(element)") 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/i.swift: -------------------------------------------------------------------------------- 1 | // 2 | // i.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Differentiator 11 | import RxDataSources 12 | 13 | struct i { 14 | let identity: Int 15 | let value: String 16 | 17 | init(_ identity: Int, _ value: String) { 18 | self.identity = identity 19 | self.value = value 20 | } 21 | } 22 | 23 | extension i: IdentifiableType, Equatable { 24 | } 25 | 26 | func == (lhs: i, rhs: i) -> Bool { 27 | return lhs.identity == rhs.identity && lhs.value == rhs.value 28 | } 29 | 30 | extension i: CustomDebugStringConvertible { 31 | public var debugDescription: String { 32 | return "i(\(identity), \(value))" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/RxDataSourcesTests/s.swift: -------------------------------------------------------------------------------- 1 | // 2 | // s.swift 3 | // RxDataSources 4 | // 5 | // Created by Krunoslav Zaher on 11/26/16. 6 | // Copyright © 2016 kzaher. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Differentiator 11 | import RxDataSources 12 | 13 | /** 14 | Test section. Name is so short for readability sake. 15 | */ 16 | struct s { 17 | let identity: Int 18 | let items: [i] 19 | } 20 | 21 | extension s { 22 | init(_ identity: Int, _ items: [i]) { 23 | self.identity = identity 24 | self.items = items 25 | } 26 | } 27 | 28 | extension s: AnimatableSectionModelType { 29 | typealias Item = i 30 | 31 | init(original: s, items: [Item]) { 32 | self.identity = original.identity 33 | self.items = items 34 | } 35 | } 36 | 37 | extension s: Equatable { 38 | 39 | } 40 | 41 | func == (lhs: s, rhs: s) -> Bool { 42 | return lhs.identity == rhs.identity && lhs.items == rhs.items 43 | } 44 | 45 | extension s: CustomDebugStringConvertible { 46 | var debugDescription: String { 47 | let itemDescriptions = items.map { "\n \($0)," }.joined(separator: "") 48 | return "s(\(identity),\(itemDescriptions)\n)" 49 | } 50 | } 51 | 52 | struct sInvalidInitializerImplementation1 { 53 | let identity: Int 54 | let items: [i] 55 | 56 | init(_ identity: Int, _ items: [i]) { 57 | self.identity = identity 58 | self.items = items 59 | } 60 | } 61 | 62 | func == (lhs: sInvalidInitializerImplementation1, rhs: sInvalidInitializerImplementation1) -> Bool { 63 | return lhs.identity == rhs.identity && lhs.items == rhs.items 64 | } 65 | 66 | extension sInvalidInitializerImplementation1: AnimatableSectionModelType, Equatable { 67 | typealias Item = i 68 | 69 | init(original: sInvalidInitializerImplementation1, items: [Item]) { 70 | self.identity = original.identity 71 | self.items = items + items 72 | } 73 | } 74 | 75 | struct sInvalidInitializerImplementation2 { 76 | let identity: Int 77 | let items: [i] 78 | 79 | init(_ identity: Int, _ items: [i]) { 80 | self.identity = identity 81 | self.items = items 82 | } 83 | } 84 | 85 | extension sInvalidInitializerImplementation2: AnimatableSectionModelType, Equatable { 86 | typealias Item = i 87 | 88 | init(original: sInvalidInitializerImplementation2, items: [Item]) { 89 | self.identity = -1 90 | self.items = items 91 | } 92 | } 93 | 94 | func == (lhs: sInvalidInitializerImplementation2, rhs: sInvalidInitializerImplementation2) -> Bool { 95 | return lhs.identity == rhs.identity && lhs.items == rhs.items 96 | } 97 | --------------------------------------------------------------------------------