├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── DataSource.podspec ├── DataSource.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── DataSource-iOS.xcscheme │ └── DataSource-tvOS.xcscheme ├── DataSource.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── DataSource.xcscmblueprint │ └── IDEWorkspaceChecks.plist ├── DataSource ├── CollectionView │ ├── CollectionViewCell.swift │ ├── CollectionViewChangeTarget.swift │ ├── CollectionViewDataSource.swift │ └── CollectionViewReusableView.swift ├── DataChange.swift ├── DataChangeTarget.swift ├── DataChangeTypes │ ├── DataChangeBatch.swift │ ├── DataChangeDeleteItems.swift │ ├── DataChangeDeleteSections.swift │ ├── DataChangeInsertItems.swift │ ├── DataChangeInsertSections.swift │ ├── DataChangeMoveItem.swift │ ├── DataChangeMoveSection.swift │ ├── DataChangeReloadData.swift │ ├── DataChangeReloadItems.swift │ └── DataChangeReloadSections.swift ├── DataSource.h ├── DataSource.swift ├── DataSourceCellDescriptor.swift ├── DataSourceItemReceiver.swift ├── DataSourceTypes │ ├── AutoDiff.swift │ ├── AutoDiffDataSource.swift │ ├── AutoDiffSectionsDataSource.swift │ ├── CompositeDataSource.swift │ ├── DataSourceSection.swift │ ├── EmptyDataSource.swift │ ├── FetchedResultsDataSource.swift │ ├── KVODataSource.swift │ ├── MappedDataSource.swift │ ├── MutableCompositeDataSource.swift │ ├── MutableDataSource.swift │ ├── ProxyDataSource.swift │ └── StaticDataSource.swift ├── IndexPathExtensions.swift ├── Info.plist └── TableView │ ├── TableViewCell.swift │ ├── TableViewChangeTarget.swift │ ├── TableViewDataSource.swift │ ├── TableViewDataSourceWithHeaderFooterTitles.swift │ ├── TableViewDataSourceWithHeaderFooterViews.swift │ └── TableViewHeaderFooterView.swift ├── DataSourceTests ├── AutoDiffDataSourceTests.swift ├── AutoDiffSectionsDataSourceTests.swift ├── CollectionViewDataSourceSharedConfiguration.swift ├── CollectionViewDataSourceTests.swift ├── CompositeDataSourceTests.swift ├── CoreDataManager.swift ├── DataSourceSharedConfiguration.swift ├── EmptyDataSourceTests.swift ├── FetchedResultsDataSourceTests.swift ├── Info.plist ├── Items.xcdatamodeld │ └── Items.xcdatamodel │ │ └── contents ├── ItemsExtensions.swift ├── MappedDataSourceTests.swift ├── MutableCompositeDataSourceTests.swift ├── MutableDataSourceTests.swift ├── ProxyDataSourceTests.swift ├── QuickSpecWithDataSets.swift ├── StaticDataSourceTests.swift ├── TableViewDataSourceSharedConfiguration.swift ├── TableViewDataSourceTests.swift ├── TableViewDataSourceWithHeaderFooterTitlesTests.swift ├── TableViewDataSourceWithHeaderFooterViewsTests.swift └── TestCells.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata 3 | *.xccheckout 4 | Carthage/Build 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/ReactiveSwift"] 2 | path = Carthage/Checkouts/ReactiveSwift 3 | url = https://github.com/ReactiveCocoa/ReactiveSwift.git 4 | [submodule "Carthage/Checkouts/Quick"] 5 | path = Carthage/Checkouts/Quick 6 | url = https://github.com/Quick/Quick.git 7 | [submodule "Carthage/Checkouts/Nimble"] 8 | path = Carthage/Checkouts/Nimble 9 | url = https://github.com/Quick/Nimble.git 10 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - anyobject_protocol 3 | - block_based_kvo 4 | - class_delegate_protocol 5 | - contains_over_first_not_nil 6 | - convenience_type 7 | - closing_brace 8 | - closure_end_indentation 9 | - closure_parameter_position 10 | - closure_spacing 11 | - colon 12 | - comma 13 | - compiler_protocol_init 14 | - conditional_returns_on_newline 15 | - control_statement 16 | - custom_rules 17 | - discarded_notification_center_observer 18 | - discouraged_direct_init 19 | - dynamic_inline 20 | - empty_count 21 | - empty_enum_arguments 22 | - empty_string 23 | - empty_parameters 24 | - empty_parentheses_with_trailing_closure 25 | - empty_xctest_method 26 | - explicit_init 27 | - fallthrough 28 | - fatal_error_message 29 | - file_header 30 | - file_length 31 | - first_where 32 | - for_where 33 | - function_body_length 34 | - generic_type_name 35 | - identifier_name 36 | - implicit_getter 37 | - is_disjoint 38 | - joined_default_parameter 39 | - leading_whitespace 40 | - legacy_cggeometry_functions 41 | - legacy_constant 42 | - legacy_constructor 43 | - legacy_nsgeometry_functions 44 | - line_length 45 | - literal_expression_end_indentation 46 | - lower_acl_than_parent 47 | - mark 48 | - modifier_order 49 | - multiline_arguments 50 | - multiline_parameters 51 | - multiple_closures_with_trailing_closure 52 | - nimble_operator 53 | - no_extension_access_modifier 54 | - no_fallthrough_only 55 | - notification_center_detachment 56 | - object_literal 57 | - operator_usage_whitespace 58 | - overridden_super_call 59 | - override_in_extension 60 | - private_action 61 | - private_outlet 62 | - private_over_fileprivate 63 | - private_unit_test 64 | - prohibited_super_call 65 | - protocol_property_accessors_order 66 | - redundant_discardable_let 67 | - redundant_optional_initialization 68 | - redundant_set_access_control 69 | - redundant_string_enum_value 70 | - redundant_void_return 71 | - return_arrow_whitespace 72 | - shorthand_operator 73 | - statement_position 74 | - sorted_first_last 75 | - sorted_imports 76 | - superfluous_disable_command 77 | - switch_case_alignment 78 | - switch_case_on_newline 79 | - syntactic_sugar 80 | - todo 81 | - trailing_comma 82 | - trailing_newline 83 | - trailing_semicolon 84 | - trailing_whitespace 85 | - type_body_length 86 | - unneeded_break_in_switch 87 | - unneeded_parentheses_in_closure_argument 88 | - unused_enumerated 89 | - unused_optional_binding 90 | - unused_closure_parameter 91 | - untyped_error_in_catch 92 | - valid_ibinspectable 93 | - vertical_parameter_alignment 94 | - vertical_parameter_alignment_on_call 95 | - vertical_whitespace 96 | - void_return 97 | - weak_delegate 98 | - xctfail_message 99 | - yoda_condition 100 | included: 101 | - . 102 | excluded: 103 | - Carthage 104 | - Pods 105 | file_header: 106 | required_pattern: | 107 | \/\/ 108 | \/\/ .*?\.(?:swift|h|m) 109 | \/\/ (DataSource.*) 110 | \/\/ 111 | \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2,4}\.? 112 | \/\/ Copyright (?:©|\(c\)) \d{4} Fueled\. All rights reserved\. 113 | \/\/ 114 | file_name: 115 | suffix_pattern: "\\+.*" 116 | vertical_whitespace: 117 | severity: error 118 | syntactic_sugar: error 119 | fatal_error_message: error 120 | override_in_extension: error 121 | no_extension_access_modifier: warning 122 | type_body_length: 123 | warning: 300 124 | error: 400 125 | line_length: 126 | ignores_interpolated_strings: true 127 | warning: 220 128 | error: 99999 # Default is 200, so swiftlint would output an error if the file length is between 200 and 220 129 | file_length: 130 | warning: 1000 131 | error: 9999 132 | ignore_comment_only_lines: true 133 | type_name: 134 | min_length: 2 135 | trailing_semicolon: error 136 | function_body_length: 137 | warning: 150 138 | error: 300 139 | statement_position: 140 | statement_mode: default 141 | severity: error 142 | colon: 143 | severity: error 144 | private_outlet: 145 | severity: warning 146 | allow_private_set: true 147 | trailing_comma: 148 | mandatory_comma: true 149 | identifier_name: 150 | min_length: 151 | warning: 3 152 | max_length: 153 | warning: 70 154 | error: 100 155 | severity: error 156 | excluded: 157 | - x 158 | - y 159 | - z 160 | - w 161 | - i 162 | - j 163 | - k 164 | - l 165 | - id 166 | - url 167 | - r 168 | - g 169 | - b 170 | - a 171 | - t 172 | - v 173 | - on 174 | - no 175 | - qa 176 | return_arrow_whitespace: error 177 | opening_brace: error 178 | legacy_constructor: error 179 | legacy_constant: error 180 | leading_whitespace: error 181 | empty_count: warning 182 | shorthand_operator: warning 183 | reporter: "xcode" 184 | custom_rules: 185 | protocol_conformance: 186 | name: "Protocol Conformance" 187 | message: "Protocol conformance should be declared in separate extensions in the same file" 188 | regex: "(class|struct|extension)[[:space:]]+(?i:(?![^d]*delegate:))[^'\"()<>{},!?:]+:([^'\"<>(){},!?:]+,)+[^'\"<>(){},!?:]*\\{" 189 | match_kinds: 190 | - keyword 191 | severity: warning 192 | indentation_character: 193 | name: "Indentation" 194 | message: "Tabs should be used rather than spaces. This error may not be displayed exactly at the location of the violation but just above it." 195 | regex: "[^\n]*\n+ +" 196 | severity: error 197 | opening_brace: 198 | name: "Opening brace" 199 | message: "Opening braces should be preceded by a single space and on the same line as the declaration, or optionally on a new line if the statement is multiline." 200 | regex: "(?<=\\s)(? 6.1 2 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.4" 2 | github "Quick/Quick" "v2.2.0" 3 | github "ReactiveCocoa/ReactiveSwift" "6.1.0" 4 | -------------------------------------------------------------------------------- /DataSource.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DataSource' 3 | s.version = '1.0.1' 4 | s.summary = 'A Swift framework that helps to deal with sectioned collections of collection items in an MVVM fashion.' 5 | s.homepage = 'https://github.com/Fueled/DataSource' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.authors = 'Fueled' 8 | s.ios.deployment_target = '8.0' 9 | s.tvos.deployment_target = '12.0' 10 | s.swift_version = '5' 11 | s.source = { :git => 'https://github.com/Fueled/DataSource.git', :tag => s.version } 12 | s.source_files = 'DataSource/**/*.swift' 13 | s.dependency 'ReactiveSwift', '~> 6.1' 14 | end 15 | -------------------------------------------------------------------------------- /DataSource.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 021FAD451BB2D77C001300A0 /* DataSourceItemReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FAD441BB2D77C001300A0 /* DataSourceItemReceiver.swift */; }; 11 | 022B0F9F1B2F2AE500DD4DCD /* DataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 022B0F9E1B2F2AE500DD4DCD /* DataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 022B0FDC1B2F2D4C00DD4DCD /* CollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB61B2F2D4B00DD4DCD /* CollectionViewCell.swift */; }; 13 | 022B0FDD1B2F2D4C00DD4DCD /* CollectionViewChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB71B2F2D4B00DD4DCD /* CollectionViewChangeTarget.swift */; }; 14 | 022B0FDE1B2F2D4C00DD4DCD /* CollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB81B2F2D4B00DD4DCD /* CollectionViewDataSource.swift */; }; 15 | 022B0FDF1B2F2D4C00DD4DCD /* CollectionViewReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB91B2F2D4B00DD4DCD /* CollectionViewReusableView.swift */; }; 16 | 022B0FE01B2F2D4C00DD4DCD /* DataChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBA1B2F2D4C00DD4DCD /* DataChange.swift */; }; 17 | 022B0FE11B2F2D4C00DD4DCD /* DataChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBB1B2F2D4C00DD4DCD /* DataChangeTarget.swift */; }; 18 | 022B0FE21B2F2D4C00DD4DCD /* DataChangeBatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBD1B2F2D4C00DD4DCD /* DataChangeBatch.swift */; }; 19 | 022B0FE31B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBE1B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift */; }; 20 | 022B0FE41B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBF1B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift */; }; 21 | 022B0FE51B2F2D4C00DD4DCD /* DataChangeInsertItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC01B2F2D4C00DD4DCD /* DataChangeInsertItems.swift */; }; 22 | 022B0FE61B2F2D4C00DD4DCD /* DataChangeInsertSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC11B2F2D4C00DD4DCD /* DataChangeInsertSections.swift */; }; 23 | 022B0FE71B2F2D4C00DD4DCD /* DataChangeMoveItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC21B2F2D4C00DD4DCD /* DataChangeMoveItem.swift */; }; 24 | 022B0FE81B2F2D4C00DD4DCD /* DataChangeMoveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC31B2F2D4C00DD4DCD /* DataChangeMoveSection.swift */; }; 25 | 022B0FE91B2F2D4C00DD4DCD /* DataChangeReloadData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC41B2F2D4C00DD4DCD /* DataChangeReloadData.swift */; }; 26 | 022B0FEA1B2F2D4C00DD4DCD /* DataChangeReloadItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC51B2F2D4C00DD4DCD /* DataChangeReloadItems.swift */; }; 27 | 022B0FEB1B2F2D4C00DD4DCD /* DataChangeReloadSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC61B2F2D4C00DD4DCD /* DataChangeReloadSections.swift */; }; 28 | 022B0FEC1B2F2D4C00DD4DCD /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC71B2F2D4C00DD4DCD /* DataSource.swift */; }; 29 | 022B0FED1B2F2D4C00DD4DCD /* AutoDiffDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC91B2F2D4C00DD4DCD /* AutoDiffDataSource.swift */; }; 30 | 022B0FEE1B2F2D4C00DD4DCD /* CompositeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCA1B2F2D4C00DD4DCD /* CompositeDataSource.swift */; }; 31 | 022B0FEF1B2F2D4C00DD4DCD /* EmptyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCB1B2F2D4C00DD4DCD /* EmptyDataSource.swift */; }; 32 | 022B0FF01B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCC1B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift */; }; 33 | 022B0FF11B2F2D4C00DD4DCD /* KVODataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCD1B2F2D4C00DD4DCD /* KVODataSource.swift */; }; 34 | 022B0FF21B2F2D4C00DD4DCD /* MappedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCE1B2F2D4C00DD4DCD /* MappedDataSource.swift */; }; 35 | 022B0FF31B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCF1B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift */; }; 36 | 022B0FF41B2F2D4C00DD4DCD /* MutableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD01B2F2D4C00DD4DCD /* MutableDataSource.swift */; }; 37 | 022B0FF51B2F2D4C00DD4DCD /* ProxyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD11B2F2D4C00DD4DCD /* ProxyDataSource.swift */; }; 38 | 022B0FF61B2F2D4C00DD4DCD /* StaticDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD21B2F2D4C00DD4DCD /* StaticDataSource.swift */; }; 39 | 022B0FF71B2F2D4C00DD4DCD /* DataSourceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD31B2F2D4C00DD4DCD /* DataSourceSection.swift */; }; 40 | 022B0FF81B2F2D4C00DD4DCD /* IndexPathExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD41B2F2D4C00DD4DCD /* IndexPathExtensions.swift */; }; 41 | 022B0FF91B2F2D4C00DD4DCD /* TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD61B2F2D4C00DD4DCD /* TableViewCell.swift */; }; 42 | 022B0FFA1B2F2D4C00DD4DCD /* TableViewChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD71B2F2D4C00DD4DCD /* TableViewChangeTarget.swift */; }; 43 | 022B0FFB1B2F2D4C00DD4DCD /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD81B2F2D4C00DD4DCD /* TableViewDataSource.swift */; }; 44 | 022B0FFC1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD91B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift */; }; 45 | 022B0FFD1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FDA1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift */; }; 46 | 022B0FFE1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FDB1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift */; }; 47 | 028602ED1B7E076300474D04 /* AutoDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028602EC1B7E076200474D04 /* AutoDiff.swift */; }; 48 | 028602F51B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028602F41B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift */; }; 49 | 0286CF9F1BE26FB900F76C86 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0286CF9E1BE26FB900F76C86 /* ReactiveSwift.framework */; }; 50 | 6B020AFD22045C6000DBAE00 /* TestCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020AFC22045C6000DBAE00 /* TestCells.swift */; }; 51 | 6B020B072208382000DBAE00 /* DataSourceSharedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020B062208382000DBAE00 /* DataSourceSharedConfiguration.swift */; }; 52 | 6B020B09220842B500DBAE00 /* StaticDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020B08220842B500DBAE00 /* StaticDataSourceTests.swift */; }; 53 | 6B020B0B2208479400DBAE00 /* EmptyDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020B0A2208479400DBAE00 /* EmptyDataSourceTests.swift */; }; 54 | 6B020B0D2208609B00DBAE00 /* QuickSpecWithDataSets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020B0C2208609B00DBAE00 /* QuickSpecWithDataSets.swift */; }; 55 | 6B020B11220864FC00DBAE00 /* ProxyDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B020B10220864FC00DBAE00 /* ProxyDataSourceTests.swift */; }; 56 | 6B4082C622147FDA00A4CA90 /* Items.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6B4082C422147FDA00A4CA90 /* Items.xcdatamodeld */; }; 57 | 6B76E6572202E36E00D2B33E /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B76E6562202E36E00D2B33E /* Nimble.framework */; }; 58 | 6B76E6592202E36E00D2B33E /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B76E6582202E36E00D2B33E /* Quick.framework */; }; 59 | 6B76E65B2202E36E00D2B33E /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B76E65A2202E36E00D2B33E /* ReactiveSwift.framework */; }; 60 | 6B76E65E2202E36E00D2B33E /* DataSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 022B0F991B2F2AE500DD4DCD /* DataSource.framework */; }; 61 | 6B7E4AE022115E3700EC9031 /* FetchedResultsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7E4ADF22115E3700EC9031 /* FetchedResultsDataSourceTests.swift */; }; 62 | 6B8941982213205500294F7D /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8941972213205500294F7D /* CoreDataManager.swift */; }; 63 | 6BCA28EA2208798600D99D97 /* MutableDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28E92208798600D99D97 /* MutableDataSourceTests.swift */; }; 64 | 6BCA28EC2209662C00D99D97 /* TableViewDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28EB2209662C00D99D97 /* TableViewDataSourceTests.swift */; }; 65 | 6BCA28EE2209793A00D99D97 /* TableViewDataSourceWithHeaderFooterTitlesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28ED2209793A00D99D97 /* TableViewDataSourceWithHeaderFooterTitlesTests.swift */; }; 66 | 6BCA28F0220979B600D99D97 /* TableViewDataSourceSharedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28EF220979B600D99D97 /* TableViewDataSourceSharedConfiguration.swift */; }; 67 | 6BCA28F2220981B800D99D97 /* TableViewDataSourceWithHeaderFooterViewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28F1220981B800D99D97 /* TableViewDataSourceWithHeaderFooterViewsTests.swift */; }; 68 | 6BCA28F42209CF4F00D99D97 /* MappedDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCA28F32209CF4F00D99D97 /* MappedDataSourceTests.swift */; }; 69 | 6BE5AD6B22144B2400066998 /* ItemsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE5AD6A22144B2400066998 /* ItemsExtensions.swift */; }; 70 | 6BEC0E422201B9FC00AEDC7E /* DataSourceCellDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEC0E412201B9FC00AEDC7E /* DataSourceCellDescriptor.swift */; }; 71 | 6BF8F6D5220A9024007F6209 /* MutableCompositeDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6D4220A9024007F6209 /* MutableCompositeDataSourceTests.swift */; }; 72 | 6BF8F6D9220B2633007F6209 /* CompositeDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6D8220B2633007F6209 /* CompositeDataSourceTests.swift */; }; 73 | 6BF8F6DB220B2887007F6209 /* AutoDiffDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6DA220B2887007F6209 /* AutoDiffDataSourceTests.swift */; }; 74 | 6BF8F6DD220B28A6007F6209 /* AutoDiffSectionsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6DC220B28A6007F6209 /* AutoDiffSectionsDataSourceTests.swift */; }; 75 | 6BF8F6E0220B28F0007F6209 /* CollectionViewDataSourceSharedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6DF220B28F0007F6209 /* CollectionViewDataSourceSharedConfiguration.swift */; }; 76 | 6BF8F6E2220B2904007F6209 /* CollectionViewDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF8F6E1220B2904007F6209 /* CollectionViewDataSourceTests.swift */; }; 77 | FCBEE8E41DA8F4AF00F59863 /* CollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB61B2F2D4B00DD4DCD /* CollectionViewCell.swift */; }; 78 | FCBEE8E51DA8F4AF00F59863 /* CollectionViewChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB71B2F2D4B00DD4DCD /* CollectionViewChangeTarget.swift */; }; 79 | FCBEE8E61DA8F4AF00F59863 /* CollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB81B2F2D4B00DD4DCD /* CollectionViewDataSource.swift */; }; 80 | FCBEE8E71DA8F4AF00F59863 /* CollectionViewReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FB91B2F2D4B00DD4DCD /* CollectionViewReusableView.swift */; }; 81 | FCBEE8E81DA8F4AF00F59863 /* DataChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBA1B2F2D4C00DD4DCD /* DataChange.swift */; }; 82 | FCBEE8E91DA8F4AF00F59863 /* DataChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBB1B2F2D4C00DD4DCD /* DataChangeTarget.swift */; }; 83 | FCBEE8EA1DA8F4AF00F59863 /* DataChangeBatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBD1B2F2D4C00DD4DCD /* DataChangeBatch.swift */; }; 84 | FCBEE8EB1DA8F4AF00F59863 /* DataChangeDeleteItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBE1B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift */; }; 85 | FCBEE8EC1DA8F4AF00F59863 /* DataChangeDeleteSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FBF1B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift */; }; 86 | FCBEE8ED1DA8F4AF00F59863 /* DataChangeInsertItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC01B2F2D4C00DD4DCD /* DataChangeInsertItems.swift */; }; 87 | FCBEE8EE1DA8F4AF00F59863 /* DataChangeInsertSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC11B2F2D4C00DD4DCD /* DataChangeInsertSections.swift */; }; 88 | FCBEE8EF1DA8F4AF00F59863 /* DataChangeMoveItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC21B2F2D4C00DD4DCD /* DataChangeMoveItem.swift */; }; 89 | FCBEE8F01DA8F4AF00F59863 /* DataChangeMoveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC31B2F2D4C00DD4DCD /* DataChangeMoveSection.swift */; }; 90 | FCBEE8F11DA8F4AF00F59863 /* DataChangeReloadData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC41B2F2D4C00DD4DCD /* DataChangeReloadData.swift */; }; 91 | FCBEE8F21DA8F4AF00F59863 /* DataChangeReloadItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC51B2F2D4C00DD4DCD /* DataChangeReloadItems.swift */; }; 92 | FCBEE8F31DA8F4AF00F59863 /* DataChangeReloadSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC61B2F2D4C00DD4DCD /* DataChangeReloadSections.swift */; }; 93 | FCBEE8F41DA8F4AF00F59863 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC71B2F2D4C00DD4DCD /* DataSource.swift */; }; 94 | FCBEE8F51DA8F4AF00F59863 /* DataSourceItemReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021FAD441BB2D77C001300A0 /* DataSourceItemReceiver.swift */; }; 95 | FCBEE8F61DA8F4AF00F59863 /* AutoDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028602EC1B7E076200474D04 /* AutoDiff.swift */; }; 96 | FCBEE8F71DA8F4AF00F59863 /* AutoDiffDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FC91B2F2D4C00DD4DCD /* AutoDiffDataSource.swift */; }; 97 | FCBEE8F81DA8F4AF00F59863 /* AutoDiffSectionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028602F41B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift */; }; 98 | FCBEE8F91DA8F4AF00F59863 /* CompositeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCA1B2F2D4C00DD4DCD /* CompositeDataSource.swift */; }; 99 | FCBEE8FA1DA8F4AF00F59863 /* DataSourceSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD31B2F2D4C00DD4DCD /* DataSourceSection.swift */; }; 100 | FCBEE8FB1DA8F4AF00F59863 /* EmptyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCB1B2F2D4C00DD4DCD /* EmptyDataSource.swift */; }; 101 | FCBEE8FC1DA8F4AF00F59863 /* FetchedResultsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCC1B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift */; }; 102 | FCBEE8FD1DA8F4AF00F59863 /* KVODataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCD1B2F2D4C00DD4DCD /* KVODataSource.swift */; }; 103 | FCBEE8FE1DA8F4AF00F59863 /* MappedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCE1B2F2D4C00DD4DCD /* MappedDataSource.swift */; }; 104 | FCBEE8FF1DA8F4AF00F59863 /* MutableCompositeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FCF1B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift */; }; 105 | FCBEE9001DA8F4AF00F59863 /* MutableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD01B2F2D4C00DD4DCD /* MutableDataSource.swift */; }; 106 | FCBEE9011DA8F4AF00F59863 /* ProxyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD11B2F2D4C00DD4DCD /* ProxyDataSource.swift */; }; 107 | FCBEE9021DA8F4AF00F59863 /* StaticDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD21B2F2D4C00DD4DCD /* StaticDataSource.swift */; }; 108 | FCBEE9031DA8F4AF00F59863 /* IndexPathExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD41B2F2D4C00DD4DCD /* IndexPathExtensions.swift */; }; 109 | FCBEE9041DA8F4AF00F59863 /* TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD61B2F2D4C00DD4DCD /* TableViewCell.swift */; }; 110 | FCBEE9051DA8F4AF00F59863 /* TableViewChangeTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD71B2F2D4C00DD4DCD /* TableViewChangeTarget.swift */; }; 111 | FCBEE9061DA8F4AF00F59863 /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD81B2F2D4C00DD4DCD /* TableViewDataSource.swift */; }; 112 | FCBEE9071DA8F4AF00F59863 /* TableViewDataSourceWithHeaderFooterTitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FD91B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift */; }; 113 | FCBEE9081DA8F4AF00F59863 /* TableViewDataSourceWithHeaderFooterViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FDA1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift */; }; 114 | FCBEE9091DA8F4AF00F59863 /* TableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B0FDB1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift */; }; 115 | FCBEE90A1DA8F4BB00F59863 /* DataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 022B0F9E1B2F2AE500DD4DCD /* DataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 116 | FCBEE90B1DA8F4D400F59863 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC9E4A5D1DA83C7C00258CCF /* ReactiveSwift.framework */; }; 117 | /* End PBXBuildFile section */ 118 | 119 | /* Begin PBXFileReference section */ 120 | 021FAD441BB2D77C001300A0 /* DataSourceItemReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceItemReceiver.swift; sourceTree = ""; }; 121 | 022B0F991B2F2AE500DD4DCD /* DataSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DataSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 122 | 022B0F9D1B2F2AE500DD4DCD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | 022B0F9E1B2F2AE500DD4DCD /* DataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DataSource.h; sourceTree = ""; }; 124 | 022B0FA41B2F2AE500DD4DCD /* DataSourceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DataSourceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 125 | 022B0FAA1B2F2AE500DD4DCD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 126 | 022B0FB61B2F2D4B00DD4DCD /* CollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewCell.swift; sourceTree = ""; }; 127 | 022B0FB71B2F2D4B00DD4DCD /* CollectionViewChangeTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewChangeTarget.swift; sourceTree = ""; }; 128 | 022B0FB81B2F2D4B00DD4DCD /* CollectionViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewDataSource.swift; sourceTree = ""; }; 129 | 022B0FB91B2F2D4B00DD4DCD /* CollectionViewReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewReusableView.swift; sourceTree = ""; }; 130 | 022B0FBA1B2F2D4C00DD4DCD /* DataChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChange.swift; sourceTree = ""; }; 131 | 022B0FBB1B2F2D4C00DD4DCD /* DataChangeTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeTarget.swift; sourceTree = ""; }; 132 | 022B0FBD1B2F2D4C00DD4DCD /* DataChangeBatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeBatch.swift; sourceTree = ""; }; 133 | 022B0FBE1B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeDeleteItems.swift; sourceTree = ""; }; 134 | 022B0FBF1B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeDeleteSections.swift; sourceTree = ""; }; 135 | 022B0FC01B2F2D4C00DD4DCD /* DataChangeInsertItems.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeInsertItems.swift; sourceTree = ""; }; 136 | 022B0FC11B2F2D4C00DD4DCD /* DataChangeInsertSections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeInsertSections.swift; sourceTree = ""; }; 137 | 022B0FC21B2F2D4C00DD4DCD /* DataChangeMoveItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeMoveItem.swift; sourceTree = ""; }; 138 | 022B0FC31B2F2D4C00DD4DCD /* DataChangeMoveSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeMoveSection.swift; sourceTree = ""; }; 139 | 022B0FC41B2F2D4C00DD4DCD /* DataChangeReloadData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeReloadData.swift; sourceTree = ""; }; 140 | 022B0FC51B2F2D4C00DD4DCD /* DataChangeReloadItems.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeReloadItems.swift; sourceTree = ""; }; 141 | 022B0FC61B2F2D4C00DD4DCD /* DataChangeReloadSections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChangeReloadSections.swift; sourceTree = ""; }; 142 | 022B0FC71B2F2D4C00DD4DCD /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; 143 | 022B0FC91B2F2D4C00DD4DCD /* AutoDiffDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoDiffDataSource.swift; sourceTree = ""; }; 144 | 022B0FCA1B2F2D4C00DD4DCD /* CompositeDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositeDataSource.swift; sourceTree = ""; }; 145 | 022B0FCB1B2F2D4C00DD4DCD /* EmptyDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDataSource.swift; sourceTree = ""; }; 146 | 022B0FCC1B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchedResultsDataSource.swift; sourceTree = ""; }; 147 | 022B0FCD1B2F2D4C00DD4DCD /* KVODataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KVODataSource.swift; sourceTree = ""; }; 148 | 022B0FCE1B2F2D4C00DD4DCD /* MappedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappedDataSource.swift; sourceTree = ""; }; 149 | 022B0FCF1B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableCompositeDataSource.swift; sourceTree = ""; }; 150 | 022B0FD01B2F2D4C00DD4DCD /* MutableDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableDataSource.swift; sourceTree = ""; }; 151 | 022B0FD11B2F2D4C00DD4DCD /* ProxyDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyDataSource.swift; sourceTree = ""; }; 152 | 022B0FD21B2F2D4C00DD4DCD /* StaticDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticDataSource.swift; sourceTree = ""; }; 153 | 022B0FD31B2F2D4C00DD4DCD /* DataSourceSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceSection.swift; sourceTree = ""; }; 154 | 022B0FD41B2F2D4C00DD4DCD /* IndexPathExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndexPathExtensions.swift; sourceTree = ""; }; 155 | 022B0FD61B2F2D4C00DD4DCD /* TableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewCell.swift; sourceTree = ""; }; 156 | 022B0FD71B2F2D4C00DD4DCD /* TableViewChangeTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewChangeTarget.swift; sourceTree = ""; }; 157 | 022B0FD81B2F2D4C00DD4DCD /* TableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; }; 158 | 022B0FD91B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceWithHeaderFooterTitles.swift; sourceTree = ""; }; 159 | 022B0FDA1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceWithHeaderFooterViews.swift; sourceTree = ""; }; 160 | 022B0FDB1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewHeaderFooterView.swift; sourceTree = ""; }; 161 | 028602EC1B7E076200474D04 /* AutoDiff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoDiff.swift; sourceTree = ""; }; 162 | 028602F41B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoDiffSectionsDataSource.swift; sourceTree = ""; }; 163 | 0286CF9E1BE26FB900F76C86 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 164 | 0286CFA01BE26FC000F76C86 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 165 | 6B020AFC22045C6000DBAE00 /* TestCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCells.swift; sourceTree = ""; }; 166 | 6B020B062208382000DBAE00 /* DataSourceSharedConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSharedConfiguration.swift; sourceTree = ""; }; 167 | 6B020B08220842B500DBAE00 /* StaticDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticDataSourceTests.swift; sourceTree = ""; }; 168 | 6B020B0A2208479400DBAE00 /* EmptyDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyDataSourceTests.swift; sourceTree = ""; }; 169 | 6B020B0C2208609B00DBAE00 /* QuickSpecWithDataSets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSpecWithDataSets.swift; sourceTree = ""; }; 170 | 6B020B10220864FC00DBAE00 /* ProxyDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyDataSourceTests.swift; sourceTree = ""; }; 171 | 6B4082C522147FDA00A4CA90 /* Items.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Items.xcdatamodel; sourceTree = ""; }; 172 | 6B76E6562202E36E00D2B33E /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 173 | 6B76E6582202E36E00D2B33E /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 174 | 6B76E65A2202E36E00D2B33E /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 175 | 6B76E65C2202E36E00D2B33E /* Result.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Result.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 176 | 6B7E4ADF22115E3700EC9031 /* FetchedResultsDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchedResultsDataSourceTests.swift; sourceTree = ""; }; 177 | 6B8941972213205500294F7D /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; 178 | 6BCA28E92208798600D99D97 /* MutableDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableDataSourceTests.swift; sourceTree = ""; }; 179 | 6BCA28EB2209662C00D99D97 /* TableViewDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceTests.swift; sourceTree = ""; }; 180 | 6BCA28ED2209793A00D99D97 /* TableViewDataSourceWithHeaderFooterTitlesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceWithHeaderFooterTitlesTests.swift; sourceTree = ""; }; 181 | 6BCA28EF220979B600D99D97 /* TableViewDataSourceSharedConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceSharedConfiguration.swift; sourceTree = ""; }; 182 | 6BCA28F1220981B800D99D97 /* TableViewDataSourceWithHeaderFooterViewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSourceWithHeaderFooterViewsTests.swift; sourceTree = ""; }; 183 | 6BCA28F32209CF4F00D99D97 /* MappedDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MappedDataSourceTests.swift; sourceTree = ""; }; 184 | 6BE5AD6A22144B2400066998 /* ItemsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsExtensions.swift; sourceTree = ""; }; 185 | 6BEC0E412201B9FC00AEDC7E /* DataSourceCellDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceCellDescriptor.swift; sourceTree = ""; }; 186 | 6BF8F6D4220A9024007F6209 /* MutableCompositeDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableCompositeDataSourceTests.swift; sourceTree = ""; }; 187 | 6BF8F6D8220B2633007F6209 /* CompositeDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeDataSourceTests.swift; sourceTree = ""; }; 188 | 6BF8F6DA220B2887007F6209 /* AutoDiffDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDiffDataSourceTests.swift; sourceTree = ""; }; 189 | 6BF8F6DC220B28A6007F6209 /* AutoDiffSectionsDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDiffSectionsDataSourceTests.swift; sourceTree = ""; }; 190 | 6BF8F6DF220B28F0007F6209 /* CollectionViewDataSourceSharedConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDataSourceSharedConfiguration.swift; sourceTree = ""; }; 191 | 6BF8F6E1220B2904007F6209 /* CollectionViewDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDataSourceTests.swift; sourceTree = ""; }; 192 | FC9E4A5D1DA83C7C00258CCF /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = "../../Library/Developer/Xcode/DerivedData/DataSource-bxcnryyouvctozbdlqspowjdewgq/Build/Products/Debug-appletvsimulator/ReactiveSwift.framework"; sourceTree = ""; }; 193 | FC9E4A5E1DA83C7C00258CCF /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = "../../Library/Developer/Xcode/DerivedData/DataSource-bxcnryyouvctozbdlqspowjdewgq/Build/Products/Debug-appletvsimulator/Result.framework"; sourceTree = ""; }; 194 | FCBEE8DC1DA8F41C00F59863 /* DataSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DataSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 195 | /* End PBXFileReference section */ 196 | 197 | /* Begin PBXFrameworksBuildPhase section */ 198 | 022B0F951B2F2AE500DD4DCD /* Frameworks */ = { 199 | isa = PBXFrameworksBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 0286CF9F1BE26FB900F76C86 /* ReactiveSwift.framework in Frameworks */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | 6B76E6552202E12D00D2B33E /* Frameworks */ = { 207 | isa = PBXFrameworksBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 6B76E65E2202E36E00D2B33E /* DataSource.framework in Frameworks */, 211 | 6B76E6572202E36E00D2B33E /* Nimble.framework in Frameworks */, 212 | 6B76E6592202E36E00D2B33E /* Quick.framework in Frameworks */, 213 | 6B76E65B2202E36E00D2B33E /* ReactiveSwift.framework in Frameworks */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | FCBEE8D81DA8F41C00F59863 /* Frameworks */ = { 218 | isa = PBXFrameworksBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | FCBEE90B1DA8F4D400F59863 /* ReactiveSwift.framework in Frameworks */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXFrameworksBuildPhase section */ 226 | 227 | /* Begin PBXGroup section */ 228 | 022B0F8F1B2F2AE500DD4DCD = { 229 | isa = PBXGroup; 230 | children = ( 231 | 022B10351B2F341200DD4DCD /* Frameworks */, 232 | 022B0F9B1B2F2AE500DD4DCD /* DataSource */, 233 | 022B0FA81B2F2AE500DD4DCD /* DataSourceTests */, 234 | 022B0F9A1B2F2AE500DD4DCD /* Products */, 235 | ); 236 | sourceTree = ""; 237 | }; 238 | 022B0F9A1B2F2AE500DD4DCD /* Products */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 022B0F991B2F2AE500DD4DCD /* DataSource.framework */, 242 | 022B0FA41B2F2AE500DD4DCD /* DataSourceTests.xctest */, 243 | FCBEE8DC1DA8F41C00F59863 /* DataSource.framework */, 244 | ); 245 | name = Products; 246 | sourceTree = ""; 247 | }; 248 | 022B0F9B1B2F2AE500DD4DCD /* DataSource */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 022B0FB51B2F2D4B00DD4DCD /* CollectionView */, 252 | 022B0FBA1B2F2D4C00DD4DCD /* DataChange.swift */, 253 | 022B0FBB1B2F2D4C00DD4DCD /* DataChangeTarget.swift */, 254 | 022B0FBC1B2F2D4C00DD4DCD /* DataChangeTypes */, 255 | 022B0F9E1B2F2AE500DD4DCD /* DataSource.h */, 256 | 022B0FC71B2F2D4C00DD4DCD /* DataSource.swift */, 257 | 6BEC0E412201B9FC00AEDC7E /* DataSourceCellDescriptor.swift */, 258 | 021FAD441BB2D77C001300A0 /* DataSourceItemReceiver.swift */, 259 | 022B0FC81B2F2D4C00DD4DCD /* DataSourceTypes */, 260 | 022B0FD41B2F2D4C00DD4DCD /* IndexPathExtensions.swift */, 261 | 022B0F9C1B2F2AE500DD4DCD /* Supporting Files */, 262 | 022B0FD51B2F2D4C00DD4DCD /* TableView */, 263 | ); 264 | path = DataSource; 265 | sourceTree = ""; 266 | }; 267 | 022B0F9C1B2F2AE500DD4DCD /* Supporting Files */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | 022B0F9D1B2F2AE500DD4DCD /* Info.plist */, 271 | ); 272 | name = "Supporting Files"; 273 | sourceTree = ""; 274 | }; 275 | 022B0FA81B2F2AE500DD4DCD /* DataSourceTests */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 6BE5AD692214481600066998 /* Helpers */, 279 | 6BE5AD68221447FB00066998 /* Model */, 280 | 6BF8F6DE220B28B2007F6209 /* CollectionView */, 281 | 6B31D3FE2203372F001AFF01 /* TableView */, 282 | 6BD3F08A22031871002EA2D0 /* DataSourceTypes */, 283 | 022B0FA91B2F2AE500DD4DCD /* Supporting Files */, 284 | ); 285 | path = DataSourceTests; 286 | sourceTree = ""; 287 | }; 288 | 022B0FA91B2F2AE500DD4DCD /* Supporting Files */ = { 289 | isa = PBXGroup; 290 | children = ( 291 | 022B0FAA1B2F2AE500DD4DCD /* Info.plist */, 292 | ); 293 | name = "Supporting Files"; 294 | sourceTree = ""; 295 | }; 296 | 022B0FB51B2F2D4B00DD4DCD /* CollectionView */ = { 297 | isa = PBXGroup; 298 | children = ( 299 | 022B0FB61B2F2D4B00DD4DCD /* CollectionViewCell.swift */, 300 | 022B0FB71B2F2D4B00DD4DCD /* CollectionViewChangeTarget.swift */, 301 | 022B0FB81B2F2D4B00DD4DCD /* CollectionViewDataSource.swift */, 302 | 022B0FB91B2F2D4B00DD4DCD /* CollectionViewReusableView.swift */, 303 | ); 304 | path = CollectionView; 305 | sourceTree = ""; 306 | }; 307 | 022B0FBC1B2F2D4C00DD4DCD /* DataChangeTypes */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | 022B0FBD1B2F2D4C00DD4DCD /* DataChangeBatch.swift */, 311 | 022B0FBE1B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift */, 312 | 022B0FBF1B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift */, 313 | 022B0FC01B2F2D4C00DD4DCD /* DataChangeInsertItems.swift */, 314 | 022B0FC11B2F2D4C00DD4DCD /* DataChangeInsertSections.swift */, 315 | 022B0FC21B2F2D4C00DD4DCD /* DataChangeMoveItem.swift */, 316 | 022B0FC31B2F2D4C00DD4DCD /* DataChangeMoveSection.swift */, 317 | 022B0FC41B2F2D4C00DD4DCD /* DataChangeReloadData.swift */, 318 | 022B0FC51B2F2D4C00DD4DCD /* DataChangeReloadItems.swift */, 319 | 022B0FC61B2F2D4C00DD4DCD /* DataChangeReloadSections.swift */, 320 | ); 321 | path = DataChangeTypes; 322 | sourceTree = ""; 323 | }; 324 | 022B0FC81B2F2D4C00DD4DCD /* DataSourceTypes */ = { 325 | isa = PBXGroup; 326 | children = ( 327 | 028602EC1B7E076200474D04 /* AutoDiff.swift */, 328 | 022B0FC91B2F2D4C00DD4DCD /* AutoDiffDataSource.swift */, 329 | 028602F41B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift */, 330 | 022B0FCA1B2F2D4C00DD4DCD /* CompositeDataSource.swift */, 331 | 022B0FD31B2F2D4C00DD4DCD /* DataSourceSection.swift */, 332 | 022B0FCB1B2F2D4C00DD4DCD /* EmptyDataSource.swift */, 333 | 022B0FCC1B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift */, 334 | 022B0FCD1B2F2D4C00DD4DCD /* KVODataSource.swift */, 335 | 022B0FCE1B2F2D4C00DD4DCD /* MappedDataSource.swift */, 336 | 022B0FCF1B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift */, 337 | 022B0FD01B2F2D4C00DD4DCD /* MutableDataSource.swift */, 338 | 022B0FD11B2F2D4C00DD4DCD /* ProxyDataSource.swift */, 339 | 022B0FD21B2F2D4C00DD4DCD /* StaticDataSource.swift */, 340 | ); 341 | path = DataSourceTypes; 342 | sourceTree = ""; 343 | }; 344 | 022B0FD51B2F2D4C00DD4DCD /* TableView */ = { 345 | isa = PBXGroup; 346 | children = ( 347 | 022B0FD61B2F2D4C00DD4DCD /* TableViewCell.swift */, 348 | 022B0FD71B2F2D4C00DD4DCD /* TableViewChangeTarget.swift */, 349 | 022B0FD81B2F2D4C00DD4DCD /* TableViewDataSource.swift */, 350 | 022B0FD91B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift */, 351 | 022B0FDA1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift */, 352 | 022B0FDB1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift */, 353 | ); 354 | path = TableView; 355 | sourceTree = ""; 356 | }; 357 | 022B10351B2F341200DD4DCD /* Frameworks */ = { 358 | isa = PBXGroup; 359 | children = ( 360 | 6B76E6562202E36E00D2B33E /* Nimble.framework */, 361 | 6B76E6582202E36E00D2B33E /* Quick.framework */, 362 | 6B76E65A2202E36E00D2B33E /* ReactiveSwift.framework */, 363 | 6B76E65C2202E36E00D2B33E /* Result.framework */, 364 | FC9E4A5D1DA83C7C00258CCF /* ReactiveSwift.framework */, 365 | FC9E4A5E1DA83C7C00258CCF /* Result.framework */, 366 | 0286CFA01BE26FC000F76C86 /* Result.framework */, 367 | 0286CF9E1BE26FB900F76C86 /* ReactiveSwift.framework */, 368 | ); 369 | name = Frameworks; 370 | sourceTree = ""; 371 | }; 372 | 6B31D3FE2203372F001AFF01 /* TableView */ = { 373 | isa = PBXGroup; 374 | children = ( 375 | 6BCA28EF220979B600D99D97 /* TableViewDataSourceSharedConfiguration.swift */, 376 | 6BCA28EB2209662C00D99D97 /* TableViewDataSourceTests.swift */, 377 | 6BCA28ED2209793A00D99D97 /* TableViewDataSourceWithHeaderFooterTitlesTests.swift */, 378 | 6BCA28F1220981B800D99D97 /* TableViewDataSourceWithHeaderFooterViewsTests.swift */, 379 | ); 380 | name = TableView; 381 | sourceTree = ""; 382 | }; 383 | 6BD3F08A22031871002EA2D0 /* DataSourceTypes */ = { 384 | isa = PBXGroup; 385 | children = ( 386 | 6B020B0C2208609B00DBAE00 /* QuickSpecWithDataSets.swift */, 387 | 6B020B062208382000DBAE00 /* DataSourceSharedConfiguration.swift */, 388 | 6B020B0A2208479400DBAE00 /* EmptyDataSourceTests.swift */, 389 | 6B020B08220842B500DBAE00 /* StaticDataSourceTests.swift */, 390 | 6B020B10220864FC00DBAE00 /* ProxyDataSourceTests.swift */, 391 | 6BCA28E92208798600D99D97 /* MutableDataSourceTests.swift */, 392 | 6BCA28F32209CF4F00D99D97 /* MappedDataSourceTests.swift */, 393 | 6BF8F6D4220A9024007F6209 /* MutableCompositeDataSourceTests.swift */, 394 | 6BF8F6D8220B2633007F6209 /* CompositeDataSourceTests.swift */, 395 | 6BF8F6DA220B2887007F6209 /* AutoDiffDataSourceTests.swift */, 396 | 6BF8F6DC220B28A6007F6209 /* AutoDiffSectionsDataSourceTests.swift */, 397 | 6B7E4ADF22115E3700EC9031 /* FetchedResultsDataSourceTests.swift */, 398 | ); 399 | name = DataSourceTypes; 400 | sourceTree = ""; 401 | }; 402 | 6BE5AD68221447FB00066998 /* Model */ = { 403 | isa = PBXGroup; 404 | children = ( 405 | 6B020AFC22045C6000DBAE00 /* TestCells.swift */, 406 | 6B4082C422147FDA00A4CA90 /* Items.xcdatamodeld */, 407 | ); 408 | name = Model; 409 | sourceTree = ""; 410 | }; 411 | 6BE5AD692214481600066998 /* Helpers */ = { 412 | isa = PBXGroup; 413 | children = ( 414 | 6B8941972213205500294F7D /* CoreDataManager.swift */, 415 | 6BE5AD6A22144B2400066998 /* ItemsExtensions.swift */, 416 | ); 417 | name = Helpers; 418 | sourceTree = ""; 419 | }; 420 | 6BF8F6DE220B28B2007F6209 /* CollectionView */ = { 421 | isa = PBXGroup; 422 | children = ( 423 | 6BF8F6DF220B28F0007F6209 /* CollectionViewDataSourceSharedConfiguration.swift */, 424 | 6BF8F6E1220B2904007F6209 /* CollectionViewDataSourceTests.swift */, 425 | ); 426 | name = CollectionView; 427 | sourceTree = ""; 428 | }; 429 | /* End PBXGroup section */ 430 | 431 | /* Begin PBXHeadersBuildPhase section */ 432 | 022B0F961B2F2AE500DD4DCD /* Headers */ = { 433 | isa = PBXHeadersBuildPhase; 434 | buildActionMask = 2147483647; 435 | files = ( 436 | 022B0F9F1B2F2AE500DD4DCD /* DataSource.h in Headers */, 437 | ); 438 | runOnlyForDeploymentPostprocessing = 0; 439 | }; 440 | FCBEE8D91DA8F41C00F59863 /* Headers */ = { 441 | isa = PBXHeadersBuildPhase; 442 | buildActionMask = 2147483647; 443 | files = ( 444 | FCBEE90A1DA8F4BB00F59863 /* DataSource.h in Headers */, 445 | ); 446 | runOnlyForDeploymentPostprocessing = 0; 447 | }; 448 | /* End PBXHeadersBuildPhase section */ 449 | 450 | /* Begin PBXNativeTarget section */ 451 | 022B0F981B2F2AE500DD4DCD /* DataSource-iOS */ = { 452 | isa = PBXNativeTarget; 453 | buildConfigurationList = 022B0FAF1B2F2AE500DD4DCD /* Build configuration list for PBXNativeTarget "DataSource-iOS" */; 454 | buildPhases = ( 455 | 022B0F941B2F2AE500DD4DCD /* Sources */, 456 | 022B0F951B2F2AE500DD4DCD /* Frameworks */, 457 | 022B0F961B2F2AE500DD4DCD /* Headers */, 458 | 022B0F971B2F2AE500DD4DCD /* Resources */, 459 | ); 460 | buildRules = ( 461 | ); 462 | dependencies = ( 463 | ); 464 | name = "DataSource-iOS"; 465 | productName = DataSource; 466 | productReference = 022B0F991B2F2AE500DD4DCD /* DataSource.framework */; 467 | productType = "com.apple.product-type.framework"; 468 | }; 469 | 022B0FA31B2F2AE500DD4DCD /* DataSourceTests */ = { 470 | isa = PBXNativeTarget; 471 | buildConfigurationList = 022B0FB21B2F2AE500DD4DCD /* Build configuration list for PBXNativeTarget "DataSourceTests" */; 472 | buildPhases = ( 473 | 022B0FA01B2F2AE500DD4DCD /* Sources */, 474 | 022B0FA21B2F2AE500DD4DCD /* Resources */, 475 | 6B76E6552202E12D00D2B33E /* Frameworks */, 476 | ); 477 | buildRules = ( 478 | ); 479 | dependencies = ( 480 | ); 481 | name = DataSourceTests; 482 | productName = DataSourceTests; 483 | productReference = 022B0FA41B2F2AE500DD4DCD /* DataSourceTests.xctest */; 484 | productType = "com.apple.product-type.bundle.unit-test"; 485 | }; 486 | FCBEE8DB1DA8F41C00F59863 /* DataSource-tvOS */ = { 487 | isa = PBXNativeTarget; 488 | buildConfigurationList = FCBEE8E11DA8F41D00F59863 /* Build configuration list for PBXNativeTarget "DataSource-tvOS" */; 489 | buildPhases = ( 490 | FCBEE8D71DA8F41C00F59863 /* Sources */, 491 | FCBEE8D81DA8F41C00F59863 /* Frameworks */, 492 | FCBEE8D91DA8F41C00F59863 /* Headers */, 493 | FCBEE8DA1DA8F41C00F59863 /* Resources */, 494 | ); 495 | buildRules = ( 496 | ); 497 | dependencies = ( 498 | ); 499 | name = "DataSource-tvOS"; 500 | productName = "DataSource-tvOS"; 501 | productReference = FCBEE8DC1DA8F41C00F59863 /* DataSource.framework */; 502 | productType = "com.apple.product-type.framework"; 503 | }; 504 | /* End PBXNativeTarget section */ 505 | 506 | /* Begin PBXProject section */ 507 | 022B0F901B2F2AE500DD4DCD /* Project object */ = { 508 | isa = PBXProject; 509 | attributes = { 510 | LastSwiftMigration = 0700; 511 | LastSwiftUpdateCheck = 0700; 512 | LastUpgradeCheck = 1100; 513 | ORGANIZATIONNAME = Fueled; 514 | TargetAttributes = { 515 | 022B0F981B2F2AE500DD4DCD = { 516 | CreatedOnToolsVersion = 6.3.2; 517 | LastSwiftMigration = 1020; 518 | }; 519 | 022B0FA31B2F2AE500DD4DCD = { 520 | CreatedOnToolsVersion = 6.3.2; 521 | LastSwiftMigration = 1020; 522 | }; 523 | FCBEE8DB1DA8F41C00F59863 = { 524 | CreatedOnToolsVersion = 8.0; 525 | ProvisioningStyle = Manual; 526 | }; 527 | }; 528 | }; 529 | buildConfigurationList = 022B0F931B2F2AE500DD4DCD /* Build configuration list for PBXProject "DataSource" */; 530 | compatibilityVersion = "Xcode 3.2"; 531 | developmentRegion = en; 532 | hasScannedForEncodings = 0; 533 | knownRegions = ( 534 | en, 535 | Base, 536 | ); 537 | mainGroup = 022B0F8F1B2F2AE500DD4DCD; 538 | productRefGroup = 022B0F9A1B2F2AE500DD4DCD /* Products */; 539 | projectDirPath = ""; 540 | projectRoot = ""; 541 | targets = ( 542 | 022B0F981B2F2AE500DD4DCD /* DataSource-iOS */, 543 | FCBEE8DB1DA8F41C00F59863 /* DataSource-tvOS */, 544 | 022B0FA31B2F2AE500DD4DCD /* DataSourceTests */, 545 | ); 546 | }; 547 | /* End PBXProject section */ 548 | 549 | /* Begin PBXResourcesBuildPhase section */ 550 | 022B0F971B2F2AE500DD4DCD /* Resources */ = { 551 | isa = PBXResourcesBuildPhase; 552 | buildActionMask = 2147483647; 553 | files = ( 554 | ); 555 | runOnlyForDeploymentPostprocessing = 0; 556 | }; 557 | 022B0FA21B2F2AE500DD4DCD /* Resources */ = { 558 | isa = PBXResourcesBuildPhase; 559 | buildActionMask = 2147483647; 560 | files = ( 561 | ); 562 | runOnlyForDeploymentPostprocessing = 0; 563 | }; 564 | FCBEE8DA1DA8F41C00F59863 /* Resources */ = { 565 | isa = PBXResourcesBuildPhase; 566 | buildActionMask = 2147483647; 567 | files = ( 568 | ); 569 | runOnlyForDeploymentPostprocessing = 0; 570 | }; 571 | /* End PBXResourcesBuildPhase section */ 572 | 573 | /* Begin PBXSourcesBuildPhase section */ 574 | 022B0F941B2F2AE500DD4DCD /* Sources */ = { 575 | isa = PBXSourcesBuildPhase; 576 | buildActionMask = 2147483647; 577 | files = ( 578 | 028602F51B7E1D2B00474D04 /* AutoDiffSectionsDataSource.swift in Sources */, 579 | 022B0FDF1B2F2D4C00DD4DCD /* CollectionViewReusableView.swift in Sources */, 580 | 022B0FF61B2F2D4C00DD4DCD /* StaticDataSource.swift in Sources */, 581 | 022B0FFB1B2F2D4C00DD4DCD /* TableViewDataSource.swift in Sources */, 582 | 022B0FEC1B2F2D4C00DD4DCD /* DataSource.swift in Sources */, 583 | 022B0FFE1B2F2D4C00DD4DCD /* TableViewHeaderFooterView.swift in Sources */, 584 | 022B0FE51B2F2D4C00DD4DCD /* DataChangeInsertItems.swift in Sources */, 585 | 022B0FF51B2F2D4C00DD4DCD /* ProxyDataSource.swift in Sources */, 586 | 022B0FFA1B2F2D4C00DD4DCD /* TableViewChangeTarget.swift in Sources */, 587 | 022B0FEE1B2F2D4C00DD4DCD /* CompositeDataSource.swift in Sources */, 588 | 022B0FDD1B2F2D4C00DD4DCD /* CollectionViewChangeTarget.swift in Sources */, 589 | 022B0FED1B2F2D4C00DD4DCD /* AutoDiffDataSource.swift in Sources */, 590 | 022B0FE11B2F2D4C00DD4DCD /* DataChangeTarget.swift in Sources */, 591 | 022B0FF41B2F2D4C00DD4DCD /* MutableDataSource.swift in Sources */, 592 | 022B0FE31B2F2D4C00DD4DCD /* DataChangeDeleteItems.swift in Sources */, 593 | 022B0FE71B2F2D4C00DD4DCD /* DataChangeMoveItem.swift in Sources */, 594 | 022B0FE91B2F2D4C00DD4DCD /* DataChangeReloadData.swift in Sources */, 595 | 022B0FEF1B2F2D4C00DD4DCD /* EmptyDataSource.swift in Sources */, 596 | 022B0FDE1B2F2D4C00DD4DCD /* CollectionViewDataSource.swift in Sources */, 597 | 022B0FF91B2F2D4C00DD4DCD /* TableViewCell.swift in Sources */, 598 | 022B0FEB1B2F2D4C00DD4DCD /* DataChangeReloadSections.swift in Sources */, 599 | 022B0FE41B2F2D4C00DD4DCD /* DataChangeDeleteSections.swift in Sources */, 600 | 022B0FF01B2F2D4C00DD4DCD /* FetchedResultsDataSource.swift in Sources */, 601 | 022B0FE81B2F2D4C00DD4DCD /* DataChangeMoveSection.swift in Sources */, 602 | 021FAD451BB2D77C001300A0 /* DataSourceItemReceiver.swift in Sources */, 603 | 022B0FF21B2F2D4C00DD4DCD /* MappedDataSource.swift in Sources */, 604 | 022B0FF71B2F2D4C00DD4DCD /* DataSourceSection.swift in Sources */, 605 | 022B0FDC1B2F2D4C00DD4DCD /* CollectionViewCell.swift in Sources */, 606 | 022B0FE61B2F2D4C00DD4DCD /* DataChangeInsertSections.swift in Sources */, 607 | 028602ED1B7E076300474D04 /* AutoDiff.swift in Sources */, 608 | 022B0FE01B2F2D4C00DD4DCD /* DataChange.swift in Sources */, 609 | 022B0FE21B2F2D4C00DD4DCD /* DataChangeBatch.swift in Sources */, 610 | 6BEC0E422201B9FC00AEDC7E /* DataSourceCellDescriptor.swift in Sources */, 611 | 022B0FF81B2F2D4C00DD4DCD /* IndexPathExtensions.swift in Sources */, 612 | 022B0FEA1B2F2D4C00DD4DCD /* DataChangeReloadItems.swift in Sources */, 613 | 022B0FF31B2F2D4C00DD4DCD /* MutableCompositeDataSource.swift in Sources */, 614 | 022B0FF11B2F2D4C00DD4DCD /* KVODataSource.swift in Sources */, 615 | 022B0FFC1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterTitles.swift in Sources */, 616 | 022B0FFD1B2F2D4C00DD4DCD /* TableViewDataSourceWithHeaderFooterViews.swift in Sources */, 617 | ); 618 | runOnlyForDeploymentPostprocessing = 0; 619 | }; 620 | 022B0FA01B2F2AE500DD4DCD /* Sources */ = { 621 | isa = PBXSourcesBuildPhase; 622 | buildActionMask = 2147483647; 623 | files = ( 624 | 6B4082C622147FDA00A4CA90 /* Items.xcdatamodeld in Sources */, 625 | 6B020B09220842B500DBAE00 /* StaticDataSourceTests.swift in Sources */, 626 | 6BCA28EE2209793A00D99D97 /* TableViewDataSourceWithHeaderFooterTitlesTests.swift in Sources */, 627 | 6BF8F6E2220B2904007F6209 /* CollectionViewDataSourceTests.swift in Sources */, 628 | 6BCA28F42209CF4F00D99D97 /* MappedDataSourceTests.swift in Sources */, 629 | 6B020AFD22045C6000DBAE00 /* TestCells.swift in Sources */, 630 | 6BCA28EC2209662C00D99D97 /* TableViewDataSourceTests.swift in Sources */, 631 | 6BF8F6D5220A9024007F6209 /* MutableCompositeDataSourceTests.swift in Sources */, 632 | 6BCA28F0220979B600D99D97 /* TableViewDataSourceSharedConfiguration.swift in Sources */, 633 | 6BCA28EA2208798600D99D97 /* MutableDataSourceTests.swift in Sources */, 634 | 6B7E4AE022115E3700EC9031 /* FetchedResultsDataSourceTests.swift in Sources */, 635 | 6BE5AD6B22144B2400066998 /* ItemsExtensions.swift in Sources */, 636 | 6B8941982213205500294F7D /* CoreDataManager.swift in Sources */, 637 | 6BF8F6DB220B2887007F6209 /* AutoDiffDataSourceTests.swift in Sources */, 638 | 6BF8F6DD220B28A6007F6209 /* AutoDiffSectionsDataSourceTests.swift in Sources */, 639 | 6B020B0D2208609B00DBAE00 /* QuickSpecWithDataSets.swift in Sources */, 640 | 6BF8F6D9220B2633007F6209 /* CompositeDataSourceTests.swift in Sources */, 641 | 6B020B0B2208479400DBAE00 /* EmptyDataSourceTests.swift in Sources */, 642 | 6BF8F6E0220B28F0007F6209 /* CollectionViewDataSourceSharedConfiguration.swift in Sources */, 643 | 6BCA28F2220981B800D99D97 /* TableViewDataSourceWithHeaderFooterViewsTests.swift in Sources */, 644 | 6B020B072208382000DBAE00 /* DataSourceSharedConfiguration.swift in Sources */, 645 | 6B020B11220864FC00DBAE00 /* ProxyDataSourceTests.swift in Sources */, 646 | ); 647 | runOnlyForDeploymentPostprocessing = 0; 648 | }; 649 | FCBEE8D71DA8F41C00F59863 /* Sources */ = { 650 | isa = PBXSourcesBuildPhase; 651 | buildActionMask = 2147483647; 652 | files = ( 653 | FCBEE8E41DA8F4AF00F59863 /* CollectionViewCell.swift in Sources */, 654 | FCBEE8E51DA8F4AF00F59863 /* CollectionViewChangeTarget.swift in Sources */, 655 | FCBEE8E61DA8F4AF00F59863 /* CollectionViewDataSource.swift in Sources */, 656 | FCBEE8E71DA8F4AF00F59863 /* CollectionViewReusableView.swift in Sources */, 657 | FCBEE8E81DA8F4AF00F59863 /* DataChange.swift in Sources */, 658 | FCBEE8E91DA8F4AF00F59863 /* DataChangeTarget.swift in Sources */, 659 | FCBEE8EA1DA8F4AF00F59863 /* DataChangeBatch.swift in Sources */, 660 | FCBEE8EB1DA8F4AF00F59863 /* DataChangeDeleteItems.swift in Sources */, 661 | FCBEE8EC1DA8F4AF00F59863 /* DataChangeDeleteSections.swift in Sources */, 662 | FCBEE8ED1DA8F4AF00F59863 /* DataChangeInsertItems.swift in Sources */, 663 | FCBEE8EE1DA8F4AF00F59863 /* DataChangeInsertSections.swift in Sources */, 664 | FCBEE8EF1DA8F4AF00F59863 /* DataChangeMoveItem.swift in Sources */, 665 | FCBEE8F01DA8F4AF00F59863 /* DataChangeMoveSection.swift in Sources */, 666 | FCBEE8F11DA8F4AF00F59863 /* DataChangeReloadData.swift in Sources */, 667 | FCBEE8F21DA8F4AF00F59863 /* DataChangeReloadItems.swift in Sources */, 668 | FCBEE8F31DA8F4AF00F59863 /* DataChangeReloadSections.swift in Sources */, 669 | FCBEE8F41DA8F4AF00F59863 /* DataSource.swift in Sources */, 670 | FCBEE8F51DA8F4AF00F59863 /* DataSourceItemReceiver.swift in Sources */, 671 | FCBEE8F61DA8F4AF00F59863 /* AutoDiff.swift in Sources */, 672 | FCBEE8F71DA8F4AF00F59863 /* AutoDiffDataSource.swift in Sources */, 673 | FCBEE8F81DA8F4AF00F59863 /* AutoDiffSectionsDataSource.swift in Sources */, 674 | FCBEE8F91DA8F4AF00F59863 /* CompositeDataSource.swift in Sources */, 675 | FCBEE8FA1DA8F4AF00F59863 /* DataSourceSection.swift in Sources */, 676 | FCBEE8FB1DA8F4AF00F59863 /* EmptyDataSource.swift in Sources */, 677 | FCBEE8FC1DA8F4AF00F59863 /* FetchedResultsDataSource.swift in Sources */, 678 | FCBEE8FD1DA8F4AF00F59863 /* KVODataSource.swift in Sources */, 679 | FCBEE8FE1DA8F4AF00F59863 /* MappedDataSource.swift in Sources */, 680 | FCBEE8FF1DA8F4AF00F59863 /* MutableCompositeDataSource.swift in Sources */, 681 | FCBEE9001DA8F4AF00F59863 /* MutableDataSource.swift in Sources */, 682 | FCBEE9011DA8F4AF00F59863 /* ProxyDataSource.swift in Sources */, 683 | FCBEE9021DA8F4AF00F59863 /* StaticDataSource.swift in Sources */, 684 | FCBEE9031DA8F4AF00F59863 /* IndexPathExtensions.swift in Sources */, 685 | FCBEE9041DA8F4AF00F59863 /* TableViewCell.swift in Sources */, 686 | FCBEE9051DA8F4AF00F59863 /* TableViewChangeTarget.swift in Sources */, 687 | FCBEE9061DA8F4AF00F59863 /* TableViewDataSource.swift in Sources */, 688 | FCBEE9071DA8F4AF00F59863 /* TableViewDataSourceWithHeaderFooterTitles.swift in Sources */, 689 | FCBEE9081DA8F4AF00F59863 /* TableViewDataSourceWithHeaderFooterViews.swift in Sources */, 690 | FCBEE9091DA8F4AF00F59863 /* TableViewHeaderFooterView.swift in Sources */, 691 | ); 692 | runOnlyForDeploymentPostprocessing = 0; 693 | }; 694 | /* End PBXSourcesBuildPhase section */ 695 | 696 | /* Begin XCBuildConfiguration section */ 697 | 022B0FAD1B2F2AE500DD4DCD /* Debug */ = { 698 | isa = XCBuildConfiguration; 699 | buildSettings = { 700 | ALWAYS_SEARCH_USER_PATHS = NO; 701 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 702 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 703 | CLANG_CXX_LIBRARY = "libc++"; 704 | CLANG_ENABLE_MODULES = YES; 705 | CLANG_ENABLE_OBJC_ARC = YES; 706 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 707 | CLANG_WARN_BOOL_CONVERSION = YES; 708 | CLANG_WARN_COMMA = YES; 709 | CLANG_WARN_CONSTANT_CONVERSION = YES; 710 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 711 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 712 | CLANG_WARN_EMPTY_BODY = YES; 713 | CLANG_WARN_ENUM_CONVERSION = YES; 714 | CLANG_WARN_INFINITE_RECURSION = YES; 715 | CLANG_WARN_INT_CONVERSION = YES; 716 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 717 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 718 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 719 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 720 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 721 | CLANG_WARN_STRICT_PROTOTYPES = YES; 722 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 723 | CLANG_WARN_UNREACHABLE_CODE = YES; 724 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 725 | COPY_PHASE_STRIP = NO; 726 | CURRENT_PROJECT_VERSION = 1; 727 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 728 | ENABLE_STRICT_OBJC_MSGSEND = YES; 729 | ENABLE_TESTABILITY = YES; 730 | GCC_C_LANGUAGE_STANDARD = gnu99; 731 | GCC_DYNAMIC_NO_PIC = NO; 732 | GCC_NO_COMMON_BLOCKS = YES; 733 | GCC_OPTIMIZATION_LEVEL = 0; 734 | GCC_PREPROCESSOR_DEFINITIONS = ( 735 | "DEBUG=1", 736 | "$(inherited)", 737 | ); 738 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 739 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 740 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 741 | GCC_WARN_UNDECLARED_SELECTOR = YES; 742 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 743 | GCC_WARN_UNUSED_FUNCTION = YES; 744 | GCC_WARN_UNUSED_VARIABLE = YES; 745 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 746 | MTL_ENABLE_DEBUG_INFO = YES; 747 | ONLY_ACTIVE_ARCH = YES; 748 | PRODUCT_NAME = "$(PROJECT_NAME)"; 749 | SDKROOT = iphoneos; 750 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 751 | SWIFT_VERSION = 5.0; 752 | TARGETED_DEVICE_FAMILY = "1,2"; 753 | VERSIONING_SYSTEM = "apple-generic"; 754 | VERSION_INFO_PREFIX = ""; 755 | }; 756 | name = Debug; 757 | }; 758 | 022B0FAE1B2F2AE500DD4DCD /* Release */ = { 759 | isa = XCBuildConfiguration; 760 | buildSettings = { 761 | ALWAYS_SEARCH_USER_PATHS = NO; 762 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 763 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 764 | CLANG_CXX_LIBRARY = "libc++"; 765 | CLANG_ENABLE_MODULES = YES; 766 | CLANG_ENABLE_OBJC_ARC = YES; 767 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 768 | CLANG_WARN_BOOL_CONVERSION = YES; 769 | CLANG_WARN_COMMA = YES; 770 | CLANG_WARN_CONSTANT_CONVERSION = YES; 771 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 772 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 773 | CLANG_WARN_EMPTY_BODY = YES; 774 | CLANG_WARN_ENUM_CONVERSION = YES; 775 | CLANG_WARN_INFINITE_RECURSION = YES; 776 | CLANG_WARN_INT_CONVERSION = YES; 777 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 778 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 779 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 780 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 781 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 782 | CLANG_WARN_STRICT_PROTOTYPES = YES; 783 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 784 | CLANG_WARN_UNREACHABLE_CODE = YES; 785 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 786 | COPY_PHASE_STRIP = NO; 787 | CURRENT_PROJECT_VERSION = 1; 788 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 789 | ENABLE_NS_ASSERTIONS = NO; 790 | ENABLE_STRICT_OBJC_MSGSEND = YES; 791 | GCC_C_LANGUAGE_STANDARD = gnu99; 792 | GCC_NO_COMMON_BLOCKS = YES; 793 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 794 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 795 | GCC_WARN_UNDECLARED_SELECTOR = YES; 796 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 797 | GCC_WARN_UNUSED_FUNCTION = YES; 798 | GCC_WARN_UNUSED_VARIABLE = YES; 799 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 800 | MTL_ENABLE_DEBUG_INFO = NO; 801 | PRODUCT_NAME = "$(PROJECT_NAME)"; 802 | SDKROOT = iphoneos; 803 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 804 | SWIFT_VERSION = 5.0; 805 | TARGETED_DEVICE_FAMILY = "1,2"; 806 | VALIDATE_PRODUCT = YES; 807 | VERSIONING_SYSTEM = "apple-generic"; 808 | VERSION_INFO_PREFIX = ""; 809 | }; 810 | name = Release; 811 | }; 812 | 022B0FB01B2F2AE500DD4DCD /* Debug */ = { 813 | isa = XCBuildConfiguration; 814 | buildSettings = { 815 | APPLICATION_EXTENSION_API_ONLY = YES; 816 | CODE_SIGN_IDENTITY = ""; 817 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 818 | DEFINES_MODULE = YES; 819 | DYLIB_COMPATIBILITY_VERSION = 1; 820 | DYLIB_CURRENT_VERSION = 1; 821 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 822 | FRAMEWORK_SEARCH_PATHS = ( 823 | "$(DEVELOPER_FRAMEWORKS_DIR)", 824 | "$(inherited)", 825 | ); 826 | INFOPLIST_FILE = DataSource/Info.plist; 827 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 828 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 829 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 830 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; 831 | SKIP_INSTALL = YES; 832 | SWIFT_VERSION = 5.0; 833 | }; 834 | name = Debug; 835 | }; 836 | 022B0FB11B2F2AE500DD4DCD /* Release */ = { 837 | isa = XCBuildConfiguration; 838 | buildSettings = { 839 | APPLICATION_EXTENSION_API_ONLY = YES; 840 | CODE_SIGN_IDENTITY = ""; 841 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 842 | DEFINES_MODULE = YES; 843 | DYLIB_COMPATIBILITY_VERSION = 1; 844 | DYLIB_CURRENT_VERSION = 1; 845 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 846 | FRAMEWORK_SEARCH_PATHS = ( 847 | "$(DEVELOPER_FRAMEWORKS_DIR)", 848 | "$(inherited)", 849 | ); 850 | INFOPLIST_FILE = DataSource/Info.plist; 851 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 852 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 853 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 854 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; 855 | SKIP_INSTALL = YES; 856 | SWIFT_VERSION = 5.0; 857 | }; 858 | name = Release; 859 | }; 860 | 022B0FB31B2F2AE500DD4DCD /* Debug */ = { 861 | isa = XCBuildConfiguration; 862 | buildSettings = { 863 | CODE_SIGN_IDENTITY = "iPhone Developer"; 864 | DEVELOPMENT_TEAM = ""; 865 | FRAMEWORK_SEARCH_PATHS = ( 866 | "$(DEVELOPER_FRAMEWORKS_DIR)", 867 | "$(inherited)", 868 | ); 869 | GCC_PREPROCESSOR_DEFINITIONS = ( 870 | "DEBUG=1", 871 | "$(inherited)", 872 | ); 873 | INFOPLIST_FILE = DataSourceTests/Info.plist; 874 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 875 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; 876 | PRODUCT_NAME = "$(TARGET_NAME)"; 877 | SWIFT_VERSION = 5.0; 878 | }; 879 | name = Debug; 880 | }; 881 | 022B0FB41B2F2AE500DD4DCD /* Release */ = { 882 | isa = XCBuildConfiguration; 883 | buildSettings = { 884 | CODE_SIGN_IDENTITY = "iPhone Developer"; 885 | DEVELOPMENT_TEAM = ""; 886 | FRAMEWORK_SEARCH_PATHS = ( 887 | "$(DEVELOPER_FRAMEWORKS_DIR)", 888 | "$(inherited)", 889 | ); 890 | INFOPLIST_FILE = DataSourceTests/Info.plist; 891 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 892 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; 893 | PRODUCT_NAME = "$(TARGET_NAME)"; 894 | SWIFT_VERSION = 5.0; 895 | }; 896 | name = Release; 897 | }; 898 | FCBEE8E21DA8F41D00F59863 /* Debug */ = { 899 | isa = XCBuildConfiguration; 900 | buildSettings = { 901 | APPLICATION_EXTENSION_API_ONLY = YES; 902 | CLANG_ANALYZER_NONNULL = YES; 903 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 904 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 905 | CODE_SIGN_IDENTITY = ""; 906 | DEBUG_INFORMATION_FORMAT = dwarf; 907 | DEFINES_MODULE = YES; 908 | DEVELOPMENT_TEAM = ""; 909 | DYLIB_COMPATIBILITY_VERSION = 1; 910 | DYLIB_CURRENT_VERSION = 1; 911 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 912 | INFOPLIST_FILE = DataSource/Info.plist; 913 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 914 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 915 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.DataSource-tvOS"; 916 | SDKROOT = appletvos; 917 | SKIP_INSTALL = YES; 918 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 919 | TARGETED_DEVICE_FAMILY = 3; 920 | TVOS_DEPLOYMENT_TARGET = 12.0; 921 | }; 922 | name = Debug; 923 | }; 924 | FCBEE8E31DA8F41D00F59863 /* Release */ = { 925 | isa = XCBuildConfiguration; 926 | buildSettings = { 927 | APPLICATION_EXTENSION_API_ONLY = YES; 928 | CLANG_ANALYZER_NONNULL = YES; 929 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 930 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 931 | CODE_SIGN_IDENTITY = ""; 932 | DEFINES_MODULE = YES; 933 | DEVELOPMENT_TEAM = ""; 934 | DYLIB_COMPATIBILITY_VERSION = 1; 935 | DYLIB_CURRENT_VERSION = 1; 936 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 937 | INFOPLIST_FILE = DataSource/Info.plist; 938 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 939 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 940 | PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.DataSource-tvOS"; 941 | SDKROOT = appletvos; 942 | SKIP_INSTALL = YES; 943 | TARGETED_DEVICE_FAMILY = 3; 944 | TVOS_DEPLOYMENT_TARGET = 12.0; 945 | }; 946 | name = Release; 947 | }; 948 | /* End XCBuildConfiguration section */ 949 | 950 | /* Begin XCConfigurationList section */ 951 | 022B0F931B2F2AE500DD4DCD /* Build configuration list for PBXProject "DataSource" */ = { 952 | isa = XCConfigurationList; 953 | buildConfigurations = ( 954 | 022B0FAD1B2F2AE500DD4DCD /* Debug */, 955 | 022B0FAE1B2F2AE500DD4DCD /* Release */, 956 | ); 957 | defaultConfigurationIsVisible = 0; 958 | defaultConfigurationName = Release; 959 | }; 960 | 022B0FAF1B2F2AE500DD4DCD /* Build configuration list for PBXNativeTarget "DataSource-iOS" */ = { 961 | isa = XCConfigurationList; 962 | buildConfigurations = ( 963 | 022B0FB01B2F2AE500DD4DCD /* Debug */, 964 | 022B0FB11B2F2AE500DD4DCD /* Release */, 965 | ); 966 | defaultConfigurationIsVisible = 0; 967 | defaultConfigurationName = Release; 968 | }; 969 | 022B0FB21B2F2AE500DD4DCD /* Build configuration list for PBXNativeTarget "DataSourceTests" */ = { 970 | isa = XCConfigurationList; 971 | buildConfigurations = ( 972 | 022B0FB31B2F2AE500DD4DCD /* Debug */, 973 | 022B0FB41B2F2AE500DD4DCD /* Release */, 974 | ); 975 | defaultConfigurationIsVisible = 0; 976 | defaultConfigurationName = Release; 977 | }; 978 | FCBEE8E11DA8F41D00F59863 /* Build configuration list for PBXNativeTarget "DataSource-tvOS" */ = { 979 | isa = XCConfigurationList; 980 | buildConfigurations = ( 981 | FCBEE8E21DA8F41D00F59863 /* Debug */, 982 | FCBEE8E31DA8F41D00F59863 /* Release */, 983 | ); 984 | defaultConfigurationIsVisible = 0; 985 | defaultConfigurationName = Release; 986 | }; 987 | /* End XCConfigurationList section */ 988 | 989 | /* Begin XCVersionGroup section */ 990 | 6B4082C422147FDA00A4CA90 /* Items.xcdatamodeld */ = { 991 | isa = XCVersionGroup; 992 | children = ( 993 | 6B4082C522147FDA00A4CA90 /* Items.xcdatamodel */, 994 | ); 995 | currentVersion = 6B4082C522147FDA00A4CA90 /* Items.xcdatamodel */; 996 | path = Items.xcdatamodeld; 997 | sourceTree = ""; 998 | versionGroupType = wrapper.xcdatamodel; 999 | }; 1000 | /* End XCVersionGroup section */ 1001 | }; 1002 | rootObject = 022B0F901B2F2AE500DD4DCD /* Project object */; 1003 | } 1004 | -------------------------------------------------------------------------------- /DataSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DataSource.xcodeproj/xcshareddata/xcschemes/DataSource-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /DataSource.xcodeproj/xcshareddata/xcschemes/DataSource-tvOS.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 | -------------------------------------------------------------------------------- /DataSource.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DataSource.xcworkspace/xcshareddata/DataSource.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "490E57B35E951D5E5221A47EAB9C248E2FEBC174", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "E084C86B03F81D63323C9E7510697EA528A758C7" : 0, 8 | "490E57B35E951D5E5221A47EAB9C248E2FEBC174" : 0, 9 | "51B210D0D41486D3ADD6940D57E4480BBD4DE4C5" : 0, 10 | "956D2B21DD155C49504BB67697A67F7C5351A353" : 0 11 | }, 12 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "30446CE5-B74F-4120-99FE-4BA20BB26C4B", 13 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 14 | "E084C86B03F81D63323C9E7510697EA528A758C7" : "DataSource\/Carthage\/Checkouts\/ReactiveSwift\/Carthage\/Checkouts\/xcconfigs\/", 15 | "490E57B35E951D5E5221A47EAB9C248E2FEBC174" : "DataSource\/", 16 | "51B210D0D41486D3ADD6940D57E4480BBD4DE4C5" : "DataSource\/Carthage\/Checkouts\/ReactiveSwift\/", 17 | "956D2B21DD155C49504BB67697A67F7C5351A353" : "DataSource\/Carthage\/Checkouts\/Result\/" 18 | }, 19 | "DVTSourceControlWorkspaceBlueprintNameKey" : "DataSource", 20 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 21 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "DataSource.xcworkspace", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 23 | { 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com-me:Vadim-Yelagin\/DataSource.git", 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "490E57B35E951D5E5221A47EAB9C248E2FEBC174" 27 | }, 28 | { 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/ReactiveCocoa\/ReactiveCocoa.git", 30 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "51B210D0D41486D3ADD6940D57E4480BBD4DE4C5" 32 | }, 33 | { 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/antitypical\/Result.git", 35 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "956D2B21DD155C49504BB67697A67F7C5351A353" 37 | }, 38 | { 39 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/jspahrsummers\/xcconfigs.git", 40 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "E084C86B03F81D63323C9E7510697EA528A758C7" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /DataSource.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DataSource/CollectionView/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 10/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | import UIKit 11 | 12 | /// `UICollectionViewCell` subclass that implements `DataSourceItemReceiver` protocol 13 | /// by putting received dataSource items into a `MutableProperty` called `cellModel`. 14 | /// - note: 15 | /// You are not required to subclass `CollectionViewCell` class in order 16 | /// to use your cell subclass with `CollectionViewDataSource`. 17 | /// Instead you can implement `DataSourceItemReceiver` 18 | /// protocol directly in any `UICollectionViewCell` subclass. 19 | open class CollectionViewCell: UICollectionViewCell, DataSourceItemReceiver { 20 | 21 | public final let cellModel = MutableProperty(nil) 22 | 23 | open func ds_setItem(_ item: Any) { 24 | self.cellModel.value = item 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/CollectionView/CollectionViewChangeTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewChangeTarget.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 10/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UICollectionView: DataChangeTarget { 13 | 14 | public func ds_performBatchChanges(_ batchChanges: @escaping () -> Void) { 15 | self.performBatchUpdates(batchChanges, completion: nil) 16 | } 17 | 18 | public func ds_deleteItems(at indexPaths: [IndexPath]) { 19 | self.deleteItems(at: indexPaths) 20 | } 21 | 22 | public func ds_deleteSections(_ sections: [Int]) { 23 | self.deleteSections(IndexSet(dsIntegers: sections)) 24 | } 25 | 26 | public func ds_insertItems(at indexPaths: [IndexPath]) { 27 | self.insertItems(at: indexPaths) 28 | } 29 | 30 | public func ds_insertSections(_ sections: [Int]) { 31 | self.insertSections(IndexSet(dsIntegers: sections)) 32 | } 33 | 34 | public func ds_moveItem(at oldIndexPath: IndexPath, to newIndexPath: IndexPath) { 35 | self.moveItem(at: oldIndexPath, to: newIndexPath) 36 | } 37 | 38 | public func ds_moveSection(_ oldSection: Int, toSection newSection: Int) { 39 | self.moveSection(oldSection, toSection: newSection) 40 | } 41 | 42 | public func ds_reloadData() { 43 | self.reloadData() 44 | } 45 | 46 | public func ds_reloadItems(at indexPaths: [IndexPath]) { 47 | self.reloadItems(at: indexPaths) 48 | } 49 | 50 | public func ds_reloadSections(_ sections: [Int]) { 51 | self.reloadSections(IndexSet(dsIntegers: sections)) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /DataSource/CollectionView/CollectionViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 10/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | import UIKit 12 | 13 | /// An object that implements `UICollectionViewDataSource` protocol 14 | /// by returning the data from an associated dataSource. 15 | /// 16 | /// The number of section and numbers of items in sections 17 | /// are taken directly from the dataSource. 18 | /// 19 | /// The cells are dequeued from a collectionView 20 | /// by reuseIdentifiers returned by `reuseIdentifierForItem` function. 21 | /// 22 | /// Supplementary views are dequeued from a collectionView by reuseIdentifiers 23 | /// returned by `reuseIdentifierForSupplementaryItem` function. 24 | /// 25 | /// If a cell or reusableView implements the `DataSourceItemReceiver` protocol 26 | /// (e.g. by subclassing the `CollectionViewCell` or `CollectionViewReusableView` class), 27 | /// the item at the indexPath is passed to it via `ds_setItem` method. 28 | /// 29 | /// A collectionViewDataSource observes changes of the associated dataSource 30 | /// and applies those changes to the associated collectionView. 31 | open class CollectionViewDataSource: NSObject, UICollectionViewDataSource { 32 | // swiftlint:disable private_outlet 33 | @IBOutlet public final var collectionView: UICollectionView? 34 | 35 | public final let dataSource = ProxyDataSource() 36 | 37 | public final var reuseIdentifierForItem: (IndexPath, Any) -> String = { 38 | _, _ in "DefaultCell" 39 | } 40 | 41 | public final var reuseIdentifierForSupplementaryItem: (String, Int, Any) -> String = { 42 | _, _, _ in "DefaultSupplementaryView" 43 | } 44 | 45 | public final var dataChangeTarget: DataChangeTarget? 46 | 47 | private let disposable = CompositeDisposable() 48 | 49 | override public init() { 50 | super.init() 51 | self.disposable += self.dataSource.changes.observeValues { [weak self] change in 52 | if let self = self, let dataChangeTarget = self.dataChangeTarget ?? self.collectionView { 53 | change.apply(to: dataChangeTarget) 54 | } 55 | } 56 | } 57 | 58 | deinit { 59 | self.disposable.dispose() 60 | } 61 | 62 | open func configureCell(_ cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 63 | let item = self.dataSource.item(at: indexPath) 64 | configureReceiver(cell, withItem: item) 65 | } 66 | 67 | open func configureCellForItem(at indexPath: IndexPath) { 68 | if let cell = self.collectionView?.cellForItem(at: indexPath) { 69 | self.configureCell(cell, forItemAt: indexPath) 70 | } 71 | } 72 | 73 | open func configureVisibleCells() { 74 | if let indexPaths = self.collectionView?.indexPathsForVisibleItems { 75 | for indexPath in indexPaths { 76 | self.configureCellForItem(at: indexPath) 77 | } 78 | } 79 | } 80 | 81 | open func numberOfSections(in collectionView: UICollectionView) -> Int { 82 | return self.dataSource.numberOfSections 83 | } 84 | 85 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 86 | return self.dataSource.numberOfItemsInSection(section) 87 | } 88 | 89 | open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 90 | let section = (indexPath as NSIndexPath).section 91 | guard let item = self.dataSource.supplementaryItemOfKind(kind, inSection: section) else { 92 | fatalError("Expected item for collection view supplementary item of kind \(kind) in section \(section), but found nil") 93 | } 94 | let reuseIdentifier = self.reuseIdentifierForSupplementaryItem(kind, section, item) 95 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifier, for: indexPath) 96 | configureReceiver(view, withItem: item) 97 | return view 98 | } 99 | 100 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 101 | let item: Any = self.dataSource.item(at: indexPath) 102 | let reuseIdentifier = self.reuseIdentifierForItem(indexPath, item) 103 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) 104 | self.configureCell(cell, forItemAt: indexPath) 105 | return cell 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /DataSource/CollectionView/CollectionViewReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewReusableView.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | import UIKit 11 | 12 | /// `UICollectionReusableView` subclass that implements `DataSourceItemReceiver` protocol 13 | /// by putting received dataSource items into a `MutableProperty` called `viewModel`. 14 | /// - note: 15 | /// You are not required to subclass `CollectionViewReusableView` class in order 16 | /// to use your cell subclass with `CollectionViewDataSource`. 17 | /// Instead you can implement `DataSourceItemReceiver` 18 | /// protocol directly in any `UICollectionReusableView` subclass. 19 | open class CollectionViewReusableView: UICollectionReusableView, DataSourceItemReceiver { 20 | 21 | public final let viewModel = MutableProperty(nil) 22 | 23 | open func ds_setItem(_ item: Any) { 24 | self.viewModel.value = item 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/DataChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChange.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A value representing a modification of a dataSource contents. 12 | /// - seealso: DataSource 13 | public protocol DataChange { 14 | 15 | /// Applies the dataChange to a given target. 16 | func apply(to target: DataChangeTarget) 17 | 18 | /// Returns a new dataChange of same type with its section indicies transformed 19 | /// by the given function. 20 | func mapSections(_ transform: (Int) -> Int) -> Self 21 | 22 | } 23 | -------------------------------------------------------------------------------- /DataSource/DataChangeTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeTarget.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A target onto which different types of dataChanges can be applied. 12 | /// When a dataChange is applied, the target transitions from reflecting 13 | /// the state of the corresponding dataSource prior to the dataChange 14 | /// to reflecting the dataSource state after the dataChange. 15 | /// 16 | /// `UITableView` and `UICollectionView` are implementing this protocol. 17 | public protocol DataChangeTarget { 18 | 19 | func ds_performBatchChanges(_ batchChanges: @escaping () -> Void) 20 | 21 | func ds_deleteItems(at indexPaths: [IndexPath]) 22 | 23 | func ds_deleteSections(_ sections: [Int]) 24 | 25 | func ds_insertItems(at indexPaths: [IndexPath]) 26 | 27 | func ds_insertSections(_ sections: [Int]) 28 | 29 | func ds_moveItem(at oldIndexPath: IndexPath, to newIndexPath: IndexPath) 30 | 31 | func ds_moveSection(_ oldSection: Int, toSection newSection: Int) 32 | 33 | func ds_reloadData() 34 | 35 | func ds_reloadItems(at indexPaths: [IndexPath]) 36 | 37 | func ds_reloadSections(_ sections: [Int]) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeBatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeBatch.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeBatch: DataChange { 12 | 13 | public let changes: [DataChange] 14 | 15 | public init(_ changes: [DataChange]) { 16 | self.changes = changes 17 | } 18 | 19 | public func apply(to target: DataChangeTarget) { 20 | target.ds_performBatchChanges { 21 | for change in self.changes { 22 | change.apply(to: target) 23 | } 24 | } 25 | } 26 | 27 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeBatch { 28 | let mapped = self.changes.map { $0.mapSections(transform) } 29 | return DataChangeBatch(mapped) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeDeleteItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeDeleteItems.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeDeleteItems: DataChange { 12 | 13 | public let indexPaths: [IndexPath] 14 | 15 | public init(_ indexPaths: [IndexPath]) { 16 | self.indexPaths = indexPaths 17 | } 18 | 19 | public init (_ indexPath: IndexPath) { 20 | self.init([indexPath]) 21 | } 22 | 23 | public func apply(to target: DataChangeTarget) { 24 | target.ds_deleteItems(at: indexPaths) 25 | } 26 | 27 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeDeleteItems { 28 | return DataChangeDeleteItems(indexPaths.map { $0.ds_mapSection(transform) }) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeDeleteSections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeDeleteSections.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeDeleteSections: DataChange { 12 | 13 | public let sections: [Int] 14 | 15 | public init(_ sections: [Int]) { 16 | self.sections = sections 17 | } 18 | 19 | public func apply(to target: DataChangeTarget) { 20 | target.ds_deleteSections(sections) 21 | } 22 | 23 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeDeleteSections { 24 | return DataChangeDeleteSections(sections.map(transform)) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeInsertItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeInsertItems.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeInsertItems: DataChange { 12 | 13 | public let indexPaths: [IndexPath] 14 | 15 | public init(_ indexPaths: [IndexPath]) { 16 | self.indexPaths = indexPaths 17 | } 18 | 19 | public init (_ indexPath: IndexPath) { 20 | self.init([indexPath]) 21 | } 22 | 23 | public func apply(to target: DataChangeTarget) { 24 | target.ds_insertItems(at: indexPaths) 25 | } 26 | 27 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeInsertItems { 28 | return DataChangeInsertItems(indexPaths.map { $0.ds_mapSection(transform) }) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeInsertSections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeInsertSections.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeInsertSections: DataChange { 12 | 13 | public let sections: [Int] 14 | 15 | public init(_ sections: [Int]) { 16 | self.sections = sections 17 | } 18 | 19 | public func apply(to target: DataChangeTarget) { 20 | target.ds_insertSections(sections) 21 | } 22 | 23 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeInsertSections { 24 | return DataChangeInsertSections(sections.map(transform)) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeMoveItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeMoveItem.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeMoveItem: DataChange { 12 | 13 | public let fromIndexPath: IndexPath 14 | public let toIndexPath: IndexPath 15 | 16 | public init(from fromIndexPath: IndexPath, to toIndexPath: IndexPath) { 17 | self.fromIndexPath = fromIndexPath 18 | self.toIndexPath = toIndexPath 19 | } 20 | 21 | public func apply(to target: DataChangeTarget) { 22 | target.ds_moveItem(at: fromIndexPath, to: toIndexPath) 23 | } 24 | 25 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeMoveItem { 26 | return DataChangeMoveItem(from: fromIndexPath.ds_mapSection(transform), to: toIndexPath.ds_mapSection(transform)) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeMoveSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeMoveSection.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeMoveSection: DataChange { 12 | 13 | public let fromSection: Int 14 | public let toSection: Int 15 | 16 | public init(from fromSection: Int, to toSection: Int) { 17 | self.fromSection = fromSection 18 | self.toSection = toSection 19 | } 20 | 21 | public func apply(to target: DataChangeTarget) { 22 | target.ds_moveSection(fromSection, toSection: toSection) 23 | } 24 | 25 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeMoveSection { 26 | return DataChangeMoveSection(from: transform(fromSection), to: transform(toSection)) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeReloadData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeReloadData.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeReloadData: DataChange { 12 | 13 | public init() {} 14 | 15 | public func apply(to target: DataChangeTarget) { 16 | target.ds_reloadData() 17 | } 18 | 19 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeReloadData { 20 | return self 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeReloadItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeReloadItems.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeReloadItems: DataChange { 12 | 13 | public let indexPaths: [IndexPath] 14 | 15 | public init(_ indexPaths: [IndexPath]) { 16 | self.indexPaths = indexPaths 17 | } 18 | 19 | public init (_ indexPath: IndexPath) { 20 | self.init([indexPath]) 21 | } 22 | 23 | public func apply(to target: DataChangeTarget) { 24 | target.ds_reloadItems(at: indexPaths) 25 | } 26 | 27 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeReloadItems { 28 | return DataChangeReloadItems(indexPaths.map { $0.ds_mapSection(transform) }) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /DataSource/DataChangeTypes/DataChangeReloadSections.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataChangeReloadSections.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataChangeReloadSections: DataChange { 12 | 13 | public let sections: [Int] 14 | 15 | public init(_ sections: [Int]) { 16 | self.sections = sections 17 | } 18 | 19 | public func apply(to target: DataChangeTarget) { 20 | target.ds_reloadSections(sections) 21 | } 22 | 23 | public func mapSections(_ transform: (Int) -> Int) -> DataChangeReloadSections { 24 | return DataChangeReloadSections(sections.map(transform)) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/DataSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Fueled. All rights reserved. 3 | // 4 | 5 | /// Project version number for DataSource. 6 | extern double DataSourceVersionNumber; 7 | 8 | /// Project version string for DataSource. 9 | extern const unsigned char DataSourceVersionString[]; 10 | -------------------------------------------------------------------------------- /DataSource/DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// A provider of items grouped into sections. 13 | /// Each section can optionally have a collection 14 | /// of supplementary items identified by arbitrary strings called kinds. 15 | /// 16 | /// A dataSource can be mutable, i.e. change the number and/or contents of its sections. 17 | /// Immediately after any such change, a dataSource emits a dataChange value representing 18 | /// that change via its `changes` property. 19 | public protocol DataSource { 20 | 21 | /// A push-driven stream of values that represent every modification 22 | /// of the dataSource contents immediately after they happen. 23 | var changes: Signal { get } 24 | 25 | var numberOfSections: Int { get } 26 | 27 | func numberOfItemsInSection(_ section: Int) -> Int 28 | 29 | func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? 30 | 31 | func item(at indexPath: IndexPath) -> Any 32 | 33 | /// Asks the dataSource for the original dataSource that contains the item at the given indexPath, 34 | /// and the indexPath of that item in that dataSource. 35 | /// 36 | /// If the receiving dataSource is composed of other dataSources that provide its items, 37 | /// this method finds the dataSource responsible for providing an item for the given indexPath, 38 | /// and this method is called on it recursively. 39 | /// 40 | /// Otherwise, this method simply returns the receiving dataSource itself and the given indexPath unchanged. 41 | func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /DataSource/DataSourceCellDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSourceCellDescriptor.swift 3 | // DataSource 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class CellDescriptor: NSObject { 12 | public let reuseIdentifier: String 13 | public let prototypeSource: PrototypeSource 14 | public let isMatching: (IndexPath, Any) -> Bool 15 | 16 | public init( 17 | _ reuseIdentifier: String, 18 | _ prototypeSource: PrototypeSource = .storyboard, 19 | isMatching: @escaping (IndexPath, Any) -> Bool) 20 | { 21 | self.reuseIdentifier = reuseIdentifier 22 | self.prototypeSource = prototypeSource 23 | self.isMatching = isMatching 24 | } 25 | 26 | public convenience init( 27 | _ reuseIdentifier: String, 28 | _ itemType: Item.Type, 29 | _ prototypeSource: PrototypeSource = .storyboard) 30 | { 31 | self.init(reuseIdentifier, prototypeSource) { $1 is Item } 32 | } 33 | } 34 | 35 | public class HeaderFooterDescriptor: NSObject { 36 | public let reuseIdentifier: String 37 | public let prototypeSource: PrototypeSource 38 | public let isMatching: (Int, Any) -> Bool 39 | 40 | public init( 41 | _ reuseIdentifier: String, 42 | _ prototypeSource: PrototypeSource, 43 | isMatching: @escaping (Int, Any) -> Bool) 44 | { 45 | self.reuseIdentifier = reuseIdentifier 46 | self.prototypeSource = prototypeSource 47 | self.isMatching = isMatching 48 | } 49 | 50 | public convenience init( 51 | _ reuseIdentifier: String, 52 | _ itemType: Item.Type, 53 | _ prototypeSource: PrototypeSource = .storyboard) 54 | { 55 | self.init(reuseIdentifier, prototypeSource) { $1 is Item } 56 | } 57 | } 58 | 59 | public enum PrototypeSource { 60 | case storyboard 61 | case nib(UINib) 62 | case `class`(AnyObject.Type) 63 | } 64 | 65 | extension CollectionViewDataSource { 66 | @objc open func configure(_ collectionView: UICollectionView, using cellDescriptors: [CellDescriptor]) { 67 | self.reuseIdentifierForItem = { indexPath, item in 68 | guard let reuseIdentifier = cellDescriptors.first(where: { $0.isMatching(indexPath, item) })?.reuseIdentifier else { 69 | fatalError("Unable to determine reuse identifier") 70 | } 71 | return reuseIdentifier 72 | } 73 | for descriptor in cellDescriptors { 74 | switch descriptor.prototypeSource { 75 | case .storyboard: 76 | break 77 | case .nib(let nib): 78 | collectionView.register(nib, forCellWithReuseIdentifier: descriptor.reuseIdentifier) 79 | case .class(let type): 80 | collectionView.register(type, forCellWithReuseIdentifier: descriptor.reuseIdentifier) 81 | } 82 | } 83 | collectionView.dataSource = self 84 | self.collectionView = collectionView 85 | collectionView.performBatchUpdates(nil) 86 | } 87 | } 88 | 89 | extension TableViewDataSource { 90 | @objc open func configure(_ tableView: UITableView, using cellDescriptors: [CellDescriptor]) { 91 | self.reuseIdentifierForItem = { indexPath, item in 92 | guard let reuseIdentifier = cellDescriptors.first(where: { $0.isMatching(indexPath, item) })?.reuseIdentifier else { 93 | fatalError("Unable to determine reuse identifier") 94 | } 95 | return reuseIdentifier 96 | } 97 | for descriptor in cellDescriptors { 98 | switch descriptor.prototypeSource { 99 | case .storyboard: 100 | break 101 | case .nib(let nib): 102 | tableView.register(nib, forCellReuseIdentifier: descriptor.reuseIdentifier) 103 | case .class(let type): 104 | tableView.register(type, forCellReuseIdentifier: descriptor.reuseIdentifier) 105 | } 106 | } 107 | tableView.dataSource = self 108 | self.tableView = tableView 109 | if #available(iOS 11.0, *) { 110 | tableView.performBatchUpdates(nil) 111 | } 112 | } 113 | } 114 | 115 | extension TableViewDataSourceWithHeaderFooterViews { 116 | @objc open func configure(_ tableView: UITableView, using cellDescriptors: [CellDescriptor], headerDescriptors: [HeaderFooterDescriptor], footerDescriptors: [HeaderFooterDescriptor]) { 117 | self.reuseIdentifierForItem = { indexPath, item in 118 | guard let reuseIdentifier = cellDescriptors.first(where: { $0.isMatching(indexPath, item) })?.reuseIdentifier else { 119 | fatalError("Unable to determine reuse identifier") 120 | } 121 | return reuseIdentifier 122 | } 123 | for descriptor in cellDescriptors { 124 | switch descriptor.prototypeSource { 125 | case .storyboard: 126 | break 127 | case .nib(let nib): 128 | tableView.register(nib, forCellReuseIdentifier: descriptor.reuseIdentifier) 129 | case .class(let type): 130 | tableView.register(type, forCellReuseIdentifier: descriptor.reuseIdentifier) 131 | } 132 | } 133 | self.reuseIdentifierForHeaderItem = { section, item in 134 | guard let reuseIdentifier = headerDescriptors.first(where: { $0.isMatching(section, item) })?.reuseIdentifier else { 135 | fatalError("Unable to determine reuse identifier") 136 | } 137 | return reuseIdentifier 138 | } 139 | 140 | for headerDescriptor in headerDescriptors { 141 | switch headerDescriptor.prototypeSource { 142 | case .storyboard: 143 | break 144 | case .nib(let nib): 145 | tableView.register(nib, forHeaderFooterViewReuseIdentifier: headerDescriptor.reuseIdentifier) 146 | case .class(let type): 147 | tableView.register(type, forHeaderFooterViewReuseIdentifier: headerDescriptor.reuseIdentifier) 148 | } 149 | } 150 | 151 | self.reuseIdentifierForFooterItem = { section, item in 152 | guard let reuseIdentifier = footerDescriptors.first(where: { $0.isMatching(section, item) })?.reuseIdentifier else { 153 | fatalError("Unable to determine reuse identifier") 154 | } 155 | return reuseIdentifier 156 | } 157 | 158 | for footerDescriptor in footerDescriptors { 159 | switch footerDescriptor.prototypeSource { 160 | case .storyboard: 161 | break 162 | case .nib(let nib): 163 | tableView.register(nib, forHeaderFooterViewReuseIdentifier: footerDescriptor.reuseIdentifier) 164 | case .class(let type): 165 | tableView.register(type, forHeaderFooterViewReuseIdentifier: footerDescriptor.reuseIdentifier) 166 | } 167 | } 168 | 169 | tableView.dataSource = self 170 | self.tableView = tableView 171 | if #available(iOS 11.0, *) { 172 | tableView.performBatchUpdates(nil) 173 | } 174 | } 175 | } 176 | 177 | public protocol ReusableItem: AnyObject { 178 | static var reuseIdentifier: String { get } 179 | } 180 | 181 | public protocol ReusableNib: AnyObject { 182 | static var nib: UINib { get } 183 | } 184 | 185 | extension ReusableItem where Self: UIView { 186 | public static var reuseIdentifier: String { return String(describing: self).components(separatedBy: ".").last! } 187 | } 188 | 189 | extension ReusableNib where Self: UIView, Self: ReusableItem { 190 | public static var nib: UINib { return UINib(nibName: self.reuseIdentifier, bundle: nil) } 191 | } 192 | -------------------------------------------------------------------------------- /DataSource/DataSourceItemReceiver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSourceItemReceiver.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 23/09/15. 6 | // Copyright © 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Implement this protocol in your `UITableViewCell` or `UICollectionView` subclass 12 | /// to make it receive corresponding items of the dataSource associated 13 | /// with a `TableViewDataSource` or a `CollectionViewDataSource`. 14 | /// - note: Only a class implemented in Swift can conform to this protocol. 15 | /// For Objective-C classes see `DataSourceObjectItemReceiver`. 16 | public protocol DataSourceItemReceiver { 17 | 18 | func ds_setItem(_ item: Any) 19 | 20 | } 21 | 22 | /// Implement this protocol in your `UITableViewCell` or `UICollectionView` subclass 23 | /// to make it receive corresponding items of the dataSource associated 24 | /// with a `TableViewDataSource` or a `CollectionViewDataSource`. 25 | /// - note: This protocol allows only settings of items of object type. 26 | /// Use `DataSourceItemReceiver` protocol instead if you don't need to implement 27 | /// your class in Objective-C. 28 | @objc public protocol DataSourceObjectItemReceiver { 29 | 30 | @objc func ds_setItem(_ item: AnyObject) 31 | 32 | } 33 | 34 | func configureReceiver(_ receiver: AnyObject, withItem item: Any) { 35 | if let receiver = receiver as? DataSourceItemReceiver { 36 | receiver.ds_setItem(item) 37 | } else if let receiver = receiver as? DataSourceObjectItemReceiver { 38 | receiver.ds_setItem(item as AnyObject) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/AutoDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiff.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/08/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable convenience_type 12 | public struct AutoDiff { 13 | 14 | public struct Result { 15 | 16 | public var matches: [(Int, Int)] = [] 17 | public var deleted: [Int] = [] 18 | public var inserted: [Int] = [] 19 | public var moved: [(Int, Int)] = [] 20 | 21 | public func toSectionChanges() -> [DataChange] { 22 | var changes: [DataChange] = [] 23 | if !deleted.isEmpty { 24 | changes.append(DataChangeDeleteSections(deleted)) 25 | } 26 | if !inserted.isEmpty { 27 | changes.append(DataChangeInsertSections(inserted)) 28 | } 29 | for (fromSection, toSection) in moved { 30 | changes.append(DataChangeMoveSection(from: fromSection, to: toSection)) 31 | } 32 | return changes 33 | } 34 | 35 | public func toItemChanges(oldSection: Int, newSection: Int) -> [DataChange] { 36 | var changes: [DataChange] = [] 37 | if !deleted.isEmpty { 38 | let indexPaths = deleted.map { 39 | IndexPath(item: $0, section: oldSection) 40 | } 41 | changes.append(DataChangeDeleteItems(indexPaths)) 42 | } 43 | if !inserted.isEmpty { 44 | let indexPaths = inserted.map { 45 | IndexPath(item: $0, section: newSection) 46 | } 47 | changes.append(DataChangeInsertItems(indexPaths)) 48 | } 49 | for (fromItem, toItem) in moved { 50 | let fromPath = IndexPath(item: fromItem, section: oldSection) 51 | let toPath = IndexPath(item: toItem, section: newSection) 52 | changes.append(DataChangeMoveItem(from: fromPath, to: toPath)) 53 | } 54 | return changes 55 | } 56 | 57 | public func toItemChanges(_ section: Int = 0) -> [DataChange] { 58 | return toItemChanges(oldSection: section, newSection: section) 59 | } 60 | 61 | } 62 | 63 | // http://en.wikipedia.org/wiki/Longest_common_subsequence_problem 64 | public static func compare(old: [T], new: [T], findMoves: Bool, compare: (T, T) -> Bool) -> Result { 65 | var moves = Array(repeating: Array(repeating: LCSMove.unknown, count: new.count), count: old.count) 66 | var lengths = Array(repeating: Array(repeating: 0, count: new.count), count: old.count) 67 | func getLength(_ iOld: Int, _ iNew: Int) -> Int { 68 | return (iOld >= 0 && iNew >= 0) ? lengths[iOld][iNew] : 0 69 | } 70 | // fill the table 71 | for iOld in 0 ..< old.count { 72 | for iNew in 0 ..< new.count { 73 | var curMove: LCSMove 74 | var curLength: Int 75 | if compare(old[iOld], new[iNew]) { 76 | curMove = .match 77 | curLength = getLength(iOld - 1, iNew - 1) + 1 78 | } else { 79 | let prevOldLength = getLength(iOld - 1, iNew) 80 | let prevNewLength = getLength(iOld, iNew - 1) 81 | if prevOldLength > prevNewLength { 82 | curMove = .fromPrevOld 83 | curLength = prevOldLength 84 | } else { 85 | curMove = .fromPrevNew 86 | curLength = prevNewLength 87 | } 88 | } 89 | moves[iOld][iNew] = curMove 90 | lengths[iOld][iNew] = curLength 91 | } 92 | } 93 | // collect longest sequence of matches from filled table 94 | var result = Result() 95 | var iOld = old.count - 1 96 | var iNew = new.count - 1 97 | while iOld >= 0 && iNew >= 0 { 98 | switch moves[iOld][iNew] { 99 | case .fromPrevOld: 100 | result.deleted.append(iOld) 101 | iOld -= 1 102 | case .fromPrevNew: 103 | result.inserted.append(iNew) 104 | iNew -= 1 105 | default: 106 | let pair = (iOld, iNew) 107 | result.matches.append(pair) 108 | iOld -= 1 109 | iNew -= 1 110 | } 111 | } 112 | while iOld >= 0 { 113 | result.deleted.append(iOld) 114 | iOld -= 1 115 | } 116 | while iNew >= 0 { 117 | result.inserted.append(iNew) 118 | iNew -= 1 119 | } 120 | if !findMoves { 121 | return result 122 | } 123 | // replace delete & insert of equal elements with a move 124 | var inserted2: [Int] = [] 125 | for iNew in result.inserted { 126 | let newItem = new[iNew] 127 | if let (j, iOld) = findFirst(result.deleted, { compare(old[$0], newItem) }) { 128 | result.deleted.remove(at: j) 129 | result.moved.append((iOld, iNew)) 130 | } else { 131 | inserted2.append(iNew) 132 | } 133 | } 134 | result.inserted = inserted2 135 | return result 136 | } 137 | 138 | private enum LCSMove { 139 | case unknown 140 | case fromPrevOld 141 | case fromPrevNew 142 | case match 143 | } 144 | 145 | } 146 | 147 | private func findFirst 148 | (_ source: S, _ predicate: (S.Iterator.Element) -> Bool) 149 | -> (Int, S.Iterator.Element)? 150 | { 151 | for (idx, s) in source.enumerated() { 152 | if predicate(s) { 153 | return (idx, s) 154 | } 155 | } 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/AutoDiffDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiffDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 10/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has one section of items of type T. 13 | /// 14 | /// Whenever the array of items is changed, the autoDiffDataSource compares 15 | /// each pair of old and new items via `compare` function, 16 | /// and produces minimal batch of dataChanges that delete, 17 | /// insert and move individual items. 18 | public final class AutoDiffDataSource: DataSource { 19 | 20 | public let changes: Signal 21 | private let observer: Signal.Observer 22 | private let disposable: Disposable 23 | 24 | /// Mutable array of items in the only section of the autoDiffDataSource. 25 | /// 26 | /// Every modification of the array causes calculation 27 | /// and emission of appropriate dataChanges. 28 | public let items: MutableProperty<[T]> 29 | 30 | public let supplementaryItems: [String: Any] 31 | 32 | /// Function that is used to compare a pair of items for equality. 33 | /// Returns `true` if the items are equal, and no dataChange is required 34 | /// to replace the first item with the second. 35 | public let compare: (T, T) -> Bool 36 | 37 | /// Creates an autoDiffDataSource. 38 | /// - parameters: 39 | /// - items: Initial array of items of the only section of the autoDiffDataSource. 40 | /// - supplementaryItems: Supplementary items of the section. 41 | /// - findMoves: Set `findMoves` to `false` to make the dataSource emit 42 | /// a pair of deletion and insertion instead of item movement dataChanges. 43 | /// - compare: Function that is used to compare a pair of items for equality. 44 | public init( 45 | _ items: [T] = [], 46 | supplementaryItems: [String: Any] = [:], 47 | findMoves: Bool = true, 48 | compare: @escaping (T, T) -> Bool) 49 | { 50 | (self.changes, self.observer) = Signal.pipe() 51 | self.items = MutableProperty(items) 52 | self.supplementaryItems = supplementaryItems 53 | self.compare = compare 54 | func autoDiff(_ old: [T], new: [T]) -> DataChange { 55 | let result = AutoDiff.compare(old: old, new: new, findMoves: findMoves, compare: compare) 56 | return DataChangeBatch(result.toItemChanges()) 57 | } 58 | self.disposable = self.items.producer 59 | .combinePrevious(items) 60 | .skip(first: 1) 61 | .map(autoDiff) 62 | .start(self.observer) 63 | } 64 | 65 | deinit { 66 | self.observer.sendCompleted() 67 | self.disposable.dispose() 68 | } 69 | 70 | public let numberOfSections = 1 71 | 72 | public func numberOfItemsInSection(_ section: Int) -> Int { 73 | return self.items.value.count 74 | } 75 | 76 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 77 | return self.supplementaryItems[kind] 78 | } 79 | 80 | public func item(at indexPath: IndexPath) -> Any { 81 | return self.items.value[indexPath.item] 82 | } 83 | 84 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 85 | return (self, indexPath) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/AutoDiffSectionsDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiffSectionsDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/08/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has an arbitrary 13 | /// number of sections of items of type T. 14 | /// 15 | /// Whenever the array of sections is changed, the dataSource compares 16 | /// each pair of old and new sections via `compareSections` function, 17 | /// and produces minimal set of dataChanges that delete and insert 18 | /// non-matching sections. Then it compares items (via `compareItems` function) 19 | /// within each pair of matching sections and produces minimal sets of dataChanges 20 | /// for non-matching items within those sections. 21 | /// 22 | /// `AutoDiffSectionsDataSource` never generates movement of sections. 23 | /// Items are only compared withing sections, hence movement of items 24 | /// between sections are never found either. 25 | /// 26 | /// When comparing sections, `AutoDiffSectionsDataSource` does not rely 27 | /// on the items they comprise. Instead it calls `compareSections` that 28 | /// you provide it with. Sections are usually compared based on some 29 | /// userData that is used to identify them. Such data can be stored in 30 | /// sections' `supplementaryItems` dictionary under some user-defined key. 31 | public final class AutoDiffSectionsDataSource: DataSource { 32 | 33 | public let changes: Signal 34 | private let observer: Signal.Observer 35 | private let disposable: Disposable 36 | 37 | /// Mutable array of dataSourceSections. 38 | /// 39 | /// Every modification of the array causes calculation 40 | /// and emission of appropriate dataChanges. 41 | public let sections: MutableProperty<[DataSourceSection]> 42 | 43 | /// Function that is used to compare a pair of sections for identity. 44 | /// Returns `true` if the sections are identical, in which case the items 45 | /// withing them are compared via `compareItems` function. 46 | /// Otherwise, the old section is deleted and the new section is inserted, 47 | /// together with all of their corresponding items. 48 | public let compareSections: (DataSourceSection, DataSourceSection) -> Bool 49 | 50 | /// Function that is used to compare a pair of items for equality. 51 | /// Returns `true` if the items are equal, and no dataChange is required 52 | /// to replace the first item with the second. 53 | public let compareItems: (T, T) -> Bool 54 | 55 | /// Creates an autoDiffSectionsDataSource. 56 | /// - parameters: 57 | /// - sections: Initial array of sections of the autoDiffDataSource. 58 | /// - findItemMoves: Set `findItemMoves` to `false` to make the dataSource emit 59 | /// a pair of deletion and insertion instead of item movement dataChanges. 60 | /// Section moves are never generated. 61 | /// - compareSections: Function that is used to compare a pair of sections for identity. 62 | /// - compareItems: Function that is used to compare a pair of items for equality. 63 | public init( 64 | sections: [DataSourceSection] = [], 65 | findItemMoves: Bool = true, 66 | compareSections: @escaping (DataSourceSection, DataSourceSection) -> Bool, 67 | compareItems: @escaping (T, T) -> Bool) 68 | { 69 | (self.changes, self.observer) = Signal.pipe() 70 | self.sections = MutableProperty(sections) 71 | self.compareSections = compareSections 72 | self.compareItems = compareItems 73 | func autoDiff(_ oldSections: [DataSourceSection], newSections: [DataSourceSection]) -> DataChange 74 | { 75 | let sectionsResult = AutoDiff.compare( 76 | old: oldSections, 77 | new: newSections, 78 | findMoves: false, 79 | compare: compareSections) 80 | var changes = sectionsResult.toSectionChanges() 81 | for (oldIndex, newIndex) in sectionsResult.matches { 82 | let oldItems = oldSections[oldIndex].items 83 | let newItems = newSections[newIndex].items 84 | let itemsResult = AutoDiff.compare( 85 | old: oldItems, 86 | new: newItems, 87 | findMoves: findItemMoves, 88 | compare: compareItems) 89 | changes += itemsResult.toItemChanges(oldSection: oldIndex, newSection: newIndex) 90 | } 91 | return DataChangeBatch(changes) 92 | } 93 | self.disposable = self.sections.producer 94 | .combinePrevious(sections) 95 | .skip(first: 1) 96 | .map(autoDiff) 97 | .start(self.observer) 98 | } 99 | 100 | deinit { 101 | self.observer.sendCompleted() 102 | self.disposable.dispose() 103 | } 104 | 105 | public var numberOfSections: Int { 106 | return self.sections.value.count 107 | } 108 | 109 | public func numberOfItemsInSection(_ section: Int) -> Int { 110 | return self.sections.value[section].items.count 111 | } 112 | 113 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 114 | return self.sections.value[section].supplementaryItems[kind] 115 | } 116 | 117 | public func item(at indexPath: IndexPath) -> Any { 118 | return self.sections.value[indexPath.section].items[indexPath.item] 119 | } 120 | 121 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 122 | return (self, indexPath) 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/CompositeDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositeDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that is composed of an array 13 | /// of other dataSources (called inner dataSources). 14 | /// 15 | /// Sections of inner dataSources become the sections of the compositeDataSource 16 | /// in the following order: first all the sections of the first inner dataSource, 17 | /// then all the sections of the second inner dataSource, and so on. 18 | /// 19 | /// CompositeDataSource listens to dataChanges in all of its inner dataSources 20 | /// and emits them as its own changes, after mapping section indices in them 21 | /// to correspond to the structure of the compositeDataSource. 22 | public final class CompositeDataSource: DataSource { 23 | 24 | public let changes: Signal 25 | private let observer: Signal.Observer 26 | private let disposable = CompositeDisposable() 27 | 28 | public let innerDataSources: [DataSource] 29 | 30 | public init(_ inner: [DataSource]) { 31 | (self.changes, self.observer) = Signal.pipe() 32 | self.innerDataSources = inner 33 | for (index, dataSource) in inner.enumerated() { 34 | self.disposable += dataSource.changes.observeValues { [weak self] change in 35 | if let self = self { 36 | let map = mapOutside(self.innerDataSources, index) 37 | let mapped = change.mapSections(map) 38 | self.observer.send(value: mapped) 39 | } 40 | } 41 | } 42 | } 43 | 44 | deinit { 45 | self.observer.sendCompleted() 46 | self.disposable.dispose() 47 | } 48 | 49 | public var numberOfSections: Int { 50 | return self.innerDataSources.reduce(0) { subtotal, dataSource in 51 | return subtotal + dataSource.numberOfSections 52 | } 53 | } 54 | 55 | public func numberOfItemsInSection(_ section: Int) -> Int { 56 | let (index, innerSection) = mapInside(self.innerDataSources, section) 57 | return self.innerDataSources[index].numberOfItemsInSection(innerSection) 58 | } 59 | 60 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 61 | let (index, innerSection) = mapInside(self.innerDataSources, section) 62 | return self.innerDataSources[index].supplementaryItemOfKind(kind, inSection: innerSection) 63 | } 64 | 65 | public func item(at indexPath: IndexPath) -> Any { 66 | let (index, innerSection) = mapInside(self.innerDataSources, (indexPath as NSIndexPath).section) 67 | let innerPath = indexPath.ds_setSection(innerSection) 68 | return self.innerDataSources[index].item(at: innerPath) 69 | } 70 | 71 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 72 | let (index, innerSection) = mapInside(self.innerDataSources, (indexPath as NSIndexPath).section) 73 | let innerPath = indexPath.ds_setSection(innerSection) 74 | return self.innerDataSources[index].leafDataSource(at: innerPath) 75 | } 76 | 77 | } 78 | 79 | func mapInside(_ inner: [DataSource], _ outerSection: Int) -> (Int, Int) { 80 | var innerSection = outerSection 81 | var index = 0 82 | while innerSection >= inner[index].numberOfSections { 83 | innerSection -= inner[index].numberOfSections 84 | index += 1 85 | } 86 | return (index, innerSection) 87 | } 88 | 89 | func mapOutside(_ inner: [DataSource], _ index: Int) -> (Int) -> Int { 90 | return { innerSection in 91 | var outerSection = innerSection 92 | for i in 0 ..< index { 93 | outerSection += inner[i].numberOfSections 94 | } 95 | return outerSection 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/DataSourceSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSourceSection.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DataSourceSection { 12 | 13 | public var items: [T] 14 | public var supplementaryItems: [String: Any] 15 | 16 | public init(items: [T], supplementaryItems: [String: Any] = [:]) { 17 | self.items = items 18 | self.supplementaryItems = supplementaryItems 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/EmptyDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has zero sections. 13 | /// 14 | /// Never emits any dataChanges. 15 | public final class EmptyDataSource: DataSource { 16 | 17 | public let changes: Signal 18 | private let observer: Signal.Observer 19 | 20 | public init() { 21 | (self.changes, self.observer) = Signal.pipe() 22 | } 23 | 24 | deinit { 25 | self.observer.sendCompleted() 26 | } 27 | 28 | public let numberOfSections = 0 29 | 30 | public func numberOfItemsInSection(_ section: Int) -> Int { 31 | fatalError("Trying to access EmptyDataSource.numberOfItemsInSection(_:)") 32 | } 33 | 34 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 35 | fatalError("Trying to access EmptyDataSource.supplementaryItemOfKind(_:inSection:)") 36 | } 37 | 38 | public func item(at indexPath: IndexPath) -> Any { 39 | fatalError("Trying to access EmptyDataSource.item(at:)") 40 | } 41 | 42 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 43 | fatalError("Trying to access EmptyDataSource.leafDataSource(at:)") 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/FetchedResultsDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchedResultsDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | import ReactiveSwift 12 | import UIKit 13 | 14 | /// `DataSource` implementation whose items are Core Data managed objects fetched by an `NSFetchedResultsController`. 15 | /// 16 | /// Returns names of fetched sections as supplementary items of `UICollectionElementKindSectionHeader` kind. 17 | /// 18 | /// Uses `NSFetchedResultsControllerDelegate` protocol internally to observe changes 19 | /// in fetched objects and emit them as its own dataChanges. 20 | public final class FetchedResultsDataSource: DataSource { 21 | 22 | public let changes: Signal 23 | private let observer: Signal.Observer 24 | 25 | private let frc: NSFetchedResultsController 26 | // swiftlint:disable weak_delegate 27 | private let frcDelegate: Delegate 28 | 29 | public init(fetchRequest: NSFetchRequest, managedObjectContext: NSManagedObjectContext, sectionNameKeyPath: String? = nil, cacheName: String? = nil) throws { 30 | (self.changes, self.observer) = Signal.pipe() 31 | self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName) 32 | self.frcDelegate = Delegate(observer: self.observer) 33 | self.frc.delegate = self.frcDelegate 34 | 35 | try self.frc.performFetch() 36 | } 37 | 38 | deinit { 39 | self.frc.delegate = nil 40 | self.observer.sendCompleted() 41 | } 42 | 43 | private func infoForSection(_ section: Int) -> NSFetchedResultsSectionInfo { 44 | return self.frc.sections![section] 45 | } 46 | 47 | public var numberOfSections: Int { 48 | return self.frc.sections?.count ?? 0 49 | } 50 | 51 | public func numberOfItemsInSection(_ section: Int) -> Int { 52 | let sectionInfo = self.infoForSection(section) 53 | return sectionInfo.numberOfObjects 54 | } 55 | 56 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 57 | if kind != UICollectionView.elementKindSectionHeader { 58 | return nil 59 | } 60 | let sectionInfo = self.infoForSection(section) 61 | return sectionInfo.name 62 | } 63 | 64 | public func item(at indexPath: IndexPath) -> Any { 65 | let sectionInfo = self.infoForSection((indexPath as NSIndexPath).section) 66 | return sectionInfo.objects![(indexPath as NSIndexPath).item] 67 | } 68 | 69 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 70 | return (self, indexPath) 71 | } 72 | 73 | @objc private final class Delegate: NSObject, NSFetchedResultsControllerDelegate { 74 | 75 | let observer: Signal.Observer 76 | var currentBatch: [DataChange] = [] 77 | 78 | init(observer: Signal.Observer) { 79 | self.observer = observer 80 | } 81 | 82 | @objc func controllerWillChangeContent(_ controller: NSFetchedResultsController) { 83 | self.currentBatch = [] 84 | } 85 | 86 | @objc func controllerDidChangeContent(_ controller: NSFetchedResultsController) { 87 | self.observer.send(value: DataChangeBatch(self.currentBatch)) 88 | self.currentBatch = [] 89 | } 90 | 91 | @objc func controller( 92 | _ controller: NSFetchedResultsController, 93 | didChange sectionInfo: NSFetchedResultsSectionInfo, 94 | atSectionIndex sectionIndex: Int, 95 | for type: NSFetchedResultsChangeType) 96 | { 97 | switch type { 98 | case .insert: 99 | self.currentBatch.append(DataChangeInsertSections([sectionIndex])) 100 | case .delete: 101 | self.currentBatch.append(DataChangeDeleteSections([sectionIndex])) 102 | default: 103 | break 104 | } 105 | } 106 | 107 | @objc func controller( 108 | _ controller: NSFetchedResultsController, 109 | didChange anObject: Any, 110 | at indexPath: IndexPath?, 111 | for type: NSFetchedResultsChangeType, 112 | newIndexPath: IndexPath?) 113 | { 114 | switch type { 115 | case .insert: 116 | self.currentBatch.append(DataChangeInsertItems(newIndexPath!)) 117 | case .delete: 118 | self.currentBatch.append(DataChangeDeleteItems(indexPath!)) 119 | case .move: 120 | self.currentBatch.append(DataChangeMoveItem(from: indexPath!, to: newIndexPath!)) 121 | case .update: 122 | self.currentBatch.append(DataChangeReloadItems(indexPath!)) 123 | @unknown default: 124 | NSLog("Unhandled case for NSFetchedResultsChangeType: \(type). DataSource should be updated to account for it or it could lead to unexpected results.") 125 | assertionFailure() 126 | } 127 | } 128 | 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/KVODataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KVODataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has a single section and 13 | /// uses key-value coding (KVC) to returns objects from an ordered 14 | /// to-many relation of a target object as its items. 15 | /// 16 | /// Uses key-value observing (KVO) internally to observe changes 17 | /// in the to-many relationship and emit them as its own dataChanges. 18 | public final class KVODataSource: NSObject, DataSource { 19 | 20 | public let changes: Signal 21 | private let observer: Signal.Observer 22 | 23 | public let target: NSObject 24 | public let keyPath: String 25 | public let supplementaryItems: [String: Any] 26 | 27 | public init(target: NSObject, keyPath: String, supplementaryItems: [String: Any] = [:]) { 28 | (self.changes, self.observer) = Signal.pipe() 29 | self.target = target 30 | self.keyPath = keyPath 31 | self.supplementaryItems = supplementaryItems 32 | super.init() 33 | self.target.addObserver(self, forKeyPath: self.keyPath, options: [], context: nil) 34 | } 35 | 36 | deinit { 37 | self.target.removeObserver(self, forKeyPath: self.keyPath, context: nil) 38 | self.observer.sendCompleted() 39 | } 40 | 41 | public let numberOfSections = 1 42 | 43 | public func numberOfItemsInSection(_ section: Int) -> Int { 44 | return self.items.count 45 | } 46 | 47 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 48 | return self.supplementaryItems[kind] 49 | } 50 | 51 | public func item(at indexPath: IndexPath) -> Any { 52 | return self.items[(indexPath as NSIndexPath).item] 53 | } 54 | 55 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 56 | return (self, indexPath) 57 | } 58 | 59 | private var items: NSArray { 60 | return self.target.value(forKeyPath: self.keyPath) as! NSArray 61 | } 62 | 63 | // swiftlint:disable block_based_kvo 64 | override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 65 | if let target = object as? NSObject, 66 | let change = change, 67 | let type = change[NSKeyValueChangeKey.kindKey] as? NSKeyValueChange, 68 | let indices = change[NSKeyValueChangeKey.indexesKey] as? IndexSet, 69 | keyPath == self.keyPath && target == self.target 70 | { 71 | self.observeChangeOfType(type, atIndices: indices) 72 | } 73 | } 74 | 75 | private func observeChangeOfType(_ type: NSKeyValueChange, atIndices indices: IndexSet) { 76 | var indexPaths: [IndexPath] = [] 77 | for index in indices { 78 | indexPaths.append(IndexPath(item: index, section: 0)) 79 | } 80 | switch type { 81 | case .insertion: 82 | self.observer.send(value: DataChangeInsertItems(indexPaths)) 83 | case .removal: 84 | self.observer.send(value: DataChangeDeleteItems(indexPaths)) 85 | case .replacement: 86 | self.observer.send(value: DataChangeReloadItems(indexPaths)) 87 | case .setting: 88 | self.observer.send(value: DataChangeReloadSections([0])) 89 | @unknown default: 90 | NSLog("Unhandled case for NSKeyValueChange: \(type). DataSource should be updated to account for it or it could lead to unexpected results.") 91 | assertionFailure() 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/MappedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappedDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that returns data from 13 | /// another dataSource (called inner dataSource) after transforming 14 | /// its items with `transform` function and transforming its 15 | /// supplementary items with `supplementaryTransform` function. 16 | /// 17 | /// MappedDataSource listens to dataChanges of its inner dataSource 18 | /// and emits them as its own changes. 19 | public final class MappedDataSource: DataSource { 20 | 21 | public let changes: Signal 22 | private let observer: Signal.Observer 23 | private let disposable: Disposable? 24 | 25 | public let innerDataSource: DataSource 26 | 27 | /// Function that is applied to items of the inner dataSource 28 | /// before they are returned as items of the mappedDataSource. 29 | private let transform: (Any) -> Any 30 | 31 | /// Function that is applied to supplementary items of the inner dataSource 32 | /// before they are returned as supplementary items of the mappedDataSource. 33 | /// 34 | /// The first parameter is the kind of the supplementary item. 35 | private let supplementaryTransform: (String, Any?) -> Any? 36 | 37 | public init(_ inner: DataSource, supplementaryTransform: @escaping ((String, Any?) -> Any?) = { $1 }, transform: @escaping (Any) -> Any) { 38 | (self.changes, self.observer) = Signal.pipe() 39 | self.innerDataSource = inner 40 | self.transform = transform 41 | self.supplementaryTransform = supplementaryTransform 42 | self.disposable = inner.changes.observe(self.observer) 43 | } 44 | 45 | deinit { 46 | self.observer.sendCompleted() 47 | self.disposable?.dispose() 48 | } 49 | 50 | public var numberOfSections: Int { 51 | let inner = self.innerDataSource 52 | return inner.numberOfSections 53 | } 54 | 55 | public func numberOfItemsInSection(_ section: Int) -> Int { 56 | let inner = self.innerDataSource 57 | return inner.numberOfItemsInSection(section) 58 | } 59 | 60 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 61 | let inner = self.innerDataSource 62 | let supplementaryItem = inner.supplementaryItemOfKind(kind, inSection: section) 63 | return self.supplementaryTransform(kind, supplementaryItem) 64 | } 65 | 66 | public func item(at indexPath: IndexPath) -> Any { 67 | let inner = self.innerDataSource 68 | let item = inner.item(at: indexPath) 69 | return self.transform(item) 70 | } 71 | 72 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 73 | let inner = self.innerDataSource 74 | return inner.leafDataSource(at: indexPath) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/MutableCompositeDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableCompositeDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 15/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that is composed of a mutable array 13 | /// of other dataSources (called inner dataSources). 14 | /// 15 | /// See `CompositeDataSource` for details. 16 | /// 17 | /// The array of innerDataSources can be modified by calling methods that perform 18 | /// individual changes and instantly make the dataSource emit 19 | /// a corresponding dataChange. 20 | public final class MutableCompositeDataSource: DataSource { 21 | 22 | public let changes: Signal 23 | private let observer: Signal.Observer 24 | private let disposable = CompositeDisposable() 25 | 26 | private let _innerDataSources: MutableProperty<[DataSource]> 27 | 28 | public var innerDataSources: Property<[DataSource]> { 29 | return Property(_innerDataSources) 30 | } 31 | 32 | public init(_ inner: [DataSource] = []) { 33 | (self.changes, self.observer) = Signal.pipe() 34 | self._innerDataSources = MutableProperty(inner) 35 | self.disposable += self._innerDataSources.producer 36 | .flatMap(.latest, changesOfInnerDataSources) 37 | .start(self.observer) 38 | } 39 | 40 | deinit { 41 | self.observer.sendCompleted() 42 | self.disposable.dispose() 43 | } 44 | 45 | public var numberOfSections: Int { 46 | return self._innerDataSources.value.reduce(0) { subtotal, dataSource in 47 | return subtotal + dataSource.numberOfSections 48 | } 49 | } 50 | 51 | public func numberOfItemsInSection(_ section: Int) -> Int { 52 | let (index, innerSection) = mapInside(self._innerDataSources.value, section) 53 | return self._innerDataSources.value[index].numberOfItemsInSection(innerSection) 54 | } 55 | 56 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 57 | let (index, innerSection) = mapInside(self._innerDataSources.value, section) 58 | return self._innerDataSources.value[index].supplementaryItemOfKind(kind, inSection: innerSection) 59 | } 60 | 61 | public func item(at indexPath: IndexPath) -> Any { 62 | let (index, innerSection) = mapInside(self._innerDataSources.value, indexPath.section) 63 | let innerPath = indexPath.ds_setSection(innerSection) 64 | return self._innerDataSources.value[index].item(at: innerPath) 65 | } 66 | 67 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 68 | let (index, innerSection) = mapInside(self._innerDataSources.value, indexPath.section) 69 | let innerPath = indexPath.ds_setSection(innerSection) 70 | return self._innerDataSources.value[index].leafDataSource(at: innerPath) 71 | } 72 | 73 | /// Inserts a given inner dataSource at a given index 74 | /// and emits `DataChangeInsertSections` for its sections. 75 | public func insert(_ dataSource: DataSource, at index: Int) { 76 | self.insert([dataSource], at: index) 77 | } 78 | 79 | /// Inserts an array of dataSources at a given index 80 | /// and emits `DataChangeInsertSections` for their sections. 81 | public func insert(_ dataSources: [DataSource], at index: Int) { 82 | self._innerDataSources.value.insert(contentsOf: dataSources, at: index) 83 | let sections = dataSources.enumerated().flatMap { self.sections(of: $1, at: index + $0) } 84 | if !sections.isEmpty { 85 | let change = DataChangeInsertSections(sections) 86 | self.observer.send(value: change) 87 | } 88 | } 89 | 90 | /// Deletes an inner dataSource at a given index 91 | /// and emits `DataChangeDeleteSections` for its sections. 92 | public func delete(at index: Int) { 93 | self.delete(in: Range(index...index)) 94 | } 95 | 96 | /// Deletes an inner dataSource in the given range 97 | /// and emits `DataChangeDeleteSections` for its corresponding sections. 98 | public func delete(in range: Range) { 99 | let sections = range.flatMap(self.sectionsOfDataSource) 100 | self._innerDataSources.value.removeSubrange(range) 101 | if !sections.isEmpty { 102 | let change = DataChangeDeleteSections(sections) 103 | self.observer.send(value: change) 104 | } 105 | } 106 | 107 | /// Replaces an inner dataSource at a given index with another inner dataSource 108 | /// and emits a batch of `DataChangeDeleteSections` and `DataChangeInsertSections` 109 | /// for their sections. 110 | public func replaceDataSource(at index: Int, with dataSource: DataSource) { 111 | var batch: [DataChange] = [] 112 | let oldSections = self.sectionsOfDataSource(at: index) 113 | if !oldSections.isEmpty { 114 | batch.append(DataChangeDeleteSections(oldSections)) 115 | } 116 | let newSections = self.sections(of: dataSource, at: index) 117 | if !newSections.isEmpty { 118 | batch.append(DataChangeInsertSections(newSections)) 119 | } 120 | self._innerDataSources.value[index] = dataSource 121 | if !batch.isEmpty { 122 | let change = DataChangeBatch(batch) 123 | self.observer.send(value: change) 124 | } 125 | } 126 | 127 | /// Moves an inner dataSource at a given index to another index 128 | /// and emits a batch of `DataChangeMoveSection` for its sections. 129 | public func moveData(at oldIndex: Int, to newIndex: Int) { 130 | let oldLocation = mapOutside(self._innerDataSources.value, oldIndex)(0) 131 | let dataSource = self._innerDataSources.value.remove(at: oldIndex) 132 | self._innerDataSources.value.insert(dataSource, at: newIndex) 133 | let newLocation = mapOutside(self._innerDataSources.value, newIndex)(0) 134 | let numberOfSections = dataSource.numberOfSections 135 | let batch: [DataChange] = (0 ..< numberOfSections).map { 136 | DataChangeMoveSection(from: oldLocation + $0, to: newLocation + $0) 137 | } 138 | if !batch.isEmpty { 139 | let change = DataChangeBatch(batch) 140 | self.observer.send(value: change) 141 | } 142 | } 143 | 144 | private func sections(of dataSource: DataSource, at index: Int) -> [Int] { 145 | let location = mapOutside(self._innerDataSources.value, index)(0) 146 | let length = dataSource.numberOfSections 147 | return Array(location ..< location + length) 148 | } 149 | 150 | private func sectionsOfDataSource(at index: Int) -> [Int] { 151 | let dataSource = self._innerDataSources.value[index] 152 | return self.sections(of: dataSource, at: index) 153 | } 154 | 155 | } 156 | 157 | private func changesOfInnerDataSources(_ innerDataSources: [DataSource]) -> SignalProducer { 158 | let arrayOfSignals = innerDataSources.enumerated().map { index, dataSource in 159 | return dataSource.changes.map { 160 | $0.mapSections(mapOutside(innerDataSources, index)) 161 | } 162 | } 163 | return SignalProducer { observer, disposable in 164 | for signal in arrayOfSignals { 165 | disposable += signal.observe(observer) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/MutableDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has one section of items of type T. 13 | /// 14 | /// The array of items can be modified by calling methods that perform 15 | /// individual changes and instantly make the dataSource emit 16 | /// a corresponding dataChange. 17 | public final class MutableDataSource: DataSource { 18 | 19 | public let changes: Signal 20 | private let observer: Signal.Observer 21 | 22 | private let _items: MutableProperty<[T]> 23 | 24 | public var items: Property<[T]> { 25 | return Property(_items) 26 | } 27 | 28 | public let supplementaryItems: [String: Any] 29 | 30 | public init(_ items: [T] = [], supplementaryItems: [String: Any] = [:]) { 31 | (self.changes, self.observer) = Signal.pipe() 32 | self._items = MutableProperty(items) 33 | self.supplementaryItems = supplementaryItems 34 | } 35 | 36 | deinit { 37 | self.observer.sendCompleted() 38 | } 39 | 40 | public let numberOfSections = 1 41 | 42 | public func numberOfItemsInSection(_ section: Int) -> Int { 43 | return self._items.value.count 44 | } 45 | 46 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 47 | return self.supplementaryItems[kind] 48 | } 49 | 50 | public func item(at indexPath: IndexPath) -> Any { 51 | return self._items.value[indexPath.item] 52 | } 53 | 54 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 55 | return (self, indexPath) 56 | } 57 | 58 | /// Inserts a given item at a given index 59 | /// and emits `DataChangeInsertItems`. 60 | public func insertItem(_ item: T, at index: Int) { 61 | self.insertItems([item], at: index) 62 | } 63 | 64 | /// Inserts items at a given index 65 | /// and emits `DataChangeInsertItems`. 66 | public func insertItems(_ items: [T], at index: Int) { 67 | self._items.value.insert(contentsOf: items, at: index) 68 | let change = DataChangeInsertItems(items.indices.map { z(index + $0) }) 69 | self.observer.send(value: change) 70 | } 71 | 72 | /// Deletes an item at a given index 73 | /// and emits `DataChangeDeleteItems`. 74 | public func deleteItem(at index: Int) { 75 | self.deleteItems(in: Range(index...index)) 76 | } 77 | 78 | /// Deletes items in a given range 79 | /// and emits `DataChangeDeleteItems`. 80 | public func deleteItems(in range: Range) { 81 | self._items.value.removeSubrange(range) 82 | let change = DataChangeDeleteItems(range.map(z)) 83 | self.observer.send(value: change) 84 | } 85 | 86 | /// Replaces an item at a given index with another item 87 | /// and emits `DataChangeReloadItems`. 88 | public func replaceItem(at index: Int, with item: T) { 89 | self._items.value[index] = item 90 | let change = DataChangeReloadItems(z(index)) 91 | self.observer.send(value: change) 92 | } 93 | 94 | /// Moves an item at a given index to another index 95 | /// and emits `DataChangeMoveItem`. 96 | public func moveItem(at oldIndex: Int, to newIndex: Int) { 97 | let item = self._items.value.remove(at: oldIndex) 98 | self._items.value.insert(item, at: newIndex) 99 | let change = DataChangeMoveItem(from: z(oldIndex), to: z(newIndex)) 100 | self.observer.send(value: change) 101 | } 102 | 103 | /// Replaces all items with a given array of items 104 | /// and emits `DataChangeReloadSections`. 105 | public func replaceItems(with items: [T]) { 106 | self._items.value = items 107 | let change = DataChangeReloadSections([0]) 108 | self.observer.send(value: change) 109 | } 110 | 111 | } 112 | 113 | private func z(_ index: Int) -> IndexPath { 114 | return IndexPath(item: index, section: 0) 115 | } 116 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/ProxyDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that returns data from 13 | /// another dataSource (called inner dataSource). 14 | /// 15 | /// The inner dataSource can be switched to a different 16 | /// dataSource instance. In this case the proxyDataSource 17 | /// emits a dataChange reloading its entire data. 18 | /// 19 | /// ProxyDataSource listens to dataChanges of its inner dataSource 20 | /// and emits them as its own changes. 21 | public final class ProxyDataSource: DataSource { 22 | 23 | public let changes: Signal 24 | private let observer: Signal.Observer 25 | private let disposable = CompositeDisposable() 26 | private var lastDisposable: Disposable? 27 | 28 | public let innerDataSource: MutableProperty 29 | 30 | /// When `true`, switching innerDataSource produces 31 | /// a dataChange consisting of deletions of all the 32 | /// sections of the old inner dataSource and insertion of all 33 | /// the sections of the new innerDataSource. 34 | /// 35 | /// when `false`, switching innerDataSource produces `DataChangeReloadData`. 36 | public let animatesChanges: MutableProperty 37 | 38 | public init(_ inner: DataSource = EmptyDataSource(), animateChanges: Bool = true) { 39 | (self.changes, self.observer) = Signal.pipe() 40 | self.innerDataSource = MutableProperty(inner) 41 | self.animatesChanges = MutableProperty(animateChanges) 42 | self.lastDisposable = inner.changes.observe(self.observer) 43 | self.disposable += self.innerDataSource.producer 44 | .combinePrevious(inner) 45 | .skip(first: 1) 46 | .startWithValues { [weak self] old, new in 47 | if let self = self { 48 | self.lastDisposable?.dispose() 49 | self.observer.send(value: changeDataSources(old, new, self.animatesChanges.value)) 50 | self.lastDisposable = new.changes.observe(self.observer) 51 | } 52 | } 53 | } 54 | 55 | deinit { 56 | self.observer.sendCompleted() 57 | self.disposable.dispose() 58 | self.lastDisposable?.dispose() 59 | } 60 | 61 | public var numberOfSections: Int { 62 | let inner = self.innerDataSource.value 63 | return inner.numberOfSections 64 | } 65 | 66 | public func numberOfItemsInSection(_ section: Int) -> Int { 67 | let inner = self.innerDataSource.value 68 | return inner.numberOfItemsInSection(section) 69 | } 70 | 71 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 72 | let inner = self.innerDataSource.value 73 | return inner.supplementaryItemOfKind(kind, inSection: section) 74 | } 75 | 76 | public func item(at indexPath: IndexPath) -> Any { 77 | let inner = self.innerDataSource.value 78 | return inner.item(at: indexPath) 79 | } 80 | 81 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 82 | let inner = self.innerDataSource.value 83 | return inner.leafDataSource(at: indexPath) 84 | } 85 | 86 | } 87 | 88 | private func changeDataSources(_ old: DataSource, _ new: DataSource, _ animateChanges: Bool) -> DataChange { 89 | if !animateChanges { 90 | return DataChangeReloadData() 91 | } 92 | var batch: [DataChange] = [] 93 | let oldSections = old.numberOfSections 94 | if oldSections > 0 { 95 | batch.append(DataChangeDeleteSections(Array(0 ..< oldSections))) 96 | } 97 | let newSections = new.numberOfSections 98 | if newSections > 0 { 99 | batch.append(DataChangeInsertSections(Array(0 ..< newSections))) 100 | } 101 | return DataChangeBatch(batch) 102 | } 103 | -------------------------------------------------------------------------------- /DataSource/DataSourceTypes/StaticDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | /// `DataSource` implementation that has an immutable array of section. 13 | /// 14 | /// Each section contains an array of items ot type T and 15 | /// (optionally) a dictionary of supplementary items. 16 | /// 17 | /// Never emits any dataChanges. 18 | public final class StaticDataSource: DataSource { 19 | 20 | public let changes: Signal 21 | private let observer: Signal.Observer 22 | 23 | public let sections: [DataSourceSection] 24 | 25 | public init(sections: [DataSourceSection]) { 26 | (self.changes, self.observer) = Signal.pipe() 27 | self.sections = sections 28 | } 29 | 30 | deinit { 31 | self.observer.sendCompleted() 32 | } 33 | 34 | /// Convenience initialize that creates a staticDataSource 35 | /// with a single section and no supplementary items. 36 | /// 37 | /// Initialize with sections if you need multiple sections 38 | /// or any supplementary items. 39 | public convenience init(items: [T]) { 40 | self.init(sections: [DataSourceSection(items: items)]) 41 | } 42 | 43 | public var numberOfSections: Int { 44 | return self.sections.count 45 | } 46 | 47 | public func numberOfItemsInSection(_ section: Int) -> Int { 48 | return self.sections[section].items.count 49 | } 50 | 51 | public func supplementaryItemOfKind(_ kind: String, inSection section: Int) -> Any? { 52 | return self.sections[section].supplementaryItems[kind] 53 | } 54 | 55 | public func item(at indexPath: IndexPath) -> Any { 56 | return self.sections[(indexPath as NSIndexPath).section].items[(indexPath as NSIndexPath).item] 57 | } 58 | 59 | public func leafDataSource(at indexPath: IndexPath) -> (DataSource, IndexPath) { 60 | return (self, indexPath) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /DataSource/IndexPathExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPathExtensions.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension IndexPath { 12 | 13 | func ds_setSection(_ section: Int) -> IndexPath { 14 | return IndexPath(item: self.item, section: section) 15 | } 16 | 17 | func ds_mapSection(_ transform: (Int) -> Int) -> IndexPath { 18 | return IndexPath(item: self.item, section: transform(self.section)) 19 | } 20 | 21 | } 22 | 23 | extension IndexSet { 24 | 25 | public init(dsIntegers: S) where S.Iterator.Element == Int { 26 | var res = IndexSet() 27 | for i in dsIntegers { 28 | res.insert(i) 29 | } 30 | self.init(res) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /DataSource/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 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Fueled. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | import UIKit 11 | 12 | /// `UITableViewCell` subclass that implements `DataSourceItemReceiver` protocol 13 | /// by putting received dataSource items into a `MutableProperty` called `cellModel`. 14 | /// - note: 15 | /// You are not required to subclass `TableViewCell` class in order 16 | /// to use your cell subclass with `TableViewDataSource`. 17 | /// Instead you can implement `DataSourceItemReceiver` 18 | /// protocol directly in any `UITableViewCell` subclass. 19 | open class TableViewCell: UITableViewCell, DataSourceItemReceiver { 20 | 21 | public final let cellModel = MutableProperty(nil) 22 | 23 | open func ds_setItem(_ item: Any) { 24 | self.cellModel.value = item 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewChangeTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewChangeTarget.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 09/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UITableView: DataChangeTarget { 13 | 14 | public func ds_performBatchChanges(_ batchChanges: @escaping () -> Void) { 15 | self.beginUpdates() 16 | batchChanges() 17 | self.endUpdates() 18 | } 19 | 20 | public func ds_deleteItems(at indexPaths: [IndexPath]) { 21 | self.deleteRows(at: indexPaths, with: .fade) 22 | } 23 | 24 | public func ds_deleteSections(_ sections: [Int]) { 25 | self.deleteSections(IndexSet(dsIntegers: sections), with: .fade) 26 | } 27 | 28 | public func ds_insertItems(at indexPaths: [IndexPath]) { 29 | self.insertRows(at: indexPaths, with: .fade) 30 | } 31 | 32 | public func ds_insertSections(_ sections: [Int]) { 33 | self.insertSections(IndexSet(dsIntegers: sections), with: .fade) 34 | } 35 | 36 | public func ds_moveItem(at oldIndexPath: IndexPath, to newIndexPath: IndexPath) { 37 | self.moveRow(at: oldIndexPath, to: newIndexPath) 38 | } 39 | 40 | public func ds_moveSection(_ oldSection: Int, toSection newSection: Int) { 41 | self.moveSection(oldSection, toSection: newSection) 42 | } 43 | 44 | public func ds_reloadData() { 45 | self.reloadData() 46 | } 47 | 48 | public func ds_reloadItems(at indexPaths: [IndexPath]) { 49 | self.reloadRows(at: indexPaths, with: .fade) 50 | } 51 | 52 | public func ds_reloadSections(_ sections: [Int]) { 53 | self.reloadSections(IndexSet(dsIntegers: sections), with: .fade) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSource.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 04/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | import UIKit 12 | 13 | /// An object that implements `UITableViewDataSource` protocol 14 | /// by returning the data from an associated dataSource. 15 | /// 16 | /// The number of section and numbers of rows in sections 17 | /// are taken directly from the dataSource. 18 | /// 19 | /// The cells are dequeued from a tableView 20 | /// by reuseIdentifiers returned by `reuseIdentifierForItem` function. 21 | /// 22 | /// If a cell implements the `DataSourceItemReceiver` protocol 23 | /// (e.g. by subclassing the `TableViewCell` class), 24 | /// the item at the indexPath is passed to it via `ds_setItem` method. 25 | /// 26 | /// A tableViewDataSource observes changes of the associated dataSource 27 | /// and applies those changes to the associated tableView. 28 | open class TableViewDataSource: NSObject, UITableViewDataSource { 29 | // swiftlint:disable private_outlet 30 | @IBOutlet public final var tableView: UITableView? 31 | 32 | public final let dataSource = ProxyDataSource() 33 | 34 | public final var reuseIdentifierForItem: (IndexPath, Any) -> String = { 35 | _, _ in "DefaultCell" 36 | } 37 | 38 | public final var dataChangeTarget: DataChangeTarget? 39 | 40 | private let disposable = CompositeDisposable() 41 | 42 | override public init() { 43 | super.init() 44 | self.disposable += self.dataSource.changes.observeValues { [weak self] change in 45 | if let self = self, let dataChangeTarget = self.dataChangeTarget ?? self.tableView { 46 | change.apply(to: dataChangeTarget) 47 | } 48 | } 49 | } 50 | 51 | deinit { 52 | self.disposable.dispose() 53 | } 54 | 55 | open func configureCell(_ cell: UITableViewCell, forRowAt indexPath: IndexPath) { 56 | let item = self.dataSource.item(at: indexPath) 57 | configureReceiver(cell, withItem: item) 58 | } 59 | 60 | open func configureCellForRow(at indexPath: IndexPath) { 61 | if let cell = self.tableView?.cellForRow(at: indexPath) { 62 | self.configureCell(cell, forRowAt: indexPath) 63 | } 64 | } 65 | 66 | open func configureVisibleCells() { 67 | if let indexPaths = self.tableView?.indexPathsForVisibleRows { 68 | for indexPath in indexPaths { 69 | self.configureCellForRow(at: indexPath) 70 | } 71 | } 72 | } 73 | 74 | open func numberOfSections(in tableView: UITableView) -> Int { 75 | return self.dataSource.numberOfSections 76 | } 77 | 78 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 79 | return self.dataSource.numberOfItemsInSection(section) 80 | } 81 | 82 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 83 | let item: Any = self.dataSource.item(at: indexPath) 84 | let reuseIdentifier = self.reuseIdentifierForItem(indexPath, item) 85 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) 86 | self.configureCell(cell, forRowAt: indexPath) 87 | return cell 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewDataSourceWithHeaderFooterTitles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceWithHeaderFooterTitles.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A `TableViewDataSource` subclass that additionally provides 12 | /// titles for headers and footers of tableView sections. 13 | /// 14 | /// DataSource supplementary items of `UICollectionElementKindSectionHeader` kind 15 | /// are used as section header titles, provided they are of `String` type. 16 | /// 17 | /// DataSource supplementary items of `UICollectionElementKindSectionFooter` kind 18 | /// are used as section footer titles, provided they are of `String` type. 19 | open class TableViewDataSourceWithHeaderFooterTitles: TableViewDataSource { 20 | 21 | open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 22 | let item = self.dataSource.supplementaryItemOfKind(UICollectionView.elementKindSectionHeader, inSection: section) 23 | return item as? String 24 | } 25 | 26 | open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 27 | let item = self.dataSource.supplementaryItemOfKind(UICollectionView.elementKindSectionFooter, inSection: section) 28 | return item as? String 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewDataSourceWithHeaderFooterViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceWithHeaderFooterViews.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A `TableViewDataSource` subclass that additionally provides 12 | /// views for headers and footers of tableView sections. 13 | /// 14 | /// `TableViewDataSourceWithHeaderFooterViews` needs to be the tableView's delegate. 15 | /// 16 | /// Header and footer views are dequeued from a tableView 17 | /// by reuseIdentifiers returned by `reuseIdentifierForHeaderItem` 18 | /// and `reuseIdentifierForFooterItem` function. 19 | /// 20 | /// DataSource supplementary items of `UICollectionElementKindSectionHeader` kind 21 | /// are used as section header titles. 22 | /// 23 | /// DataSource supplementary items of `UICollectionElementKindSectionFooter` kind 24 | /// are used as section footer titles. 25 | open class TableViewDataSourceWithHeaderFooterViews: TableViewDataSource, UITableViewDelegate { 26 | 27 | public final var reuseIdentifierForHeaderItem: (Int, Any) -> String = { 28 | _, _ in "DefaultHeaderView" 29 | } 30 | public final var reuseIdentifierForFooterItem: (Int, Any) -> String = { 31 | _, _ in "DefaultFooterView" 32 | } 33 | 34 | open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 35 | guard let item = self.dataSource.supplementaryItemOfKind(UICollectionView.elementKindSectionHeader, inSection: section) else { 36 | return nil 37 | } 38 | let reuseIdentifier = self.reuseIdentifierForHeaderItem(section, item) 39 | let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier)! 40 | configureReceiver(view, withItem: item) 41 | return view 42 | } 43 | 44 | open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 45 | guard let item = self.dataSource.supplementaryItemOfKind(UICollectionView.elementKindSectionFooter, inSection: section) else { 46 | return nil 47 | } 48 | let reuseIdentifier = self.reuseIdentifierForFooterItem(section, item) 49 | let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier)! 50 | configureReceiver(view, withItem: item) 51 | return view 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /DataSource/TableView/TableViewHeaderFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewHeaderFooterView.swift 3 | // DataSource 4 | // 5 | // Created by Vadim Yelagin on 14/06/15. 6 | // Copyright (c) 2015 Fueled. All rights reserved. 7 | // 8 | 9 | import ReactiveSwift 10 | import UIKit 11 | 12 | /// `UITableViewHeaderFooterView` subclass that implements `DataSourceItemReceiver` protocol 13 | /// by putting received dataSource items into a `MutableProperty` called `viewModel`. 14 | /// - note: 15 | /// You are not required to subclass `TableViewHeaderFooterView` class in order 16 | /// to use your cell subclass with `TableViewDataSourceWithHeaderFooterViews`. 17 | /// Instead you can implement `DataSourceItemReceiver` 18 | /// protocol directly in any `UITableViewHeaderFooterView` subclass. 19 | open class TableViewHeaderFooterView: UITableViewHeaderFooterView, DataSourceItemReceiver { 20 | 21 | public final let viewModel = MutableProperty(nil) 22 | 23 | open func ds_setItem(_ item: Any) { 24 | self.viewModel.value = item 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /DataSourceTests/AutoDiffDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiffDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class AutoDiffDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var dataSource: AutoDiffDataSource! 17 | beforeEach { 18 | dataSource = AutoDiffDataSource(self.testDataSet, supplementaryItems: self.supplementaryItemOfKind2, findMoves: true, compare: { $0 == $1 }) 19 | } 20 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet], "SupplementaryItems": [self.supplementaryItemOfKind2]] } 21 | context("when changing dataSource items") { 22 | beforeEach { 23 | dataSource.items.value = self.testDataSet3 24 | } 25 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet3], "SupplementaryItems": [self.supplementaryItemOfKind2]] } 26 | it("should generate corresponding dataChanges") { 27 | let lastChange = MutableProperty(nil) 28 | lastChange <~ dataSource.changes 29 | expect(lastChange.value).to(beNil()) 30 | dataSource.items.value = Array(51...55) 31 | expect(lastChange.value).notTo(beNil()) 32 | expect(lastChange.value).to(beAKindOf(DataChangeBatch.self)) 33 | var batches = (lastChange.value as! DataChangeBatch).changes 34 | expect(batches.count) == 1 35 | expect(batches.first).to(beAKindOf(DataChangeDeleteItems.self)) 36 | dataSource.items.value = [52, 51, 53, 54, 55] 37 | expect(lastChange.value).to(beAKindOf(DataChangeBatch.self)) 38 | batches = (lastChange.value as! DataChangeBatch).changes 39 | expect(batches.count) == 1 40 | expect(batches.first).to(beAKindOf(DataChangeMoveItem.self)) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DataSourceTests/AutoDiffSectionsDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiffSectionsDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class AutoDiffSectionsDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var dataSource: AutoDiffSectionsDataSource! 17 | var dataSourceSection1: DataSourceSection! 18 | var dataSourceSection2: DataSourceSection! 19 | var dataSourceSections: [DataSourceSection]! 20 | beforeEach { 21 | dataSourceSection1 = DataSourceSection(items: self.testDataSet, supplementaryItems: ["sectionId": "1"]) 22 | dataSourceSection2 = DataSourceSection(items: self.testDataSet2, supplementaryItems: ["sectionId": "2"]) 23 | dataSourceSections = [dataSourceSection1, dataSourceSection2] 24 | dataSource = AutoDiffSectionsDataSource( 25 | sections: dataSourceSections, 26 | findItemMoves: true, 27 | compareSections: 28 | { 29 | let header0 = $0.supplementaryItems["sectionId"] as! String 30 | let header1 = $1.supplementaryItems["sectionId"] as! String 31 | return header0 == header1 32 | }, 33 | compareItems: { $0 == $1 }) 34 | } 35 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet, self.testDataSet2]] } 36 | context("when changing dataSource sections") { 37 | beforeEach { 38 | dataSourceSections = [dataSourceSection2, dataSourceSection1] 39 | dataSource.sections.value = dataSourceSections 40 | } 41 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet2, self.testDataSet]] } 42 | it("should generate corresponding dataChanges") { 43 | let lastChange = MutableProperty(nil) 44 | lastChange <~ dataSource.changes 45 | expect(lastChange.value).to(beNil()) 46 | dataSource.sections.value.append(DataSourceSection(items: self.testDataSet3, supplementaryItems: ["sectionId": "3"])) 47 | expect(lastChange.value).notTo(beNil()) 48 | expect(lastChange.value).to(beAKindOf(DataChangeBatch.self)) 49 | var batches = (lastChange.value as! DataChangeBatch).changes 50 | expect(batches.count) == 1 51 | expect(batches.first).to(beAKindOf(DataChangeInsertSections.self)) 52 | dataSource.sections.value[2] = DataSourceSection(items: self.testDataSet3 + [56], supplementaryItems: ["sectionId": "3"]) 53 | expect(lastChange.value).to(beAKindOf(DataChangeBatch.self)) 54 | batches = (lastChange.value as! DataChangeBatch).changes 55 | expect(batches.count) == 1 56 | expect(batches.first).to(beAKindOf(DataChangeInsertItems.self)) 57 | expect((batches.first as! DataChangeInsertItems).indexPaths.first).to(be(IndexPath(row: 6, section: 2))) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DataSourceTests/CollectionViewDataSourceSharedConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewDataSourceSharedConfiguration.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class CollectionViewDataSourceSharedConfiguration: QuickConfiguration { 14 | override class func configure(_ configuration: Configuration) { 15 | sharedExamples("CollectionViewDataSource object") { (sharedExampleContext: @escaping SharedExampleContext) in 16 | describe("CollectionView tests") { 17 | var collectionViewDataSource: CollectionViewDataSource! 18 | var initialData: [[TestCellModel]]! 19 | var collectionView: UICollectionView! 20 | beforeEach { 21 | collectionViewDataSource = sharedExampleContext()["collectionViewDataSource"] as? CollectionViewDataSource 22 | initialData = sharedExampleContext()["TestCellModels"] as? [[TestCellModel]] 23 | collectionView = sharedExampleContext()["collectionView"] as? UICollectionView 24 | } 25 | it("configure visible cells") { 26 | collectionViewDataSource.configureVisibleCells() 27 | } 28 | it("has correct number of section") { 29 | expect(collectionViewDataSource.numberOfSections(in: collectionView)) == initialData.count 30 | } 31 | it("has proper count of items") { 32 | for index in initialData.indices { 33 | expect(collectionViewDataSource.collectionView(collectionView, numberOfItemsInSection: index)) == initialData[index].count 34 | } 35 | } 36 | it("cells has correct type") { 37 | for (sectionIndex, element) in initialData.enumerated() { 38 | for itemIndex in element.indices { 39 | expect(collectionViewDataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: itemIndex, section: sectionIndex))).notTo(beNil()) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DataSourceTests/CollectionViewDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class CollectionViewDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var collectionViewDataSource: CollectionViewDataSource! 17 | var collectionView: UICollectionView! 18 | beforeEach { 19 | let dataSource = Property(value: StaticDataSource(items: self.dataSetWithTestCellModels)) 20 | collectionViewDataSource = CollectionViewDataSource() 21 | collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout()) 22 | let collectionViewDescriptors = [CellDescriptor(TestCollectionViewCell.reuseIdentifier, TestCellModel.self, .class(TestCollectionViewCell.self))] 23 | collectionViewDataSource.configure(collectionView, using: collectionViewDescriptors) 24 | collectionViewDataSource.dataSource.innerDataSource <~ dataSource.producer.map { $0 as DataSource } 25 | } 26 | itBehavesLike("CollectionViewDataSource object") { ["collectionViewDataSource": collectionViewDataSource!, "TestCellModels": [self.dataSetWithTestCellModels], "collectionView": collectionView!] } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DataSourceTests/CompositeDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositeDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class CompositeDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var dataSource: CompositeDataSource! 17 | var staticDataSources: [StaticDataSource]! 18 | beforeEach { 19 | let firstStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind)]) 20 | let secondStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet2)]) 21 | staticDataSources = [firstStaticDataSource, secondStaticDataSource] 22 | dataSource = CompositeDataSource(staticDataSources) 23 | } 24 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet, self.testDataSet2], "LeafDataSource": staticDataSources!, "SupplementaryItems": [self.supplementaryItemOfKind]] } 25 | it("should generate corresponding dataChanges") { 26 | let firstDataSource = StaticDataSource(items: [1, 2, 3]) 27 | let secondDataSource = ProxyDataSource(animateChanges: false) 28 | dataSource = CompositeDataSource([firstDataSource, secondDataSource]) 29 | let lastChange = MutableProperty(nil) 30 | lastChange <~ dataSource.changes 31 | expect(lastChange.value).to(beNil()) 32 | secondDataSource.innerDataSource.value = StaticDataSource(items: [4, 5, 6]) 33 | expect(lastChange.value).notTo(beNil()) 34 | expect(lastChange.value).to(beAKindOf(DataChangeReloadData.self)) 35 | secondDataSource.animatesChanges.value = true 36 | secondDataSource.innerDataSource.value = StaticDataSource(items: [4, 5, 6]) 37 | expect(lastChange.value).to(beAKindOf(DataChangeBatch.self)) 38 | let batches = (lastChange.value as! DataChangeBatch).changes 39 | expect(batches.count) == 2 40 | expect(batches.first).to(beAKindOf(DataChangeDeleteSections.self)) 41 | expect(batches.last).to(beAKindOf(DataChangeInsertSections.self)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DataSourceTests/CoreDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataManager.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 12/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | final class CoreDataManager { 12 | 13 | private let persistentContainer: NSPersistentContainer 14 | 15 | var context: NSManagedObjectContext { 16 | return self.persistentContainer.viewContext 17 | } 18 | 19 | init() { 20 | let managedObjectModel = NSManagedObjectModel.mergedModel(from: Bundle.allBundles)! 21 | self.persistentContainer = NSPersistentContainer(name: "Items", managedObjectModel: managedObjectModel) 22 | let description = NSPersistentStoreDescription() 23 | description.type = NSInMemoryStoreType 24 | self.persistentContainer.persistentStoreDescriptions = [description] 25 | self.persistentContainer.loadPersistentStores(completionHandler: { _, error in 26 | if let error = error as NSError? { 27 | fatalError("An error occurred while loading persistent store \(error), \(error.userInfo)") 28 | } 29 | }) 30 | } 31 | 32 | func fillItemsWithDataSet(dataSet: [Int]) { 33 | dataSet.forEach { 34 | let newItem = NSEntityDescription.insertNewObject(forEntityName: "Items", into: context) as! Items 35 | newItem.id = String($0) 36 | } 37 | self.saveContext() 38 | } 39 | 40 | private func saveContext () { 41 | do { 42 | try self.context.save() 43 | } catch { 44 | if let error = error as NSError? { 45 | fatalError("An error occurred while saving data to persistent store \(error), \(error.userInfo)") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DataSourceTests/DataSourceSharedConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSource.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import DataSource 11 | import Nimble 12 | import Quick 13 | 14 | class DataSourceSharedConfiguration: QuickConfiguration { 15 | override class func configure(_ configuration: Configuration) { 16 | sharedExamples("DataSource protocol") { (sharedExampleContext: @escaping SharedExampleContext) in 17 | describe("Datasource protocol") { 18 | var dataSource: DataSource! 19 | var initialData: [[Int]]! 20 | var leafDataSource: [DataSource]! 21 | var supplementrayItems: [[String: Int]]! 22 | beforeEach { 23 | //Avoiding swift bug https://bugs.swift.org/browse/SR-3871 24 | dataSource = (sharedExampleContext()["DataSource"] as AnyObject as? DataSource) 25 | initialData = sharedExampleContext()["InitialData"] as? [[Int]] 26 | leafDataSource = sharedExampleContext()["LeafDataSource"] as? [DataSource] ?? [dataSource] 27 | supplementrayItems = sharedExampleContext()["SupplementaryItems"] as? [[String: Int]] ?? [[:]] 28 | } 29 | it("has correct number of items in sections") { 30 | for index in initialData.indices { 31 | expect(dataSource.numberOfItemsInSection(index)) == initialData[index].count 32 | } 33 | } 34 | it("has correct number of section") { 35 | expect(dataSource.numberOfSections) == initialData.count 36 | } 37 | it("all items are the same as in input data") { 38 | for (sectionIndex, element) in initialData.enumerated() { 39 | for (itemIndex, element) in element.enumerated() { 40 | let dataSourceItem: Int 41 | if let itemAsManagedObject = (dataSource.item(at: IndexPath(item: itemIndex, section: sectionIndex)) as? NSManagedObject) { 42 | dataSourceItem = Int((itemAsManagedObject.value(forKey: "id") as! String))! 43 | } else if let itemAsAnyObject = (dataSource.item(at: IndexPath(item: itemIndex, section: sectionIndex)) as? Int) { 44 | dataSourceItem = itemAsAnyObject 45 | } else { 46 | fatalError("DataSource item should be of expected type") 47 | } 48 | expect(dataSourceItem) == element 49 | } 50 | } 51 | } 52 | it("leafDataSource equal to proper dataSource") { 53 | for (index, element) in leafDataSource.enumerated() { 54 | expect(dataSource.leafDataSource(at: IndexPath(item: 0, section: index)).0) === element 55 | } 56 | } 57 | it("has correct supplementary items") { 58 | for (sectionIndex, sectionSupplementaryItems) in supplementrayItems.enumerated() { 59 | sectionSupplementaryItems.forEach { 60 | expect(dataSource.supplementaryItemOfKind($0.key, inSection: sectionIndex) as? Int) == $0.value 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DataSourceTests/EmptyDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyDataSourceTestsNew.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class EmptyDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | var dataSource: EmptyDataSource! 16 | beforeEach { 17 | dataSource = EmptyDataSource() 18 | } 19 | it("has 0 sections") { 20 | expect(dataSource.numberOfSections) == 0 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DataSourceTests/FetchedResultsDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchedResultsDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 11/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class FetchedResultsDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | beforeSuite { 16 | self.initCoreDataManagerWithTestData(testData: self.testDataSet) 17 | } 18 | var dataSource: FetchedResultsDataSource! 19 | beforeEach { 20 | dataSource = try? FetchedResultsDataSource(fetchRequest: Items().fetchSortedRequest(), managedObjectContext: self.coreDataManager!.context) 21 | } 22 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet]] } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DataSourceTests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /DataSourceTests/Items.xcdatamodeld/Items.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DataSourceTests/ItemsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemsExtensions.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 13/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | extension Items { 12 | 13 | func fetchSortedRequest() -> NSFetchRequest { 14 | let itemsFetchRequest: NSFetchRequest = Items.fetchRequest() 15 | itemsFetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)] 16 | return itemsFetchRequest 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DataSourceTests/MappedDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappedDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class MappedDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | var dataSource: MappedDataSource! 16 | var staticDataSource: StaticDataSource! 17 | beforeEach { 18 | let dataSourceSection = [DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind)] 19 | staticDataSource = StaticDataSource(sections: dataSourceSection) 20 | dataSource = MappedDataSource(staticDataSource, supplementaryTransform: { ($1 as! Int) * 3 }, transform: { ($0 as! Int) * 2 }) 21 | } 22 | itBehavesLike("DataSource protocol") { 23 | ["DataSource": dataSource!, 24 | "InitialData": [self.testDataSet.map { $0 * 2 }], 25 | "LeafDataSource": [staticDataSource], 26 | "SupplementaryItems": [self.supplementaryItemOfKind.map { $1 * 3 }], ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DataSourceTests/MutableCompositeDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableCompositeDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 06/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class MutableCompositeDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | var dataSource: MutableCompositeDataSource! 16 | var staticDataSources: [StaticDataSource]! 17 | var firstStaticDataSource: StaticDataSource! 18 | var secondStaticDataSource: StaticDataSource! 19 | beforeEach { 20 | firstStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind)]) 21 | secondStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet2, supplementaryItems: self.supplementaryItemOfKind2)]) 22 | staticDataSources = [firstStaticDataSource, secondStaticDataSource] 23 | dataSource = MutableCompositeDataSource(staticDataSources) 24 | } 25 | itBehavesLike("DataSource protocol") { 26 | ["DataSource": dataSource!, 27 | "InitialData": [self.testDataSet, self.testDataSet2], 28 | "LeafDataSource": staticDataSources!, 29 | "SupplementaryItems": [self.supplementaryItemOfKind, self.supplementaryItemOfKind2], ] 30 | } 31 | context("when adding new dataSource") { 32 | beforeEach { 33 | let thirdStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet3, supplementaryItems: self.supplementaryItemOfKind2)]) 34 | staticDataSources = [thirdStaticDataSource, firstStaticDataSource, secondStaticDataSource] 35 | dataSource.insert(thirdStaticDataSource, at: staticDataSources.startIndex) 36 | } 37 | itBehavesLike("DataSource protocol") { 38 | ["DataSource": dataSource!, 39 | "InitialData": [self.testDataSet3, self.testDataSet, self.testDataSet2], 40 | "LeafDataSource": staticDataSources!, 41 | "SupplementaryItems": [self.supplementaryItemOfKind2, self.supplementaryItemOfKind, self.supplementaryItemOfKind2], ] 42 | } 43 | } 44 | context("when deleting a dataSource") { 45 | beforeEach { 46 | staticDataSources = [secondStaticDataSource] 47 | dataSource.delete(at: staticDataSources.startIndex) 48 | } 49 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet2], "LeafDataSource": staticDataSources!, "SupplementaryItems": [self.supplementaryItemOfKind2]] } 50 | } 51 | context("when replacing a dataSource") { 52 | beforeEach { 53 | let thirdStaticDataSource = StaticDataSource(sections: [DataSourceSection(items: self.testDataSet3)]) 54 | staticDataSources = [thirdStaticDataSource, secondStaticDataSource] 55 | dataSource.replaceDataSource(at: staticDataSources.startIndex, with: thirdStaticDataSource) 56 | } 57 | itBehavesLike("DataSource protocol") { 58 | ["DataSource": dataSource!, 59 | "InitialData": [self.testDataSet3, self.testDataSet2], 60 | "LeafDataSource": staticDataSources!, 61 | "SupplementaryItems": [[:], self.supplementaryItemOfKind2], ] 62 | } 63 | } 64 | context("when moving a dataSource") { 65 | beforeEach { 66 | staticDataSources = [secondStaticDataSource, firstStaticDataSource] 67 | dataSource.moveData(at: staticDataSources.startIndex, to: staticDataSources.index(after: staticDataSources.startIndex)) 68 | } 69 | itBehavesLike("DataSource protocol") { 70 | ["DataSource": dataSource!, 71 | "InitialData": [self.testDataSet2, self.testDataSet], 72 | "LeafDataSource": staticDataSources!, 73 | "SupplementaryItems": [self.supplementaryItemOfKind2, self.supplementaryItemOfKind], ] 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /DataSourceTests/MutableDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MutableDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class MutableDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | var dataSource: MutableDataSource! 16 | beforeEach { 17 | dataSource = MutableDataSource(self.testDataSet, supplementaryItems: self.supplementaryItemOfKind) 18 | } 19 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet], "SupplementaryItems": [self.supplementaryItemOfKind]] } 20 | context("when adding new value") { 21 | beforeEach { 22 | dataSource.insertItem(self.newElement, at: self.testDataSet.startIndex) 23 | } 24 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSetWithNewElement], "SupplementaryItems": [self.supplementaryItemOfKind]] } 25 | } 26 | context("when deleting a item") { 27 | beforeEach { 28 | dataSource.deleteItem(at: self.testDataSet.startIndex) 29 | } 30 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSetWithoutFirstElement], "SupplementaryItems": [self.supplementaryItemOfKind]] } 31 | } 32 | context("when replacing a item") { 33 | beforeEach { 34 | dataSource.replaceItem(at: self.testDataSet.startIndex, with: self.newElement) 35 | } 36 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSetWithReplacedFirstElement], "SupplementaryItems": [self.supplementaryItemOfKind]] } 37 | } 38 | context("when replacing items") { 39 | beforeEach { 40 | dataSource.replaceItems(with: self.testDataSet2) 41 | } 42 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet2], "SupplementaryItems": [self.supplementaryItemOfKind]] } 43 | } 44 | context("when moving items") { 45 | beforeEach { 46 | dataSource.moveItem(at: self.testDataSet.startIndex, to: self.testDataSet.index(after: self.testDataSet.startIndex)) 47 | } 48 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSetWithReplacedFirstWithSecondElement], "SupplementaryItems": [self.supplementaryItemOfKind]] } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DataSourceTests/ProxyDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class ProxyDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var dataSource: ProxyDataSource! 17 | var staticDataSource: StaticDataSource! 18 | beforeEach { 19 | let dataSourceSection = DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind2) 20 | staticDataSource = StaticDataSource(sections: [dataSourceSection]) 21 | dataSource = ProxyDataSource(staticDataSource) 22 | dataSource.animatesChanges.value = true 23 | } 24 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet], "LeafDataSource": [staticDataSource], "SupplementaryItems": [self.supplementaryItemOfKind2]] } 25 | context("when change inner data source value") { 26 | beforeEach { 27 | let dataSourceSection = DataSourceSection(items: self.testDataSet2, supplementaryItems: self.supplementaryItemOfKind) 28 | let dataSourceSection2 = DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind2) 29 | let dataSourceSections = [dataSourceSection, dataSourceSection2] 30 | staticDataSource = StaticDataSource(sections: dataSourceSections) 31 | dataSource.animatesChanges.value = false 32 | dataSource.innerDataSource.value = staticDataSource 33 | } 34 | itBehavesLike("DataSource protocol") { 35 | ["DataSource": dataSource!, 36 | "InitialData": [self.testDataSet2, self.testDataSet], 37 | "LeafDataSource": [staticDataSource], 38 | "SupplementaryItems": [self.supplementaryItemOfKind, self.supplementaryItemOfKind2], ] 39 | } 40 | it("should generate corresponding dataChanges") { 41 | let lastChange = MutableProperty(nil) 42 | lastChange <~ dataSource.changes 43 | expect(lastChange.value).to(beNil()) 44 | dataSource.innerDataSource.value = StaticDataSource(items: [1, 2, 3]) 45 | expect(lastChange.value).notTo(beNil()) 46 | expect(lastChange.value).to(beAKindOf(DataChangeReloadData.self)) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DataSourceTests/QuickSpecWithDataSets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickSpecWithDataSets.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import Quick 10 | 11 | class QuickSpecWithDataSets: QuickSpec { 12 | 13 | let testDataSet = Array(1...7) 14 | let testDataSet2 = Array(33...43) 15 | let testDataSet3 = Array(50...55) 16 | 17 | let newElement = Int.random(in: 100..<500) 18 | 19 | var testDataSetWithNewElement: [Int] { 20 | return [newElement] + testDataSet 21 | } 22 | 23 | var testDataSetWithoutFirstElement: [Int] { 24 | return Array(testDataSet.dropFirst()) 25 | } 26 | 27 | var testDataSetWithReplacedFirstElement: [Int] { 28 | return Array([newElement] + testDataSet.dropFirst()) 29 | } 30 | 31 | var testDataSetWithReplacedFirstWithSecondElement: [Int] { 32 | var testDataSetCopy = testDataSet 33 | testDataSetCopy.replaceSubrange(0...1, with: [testDataSet[testDataSet.index(after: testDataSet.startIndex)], testDataSet[testDataSet.startIndex]]) 34 | return testDataSetCopy 35 | } 36 | 37 | let supplementaryItemOfKind = ["item1": Int.random(in: 500..<1000), "item2": Int.random(in: 1000..<1100)] 38 | let supplementaryItemOfKind2 = ["item1": Int.random(in: 5000..<10000), "item2": Int.random(in: 10000..<11000), "item3": Int.random(in: 55..<66)] 39 | 40 | let dataSetWithTestCellModels = [TestCellModel(), TestCellModel(), TestCellModel()] 41 | let dataSetWithTestCellModels2 = [TestCellModel()] 42 | 43 | var coreDataManager: CoreDataManager! 44 | 45 | func initCoreDataManagerWithTestData(testData: [Int]) { 46 | self.coreDataManager = CoreDataManager() 47 | self.coreDataManager?.fillItemsWithDataSet(dataSet: testData) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DataSourceTests/StaticDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticDataSource.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 04/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class StaticDataSourceTests: QuickSpecWithDataSets { 14 | override func spec() { 15 | var dataSource: StaticDataSource! 16 | beforeEach { 17 | let dataSourceSection = [DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind)] 18 | dataSource = StaticDataSource(sections: dataSourceSection) 19 | } 20 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet], "SupplementaryItems": [self.supplementaryItemOfKind]] } 21 | context("StaticDataSource with multiple sections") { 22 | beforeEach { 23 | let dataSourceSection1 = DataSourceSection(items: self.testDataSet, supplementaryItems: self.supplementaryItemOfKind2) 24 | let dataSourceSection2 = DataSourceSection(items: self.testDataSet2, supplementaryItems: self.supplementaryItemOfKind) 25 | dataSource = StaticDataSource(sections: [dataSourceSection1, dataSourceSection2]) 26 | } 27 | itBehavesLike("DataSource protocol") { ["DataSource": dataSource!, "InitialData": [self.testDataSet, self.testDataSet2], "SupplementaryItems": [self.supplementaryItemOfKind2, self.supplementaryItemOfKind]] } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DataSourceTests/TableViewDataSourceSharedConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | 13 | class TableViewDataSourceSharedConfiguration: QuickConfiguration { 14 | override class func configure(_ configuration: Configuration) { 15 | sharedExamples("TableViewDataSource object") { (sharedExampleContext: @escaping SharedExampleContext) in 16 | describe("TableView tests") { 17 | var tableViewDataSource: TableViewDataSource! 18 | var initialData: [[TestCellModel]]! 19 | var testSections: [DataSourceSection]! 20 | var tableView: UITableView! 21 | beforeEach { 22 | tableViewDataSource = sharedExampleContext()["tableViewDataSource"] as? TableViewDataSource 23 | initialData = sharedExampleContext()["TestCellModels"] as? [[TestCellModel]] 24 | testSections = sharedExampleContext()["TestSections"] as? [DataSourceSection] 25 | tableView = sharedExampleContext()["tableView"] as? UITableView 26 | } 27 | it("configure visible cells") { 28 | tableViewDataSource.configureVisibleCells() 29 | } 30 | it("has correct number of section") { 31 | expect(tableViewDataSource.numberOfSections(in: tableView)) == testSections.map { $0.count } ?? 1 32 | } 33 | it("has correct number of items in sections") { 34 | for index in initialData.indices { 35 | expect(tableViewDataSource.tableView(tableView, numberOfRowsInSection: index)) == initialData[index].count 36 | } 37 | } 38 | it("cells has correct type") { 39 | for (sectionIndex, element) in initialData.enumerated() { 40 | for itemIndex in element.indices { 41 | expect(tableViewDataSource.tableView(tableView, cellForRowAt: IndexPath(item: itemIndex, section: sectionIndex))).notTo(beNil()) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DataSourceTests/TableViewDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class TableViewDataSourceTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var tableViewDataSource: TableViewDataSource! 17 | var tableView: UITableView! 18 | let testSections = [ 19 | DataSourceSection(items: self.dataSetWithTestCellModels), 20 | DataSourceSection(items: self.dataSetWithTestCellModels2) 21 | ] 22 | beforeEach { 23 | let dataSource = Property( 24 | value: StaticDataSource( 25 | sections: testSections 26 | ) 27 | ) 28 | tableViewDataSource = TableViewDataSource() 29 | tableView = UITableView(frame: CGRect.zero) 30 | let tableViewDescriptors = [CellDescriptor(TestTableViewCell.reuseIdentifier, TestCellModel.self, .class(TestTableViewCell.self))] 31 | tableViewDataSource.configure(tableView, using: tableViewDescriptors) 32 | tableViewDataSource.dataSource.innerDataSource <~ dataSource.producer.map { $0 as DataSource } 33 | } 34 | itBehavesLike("TableViewDataSource object") { 35 | [ 36 | "tableViewDataSource": tableViewDataSource!, 37 | "TestCellModels": [self.dataSetWithTestCellModels, self.dataSetWithTestCellModels2], 38 | "TestSections": testSections, 39 | "tableView": tableView! 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DataSourceTests/TableViewDataSourceWithHeaderFooterTitlesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceWithHeaderFooterTitlesTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class TableViewDataSourceWithHeaderFooterTitlesTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var tableViewDataSource: TableViewDataSourceWithHeaderFooterTitles! 17 | var tableView: UITableView! 18 | let headerTitle = "headerTitle" 19 | let footerTitle = "footerTitle" 20 | let headerSection = DataSourceSection(items: self.dataSetWithTestCellModels, supplementaryItems: [UICollectionView.elementKindSectionHeader: headerTitle, UICollectionView.elementKindSectionFooter: footerTitle]) 21 | beforeEach { 22 | 23 | let dataSource = Property(value: StaticDataSource(sections: [headerSection])) 24 | tableViewDataSource = TableViewDataSourceWithHeaderFooterTitles() 25 | tableView = UITableView(frame: CGRect.zero) 26 | let tableViewDescriptors = [CellDescriptor(TestTableViewCell.reuseIdentifier, TestCellModel.self, .class(TestTableViewCell.self))] 27 | tableViewDataSource.configure(tableView, using: tableViewDescriptors) 28 | tableViewDataSource.dataSource.innerDataSource <~ dataSource.producer.map { $0 as DataSource } 29 | } 30 | itBehavesLike("TableViewDataSource object") { 31 | [ 32 | "tableViewDataSource": tableViewDataSource!, 33 | "TestCellModels": [self.dataSetWithTestCellModels], 34 | "TestSections": [headerSection], 35 | "tableView": tableView! 36 | ] 37 | } 38 | it("has correct header") { 39 | expect(tableViewDataSource.tableView(tableView, titleForHeaderInSection: 0)) == headerTitle 40 | } 41 | it("has correct footer") { 42 | expect(tableViewDataSource.tableView(tableView, titleForFooterInSection: 0)) == footerTitle 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DataSourceTests/TableViewDataSourceWithHeaderFooterViewsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataSourceWithHeaderFooterViewsTests.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 05/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | import Nimble 11 | import Quick 12 | import ReactiveSwift 13 | 14 | class TableViewDataSourceWithHeaderFooterViewsTests: QuickSpecWithDataSets { 15 | override func spec() { 16 | var tableViewDataSource: TableViewDataSourceWithHeaderFooterViews! 17 | var tableView: UITableView! 18 | 19 | let testSections = [ 20 | DataSourceSection( 21 | items: self.dataSetWithTestCellModels, 22 | supplementaryItems: [ 23 | UICollectionView.elementKindSectionHeader: TestHeaderFirstViewModel(), 24 | UICollectionView.elementKindSectionFooter: TestFooterFirstViewModel() 25 | ] 26 | ), 27 | DataSourceSection( 28 | items: self.dataSetWithTestCellModels, 29 | supplementaryItems: [ 30 | UICollectionView.elementKindSectionHeader: TestHeaderSecondViewModel(), 31 | UICollectionView.elementKindSectionFooter: TestFooterSecondViewModel() 32 | ] 33 | ), 34 | ] 35 | 36 | beforeEach { 37 | let dataSource = Property( 38 | value: StaticDataSource( 39 | sections: testSections 40 | ) 41 | ) 42 | tableViewDataSource = TableViewDataSourceWithHeaderFooterViews() 43 | tableView = UITableView(frame: CGRect.zero) 44 | let cellDescriptors = [ 45 | CellDescriptor(TestTableViewCell.reuseIdentifier, TestCellModel.self, .class(TestTableViewCell.self)) 46 | ] 47 | tableViewDataSource.configure( 48 | tableView, 49 | using: cellDescriptors, 50 | headerDescriptors: [ 51 | HeaderFooterDescriptor(TestHeaderFirstView.reuseIdentifier, TestHeaderFirstViewModel.self, .class(TestHeaderFirstView.self)), 52 | HeaderFooterDescriptor(TestHeaderSecondView.reuseIdentifier, TestHeaderSecondViewModel.self, .class(TestHeaderSecondView.self)), 53 | ], 54 | footerDescriptors: [ 55 | HeaderFooterDescriptor(TestFooterFirstView.reuseIdentifier, TestFooterFirstViewModel.self, .class(TestFooterFirstView.self)), 56 | HeaderFooterDescriptor(TestFooterSecondView.reuseIdentifier, TestFooterSecondViewModel.self, .class(TestFooterSecondView.self)), 57 | ] 58 | ) 59 | tableViewDataSource.dataSource.innerDataSource <~ dataSource.producer.map { $0 as DataSource } 60 | } 61 | 62 | itBehavesLike("TableViewDataSource object") { 63 | [ 64 | "tableViewDataSource": tableViewDataSource!, 65 | "TestCellModels": [self.dataSetWithTestCellModels], 66 | "TestSections": testSections, 67 | "tableView": tableView! 68 | ] 69 | } 70 | it("has header") { 71 | expect(tableViewDataSource.tableView(tableView, viewForHeaderInSection: 0)).to(beAKindOf(TestHeaderFirstView.self)) 72 | expect(tableViewDataSource.tableView(tableView, viewForHeaderInSection: 1)).to(beAKindOf(TestHeaderSecondView.self)) 73 | } 74 | it("has footer") { 75 | expect(tableViewDataSource.tableView(tableView, viewForFooterInSection: 0)).to(beAKindOf(TestFooterFirstView.self)) 76 | expect(tableViewDataSource.tableView(tableView, viewForFooterInSection: 1)).to(beAKindOf(TestFooterSecondView.self)) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /DataSourceTests/TestCells.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCells.swift 3 | // DataSourceTests 4 | // 5 | // Created by Aleksei Bobrov on 01/02/2019. 6 | // Copyright © 2019 Fueled. All rights reserved. 7 | // 8 | 9 | import DataSource 10 | 11 | struct TestCellModel { } 12 | 13 | final class TestTableViewCell: TableViewCell, ReusableItem { } 14 | 15 | final class TestCollectionViewCell: CollectionViewCell, ReusableItem { } 16 | 17 | struct TestHeaderFirstViewModel { } 18 | 19 | final class TestHeaderFirstView: TableViewHeaderFooterView, ReusableItem { } 20 | 21 | struct TestHeaderSecondViewModel { } 22 | 23 | final class TestHeaderSecondView: TableViewHeaderFooterView, ReusableItem { } 24 | 25 | struct TestFooterFirstViewModel { } 26 | 27 | final class TestFooterFirstView: TableViewHeaderFooterView, ReusableItem { } 28 | 29 | struct TestFooterSecondViewModel { } 30 | 31 | final class TestFooterSecondView: TableViewHeaderFooterView, ReusableItem { } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fueled 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataSource [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 2 | 3 | DataSource is a Swift framework that helps you specify, display and manipulate sectioned collections of items in `UITableview` and `UICollectionView` in an MVVM ([Model-View-ViewModel](https://en.wikipedia.org/wiki/Model_View_ViewModel)) fashion without having to deal with index mapping or writing repetitive and error-prone code to handle and display changes of those collections. 4 | 5 | DataSource framework introduces a `DataSource` protocol that specifies a provider of items grouped into sections just like a `UITableViewDataSource` is a provider of `UITableViewCell`s or a `UICollectionViewDataSource` is a provider of `UICollectionViewCell`s. 6 | A data source for a `UITableView` or a `UICollectionView` can be constructed by combining a `DataSource` with a common routine that provides a cell for each item. 7 | 8 | ## Requirements 9 | 10 | * iOS 8.0+ 11 | * Xcode 10.2+ 12 | * [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift/releases/) 13 | 14 | ## Installation 15 | 16 | The recommended way to add DataSource to your project is with [Carthage](https://github.com/Carthage/Carthage/releases/). 17 | Add `github "Fueled/DataSource"` line to your Cartfile and proceed with your favorite way to update and integrate your Carthage dependencies. 18 | 19 | ## Example Project 20 | 21 | You will find examples of using DataSource framework in [DataSourceExample](https://github.com/Fueled/DataSourceExample) project. 22 | 23 | ## Rationale 24 | 25 | `DataSource` is decoupled from any view-layer logic and contains only model or view-model items that in turn serve as view-models for corresponding cells. 26 | Such decoupling allows usage of same `DataSource` for both table and collection views as well as compositing several `DataSource`s into a single `CompositeDataSource` object without having to deal with view-related logic. 27 | 28 | ## DataSource composition 29 | 30 | `CompositeDataSource` is one of the implementations of `DataSource` protocol. It is initialized with an array of inner `DataSource`s. The sections of a `CompositeDataSource` are sections of the first inner `DataSource`, followed by sections of the second inner `DataSource`, etc. 31 | 32 | ## Propagating changes 33 | 34 | `DataSource` protocol exposes a `Signal` of `DataChange`s that can be subscribed for in order to update associated `UITableView` or `UICollectionView` when the underlying data changes. 35 | 36 | `DataChange` is a simple protocol that comes with a set of implementations for insertion, deletion and reloading of items and sections as well as `DataChangeReloadData` and `DataChangeBatch` for handling multiple `DataChange`s at once. 37 | 38 | `CompositeDataSource` automatically merges changes from inner `DataSource`s and properly maps the indices of all `DataChange`s. This means that corresponding sections of `UITableView` or `UICollectionView` are updated whenever any of the inner `DataSource`s emit changes. 39 | 40 | ## Populating `UITableView` 41 | 42 | DataSource framework contains a `TableViewDataSource` class that implements `UITableViewDataSource` protocol and can be used to populate an associated `UITableView` with data from any `DataSource`. `TableViewDataSource` receives and handles all `DataChange`s automatically. 43 | 44 | `TableViewDataSource` expects your instances of `UITableViewCell` and `UITableViewHeaderFooterView` to conform to `DataSourceItemReceiver` protocol. This protocol contains `ds_setItem:` function that is used to populate those views with corresponding data source items. `TableViewCell` is a subclass of `UITableViewCell` that already implements `DataSourceItemReceiver` protocol by populating its `MutableProperty` called `cellModel`. Cells and views implemented in Objective-C can alternatively conform to `@objc protocol DataSourceObjectItemReceiver` with the restriction that items must be objects (of `AnyObject` type). 45 | 46 | Just make `TableViewDataSource` instance a `dataSource` of your `UITableView`, connect the `UITableView` to `tableView` outlet of `TableViewDataSource` and assing your `DataSource` instance to `TableViewDataSource`’s `dataSource.innerDataSource` property. 47 | 48 | If you have only one cell prototype in your `UITableView` you can use `”DefaultCell”` for its `reuseIdentifier`. Otherwise, set `TableViewDataSource`’s `reuseIdentifierForItem` property to a block that returns an appropriate reuse identifier of a given item at a given index path. 49 | 50 | `TableViewDataSourceWithHeaderFooterTitles` and `TableViewDataSourceWithHeaderFooterViews` subclasses can be used to provide either titles or views for section headers and footers. 51 | 52 | You can subclass `TableViewDataSource` or any of its sublcasses to extend or override any `UITableViewDataSource`-related logic. 53 | 54 | ## Populating `UICollectionView` 55 | 56 | `UICollectionView` can be populated from a `DataSource` instance by using `CollectionViewDataSource` class in the same way `UITableView` is populated from a `TableViewDataSource`. 57 | 58 | Implementations for other collection-displaying views (e.g. map views with annotations) can be crafted in a similar fashion. 59 | 60 | ## Using with CoreData, KVO and others 61 | 62 | `DataSource` protocol can be used to wrap any API that provides a collection of items and notifies of any changes of that collection. 63 | 64 | DataSource framework provides implementations for two such collection-based APIs: 65 | 66 | 1. `FetchedResultsDataSource` class implements `DataSource` protocol using `NSFetchedResultsController` for CoreData models. 67 | `FetchedResultsDataSource` can be used just as any other `DataSource` without the need to manually implement `NSFetchedResultsControllerDelegate` protocol. 68 | 2. `KVODataSource` implements Key-Value Observing (KVO) for ordered to-many relationships. 69 | 70 | In case you need to handle data changes provided by other sources e.g. Photos framework or the new Contacts framework, similar `DataSource` implementations can be easily crafted. 71 | --------------------------------------------------------------------------------