├── .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 | [](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 | 
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 |
--------------------------------------------------------------------------------