├── .gitignore ├── CellViewModel.podspec ├── CellViewModel.xcodeproj └── project.pbxproj ├── CellViewModel ├── CellViewModel.h ├── Info.plist └── Sources │ ├── Adapter │ ├── CollectionView │ │ ├── CollectionViewDataAdapter.swift │ │ ├── DiffableCollectionViewDataAdapter.swift │ │ └── LazyCollectionViewDataAdapter.swift │ ├── DiffableSection.swift │ ├── Section.swift │ ├── SectionAdapter.swift │ └── TableView │ │ ├── LazyTableViewDataAdapter.swift │ │ └── TableViewDataAdapter.swift │ ├── Extensions │ ├── UICollectionView+ViewModel.swift │ └── UITableView+ViewModels.swift │ ├── Support │ └── LanguageSupport.swift │ ├── Vendor │ └── ListDiff │ │ ├── DiffResult.swift │ │ ├── Differ.swift │ │ ├── Extensions │ │ ├── Array+Additions.swift │ │ ├── String+Diffable.swift │ │ └── UICollectionView+Diff.swift │ │ ├── LICENSE │ │ └── Library │ │ ├── Diffable.swift │ │ ├── ListDiff+BatchUpdates.swift │ │ └── ListDiff.swift │ ├── ViewControllers │ ├── BaseCollectionViewController.swift │ ├── BaseDiffableCollectionViewController.swift │ └── BaseTableViewController.swift │ └── ViewModels │ ├── Accessibility │ ├── Accessible.swift │ └── AccessiblityDisplayOptions.swift │ ├── CellViewModel.swift │ ├── DiffableCellViewModel.swift │ ├── InteractiveCellViewModel.swift │ ├── Reusable.swift │ ├── SupplementaryKind.swift │ ├── SupplementaryViewModel.swift │ └── XibInitializable.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | CellViewModel.xcodeproj/project.xcworkspace 2 | CellViewModel.xcodeproj/xcuserdata 3 | -------------------------------------------------------------------------------- /CellViewModel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "CellViewModel" 4 | 5 | s.version = "1.8.0" 6 | 7 | s.summary = "CellViewModel is a protocol that includes logic for reuse, accessibility and dequeue" 8 | 9 | s.description = "Using CellViewModel to configure you UITableViewCell or UICollectionViewCell is just a one possible approach of work with UIKit's collections." 10 | 11 | s.homepage = "https://github.com/AntonPoltoratskyi/CellViewModel" 12 | 13 | s.license = { :type => "MIT", :file => "LICENSE" } 14 | 15 | s.author = "Anton Poltoratskyi" 16 | 17 | s.platform = :ios, "9.0" 18 | 19 | s.source = { :git => "https://github.com/AntonPoltoratskyi/CellViewModel.git", :tag => "#{s.version}" } 20 | 21 | s.source_files = "CellViewModel/Sources", "CellViewModel/Sources/**/*.{swift}" 22 | 23 | s.framework = "UIKit" 24 | 25 | s.swift_versions = ['5.0'] 26 | 27 | end 28 | -------------------------------------------------------------------------------- /CellViewModel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A07429B2288423F00083D50 /* DiffableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07429A2288423F00083D50 /* DiffableSection.swift */; }; 11 | 3A07429D2288424C00083D50 /* DiffableCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07429C2288424C00083D50 /* DiffableCellViewModel.swift */; }; 12 | 3A07429F2288425E00083D50 /* DiffableCollectionViewDataAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A07429E2288425E00083D50 /* DiffableCollectionViewDataAdapter.swift */; }; 13 | 3A0742AC2288429900083D50 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 3A0742A22288429900083D50 /* LICENSE */; }; 14 | 3A0742AD2288429900083D50 /* ListDiff+BatchUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742A42288429900083D50 /* ListDiff+BatchUpdates.swift */; }; 15 | 3A0742AE2288429900083D50 /* ListDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742A52288429900083D50 /* ListDiff.swift */; }; 16 | 3A0742AF2288429900083D50 /* Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742A62288429900083D50 /* Diffable.swift */; }; 17 | 3A0742B02288429900083D50 /* Differ.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742A72288429900083D50 /* Differ.swift */; }; 18 | 3A0742B12288429900083D50 /* UICollectionView+Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742A92288429900083D50 /* UICollectionView+Diff.swift */; }; 19 | 3A0742B22288429900083D50 /* String+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742AA2288429900083D50 /* String+Diffable.swift */; }; 20 | 3A0742B32288429900083D50 /* DiffResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742AB2288429900083D50 /* DiffResult.swift */; }; 21 | 3A0742B5228843A100083D50 /* Array+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742B4228843A000083D50 /* Array+Additions.swift */; }; 22 | 3A0742B72288460800083D50 /* BaseDiffableCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0742B62288460800083D50 /* BaseDiffableCollectionViewController.swift */; }; 23 | 3A091E98225B554400F2CA4C /* InteractiveCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A091E97225B554300F2CA4C /* InteractiveCellViewModel.swift */; }; 24 | 3A574A0B23DF212800B52D69 /* SectionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A574A0A23DF212800B52D69 /* SectionAdapter.swift */; }; 25 | 851892CC228494AC00774613 /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851892CB228494AC00774613 /* BaseTableViewController.swift */; }; 26 | 851892CE228496AB00774613 /* LazyTableViewDataAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851892CD228496AB00774613 /* LazyTableViewDataAdapter.swift */; }; 27 | 851892D022849C5600774613 /* SupplementaryKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851892CF22849C5600774613 /* SupplementaryKind.swift */; }; 28 | 851A8746227F73C100666B97 /* BaseCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851A8745227F73C100666B97 /* BaseCollectionViewController.swift */; }; 29 | 851A8748227F78DD00666B97 /* LanguageSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851A8747227F78DD00666B97 /* LanguageSupport.swift */; }; 30 | 8520241B220630BC00E3F7D1 /* CellViewModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 85202419220630BC00E3F7D1 /* CellViewModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 31 | 852024332206325500E3F7D1 /* TableViewDataAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024232206325500E3F7D1 /* TableViewDataAdapter.swift */; }; 32 | 852024342206325500E3F7D1 /* CollectionViewDataAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024252206325500E3F7D1 /* CollectionViewDataAdapter.swift */; }; 33 | 852024352206325500E3F7D1 /* LazyCollectionViewDataAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024262206325500E3F7D1 /* LazyCollectionViewDataAdapter.swift */; }; 34 | 852024362206325500E3F7D1 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024272206325500E3F7D1 /* Section.swift */; }; 35 | 852024372206325500E3F7D1 /* UITableView+ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024292206325500E3F7D1 /* UITableView+ViewModels.swift */; }; 36 | 852024382206325500E3F7D1 /* UICollectionView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520242A2206325500E3F7D1 /* UICollectionView+ViewModel.swift */; }; 37 | 852024392206325500E3F7D1 /* SupplementaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520242C2206325500E3F7D1 /* SupplementaryViewModel.swift */; }; 38 | 8520243A2206325500E3F7D1 /* Reusable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520242D2206325500E3F7D1 /* Reusable.swift */; }; 39 | 8520243B2206325500E3F7D1 /* XibInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520242E2206325500E3F7D1 /* XibInitializable.swift */; }; 40 | 8520243C2206325500E3F7D1 /* CellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520242F2206325500E3F7D1 /* CellViewModel.swift */; }; 41 | 8520243D2206325500E3F7D1 /* AccessiblityDisplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024312206325500E3F7D1 /* AccessiblityDisplayOptions.swift */; }; 42 | 8520243E2206325500E3F7D1 /* Accessible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852024322206325500E3F7D1 /* Accessible.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 3A07429A2288423F00083D50 /* DiffableSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffableSection.swift; sourceTree = ""; }; 47 | 3A07429C2288424C00083D50 /* DiffableCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffableCellViewModel.swift; sourceTree = ""; }; 48 | 3A07429E2288425E00083D50 /* DiffableCollectionViewDataAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffableCollectionViewDataAdapter.swift; sourceTree = ""; }; 49 | 3A0742A22288429900083D50 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 50 | 3A0742A42288429900083D50 /* ListDiff+BatchUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ListDiff+BatchUpdates.swift"; sourceTree = ""; }; 51 | 3A0742A52288429900083D50 /* ListDiff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDiff.swift; sourceTree = ""; }; 52 | 3A0742A62288429900083D50 /* Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diffable.swift; sourceTree = ""; }; 53 | 3A0742A72288429900083D50 /* Differ.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Differ.swift; sourceTree = ""; }; 54 | 3A0742A92288429900083D50 /* UICollectionView+Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Diff.swift"; sourceTree = ""; }; 55 | 3A0742AA2288429900083D50 /* String+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Diffable.swift"; sourceTree = ""; }; 56 | 3A0742AB2288429900083D50 /* DiffResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffResult.swift; sourceTree = ""; }; 57 | 3A0742B4228843A000083D50 /* Array+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Additions.swift"; sourceTree = ""; }; 58 | 3A0742B62288460800083D50 /* BaseDiffableCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDiffableCollectionViewController.swift; sourceTree = ""; }; 59 | 3A091E97225B554300F2CA4C /* InteractiveCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveCellViewModel.swift; sourceTree = ""; }; 60 | 3A574A0A23DF212800B52D69 /* SectionAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionAdapter.swift; sourceTree = ""; }; 61 | 851892CB228494AC00774613 /* BaseTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; 62 | 851892CD228496AB00774613 /* LazyTableViewDataAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyTableViewDataAdapter.swift; sourceTree = ""; }; 63 | 851892CF22849C5600774613 /* SupplementaryKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryKind.swift; sourceTree = ""; }; 64 | 851A8745227F73C100666B97 /* BaseCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCollectionViewController.swift; sourceTree = ""; }; 65 | 851A8747227F78DD00666B97 /* LanguageSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSupport.swift; sourceTree = ""; }; 66 | 85202416220630BC00E3F7D1 /* CellViewModel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CellViewModel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 85202419220630BC00E3F7D1 /* CellViewModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CellViewModel.h; sourceTree = ""; }; 68 | 8520241A220630BC00E3F7D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 852024232206325500E3F7D1 /* TableViewDataAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewDataAdapter.swift; sourceTree = ""; }; 70 | 852024252206325500E3F7D1 /* CollectionViewDataAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewDataAdapter.swift; sourceTree = ""; }; 71 | 852024262206325500E3F7D1 /* LazyCollectionViewDataAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyCollectionViewDataAdapter.swift; sourceTree = ""; }; 72 | 852024272206325500E3F7D1 /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 73 | 852024292206325500E3F7D1 /* UITableView+ViewModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ViewModels.swift"; sourceTree = ""; }; 74 | 8520242A2206325500E3F7D1 /* UICollectionView+ViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ViewModel.swift"; sourceTree = ""; }; 75 | 8520242C2206325500E3F7D1 /* SupplementaryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupplementaryViewModel.swift; sourceTree = ""; }; 76 | 8520242D2206325500E3F7D1 /* Reusable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reusable.swift; sourceTree = ""; }; 77 | 8520242E2206325500E3F7D1 /* XibInitializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XibInitializable.swift; sourceTree = ""; }; 78 | 8520242F2206325500E3F7D1 /* CellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellViewModel.swift; sourceTree = ""; }; 79 | 852024312206325500E3F7D1 /* AccessiblityDisplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessiblityDisplayOptions.swift; sourceTree = ""; }; 80 | 852024322206325500E3F7D1 /* Accessible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Accessible.swift; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 85202413220630BC00E3F7D1 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | /* End PBXFrameworksBuildPhase section */ 92 | 93 | /* Begin PBXGroup section */ 94 | 3A0742A02288429900083D50 /* Vendor */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 3A0742A12288429900083D50 /* ListDiff */, 98 | ); 99 | path = Vendor; 100 | sourceTree = ""; 101 | }; 102 | 3A0742A12288429900083D50 /* ListDiff */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 3A0742A22288429900083D50 /* LICENSE */, 106 | 3A0742A32288429900083D50 /* Library */, 107 | 3A0742A82288429900083D50 /* Extensions */, 108 | 3A0742A72288429900083D50 /* Differ.swift */, 109 | 3A0742AB2288429900083D50 /* DiffResult.swift */, 110 | ); 111 | path = ListDiff; 112 | sourceTree = ""; 113 | }; 114 | 3A0742A32288429900083D50 /* Library */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 3A0742A52288429900083D50 /* ListDiff.swift */, 118 | 3A0742A42288429900083D50 /* ListDiff+BatchUpdates.swift */, 119 | 3A0742A62288429900083D50 /* Diffable.swift */, 120 | ); 121 | path = Library; 122 | sourceTree = ""; 123 | }; 124 | 3A0742A82288429900083D50 /* Extensions */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 3A0742A92288429900083D50 /* UICollectionView+Diff.swift */, 128 | 3A0742AA2288429900083D50 /* String+Diffable.swift */, 129 | 3A0742B4228843A000083D50 /* Array+Additions.swift */, 130 | ); 131 | path = Extensions; 132 | sourceTree = ""; 133 | }; 134 | 851892CA2284947D00774613 /* Support */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 851A8747227F78DD00666B97 /* LanguageSupport.swift */, 138 | ); 139 | path = Support; 140 | sourceTree = ""; 141 | }; 142 | 851A8744227F73AB00666B97 /* ViewControllers */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 851892CB228494AC00774613 /* BaseTableViewController.swift */, 146 | 851A8745227F73C100666B97 /* BaseCollectionViewController.swift */, 147 | 3A0742B62288460800083D50 /* BaseDiffableCollectionViewController.swift */, 148 | ); 149 | path = ViewControllers; 150 | sourceTree = ""; 151 | }; 152 | 8520240C220630BC00E3F7D1 = { 153 | isa = PBXGroup; 154 | children = ( 155 | 85202418220630BC00E3F7D1 /* CellViewModel */, 156 | 85202417220630BC00E3F7D1 /* Products */, 157 | ); 158 | sourceTree = ""; 159 | }; 160 | 85202417220630BC00E3F7D1 /* Products */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 85202416220630BC00E3F7D1 /* CellViewModel.framework */, 164 | ); 165 | name = Products; 166 | sourceTree = ""; 167 | }; 168 | 85202418220630BC00E3F7D1 /* CellViewModel */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 8520243F2206348900E3F7D1 /* Sources */, 172 | 85202419220630BC00E3F7D1 /* CellViewModel.h */, 173 | 8520241A220630BC00E3F7D1 /* Info.plist */, 174 | ); 175 | path = CellViewModel; 176 | sourceTree = ""; 177 | }; 178 | 852024212206325500E3F7D1 /* Adapter */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | 852024272206325500E3F7D1 /* Section.swift */, 182 | 3A07429A2288423F00083D50 /* DiffableSection.swift */, 183 | 3A574A0A23DF212800B52D69 /* SectionAdapter.swift */, 184 | 852024222206325500E3F7D1 /* TableView */, 185 | 852024242206325500E3F7D1 /* CollectionView */, 186 | ); 187 | path = Adapter; 188 | sourceTree = ""; 189 | }; 190 | 852024222206325500E3F7D1 /* TableView */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 852024232206325500E3F7D1 /* TableViewDataAdapter.swift */, 194 | 851892CD228496AB00774613 /* LazyTableViewDataAdapter.swift */, 195 | ); 196 | path = TableView; 197 | sourceTree = ""; 198 | }; 199 | 852024242206325500E3F7D1 /* CollectionView */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | 852024252206325500E3F7D1 /* CollectionViewDataAdapter.swift */, 203 | 852024262206325500E3F7D1 /* LazyCollectionViewDataAdapter.swift */, 204 | 3A07429E2288425E00083D50 /* DiffableCollectionViewDataAdapter.swift */, 205 | ); 206 | path = CollectionView; 207 | sourceTree = ""; 208 | }; 209 | 852024282206325500E3F7D1 /* Extensions */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 852024292206325500E3F7D1 /* UITableView+ViewModels.swift */, 213 | 8520242A2206325500E3F7D1 /* UICollectionView+ViewModel.swift */, 214 | ); 215 | path = Extensions; 216 | sourceTree = ""; 217 | }; 218 | 8520242B2206325500E3F7D1 /* ViewModels */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 8520242D2206325500E3F7D1 /* Reusable.swift */, 222 | 8520242E2206325500E3F7D1 /* XibInitializable.swift */, 223 | 8520242F2206325500E3F7D1 /* CellViewModel.swift */, 224 | 3A07429C2288424C00083D50 /* DiffableCellViewModel.swift */, 225 | 851892CF22849C5600774613 /* SupplementaryKind.swift */, 226 | 8520242C2206325500E3F7D1 /* SupplementaryViewModel.swift */, 227 | 3A091E97225B554300F2CA4C /* InteractiveCellViewModel.swift */, 228 | 852024302206325500E3F7D1 /* Accessibility */, 229 | ); 230 | path = ViewModels; 231 | sourceTree = ""; 232 | }; 233 | 852024302206325500E3F7D1 /* Accessibility */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 852024322206325500E3F7D1 /* Accessible.swift */, 237 | 852024312206325500E3F7D1 /* AccessiblityDisplayOptions.swift */, 238 | ); 239 | path = Accessibility; 240 | sourceTree = ""; 241 | }; 242 | 8520243F2206348900E3F7D1 /* Sources */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 3A0742A02288429900083D50 /* Vendor */, 246 | 8520242B2206325500E3F7D1 /* ViewModels */, 247 | 852024212206325500E3F7D1 /* Adapter */, 248 | 852024282206325500E3F7D1 /* Extensions */, 249 | 851A8744227F73AB00666B97 /* ViewControllers */, 250 | 851892CA2284947D00774613 /* Support */, 251 | ); 252 | path = Sources; 253 | sourceTree = ""; 254 | }; 255 | /* End PBXGroup section */ 256 | 257 | /* Begin PBXHeadersBuildPhase section */ 258 | 85202411220630BC00E3F7D1 /* Headers */ = { 259 | isa = PBXHeadersBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | 8520241B220630BC00E3F7D1 /* CellViewModel.h in Headers */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | /* End PBXHeadersBuildPhase section */ 267 | 268 | /* Begin PBXNativeTarget section */ 269 | 85202415220630BC00E3F7D1 /* CellViewModel */ = { 270 | isa = PBXNativeTarget; 271 | buildConfigurationList = 8520241E220630BC00E3F7D1 /* Build configuration list for PBXNativeTarget "CellViewModel" */; 272 | buildPhases = ( 273 | 85202411220630BC00E3F7D1 /* Headers */, 274 | 85202412220630BC00E3F7D1 /* Sources */, 275 | 85202413220630BC00E3F7D1 /* Frameworks */, 276 | 85202414220630BC00E3F7D1 /* Resources */, 277 | ); 278 | buildRules = ( 279 | ); 280 | dependencies = ( 281 | ); 282 | name = CellViewModel; 283 | productName = CellViewModel; 284 | productReference = 85202416220630BC00E3F7D1 /* CellViewModel.framework */; 285 | productType = "com.apple.product-type.framework"; 286 | }; 287 | /* End PBXNativeTarget section */ 288 | 289 | /* Begin PBXProject section */ 290 | 8520240D220630BC00E3F7D1 /* Project object */ = { 291 | isa = PBXProject; 292 | attributes = { 293 | LastUpgradeCheck = 1240; 294 | ORGANIZATIONNAME = "Anton Poltoratskyi"; 295 | TargetAttributes = { 296 | 85202415220630BC00E3F7D1 = { 297 | CreatedOnToolsVersion = 10.1; 298 | LastSwiftMigration = 1240; 299 | }; 300 | }; 301 | }; 302 | buildConfigurationList = 85202410220630BC00E3F7D1 /* Build configuration list for PBXProject "CellViewModel" */; 303 | compatibilityVersion = "Xcode 9.3"; 304 | developmentRegion = en; 305 | hasScannedForEncodings = 0; 306 | knownRegions = ( 307 | en, 308 | Base, 309 | ); 310 | mainGroup = 8520240C220630BC00E3F7D1; 311 | productRefGroup = 85202417220630BC00E3F7D1 /* Products */; 312 | projectDirPath = ""; 313 | projectRoot = ""; 314 | targets = ( 315 | 85202415220630BC00E3F7D1 /* CellViewModel */, 316 | ); 317 | }; 318 | /* End PBXProject section */ 319 | 320 | /* Begin PBXResourcesBuildPhase section */ 321 | 85202414220630BC00E3F7D1 /* Resources */ = { 322 | isa = PBXResourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | 3A0742AC2288429900083D50 /* LICENSE in Resources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | /* End PBXResourcesBuildPhase section */ 330 | 331 | /* Begin PBXSourcesBuildPhase section */ 332 | 85202412220630BC00E3F7D1 /* Sources */ = { 333 | isa = PBXSourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | 8520243C2206325500E3F7D1 /* CellViewModel.swift in Sources */, 337 | 3A07429B2288423F00083D50 /* DiffableSection.swift in Sources */, 338 | 852024382206325500E3F7D1 /* UICollectionView+ViewModel.swift in Sources */, 339 | 851892CE228496AB00774613 /* LazyTableViewDataAdapter.swift in Sources */, 340 | 3A0742B72288460800083D50 /* BaseDiffableCollectionViewController.swift in Sources */, 341 | 8520243B2206325500E3F7D1 /* XibInitializable.swift in Sources */, 342 | 3A0742B12288429900083D50 /* UICollectionView+Diff.swift in Sources */, 343 | 8520243D2206325500E3F7D1 /* AccessiblityDisplayOptions.swift in Sources */, 344 | 3A0742B5228843A100083D50 /* Array+Additions.swift in Sources */, 345 | 3A0742AF2288429900083D50 /* Diffable.swift in Sources */, 346 | 852024342206325500E3F7D1 /* CollectionViewDataAdapter.swift in Sources */, 347 | 851A8748227F78DD00666B97 /* LanguageSupport.swift in Sources */, 348 | 8520243E2206325500E3F7D1 /* Accessible.swift in Sources */, 349 | 3A091E98225B554400F2CA4C /* InteractiveCellViewModel.swift in Sources */, 350 | 851892D022849C5600774613 /* SupplementaryKind.swift in Sources */, 351 | 852024362206325500E3F7D1 /* Section.swift in Sources */, 352 | 3A0742AD2288429900083D50 /* ListDiff+BatchUpdates.swift in Sources */, 353 | 852024372206325500E3F7D1 /* UITableView+ViewModels.swift in Sources */, 354 | 851892CC228494AC00774613 /* BaseTableViewController.swift in Sources */, 355 | 8520243A2206325500E3F7D1 /* Reusable.swift in Sources */, 356 | 3A0742B22288429900083D50 /* String+Diffable.swift in Sources */, 357 | 3A07429D2288424C00083D50 /* DiffableCellViewModel.swift in Sources */, 358 | 852024392206325500E3F7D1 /* SupplementaryViewModel.swift in Sources */, 359 | 3A0742B02288429900083D50 /* Differ.swift in Sources */, 360 | 3A0742AE2288429900083D50 /* ListDiff.swift in Sources */, 361 | 3A0742B32288429900083D50 /* DiffResult.swift in Sources */, 362 | 851A8746227F73C100666B97 /* BaseCollectionViewController.swift in Sources */, 363 | 3A574A0B23DF212800B52D69 /* SectionAdapter.swift in Sources */, 364 | 852024332206325500E3F7D1 /* TableViewDataAdapter.swift in Sources */, 365 | 852024352206325500E3F7D1 /* LazyCollectionViewDataAdapter.swift in Sources */, 366 | 3A07429F2288425E00083D50 /* DiffableCollectionViewDataAdapter.swift in Sources */, 367 | ); 368 | runOnlyForDeploymentPostprocessing = 0; 369 | }; 370 | /* End PBXSourcesBuildPhase section */ 371 | 372 | /* Begin XCBuildConfiguration section */ 373 | 8520241C220630BC00E3F7D1 /* Debug */ = { 374 | isa = XCBuildConfiguration; 375 | buildSettings = { 376 | ALWAYS_SEARCH_USER_PATHS = NO; 377 | CLANG_ANALYZER_NONNULL = YES; 378 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 379 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 380 | CLANG_CXX_LIBRARY = "libc++"; 381 | CLANG_ENABLE_MODULES = YES; 382 | CLANG_ENABLE_OBJC_ARC = YES; 383 | CLANG_ENABLE_OBJC_WEAK = YES; 384 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 385 | CLANG_WARN_BOOL_CONVERSION = YES; 386 | CLANG_WARN_COMMA = YES; 387 | CLANG_WARN_CONSTANT_CONVERSION = YES; 388 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 389 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 390 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 400 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 401 | CLANG_WARN_STRICT_PROTOTYPES = YES; 402 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 403 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 404 | CLANG_WARN_UNREACHABLE_CODE = YES; 405 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 406 | CODE_SIGN_IDENTITY = "iPhone Developer"; 407 | COPY_PHASE_STRIP = NO; 408 | CURRENT_PROJECT_VERSION = 1; 409 | DEBUG_INFORMATION_FORMAT = dwarf; 410 | ENABLE_STRICT_OBJC_MSGSEND = YES; 411 | ENABLE_TESTABILITY = YES; 412 | GCC_C_LANGUAGE_STANDARD = gnu11; 413 | GCC_DYNAMIC_NO_PIC = NO; 414 | GCC_NO_COMMON_BLOCKS = YES; 415 | GCC_OPTIMIZATION_LEVEL = 0; 416 | GCC_PREPROCESSOR_DEFINITIONS = ( 417 | "DEBUG=1", 418 | "$(inherited)", 419 | ); 420 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 421 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 422 | GCC_WARN_UNDECLARED_SELECTOR = YES; 423 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 424 | GCC_WARN_UNUSED_FUNCTION = YES; 425 | GCC_WARN_UNUSED_VARIABLE = YES; 426 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 427 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 428 | MTL_FAST_MATH = YES; 429 | ONLY_ACTIVE_ARCH = YES; 430 | SDKROOT = iphoneos; 431 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 432 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 433 | SWIFT_VERSION = 5.0; 434 | VERSIONING_SYSTEM = "apple-generic"; 435 | VERSION_INFO_PREFIX = ""; 436 | }; 437 | name = Debug; 438 | }; 439 | 8520241D220630BC00E3F7D1 /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ALWAYS_SEARCH_USER_PATHS = NO; 443 | CLANG_ANALYZER_NONNULL = YES; 444 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 445 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 446 | CLANG_CXX_LIBRARY = "libc++"; 447 | CLANG_ENABLE_MODULES = YES; 448 | CLANG_ENABLE_OBJC_ARC = YES; 449 | CLANG_ENABLE_OBJC_WEAK = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 455 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 456 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 457 | CLANG_WARN_EMPTY_BODY = YES; 458 | CLANG_WARN_ENUM_CONVERSION = YES; 459 | CLANG_WARN_INFINITE_RECURSION = YES; 460 | CLANG_WARN_INT_CONVERSION = YES; 461 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 463 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 464 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 465 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 466 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 467 | CLANG_WARN_STRICT_PROTOTYPES = YES; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 470 | CLANG_WARN_UNREACHABLE_CODE = YES; 471 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 472 | CODE_SIGN_IDENTITY = "iPhone Developer"; 473 | COPY_PHASE_STRIP = NO; 474 | CURRENT_PROJECT_VERSION = 1; 475 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 476 | ENABLE_NS_ASSERTIONS = NO; 477 | ENABLE_STRICT_OBJC_MSGSEND = YES; 478 | GCC_C_LANGUAGE_STANDARD = gnu11; 479 | GCC_NO_COMMON_BLOCKS = YES; 480 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 481 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 482 | GCC_WARN_UNDECLARED_SELECTOR = YES; 483 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 484 | GCC_WARN_UNUSED_FUNCTION = YES; 485 | GCC_WARN_UNUSED_VARIABLE = YES; 486 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 487 | MTL_ENABLE_DEBUG_INFO = NO; 488 | MTL_FAST_MATH = YES; 489 | SDKROOT = iphoneos; 490 | SWIFT_COMPILATION_MODE = wholemodule; 491 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 492 | SWIFT_VERSION = 5.0; 493 | VALIDATE_PRODUCT = YES; 494 | VERSIONING_SYSTEM = "apple-generic"; 495 | VERSION_INFO_PREFIX = ""; 496 | }; 497 | name = Release; 498 | }; 499 | 8520241F220630BC00E3F7D1 /* Debug */ = { 500 | isa = XCBuildConfiguration; 501 | buildSettings = { 502 | CODE_SIGN_IDENTITY = ""; 503 | CODE_SIGN_STYLE = Automatic; 504 | DEFINES_MODULE = YES; 505 | DEVELOPMENT_TEAM = ""; 506 | DYLIB_COMPATIBILITY_VERSION = 1; 507 | DYLIB_CURRENT_VERSION = 1; 508 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 509 | INFOPLIST_FILE = CellViewModel/Info.plist; 510 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 511 | LD_RUNPATH_SEARCH_PATHS = ( 512 | "$(inherited)", 513 | "@executable_path/Frameworks", 514 | "@loader_path/Frameworks", 515 | ); 516 | PRODUCT_BUNDLE_IDENTIFIER = com.polant.CellViewModel; 517 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 518 | SKIP_INSTALL = YES; 519 | SWIFT_VERSION = 5.0; 520 | TARGETED_DEVICE_FAMILY = "1,2"; 521 | }; 522 | name = Debug; 523 | }; 524 | 85202420220630BC00E3F7D1 /* Release */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | CODE_SIGN_IDENTITY = ""; 528 | CODE_SIGN_STYLE = Automatic; 529 | DEFINES_MODULE = YES; 530 | DEVELOPMENT_TEAM = ""; 531 | DYLIB_COMPATIBILITY_VERSION = 1; 532 | DYLIB_CURRENT_VERSION = 1; 533 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 534 | INFOPLIST_FILE = CellViewModel/Info.plist; 535 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 536 | LD_RUNPATH_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "@executable_path/Frameworks", 539 | "@loader_path/Frameworks", 540 | ); 541 | PRODUCT_BUNDLE_IDENTIFIER = com.polant.CellViewModel; 542 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 543 | SKIP_INSTALL = YES; 544 | SWIFT_VERSION = 5.0; 545 | TARGETED_DEVICE_FAMILY = "1,2"; 546 | }; 547 | name = Release; 548 | }; 549 | /* End XCBuildConfiguration section */ 550 | 551 | /* Begin XCConfigurationList section */ 552 | 85202410220630BC00E3F7D1 /* Build configuration list for PBXProject "CellViewModel" */ = { 553 | isa = XCConfigurationList; 554 | buildConfigurations = ( 555 | 8520241C220630BC00E3F7D1 /* Debug */, 556 | 8520241D220630BC00E3F7D1 /* Release */, 557 | ); 558 | defaultConfigurationIsVisible = 0; 559 | defaultConfigurationName = Release; 560 | }; 561 | 8520241E220630BC00E3F7D1 /* Build configuration list for PBXNativeTarget "CellViewModel" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 8520241F220630BC00E3F7D1 /* Debug */, 565 | 85202420220630BC00E3F7D1 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | /* End XCConfigurationList section */ 571 | }; 572 | rootObject = 8520240D220630BC00E3F7D1 /* Project object */; 573 | } 574 | -------------------------------------------------------------------------------- /CellViewModel/CellViewModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // CellViewModel.h 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CellViewModel. 12 | FOUNDATION_EXPORT double CellViewModelVersionNumber; 13 | 14 | //! Project version string for CellViewModel. 15 | FOUNDATION_EXPORT const unsigned char CellViewModelVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /CellViewModel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/CollectionView/CollectionViewDataAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupedCollectionViewDataAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class CollectionViewDataAdapter: NSObject, UICollectionViewDataSource, SectionAdapter { 12 | 13 | open var data: [Section] = [] { 14 | didSet { 15 | if inferModelTypes { 16 | register(data) 17 | } 18 | if automaticallyReloadData { 19 | collectionView?.reloadData() 20 | } 21 | } 22 | } 23 | 24 | private weak var collectionView: UICollectionView? 25 | 26 | private let inferModelTypes: Bool 27 | 28 | private let automaticallyReloadData: Bool 29 | 30 | public init(collectionView: UICollectionView, inferModelTypes: Bool = false, automaticallyReloadData: Bool = true) { 31 | self.collectionView = collectionView 32 | self.inferModelTypes = inferModelTypes 33 | self.automaticallyReloadData = automaticallyReloadData 34 | super.init() 35 | collectionView.dataSource = self 36 | } 37 | 38 | // MARK: - UICollectionViewDataSource 39 | 40 | open func numberOfSections(in collectionView: UICollectionView) -> Int { 41 | return data.count 42 | } 43 | 44 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 45 | guard contains(section: section) else { 46 | return 0 47 | } 48 | return data[section].items.count 49 | } 50 | 51 | open func collectionView(_ collectionView: UICollectionView, 52 | viewForSupplementaryElementOfKind kind: String, 53 | at indexPath: IndexPath) -> UICollectionReusableView { 54 | guard let model = supplementaryModel(ofKind: kind, at: indexPath) else { 55 | fatalError("supplementary model = nil, at indexPath = \(indexPath)") 56 | } 57 | return collectionView.dequeueReusableSupplementaryView(with: model, for: indexPath) 58 | } 59 | 60 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 61 | return collectionView.dequeueReusableCell(with: itemModel(at: indexPath), for: indexPath) 62 | } 63 | 64 | // MARK: - Adapter 65 | 66 | open func supplementaryModel(ofKind kind: String, at indexPath: IndexPath) -> AnySupplementaryViewModel? { 67 | let section = data[indexPath.section] 68 | 69 | switch kind { 70 | case collectionSectionHeaderType: 71 | return section.header 72 | case collectionSectionFooterType: 73 | return section.footer 74 | default: 75 | return nil 76 | } 77 | } 78 | 79 | open func supplementaryModel(ofKind kind: String, in section: Int) -> AnySupplementaryViewModel? { 80 | let indexPath = IndexPath(item: 0, section: section) 81 | return supplementaryModel(ofKind: kind, at: indexPath) 82 | } 83 | 84 | open func headerModel(in section: Int) -> AnySupplementaryViewModel? { 85 | let indexPath = IndexPath(item: 0, section: section) 86 | return supplementaryModel(ofKind: collectionSectionHeaderType, at: indexPath) 87 | } 88 | 89 | open func footerModel(in section: Int) -> AnySupplementaryViewModel? { 90 | let indexPath = IndexPath(item: 0, section: section) 91 | return supplementaryModel(ofKind: collectionSectionFooterType, at: indexPath) 92 | } 93 | 94 | // MARK: - Type Registration 95 | 96 | private func register(_ data: [Section]) { 97 | for section in data { 98 | if let header = section.header { 99 | collectionView?.register(type(of: header)) 100 | } 101 | if let footer = section.footer { 102 | collectionView?.register(type(of: footer)) 103 | } 104 | for item in section.items { 105 | collectionView?.register(type(of: item)) 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/CollectionView/DiffableCollectionViewDataAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableCollectionViewDataAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class DiffableCollectionViewDataAdapter: NSObject, UICollectionViewDataSource { 12 | 13 | public private(set) var data: [DiffableSection] = [] 14 | 15 | private let differ: Differ = ListDiffer() 16 | 17 | private weak var collectionView: UICollectionView? 18 | 19 | private let inferModelTypes: Bool 20 | 21 | public init(collectionView: UICollectionView, inferModelTypes: Bool = false) { 22 | self.collectionView = collectionView 23 | self.inferModelTypes = inferModelTypes 24 | super.init() 25 | collectionView.dataSource = self 26 | } 27 | 28 | // MARK: - UICollectionViewDataSource 29 | 30 | open func numberOfSections(in collectionView: UICollectionView) -> Int { 31 | return data.count 32 | } 33 | 34 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 35 | return data[section].items.count 36 | } 37 | 38 | open func collectionView(_ collectionView: UICollectionView, 39 | viewForSupplementaryElementOfKind kind: String, 40 | at indexPath: IndexPath) -> UICollectionReusableView { 41 | guard let model = supplementaryModel(ofKind: kind, at: indexPath) else { 42 | fatalError("supplementary model = nil, at indexPath = \(indexPath)") 43 | } 44 | return collectionView.dequeueReusableSupplementaryView(with: model, for: indexPath) 45 | } 46 | 47 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 48 | return collectionView.dequeueReusableCell(with: itemModel(at: indexPath), for: indexPath) 49 | } 50 | 51 | // MARK: - Adapter 52 | 53 | open func sectionModel(at index: Int) -> DiffableSection { 54 | return data[index] 55 | } 56 | 57 | open func supplementaryModel(ofKind kind: String, at indexPath: IndexPath) -> AnySupplementaryViewModel? { 58 | let section = data[indexPath.section] 59 | 60 | switch kind { 61 | case collectionSectionHeaderType: 62 | return section.header 63 | case collectionSectionFooterType: 64 | return section.footer 65 | default: 66 | return nil 67 | } 68 | } 69 | 70 | open func supplementaryModel(ofKind kind: String, in section: Int) -> AnySupplementaryViewModel? { 71 | let indexPath = IndexPath(item: 0, section: section) 72 | return supplementaryModel(ofKind: kind, at: indexPath) 73 | } 74 | 75 | open func headerModel(in section: Int) -> AnySupplementaryViewModel? { 76 | let indexPath = IndexPath(item: 0, section: section) 77 | return supplementaryModel(ofKind: collectionSectionHeaderType, at: indexPath) 78 | } 79 | 80 | open func footerModel(in section: Int) -> AnySupplementaryViewModel? { 81 | let indexPath = IndexPath(item: 0, section: section) 82 | return supplementaryModel(ofKind: collectionSectionFooterType, at: indexPath) 83 | } 84 | 85 | open func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { 86 | return data[indexPath.section].items[indexPath.item] 87 | } 88 | 89 | open func contains(section: Int) -> Bool { 90 | return data.indices.contains(section) 91 | } 92 | 93 | open func containsModel(at indexPath: IndexPath) -> Bool { 94 | return contains(section: indexPath.section) && data[indexPath.section].items.indices.contains(indexPath.row) 95 | } 96 | 97 | open func update(data: [DiffableSection], animated: Bool) { 98 | collectionView?.performUpdate(animated: animated, updates: { 99 | self.update(data: data) 100 | }) 101 | } 102 | 103 | // MARK: - Diffs 104 | 105 | private func update(data: [DiffableSection]) { 106 | defer { self.data = data } 107 | 108 | if inferModelTypes { 109 | register(data) 110 | } 111 | updateSections(with: data) 112 | 113 | guard self.data.count == data.count else { 114 | assertionFailure("Mistake in diff processing logic") 115 | return 116 | } 117 | updateItems(with: data) 118 | } 119 | 120 | private func updateSections(with data: [DiffableSection]) { 121 | let oldSectionIds = self.data.map { $0.identifier } 122 | let newSectionIds = data.map { $0.identifier } 123 | 124 | let sectionDiff = differ.diffBetween(old: oldSectionIds, new: newSectionIds) 125 | 126 | guard sectionDiff.hasChanges else { 127 | return 128 | } 129 | 130 | if !sectionDiff.deletes.isEmpty { 131 | for section in sectionDiff.deletes.reversed() { 132 | self.data.remove(at: section) 133 | } 134 | collectionView?.deleteSections(sectionDiff.deletes) 135 | } 136 | if !sectionDiff.inserts.isEmpty { 137 | for sectionIndex in sectionDiff.inserts { 138 | let section = data.first { section in 139 | !self.data.contains { $0.identifier == section.identifier } 140 | } 141 | if let section = section { 142 | self.data.insert(section, at: sectionIndex) 143 | } else { 144 | assertionFailure("Mistake in diff processing logic") 145 | } 146 | } 147 | collectionView?.insertSections(sectionDiff.inserts) 148 | } 149 | 150 | for move in sectionDiff.moves { 151 | self.data.move(at: move.from, to: move.to) 152 | collectionView?.moveSection(move.from, toSection: move.to) 153 | } 154 | } 155 | 156 | private func updateItems(with data: [DiffableSection]) { 157 | var section: Int = 0 158 | for (old, new) in zip(self.data, data) { 159 | defer { section += 1 } 160 | 161 | let diff = differ.diffBetween(old: old.items.map { EquatableItem($0) }, 162 | new: new.items.map { EquatableItem($0) }) 163 | 164 | collectionView?.apply(diff, section: section) 165 | } 166 | } 167 | 168 | // MARK: - Type Registration 169 | 170 | private func register(_ data: [DiffableSection]) { 171 | for section in data { 172 | if let header = section.header { 173 | collectionView?.register(type(of: header)) 174 | } 175 | if let footer = section.footer { 176 | collectionView?.register(type(of: footer)) 177 | } 178 | for item in section.items { 179 | collectionView?.register(type(of: item)) 180 | } 181 | } 182 | } 183 | } 184 | 185 | private final class EquatableItem: Diffable, Equatable { 186 | 187 | private let model: DiffableCellViewModel 188 | 189 | init(_ model: DiffableCellViewModel) { 190 | self.model = model 191 | } 192 | 193 | static func == (lhs: EquatableItem, rhs: EquatableItem) -> Bool { 194 | return lhs.model.isEqual(to: rhs.model) 195 | } 196 | 197 | var diffIdentifier: AnyHashable { 198 | return model.diffIdentifier 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/CollectionView/LazyCollectionViewDataAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyCollectionViewDataAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class LazyCollectionViewDataAdapter: NSObject, UICollectionViewDataSource { 12 | 13 | public typealias DataProvider = (LazyCollectionViewDataAdapter, IndexPath) -> AnyCellViewModel 14 | 15 | open var data: [T] = [] { 16 | didSet { 17 | if automaticallyReloadData { 18 | collectionView?.reloadData() 19 | } 20 | } 21 | } 22 | 23 | private let dataProvider: DataProvider 24 | 25 | private let automaticallyReloadData: Bool 26 | 27 | private weak var collectionView: UICollectionView? 28 | 29 | public init(collectionView: UICollectionView, automaticallyReloadData: Bool = true, dataProvider: @escaping DataProvider) { 30 | self.collectionView = collectionView 31 | self.dataProvider = dataProvider 32 | self.automaticallyReloadData = automaticallyReloadData 33 | super.init() 34 | collectionView.dataSource = self 35 | } 36 | 37 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 38 | guard data.indices.contains(section) else { 39 | return 0 40 | } 41 | return data.count 42 | } 43 | 44 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 45 | return collectionView.dequeueReusableCell(with: itemModel(at: indexPath), for: indexPath) 46 | } 47 | 48 | open func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { 49 | return dataProvider(self, indexPath) 50 | } 51 | } 52 | 53 | extension LazyCollectionViewDataAdapter where T == AnyCellViewModel { 54 | 55 | public convenience init(collectionView: UICollectionView) { 56 | self.init(collectionView: collectionView) { dataSource, indexPath in 57 | dataSource.data[indexPath.row] 58 | } 59 | } 60 | } 61 | 62 | extension LazyCollectionViewDataAdapter where T: AnyCellViewModel { 63 | 64 | public convenience init(collectionView: UICollectionView) { 65 | self.init(collectionView: collectionView) { dataSource, indexPath in 66 | dataSource.data[indexPath.row] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/DiffableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableSection.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Supported only for UICollectionView for now 12 | public final class DiffableSection { 13 | 14 | public let identifier: String 15 | 16 | public let insets: UIEdgeInsets? 17 | 18 | public let lineSpacing: CGFloat? 19 | 20 | public var header: AnySupplementaryViewModel? 21 | 22 | public var footer: AnySupplementaryViewModel? 23 | 24 | public var items: [DiffableCellViewModel] 25 | 26 | public init(identifier: String, 27 | insets: UIEdgeInsets? = nil, 28 | lineSpacing: CGFloat? = nil, 29 | header: AnySupplementaryViewModel? = nil, 30 | footer: AnySupplementaryViewModel? = nil, 31 | items: [DiffableCellViewModel]) { 32 | self.identifier = identifier 33 | self.insets = insets 34 | self.lineSpacing = lineSpacing 35 | self.header = header 36 | self.footer = footer 37 | self.items = items 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class Section { 12 | 13 | public let identifier: String? 14 | 15 | /// Supported only for UICollectionView. NOT supported for UITableView. 16 | public var insets: UIEdgeInsets? 17 | /// Supported only for UICollectionView. NOT Unsupported for UITableView. 18 | public var lineSpacing: CGFloat? 19 | 20 | public var header: AnySupplementaryViewModel? 21 | 22 | public var footer: AnySupplementaryViewModel? 23 | 24 | public var items: [AnyCellViewModel] 25 | 26 | public init(identifier: String? = nil, 27 | insets: UIEdgeInsets? = nil, 28 | lineSpacing: CGFloat? = nil, 29 | header: AnySupplementaryViewModel? = nil, 30 | footer: AnySupplementaryViewModel? = nil, 31 | items: [AnyCellViewModel]) { 32 | self.identifier = identifier 33 | self.insets = insets 34 | self.lineSpacing = lineSpacing 35 | self.header = header 36 | self.footer = footer 37 | self.items = items 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/SectionAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 1/27/20. 6 | // Copyright © 2020 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SectionAdapter { 12 | var data: [Section] { get } 13 | 14 | func sectionModel(at index: Int) -> Section 15 | func headerModel(in section: Int) -> AnySupplementaryViewModel? 16 | func footerModel(in section: Int) -> AnySupplementaryViewModel? 17 | func itemModel(at indexPath: IndexPath) -> AnyCellViewModel 18 | 19 | func contains(section: Int) -> Bool 20 | func containsModel(at indexPath: IndexPath) -> Bool 21 | } 22 | 23 | extension SectionAdapter { 24 | 25 | public func sectionModel(at index: Int) -> Section { 26 | return data[index] 27 | } 28 | 29 | public func headerModel(in section: Int) -> AnySupplementaryViewModel? { 30 | return data[section].header 31 | } 32 | 33 | public func footerModel(in section: Int) -> AnySupplementaryViewModel? { 34 | return data[section].footer 35 | } 36 | 37 | public func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { 38 | return data[indexPath.section].items[indexPath.item] 39 | } 40 | 41 | public func contains(section: Int) -> Bool { 42 | return data.indices.contains(section) 43 | } 44 | 45 | public func containsModel(at indexPath: IndexPath) -> Bool { 46 | return contains(section: indexPath.section) 47 | && data[indexPath.section].items.indices.contains(indexPath.row) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/TableView/LazyTableViewDataAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyTableViewDataAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 09.05.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class LazyTableViewDataAdapter: NSObject, UITableViewDataSource { 12 | 13 | public typealias DataProvider = (LazyTableViewDataAdapter, IndexPath) -> AnyCellViewModel 14 | 15 | open var data: [T] = [] { 16 | didSet { 17 | if automaticallyReloadData { 18 | tableView?.reloadData() 19 | } 20 | } 21 | } 22 | 23 | private let dataProvider: DataProvider 24 | 25 | private let automaticallyReloadData: Bool 26 | 27 | private weak var tableView: UITableView? 28 | 29 | public init(tableView: UITableView, automaticallyReloadData: Bool = true, dataProvider: @escaping DataProvider) { 30 | self.tableView = tableView 31 | self.dataProvider = dataProvider 32 | self.automaticallyReloadData = automaticallyReloadData 33 | super.init() 34 | tableView.dataSource = self 35 | } 36 | 37 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | guard data.indices.contains(section) else { 39 | return 0 40 | } 41 | return data.count 42 | } 43 | 44 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 45 | return tableView.dequeueReusableCell(with: itemModel(at: indexPath), for: indexPath) 46 | } 47 | 48 | open func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { 49 | return dataProvider(self, indexPath) 50 | } 51 | } 52 | 53 | extension LazyTableViewDataAdapter where T == AnyCellViewModel { 54 | 55 | public convenience init(tableView: UITableView) { 56 | self.init(tableView: tableView) { dataSource, indexPath in 57 | dataSource.data[indexPath.row] 58 | } 59 | } 60 | } 61 | 62 | extension LazyTableViewDataAdapter where T: AnyCellViewModel { 63 | 64 | public convenience init(tableView: UITableView) { 65 | self.init(tableView: tableView) { dataSource, indexPath in 66 | dataSource.data[indexPath.row] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Adapter/TableView/TableViewDataAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewDataAdapter.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class TableViewDataAdapter: NSObject, UITableViewDataSource, SectionAdapter { 12 | 13 | open var data: [Section] = [] { 14 | didSet { 15 | if inferModelTypes { 16 | register(data) 17 | } 18 | if automaticallyReloadData { 19 | tableView?.reloadData() 20 | } 21 | } 22 | } 23 | 24 | private weak var tableView: UITableView? 25 | 26 | private let inferModelTypes: Bool 27 | 28 | private let automaticallyReloadData: Bool 29 | 30 | public init(tableView: UITableView, inferModelTypes: Bool = false, automaticallyReloadData: Bool = true) { 31 | self.tableView = tableView 32 | self.inferModelTypes = inferModelTypes 33 | self.automaticallyReloadData = automaticallyReloadData 34 | super.init() 35 | tableView.dataSource = self 36 | } 37 | 38 | // MARK: - UICollectionViewDataSource 39 | 40 | open func numberOfSections(in tableView: UITableView) -> Int { 41 | return data.count 42 | } 43 | 44 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 45 | guard contains(section: section) else { 46 | return 0 47 | } 48 | return data[section].items.count 49 | } 50 | 51 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 52 | return tableView.dequeueReusableCell(with: itemModel(at: indexPath), for: indexPath) 53 | } 54 | 55 | // MARK: - Type Registration 56 | 57 | private func register(_ data: [Section]) { 58 | for section in data { 59 | if let header = section.header { 60 | tableView?.register(type(of: header)) 61 | } 62 | if let footer = section.footer { 63 | tableView?.register(type(of: footer)) 64 | } 65 | for item in section.items { 66 | tableView?.register(type(of: item)) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Extensions/UICollectionView+ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+ViewModel.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Cell 12 | 13 | extension UICollectionView { 14 | 15 | public func dequeueReusableCell(with viewModel: AnyCellViewModel, for indexPath: IndexPath) -> UICollectionViewCell { 16 | let identifier = type(of: viewModel).uniqueIdentifier 17 | let cell = dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) 18 | cell.accessibilityIdentifier = viewModel.accessibilityIdentifier(for: indexPath) 19 | viewModel.setup(cell: cell) 20 | return cell 21 | } 22 | 23 | public func register(_ modelType: AnyCellViewModel.Type) { 24 | if let xibFileName = (modelType.cellClass as? XibInitializable.Type)?.xibFileName { 25 | let nib = UINib(nibName: xibFileName, bundle: Bundle(for: modelType.cellClass)) 26 | register(nib, forCellWithReuseIdentifier: modelType.uniqueIdentifier) 27 | 28 | } else { 29 | register(modelType.cellClass, forCellWithReuseIdentifier: modelType.uniqueIdentifier) 30 | } 31 | } 32 | 33 | public func register(_ models: [AnyCellViewModel.Type]) { 34 | models.forEach { register($0) } 35 | } 36 | 37 | public func register(_ models: AnyCellViewModel.Type...) { 38 | models.forEach { register($0) } 39 | } 40 | 41 | public func register(_ viewModel: T.Type) where T.Cell: UICollectionViewCell { 42 | register(T.Cell.self, forCellWithReuseIdentifier: T.uniqueIdentifier) 43 | } 44 | 45 | public func register(_ viewModel: T.Type) where T.Cell: UICollectionViewCell, T.Cell: XibInitializable { 46 | let nib = UINib(nibName: T.Cell.xibFileName, bundle: Bundle(for: T.Cell.self)) 47 | register(nib, forCellWithReuseIdentifier: T.uniqueIdentifier) 48 | } 49 | } 50 | 51 | // MARK: - SupplementaryView 52 | 53 | extension UICollectionView { 54 | 55 | public func dequeueReusableSupplementaryView(with viewModel: AnySupplementaryViewModel, for indexPath: IndexPath) -> UICollectionReusableView { 56 | let identifier = type(of: viewModel).uniqueIdentifier 57 | let kind = type(of: viewModel).supplementaryKind.rawValue 58 | let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: identifier, for: indexPath) 59 | view.accessibilityIdentifier = viewModel.accessibilityIdentifier(for: indexPath) 60 | viewModel.setup(view: view) 61 | return view 62 | } 63 | 64 | public func register(_ modelType: AnySupplementaryViewModel.Type) { 65 | if let xibFileName = (modelType.supplementaryViewClass as? XibInitializable.Type)?.xibFileName { 66 | let nib = UINib(nibName: xibFileName, bundle: Bundle(for: modelType.supplementaryViewClass)) 67 | register(nib, forSupplementaryViewOfKind: modelType.supplementaryKind.rawValue, withReuseIdentifier: modelType.uniqueIdentifier) 68 | 69 | } else { 70 | register(modelType.supplementaryViewClass, 71 | forSupplementaryViewOfKind: modelType.supplementaryKind.rawValue, 72 | withReuseIdentifier: modelType.uniqueIdentifier) 73 | } 74 | } 75 | 76 | public func register(_ models: [AnySupplementaryViewModel.Type]) { 77 | models.forEach { register($0) } 78 | } 79 | 80 | public func register(_ models: AnySupplementaryViewModel.Type...) { 81 | models.forEach { register($0) } 82 | } 83 | 84 | public func register(_ supplementaryModel: T.Type) { 85 | register(T.View.self, forSupplementaryViewOfKind: T.supplementaryKind.rawValue, withReuseIdentifier: T.uniqueIdentifier) 86 | } 87 | 88 | public func register(_ supplementaryModel: T.Type) where T.View: XibInitializable { 89 | let nib = UINib(nibName: T.View.xibFileName, bundle: Bundle(for: T.View.self)) 90 | register(nib, forSupplementaryViewOfKind: T.supplementaryKind.rawValue, withReuseIdentifier: T.uniqueIdentifier) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Extensions/UITableView+ViewModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+ViewModels.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Cell 12 | 13 | extension UITableView { 14 | 15 | public func dequeueReusableCell(with viewModel: AnyCellViewModel, for indexPath: IndexPath) -> UITableViewCell { 16 | let identifier = type(of: viewModel).uniqueIdentifier 17 | let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath) 18 | cell.accessibilityIdentifier = viewModel.accessibilityIdentifier(for: indexPath) 19 | viewModel.setup(cell: cell) 20 | return cell 21 | } 22 | 23 | public func register(_ modelType: AnyCellViewModel.Type) { 24 | if let xibFileName = (modelType.cellClass as? XibInitializable.Type)?.xibFileName { 25 | let nib = UINib(nibName: xibFileName, bundle: Bundle(for: modelType.cellClass)) 26 | register(nib, forCellReuseIdentifier: modelType.uniqueIdentifier) 27 | 28 | } else { 29 | register(modelType.cellClass, forCellReuseIdentifier: modelType.uniqueIdentifier) 30 | } 31 | } 32 | 33 | public func register(_ models: [AnyCellViewModel.Type]) { 34 | models.forEach { register($0) } 35 | } 36 | 37 | public func register(_ models: AnyCellViewModel.Type...) { 38 | models.forEach { register($0) } 39 | } 40 | 41 | public func register(_ viewModel: T.Type) where T.Cell: UITableViewCell { 42 | register(T.Cell.self, forCellReuseIdentifier: T.uniqueIdentifier) 43 | } 44 | 45 | public func register(_ viewModel: T.Type) where T.Cell: UITableViewCell, T.Cell: XibInitializable { 46 | let nib = UINib(nibName: T.Cell.xibFileName, bundle: Bundle(for: T.Cell.self)) 47 | register(nib, forCellReuseIdentifier: T.uniqueIdentifier) 48 | } 49 | } 50 | 51 | // MARK: - Header / Footer 52 | 53 | extension UITableView { 54 | 55 | public func dequeueReusableSupplementaryView(with viewModel: AnySupplementaryViewModel, for section: Int) -> UITableViewHeaderFooterView? { 56 | let identifier = type(of: viewModel).uniqueIdentifier 57 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: identifier) else { 58 | return nil 59 | } 60 | view.accessibilityIdentifier = viewModel.accessibilityIdentifier(for: section) 61 | viewModel.setup(view: view) 62 | return view 63 | } 64 | 65 | public func register(_ modelType: AnySupplementaryViewModel.Type) { 66 | if let xibFileName = (modelType.supplementaryViewClass as? XibInitializable.Type)?.xibFileName { 67 | let nib = UINib(nibName: xibFileName, bundle: Bundle(for: modelType.supplementaryViewClass)) 68 | register(nib, forHeaderFooterViewReuseIdentifier: modelType.uniqueIdentifier) 69 | 70 | } else { 71 | register(modelType.supplementaryViewClass, forHeaderFooterViewReuseIdentifier: modelType.uniqueIdentifier) 72 | } 73 | } 74 | 75 | public func register(_ models: [AnySupplementaryViewModel.Type]) { 76 | models.forEach { register($0) } 77 | } 78 | 79 | public func register(_ models: AnySupplementaryViewModel.Type...) { 80 | models.forEach { register($0) } 81 | } 82 | 83 | public func register(_ supplementaryModel: T.Type) { 84 | register(T.View.self, forHeaderFooterViewReuseIdentifier: T.uniqueIdentifier) 85 | } 86 | 87 | public func register(_ supplementaryModel: T.Type) where T.View: XibInitializable { 88 | let nib = UINib(nibName: T.View.xibFileName, bundle: Bundle(for: T.View.self)) 89 | register(nib, forHeaderFooterViewReuseIdentifier: T.uniqueIdentifier) 90 | } 91 | } 92 | 93 | extension UITableView { 94 | 95 | public func dequeueReusableHeaderFooterView(ofType type: T.Type) -> T { 96 | return dequeueReusableHeaderFooterView(withIdentifier: T.uniqueIdentifier) as! T 97 | } 98 | 99 | public func register(headerFooter: T.Type) { 100 | register(T.self, forHeaderFooterViewReuseIdentifier: T.uniqueIdentifier) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Support/LanguageSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LanguageSupport.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 05.05.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | #if swift(>=4.2) 12 | let collectionSectionHeaderType = UICollectionView.elementKindSectionHeader 13 | #else 14 | let collectionSectionHeaderType = UICollectionElementKindSectionHeader 15 | #endif 16 | 17 | #if swift(>=4.2) 18 | let collectionSectionFooterType = UICollectionView.elementKindSectionFooter 19 | #else 20 | let collectionSectionFooterType = UICollectionElementKindSectionFooter 21 | #endif 22 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/DiffResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffResult.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct DiffResult { 12 | public let inserts: IndexSet 13 | public let deletes: IndexSet 14 | public let moves: [List.MoveIndex] 15 | 16 | public var hasChanges: Bool { 17 | return changeCount > 0 18 | } 19 | 20 | public var changeCount: Int { 21 | return inserts.count + deletes.count + moves.count 22 | } 23 | 24 | init(result: List.Result.Batch) { 25 | inserts = result.inserts 26 | deletes = result.deletes 27 | moves = result.moves 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Differ.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Differ.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | public protocol Differ { 10 | func diffBetween(old: [T], new: [T]) -> DiffResult 11 | } 12 | 13 | public final class ListDiffer: Differ { 14 | 15 | public init() { } 16 | 17 | public func diffBetween(old: [T], new: [T]) -> DiffResult { 18 | let diff = List.diffing(old: old, new: new).forBatchUpdates() 19 | return DiffResult(result: diff) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Extensions/Array+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Additions.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 5/12/19. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | extension Array { 10 | 11 | mutating func move(at index: Int, to newIndex: Int) { 12 | let element = remove(at: index) 13 | insert(element, at: newIndex) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Extensions/String+Diffable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Diffable.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | extension String: Diffable { 10 | 11 | public var diffIdentifier: AnyHashable { 12 | return self 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Extensions/UICollectionView+Diff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+ListDiff.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | 13 | public func performUpdate(animated: Bool, updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { 14 | func applyUpdates() { 15 | performBatchUpdates(updates, completion: completion) 16 | } 17 | if animated { 18 | applyUpdates() 19 | } else { 20 | UIView.performWithoutAnimation(applyUpdates) 21 | } 22 | } 23 | 24 | public func apply(_ diff: DiffResult, section: Int = 0) { 25 | if !diff.deletes.isEmpty { 26 | deleteItems( 27 | at: diff.deletes.map { 28 | IndexPath(item: $0, section: section) 29 | } 30 | ) 31 | } 32 | if !diff.inserts.isEmpty { 33 | insertItems( 34 | at: diff.inserts.map { 35 | IndexPath(item: $0, section: section) 36 | } 37 | ) 38 | } 39 | for move in diff.moves { 40 | moveItem( 41 | at: IndexPath(item: move.from, section: section), 42 | to: IndexPath(item: move.to, section: section) 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Stan Chang Khin Boon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Library/Diffable.swift: -------------------------------------------------------------------------------- 1 | 2 | public protocol Diffable { 3 | var diffIdentifier: AnyHashable { get } 4 | } 5 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Library/ListDiff+BatchUpdates.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension List.Result { 4 | 5 | public struct Batch { 6 | public let inserts: IndexSet 7 | public let deletes: IndexSet 8 | public let moves: [List.MoveIndex] 9 | 10 | public var hasChanges: Bool { 11 | return changeCount > 0 12 | } 13 | 14 | public var changeCount: Int { 15 | return inserts.count + deletes.count + moves.count 16 | } 17 | 18 | fileprivate init(result: List.Result) { 19 | self.inserts = result.inserts 20 | self.deletes = result.deletes 21 | self.moves = result.moves 22 | } 23 | } 24 | 25 | public func forBatchUpdates() -> Batch { 26 | return Batch(result: preparedForBatchUpdates()) 27 | } 28 | 29 | private func preparedForBatchUpdates() -> List.Result { 30 | var result = self 31 | result.prepareForBatchUpdates() 32 | return result 33 | } 34 | 35 | private mutating func prepareForBatchUpdates() { 36 | // convert move+update to delete+insert, respecting the from/to of the move 37 | for (index, move) in moves.enumerated().reversed() { 38 | if updates.remove(move.from) != nil { 39 | moves.remove(at: index) 40 | deletes.insert(move.from) 41 | inserts.insert(move.to) 42 | } 43 | } 44 | 45 | // iterate all new identifiers. if its index is updated, delete from the old index and insert the new index 46 | for (key, oldIndex) in oldMap { 47 | if updates.contains(oldIndex), let newIndex = newMap[key] { 48 | deletes.insert(oldIndex) 49 | inserts.insert(newIndex) 50 | } 51 | } 52 | 53 | updates.removeAll() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CellViewModel/Sources/Vendor/ListDiff/Library/ListDiff.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private struct Stack { 4 | private(set) var items = [Element]() 5 | 6 | var isEmpty: Bool { 7 | return items.isEmpty 8 | } 9 | mutating func push(_ item: Element) { 10 | items.append(item) 11 | } 12 | mutating func pop() -> Element { 13 | return items.removeLast() 14 | } 15 | } 16 | 17 | /// https://github.com/Instagram/IGListKit/blob/master/Source/IGListDiff.mm 18 | public enum List { 19 | /// Used to track data stats while diffing. 20 | /// We expect to keep a reference of entry, thus its declaration as (final) class. 21 | private final class Entry { 22 | /// The number of times the data occurs in the old array 23 | var oldCounter: Int = 0 24 | /// The number of times the data occurs in the new array 25 | var newCounter: Int = 0 26 | /// The indexes of the data in the old array 27 | var oldIndexes: Stack = Stack() 28 | /// Flag marking if the data has been updated between arrays by equality check 29 | var updated: Bool = false 30 | /// Returns `true` if the data occur on both sides, `false` otherwise 31 | var occurOnBothSides: Bool { 32 | return newCounter > 0 && oldCounter > 0 33 | } 34 | func push(new index: Int?) { 35 | newCounter += 1 36 | oldIndexes.push(index) 37 | } 38 | func push(old index: Int?) { 39 | oldCounter += 1 40 | oldIndexes.push(index) 41 | } 42 | } 43 | 44 | /// Track both the entry and the algorithm index. Default the index to `nil` 45 | private struct Record { 46 | let entry: Entry 47 | var index: Int? 48 | init(_ entry: Entry) { 49 | self.entry = entry 50 | self.index = nil 51 | } 52 | } 53 | 54 | public struct MoveIndex: Equatable { 55 | public let from: Int 56 | public let to: Int 57 | 58 | public init(from: Int, to: Int) { 59 | self.from = from 60 | self.to = to 61 | } 62 | 63 | public static func == (lhs: MoveIndex, rhs: MoveIndex) -> Bool { 64 | return lhs.from == rhs.from && lhs.to == rhs.to 65 | } 66 | } 67 | 68 | public struct Result { 69 | public var inserts = IndexSet() 70 | public var updates = IndexSet() 71 | public var deletes = IndexSet() 72 | public var moves = [MoveIndex]() 73 | public var oldMap = [AnyHashable: Int]() 74 | public var newMap = [AnyHashable: Int]() 75 | public var hasChanges: Bool { 76 | return changeCount > 0 77 | } 78 | public var changeCount: Int { 79 | return inserts.count + deletes.count + updates.count + moves.count 80 | } 81 | public func validate(_ oldArray: [Diffable], _ newArray: [Diffable]) -> Bool { 82 | let diff = inserts.count - deletes.count 83 | return oldArray.count + diff == newArray.count 84 | } 85 | public func oldIndexFor(identifier: AnyHashable) -> Int? { 86 | return oldMap[identifier] 87 | } 88 | public func newIndexFor(identifier: AnyHashable) -> Int? { 89 | return newMap[identifier] 90 | } 91 | } 92 | 93 | // swiftlint:disable function_body_length 94 | 95 | public static func diffing(old oldArray: [T], new newArray: [T]) -> Result { 96 | // symbol table uses the old/new array `diffIdentifier` as the key and `Entry` as the value 97 | var table = [AnyHashable: Entry]() 98 | 99 | // pass 1 100 | // create an entry for every item in the new array 101 | // increment its new count for each occurence 102 | // record `nil` for each occurence of the item in the new array 103 | var newRecords = newArray.map { (newRecord) -> Record in 104 | let key = newRecord.diffIdentifier 105 | if let entry = table[key] { 106 | // add `nil` for each occurence of the item in the new array 107 | entry.push(new: nil) 108 | return Record(entry) 109 | } else { 110 | let entry = Entry() 111 | // add `nil` for each occurence of the item in the new array 112 | entry.push(new: nil) 113 | table[key] = entry 114 | return Record(entry) 115 | } 116 | } 117 | 118 | // pass 2 119 | // update or create an entry for every item in the old array 120 | // increment its old count for each occurence 121 | // record the old index for each occurence of the item in the old array 122 | // MUST be done in descending order to respect the oldIndexes stack construction 123 | var oldRecords = oldArray.enumerated().reversed().map { i, oldRecord -> Record in 124 | let key = oldRecord.diffIdentifier 125 | if let entry = table[key] { 126 | // push the old indices where the item occured onto the index stack 127 | entry.push(old: i) 128 | return Record(entry) 129 | } else { 130 | let entry = Entry() 131 | // push the old indices where the item occured onto the index stack 132 | entry.push(old: i) 133 | table[key] = entry 134 | return Record(entry) 135 | } 136 | } 137 | 138 | // pass 3 139 | // handle data that occurs in both arrays 140 | newRecords.enumerated().forEach { i, newRecord in 141 | guard newRecord.entry.occurOnBothSides else { 142 | return 143 | } 144 | let entry = newRecord.entry 145 | // grab and pop the top old index. if the item was inserted this will be nil 146 | assert(!entry.oldIndexes.isEmpty, "Old indexes is empty while iterating new item \(i). Should have nil") 147 | guard let oldIndex = entry.oldIndexes.pop() else { 148 | return 149 | } 150 | if oldIndex < oldArray.count { 151 | let n = newArray[i] 152 | let o = oldArray[oldIndex] 153 | if n != o { 154 | entry.updated = true 155 | } 156 | } 157 | 158 | // if an item occurs in the new and old array, it is unique 159 | // assign the index of new and old records to the opposite index (reverse lookup) 160 | newRecords[i].index = oldIndex 161 | oldRecords[oldIndex].index = i 162 | } 163 | 164 | // storage for final indexes 165 | var result = Result() 166 | 167 | // track offsets from deleted items to calculate where items have moved 168 | // iterate old array records checking for deletes 169 | // increment offset for each delete 170 | var runningOffset = 0 171 | let deleteOffsets = oldRecords.enumerated().map { i, oldRecord -> Int in 172 | let deleteOffset = runningOffset 173 | // if the record index in the new array doesn't exist, its a delete 174 | if oldRecord.index == nil { 175 | result.deletes.insert(i) 176 | runningOffset += 1 177 | } 178 | result.oldMap[oldArray[i].diffIdentifier] = i 179 | return deleteOffset 180 | } 181 | 182 | //reset and track offsets from inserted items to calculate where items have moved 183 | runningOffset = 0 184 | newRecords.enumerated().forEach { i, newRecord in 185 | let insertOffset = runningOffset 186 | if let oldIndex = newRecord.index { 187 | // note that an entry can be updated /and/ moved 188 | if newRecord.entry.updated { 189 | result.updates.insert(oldIndex) 190 | } 191 | 192 | // calculate the offset and determine if there was a move 193 | // if the indexes match, ignore the index 194 | let deleteOffset = deleteOffsets[oldIndex] 195 | if (oldIndex - deleteOffset + insertOffset) != i { 196 | result.moves.append(MoveIndex(from: oldIndex, to: i)) 197 | } 198 | } else { // add to inserts if the opposing index is nil 199 | result.inserts.insert(i) 200 | runningOffset += 1 201 | } 202 | result.newMap[newArray[i].diffIdentifier] = i 203 | } 204 | 205 | assert(result.validate(oldArray, newArray), "Sanity check failed applying \(result.inserts.count) inserts and \(result.deletes.count) deletes to old count \(oldArray.count) equaling new count \(newArray.count)") 206 | 207 | return result 208 | } 209 | 210 | // swiftlint:enable function_body_length 211 | } 212 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewControllers/BaseCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseCollectionViewController.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 17.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BaseCollectionViewController: UIViewController, UICollectionViewDelegateFlowLayout { 12 | 13 | private(set) open lazy var adapter = CollectionViewDataAdapter(collectionView: self._collectionView, inferModelTypes: self.automaticallyInferCellViewModelTypes) 14 | 15 | open var automaticallyInferCellViewModelTypes: Bool { 16 | return false 17 | } 18 | 19 | open var viewModels: [AnyCellViewModel.Type] { 20 | // must be implemented in subclasses 21 | return [] 22 | } 23 | 24 | open var supplementaryModels: [AnySupplementaryViewModel.Type] { 25 | // must be implemented in subclasses 26 | return [] 27 | } 28 | 29 | // MARK: - Views 30 | 31 | // must be set in subclasses 32 | open var _collectionView: UICollectionView! 33 | 34 | // MARK: - Life Cycle 35 | 36 | open override func viewDidLoad() { 37 | super.viewDidLoad() 38 | setupUI() 39 | } 40 | 41 | // MARK: - UI Setup 42 | 43 | open func setupUI() { 44 | _collectionView.delegate = self 45 | _collectionView.register(viewModels) 46 | _collectionView.register(supplementaryModels) 47 | } 48 | 49 | // MARK: - View Input 50 | 51 | open func setup(_ sections: [Section]) { 52 | adapter.data = sections 53 | } 54 | 55 | // MARK: - Collection View Delegate 56 | 57 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 58 | guard adapter.containsModel(at: indexPath), let item = adapter.itemModel(at: indexPath) as? InteractiveCellViewModel else { 59 | return 60 | } 61 | item.selectionHandler?() 62 | } 63 | 64 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 65 | guard adapter.contains(section: section) else { 66 | return .zero 67 | } 68 | return adapter.sectionModel(at: section).insets ?? .zero 69 | } 70 | 71 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 72 | guard adapter.contains(section: section) else { 73 | return 0 74 | } 75 | return adapter.sectionModel(at: section).lineSpacing ?? 0 76 | } 77 | 78 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 79 | guard adapter.contains(section: section) else { 80 | return .zero 81 | } 82 | let maxWidth = collectionView.bounds.width 83 | let height = adapter 84 | .headerModel(in: section) 85 | .flatMap { $0.height(constrainedBy: maxWidth) } 86 | ?? 0 87 | 88 | return CGSize(width: maxWidth, height: height) 89 | } 90 | 91 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 92 | guard adapter.contains(section: section) else { 93 | return .zero 94 | } 95 | let maxWidth = collectionView.bounds.width 96 | let height = adapter 97 | .footerModel(in: section) 98 | .flatMap { $0.height(constrainedBy: maxWidth) } 99 | ?? 0 100 | 101 | return CGSize(width: maxWidth, height: height) 102 | } 103 | 104 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 105 | guard adapter.containsModel(at: indexPath) else { 106 | return .zero 107 | } 108 | let insets = adapter.sectionModel(at: indexPath.section).insets ?? .zero 109 | let maxWidth = collectionView.bounds.width - insets.left - insets.right 110 | 111 | let viewModel = adapter.itemModel(at: indexPath) 112 | let height = viewModel.height(constrainedBy: maxWidth) ?? 0 113 | 114 | return CGSize(width: maxWidth, height: height) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewControllers/BaseDiffableCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDiffableCollectionViewController.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 5/12/19. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BaseDiffableCollectionViewController: UIViewController, UICollectionViewDelegateFlowLayout { 12 | 13 | private(set) open lazy var adapter = DiffableCollectionViewDataAdapter(collectionView: self._collectionView, 14 | inferModelTypes: self.automaticallyInferCellViewModelTypes) 15 | 16 | open var automaticallyInferCellViewModelTypes: Bool { 17 | return false 18 | } 19 | 20 | open var viewModels: [AnyCellViewModel.Type] { 21 | // must be implemented in subclasses 22 | return [] 23 | } 24 | 25 | open var supplementaryModels: [AnySupplementaryViewModel.Type] { 26 | // must be implemented in subclasses 27 | return [] 28 | } 29 | 30 | // MARK: - Views 31 | 32 | // must be set in subclasses 33 | open var _collectionView: UICollectionView! 34 | 35 | // MARK: - Life Cycle 36 | 37 | open override func viewDidLoad() { 38 | super.viewDidLoad() 39 | setupUI() 40 | } 41 | 42 | // MARK: - UI Setup 43 | 44 | open func setupUI() { 45 | _collectionView.delegate = self 46 | _collectionView.register(viewModels) 47 | _collectionView.register(supplementaryModels) 48 | } 49 | 50 | // MARK: - View Input 51 | 52 | open func setup(_ sections: [DiffableSection]) { 53 | adapter.update(data: sections, animated: false) 54 | } 55 | 56 | open func update(_ sections: [DiffableSection]) { 57 | adapter.update(data: sections, animated: true) 58 | } 59 | 60 | // MARK: - Collection View Delegate 61 | 62 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 63 | guard adapter.containsModel(at: indexPath), let item = adapter.itemModel(at: indexPath) as? InteractiveCellViewModel else { 64 | return 65 | } 66 | item.selectionHandler?() 67 | } 68 | 69 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 70 | guard adapter.contains(section: section) else { 71 | return .zero 72 | } 73 | return adapter.sectionModel(at: section).insets ?? .zero 74 | } 75 | 76 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 77 | guard adapter.contains(section: section) else { 78 | return .zero 79 | } 80 | return adapter.sectionModel(at: section).lineSpacing ?? 0 81 | } 82 | 83 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 84 | guard adapter.contains(section: section) else { 85 | return .zero 86 | } 87 | let maxWidth = collectionView.bounds.width 88 | let height = adapter 89 | .headerModel(in: section) 90 | .flatMap { $0.height(constrainedBy: maxWidth) } 91 | ?? 0 92 | 93 | return CGSize(width: maxWidth, height: height) 94 | } 95 | 96 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 97 | guard adapter.contains(section: section) else { 98 | return .zero 99 | } 100 | let maxWidth = collectionView.bounds.width 101 | let height = adapter 102 | .footerModel(in: section) 103 | .flatMap { $0.height(constrainedBy: maxWidth) } 104 | ?? 0 105 | 106 | return CGSize(width: maxWidth, height: height) 107 | } 108 | 109 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 110 | guard adapter.containsModel(at: indexPath) else { 111 | return .zero 112 | } 113 | let insets = adapter.sectionModel(at: indexPath.section).insets ?? .zero 114 | let maxWidth = collectionView.bounds.width - insets.left - insets.right 115 | 116 | let viewModel = adapter.itemModel(at: indexPath) 117 | let height = viewModel.height(constrainedBy: maxWidth) ?? 0 118 | 119 | return CGSize(width: maxWidth, height: height) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewControllers/BaseTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseTableViewController.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 09.05.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class BaseTableViewController: UIViewController, UITableViewDelegate { 12 | 13 | private(set) open lazy var adapter = TableViewDataAdapter(tableView: self._tableView, inferModelTypes: self.automaticallyInferCellViewModelTypes) 14 | 15 | open var automaticallyInferCellViewModelTypes: Bool { 16 | return false 17 | } 18 | 19 | open var viewModels: [AnyCellViewModel.Type] { 20 | // must be implemented in subclasses 21 | return [] 22 | } 23 | 24 | open var supplementaryModels: [AnySupplementaryViewModel.Type] { 25 | // must be implemented in subclasses 26 | return [] 27 | } 28 | 29 | // MARK: - Views 30 | 31 | // must be set in subclasses 32 | open var _tableView: UITableView! 33 | 34 | // MARK: - Life Cycle 35 | 36 | open override func viewDidLoad() { 37 | super.viewDidLoad() 38 | setupUI() 39 | } 40 | 41 | // MARK: - UI Setup 42 | 43 | open func setupUI() { 44 | _tableView.delegate = self 45 | _tableView.register(viewModels) 46 | _tableView.register(supplementaryModels) 47 | } 48 | 49 | // MARK: - View Input 50 | 51 | open func setup(_ sections: [Section]) { 52 | adapter.data = sections 53 | } 54 | 55 | // MARK: - Table View Delegate 56 | 57 | open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 58 | guard adapter.containsModel(at: indexPath), let item = adapter.itemModel(at: indexPath) as? InteractiveCellViewModel else { 59 | return 60 | } 61 | item.selectionHandler?() 62 | } 63 | 64 | open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 65 | guard adapter.containsModel(at: indexPath) else { 66 | return 0 67 | } 68 | return adapter.itemModel(at: indexPath).height(constrainedBy: tableView.bounds.width) ?? tableView.rowHeight 69 | } 70 | 71 | open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 72 | guard adapter.contains(section: section) else { 73 | return nil 74 | } 75 | return adapter.headerModel(in: section).flatMap { 76 | tableView.dequeueReusableSupplementaryView(with: $0, for: section) 77 | } 78 | } 79 | 80 | open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 81 | guard adapter.contains(section: section) else { 82 | return nil 83 | } 84 | return adapter.footerModel(in: section).flatMap { 85 | tableView.dequeueReusableSupplementaryView(with: $0, for: section) 86 | } 87 | } 88 | 89 | open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 90 | guard adapter.contains(section: section) else { 91 | return 0 92 | } 93 | return adapter.headerModel(in: section)?.height(constrainedBy: tableView.bounds.width) ?? 0 94 | } 95 | 96 | open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 97 | guard adapter.contains(section: section) else { 98 | return 0 99 | } 100 | return adapter.footerModel(in: section)?.height(constrainedBy: tableView.bounds.width) ?? 0 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/Accessibility/Accessible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accessible.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Accessible { 12 | var accessibilityIdentifier: String? { get } 13 | var accessibilityOptions: AccessibilityDisplayOptions { get } 14 | } 15 | 16 | extension Accessible { 17 | 18 | public var accessibilityIdentifier: String? { 19 | return nil 20 | } 21 | 22 | public var accessibilityOptions: AccessibilityDisplayOptions { 23 | return .none 24 | } 25 | 26 | public func accessibilityIdentifier(for indexPath: IndexPath) -> String? { 27 | guard let accessibilityIdentifier = accessibilityIdentifier else { 28 | return nil 29 | } 30 | let options = accessibilityOptions 31 | if options.contains(.section) && options.contains(.row) { 32 | return "\(accessibilityIdentifier)_\(indexPath.section)_\(indexPath.row)" 33 | 34 | } else if options.contains(.section) { 35 | return "\(accessibilityIdentifier)_\(indexPath.section)" 36 | 37 | } else if options.contains(.row) { 38 | return "\(accessibilityIdentifier)_\(indexPath.row)" 39 | 40 | } else { 41 | return accessibilityIdentifier 42 | } 43 | } 44 | 45 | public func accessibilityIdentifier(for section: Int) -> String? { 46 | guard let accessibilityIdentifier = accessibilityIdentifier else { 47 | return nil 48 | } 49 | if accessibilityOptions.contains(.section) { 50 | return "\(accessibilityIdentifier)_\(section)" 51 | } else { 52 | return accessibilityIdentifier 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/Accessibility/AccessiblityDisplayOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessiblityDisplayOptions.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | public struct AccessibilityDisplayOptions: OptionSet { 10 | public let rawValue: Int 11 | 12 | public init(rawValue: Int) { 13 | self.rawValue = rawValue 14 | } 15 | 16 | public static let none = AccessibilityDisplayOptions([]) 17 | public static let section = AccessibilityDisplayOptions(rawValue: 1 << 0) 18 | public static let row = AccessibilityDisplayOptions(rawValue: 1 << 1) 19 | public static let all = AccessibilityDisplayOptions(rawValue: Int.max) 20 | } 21 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/CellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cell+ViewModel.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias AnyViewCell = UIView 12 | 13 | public protocol AnyCellViewModel: Reusable, Accessible { 14 | static var cellClass: AnyClass { get } 15 | func setup(cell: AnyViewCell) 16 | func height(constrainedBy maxWidth: CGFloat) -> CGFloat? 17 | } 18 | 19 | extension AnyCellViewModel { 20 | public func height(constrainedBy maxWidth: CGFloat) -> CGFloat? { 21 | return nil 22 | } 23 | } 24 | 25 | public protocol CellViewModel: AnyCellViewModel { 26 | associatedtype Cell: AnyViewCell 27 | func setup(cell: Cell) 28 | } 29 | 30 | extension CellViewModel { 31 | public static var cellClass: AnyClass { 32 | return Cell.self 33 | } 34 | 35 | public func setup(cell: AnyViewCell) { 36 | // swiftlint:disable force_cast 37 | setup(cell: cell as! Cell) 38 | // swiftlint:enable force_cast 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/DiffableCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableCellViewModel.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 14.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | public protocol DiffableCellViewModel: AnyCellViewModel, Diffable { 10 | func isEqual(to model: DiffableCellViewModel) -> Bool 11 | } 12 | 13 | extension DiffableCellViewModel { 14 | public var diffIdentifier: AnyHashable { 15 | return String(describing: type(of: self)) 16 | } 17 | 18 | public func isEqual(to model: DiffableCellViewModel) -> Bool { 19 | return model is Self 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/InteractiveCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractiveCellViewModel.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 03.04.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | public protocol InteractiveCellViewModel { 10 | var selectionHandler: (() -> Void)? { get } 11 | } 12 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reusable.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Reusable { 12 | static var uniqueIdentifier: String { get } 13 | } 14 | 15 | extension Reusable { 16 | public static var uniqueIdentifier: String { 17 | return String(describing: self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/SupplementaryKind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupplementaryKind.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 09.05.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | public enum SupplementaryKind { 10 | case header 11 | case footer 12 | 13 | var rawValue: String { 14 | switch self { 15 | case .header: 16 | return collectionSectionHeaderType 17 | case .footer: 18 | return collectionSectionFooterType 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/SupplementaryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupplementaryViewModel.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias AnySupplementaryView = UIView 12 | 13 | public protocol AnySupplementaryViewModel: Reusable, Accessible { 14 | static var supplementaryKind: SupplementaryKind { get } 15 | static var supplementaryViewClass: AnyClass { get } 16 | func setup(view: AnySupplementaryView) 17 | func height(constrainedBy maxWidth: CGFloat) -> CGFloat? 18 | } 19 | 20 | extension AnySupplementaryViewModel { 21 | public static var supplementaryKind: SupplementaryKind { 22 | return .header 23 | } 24 | public func height(constrainedBy maxWidth: CGFloat) -> CGFloat? { 25 | return nil 26 | } 27 | } 28 | 29 | public protocol SupplementaryViewModel: AnySupplementaryViewModel { 30 | associatedtype View: AnySupplementaryView 31 | func setup(view: View) 32 | } 33 | 34 | extension SupplementaryViewModel { 35 | public static var supplementaryViewClass: AnyClass { 36 | return View.self 37 | } 38 | 39 | public func setup(view: AnySupplementaryView) { 40 | // swiftlint:disable force_cast 41 | setup(view: view as! View) 42 | // swiftlint:enable force_cast 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CellViewModel/Sources/ViewModels/XibInitializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XibInitializable.swift 3 | // CellViewModel 4 | // 5 | // Created by Anton Poltoratskyi on 02.02.2019. 6 | // Copyright © 2019 Anton Poltoratskyi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol XibInitializable: class { 12 | static var xibFileName: String { get } 13 | } 14 | 15 | extension XibInitializable where Self: UIView { 16 | public static var xibFileName: String { 17 | return String(describing: self) 18 | } 19 | 20 | public func loadFromXib() { 21 | let bundle = Bundle(for: type(of: self)) 22 | let nib = UINib(nibName: type(of: self).xibFileName, bundle: bundle) 23 | guard let contentView = nib.instantiate(withOwner: self, options: nil).first as? UIView else { 24 | fatalError("object not found") 25 | } 26 | contentView.translatesAutoresizingMaskIntoConstraints = false 27 | 28 | addSubview(contentView) 29 | NSLayoutConstraint.activate([ 30 | contentView.leadingAnchor.constraint(equalTo: leadingAnchor), 31 | contentView.trailingAnchor.constraint(equalTo: trailingAnchor), 32 | contentView.topAnchor.constraint(equalTo: topAnchor), 33 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor)] 34 | ) 35 | } 36 | } 37 | 38 | extension XibInitializable where Self: UIViewController { 39 | public static var xibFileName: String { 40 | return String(describing: self) 41 | } 42 | 43 | public static func instantiateFromXib() -> Self { 44 | return Self(nibName: xibFileName, bundle: Bundle(for: self)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Poltoratskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org) 2 | [![Xcode](https://img.shields.io/badge/Xcode-12.0-blue.svg)](https://developer.apple.com/xcode) 3 | [![MIT](https://img.shields.io/badge/License-MIT-red.svg)](https://opensource.org/licenses/MIT) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/CellViewModel.svg)](https://cocoapods.org/pods/CellViewModel) 5 | 6 | # CellViewModel 7 | 8 | Using CellViewModel to configure you UITableViewCell or UICollectionViewCell is just a one possible approach of work with UIKit's collections. 9 | 10 | ## Requirements: 11 | - iOS 9.0+ 12 | - Xcode 12.0+ 13 | - Swift 5.0+ 14 | 15 | ## Installation 16 | 17 | #### CocoaPods 18 | 19 | ```ruby 20 | target 'MyApp' do 21 | pod 'CellViewModel', '~> 1.8.0' 22 | end 23 | ``` 24 | 25 | #### Carthage 26 | 27 | ```ogdl 28 | github "devpolant/CellViewModel" "master" 29 | ``` 30 | 31 | ## Usage 32 | 33 | **Works with UITableView & UICollectionView** - one possible approach, inspired by **CocoaHeads**: 34 | 35 | You can move configuration logic for **UITableViewCell** or **UICollectionViewCell** from **-cellForRowAtIndexPath:** to separate types. 36 | 37 | ### Native setup 38 | 39 | 1) Create cell class and appropriate type that conforms to **CellViewModel** type: 40 | 41 | ```Swift 42 | public typealias AnyViewCell = UIView 43 | 44 | public protocol CellViewModel: AnyCellViewModel { 45 | associatedtype Cell: AnyViewCell 46 | func setup(cell: Cell) 47 | } 48 | ``` 49 | 50 | > **UserTableViewCell.swift** 51 | 52 | ```Swift 53 | import CellViewModel 54 | 55 | // MARK: - View Model 56 | 57 | struct UserCellModel: CellViewModel { 58 | var user: User 59 | 60 | func setup(cell: UserTableViewCell) { 61 | cell.nameLabel.text = user.name 62 | } 63 | } 64 | 65 | // MARK: - Cell 66 | 67 | final class UserTableViewCell: UITableViewCell, XibInitializable { 68 | @IBOutlet weak var nameLabel: UILabel! 69 | } 70 | ``` 71 | 72 | 2) Register created model type: 73 | ```Swift 74 | tableView.register(UserCellModel.self) 75 | ``` 76 | 77 | By registering model type it will be checked if cell class conforms to XibInitializable or not in order to register `UINib` or just cell's class type. 78 | 79 | 3) Then store your models in array (or your custom datasource type): 80 | 81 | ```Swift 82 | private var users: [AnyCellViewModel] = [] 83 | ``` 84 | 85 | **AnyCellViewModel** is a base protocol of **CellViewModel**. 86 | It's needed only in order to fix compiler limitation as **you can use protocols with associatedtype only as generic constraints** and can't write something like this: 87 | 88 | ```Swift 89 | private var users: [CellViewModel] = [] // won't compile 90 | ``` 91 | 92 | 4) **UITableViewDataSource** implementation is very easy, even if you have multiple cell types, because all 'cellForRow' logic is contained in our view models: 93 | 94 | ```Swift 95 | import CellViewModel 96 | 97 | class ViewController: UIViewController { 98 | 99 | @IBOutlet weak var tableView: UITableView! 100 | 101 | private var users: [AnyCellViewModel] = [] 102 | 103 | override func viewDidLoad() { 104 | super.viewDidLoad() 105 | users = User.testDataSource.map { UserCellModel(user: $0) } 106 | tableView.register(nibModel: UserCellModel.self) 107 | } 108 | } 109 | 110 | extension ViewController: UITableViewDataSource { 111 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 112 | return users.count 113 | } 114 | 115 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 116 | return tableView.dequeueReusableCell(with: tableModel(at: indexPath), for: indexPath) 117 | } 118 | 119 | private func tableModel(at indexPath: IndexPath) -> AnyCellViewModel { 120 | return users[indexPath.row] 121 | } 122 | } 123 | ``` 124 | 125 | ### Quick Setup 126 | 127 | Use existed adapters in order to perform quick setup. 128 | 129 | 1. For `UITableView` - **TableViewDataAdapter** 130 | 131 | ```swift 132 | private lazy var adapter = TableViewDataAdapter(tableView: self.tableView) 133 | 134 | // ... 135 | 136 | func setup(users: [AnyCellViewModel]) { 137 | adapter.data = users 138 | } 139 | ``` 140 | 141 | Updating `data` property will call `reloadData()`. 142 | 143 | 2. For `UICollectionView` - **CollectionViewDataAdapter**: 144 | 145 | ```swift 146 | private lazy var adapter = CollectionViewDataAdapter(tableView: self.collectionView) 147 | 148 | // ... 149 | 150 | func setup(users: [AnyCellViewModel]) { 151 | adapter.data = users 152 | } 153 | ``` 154 | 155 | Both adapters already conform to appropriate datasource protocol: `UICollectionViewDataSource` and `UITableViewDataSource`. 156 | 157 | ### Base View Controller 158 | 159 | The most simplier way to set up is to inherit from `BaseCollectionViewController`. 160 | 161 | Sometimes you need a table UI, but with some unique section insets or interitem spacing. For this case `BaseCollectionViewController` provides default implementation of `UICollectionViewDelegateFlowLayout` protocol to match the table UI for you. 162 | 163 | #### Usage 164 | 165 | ```swift 166 | 167 | final class UsersViewController: BaseCollectionViewController { 168 | 169 | @IBOutlet weak var collectionView: UICollectionView { 170 | didSet { 171 | // initialize reference in base class 172 | _collectionView = collectionView 173 | } 174 | } 175 | 176 | override var viewModels: [AnyCellViewModel.Type] { 177 | return [ 178 | UserCellModel.self, 179 | // ... add more 180 | ] 181 | } 182 | 183 | override var supplementaryModels: [AnySupplementaryViewModel.Type] { 184 | return [ 185 | UserHeaderModel.self, 186 | /// ... add more 187 | ] 188 | } 189 | 190 | // ... your domain code 191 | ``` 192 | 193 | `BaseCollectionViewController` is a wrapper for `CollectionViewDataAdapter`, so it is already have setup method: 194 | 195 | ```swift 196 | open func setup(_ sections: [Section]) { 197 | adapter.data = sections 198 | } 199 | ``` 200 | 201 | `Section` type is a container for header, footer, items models and layout information like spacings etc. 202 | 203 | ```swift 204 | public final class Section { 205 | 206 | public var insets: UIEdgeInsets? 207 | public var lineSpacing: CGFloat? 208 | public var header: AnySupplementaryViewModel? 209 | public var footer: AnySupplementaryViewModel? 210 | public var items: [AnyCellViewModel] 211 | 212 | /// ... 213 | } 214 | ``` 215 | 216 | Override `automaticallyInferCellViewModelTypes` in order to allow to automatically infer type of used view models instead of explicitly declare them in `viewModels` and `supplementaryModels` properties. 217 | 218 | ```swift 219 | override var automaticallyInferCellViewModelTypes: Bool { 220 | return true 221 | } 222 | ``` 223 | 224 | 225 | ### Accessibility 226 | 227 | Sometimes there is a need to define `accessibilityIdentifier` for UI testing purposes. 228 | 229 | There is [Accessible](https://github.com/devpolant/CellViewModel/blob/master/CellViewModel/Sources/ViewModels/Accessibility/Accessible.swift) protocol that is conformed by CellViewModel protocol. 230 | 231 | ```swift 232 | public protocol Accessible { 233 | var accessibilityIdentifier: String? { get } 234 | var accessibilityOptions: AccessibilityDisplayOptions { get } 235 | } 236 | ``` 237 | 238 | So you need to define `accessibilityIdentifier` property in your model type implementation: 239 | 240 | ```swift 241 | struct UserCellModel: CellViewModel { 242 | 243 | var accessibilityIdentifier: String? { 244 | return "user_cell" 245 | } 246 | 247 | // ... 248 | } 249 | ``` 250 | 251 | And define `accessibilityOptions` if needed to add index path as suffix in the end of `accessibilityIdentifier`: 252 | 253 | ```swift 254 | struct UserCellModel: CellViewModel { 255 | 256 | var accessibilityIdentifier: String? { 257 | return "user_cell" 258 | } 259 | 260 | var accessibilityOptions: AccessibilityDisplayOptions { 261 | return [.row, .section] 262 | } 263 | 264 | // ... 265 | } 266 | ``` 267 | 268 | ## License 269 | 270 | **CellViewModel** is available under the MIT license. See the [LICENSE](https://github.com/devpolant/CellViewModel/blob/master/LICENSE) file for more info. 271 | --------------------------------------------------------------------------------