├── .gitignore ├── LICENSE ├── README.md ├── TableCollectionManager.podspec └── TableCollectionManager ├── TableCollectionManager.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── TableCollectionManager ├── CollectionManager │ ├── Actions │ │ ├── CollectionActions.swift │ │ └── CollectionLayoutHandler.swift │ ├── CellRegisterer │ │ └── CollectionCellRegisterer.swift │ ├── Configurable │ │ └── CollectionConfigurable.swift │ ├── HeaderFooter │ │ ├── CollectionHeaderFooter.swift │ │ └── CollectionHeaderFooterItem.swift │ ├── Manager │ │ ├── CollectionManager+UICollectionViewDataSource.swift │ │ ├── CollectionManager+UICollectionViewDelegate.swift │ │ ├── CollectionManager+UICollectionViewDelegateFlowLayout.swift │ │ ├── CollectionManager+UIScrollViewDelegate.swift │ │ └── CollectionManager.swift │ ├── Row │ │ ├── CollectionRow.swift │ │ ├── CollectionRowActionResult.swift │ │ ├── CollectionRowActionsHandler.swift │ │ └── CollectionRowItem.swift │ ├── Section │ │ ├── CollectionSection.swift │ │ └── CollectionSectionsHandler.swift │ └── Storage │ │ └── CollectionStorage.swift ├── Common │ ├── Actions │ │ └── ScrollDelegateActionsHandler.swift │ ├── Configurable │ │ └── Configurable.swift │ ├── Section │ │ ├── Section.swift │ │ └── SectionsHandler.swift │ ├── Storage │ │ ├── Storage.swift │ │ └── StorageUpdate.swift │ └── Utils │ │ └── HashableItem.swift ├── Info.plist ├── TableCollectionManager.h └── TableManager │ ├── Actions │ └── TableActions.swift │ ├── CellRegisterer │ └── TableCellRegisterer.swift │ ├── Configurable │ └── TableConfigurable.swift │ ├── HeaderFooter │ ├── TableHeaderFooter.swift │ └── TableHeaderFooterItem.swift │ ├── Manager │ ├── TableManager+UIScrollViewDelegate.swift │ ├── TableManager+UITableViewDataSource.swift │ ├── TableManager+UITableViewDelegate.swift │ └── TableManager.swift │ ├── Row │ ├── TableRow.swift │ ├── TableRowActionResult.swift │ ├── TableRowActionsHandler.swift │ └── TableRowItem.swift │ ├── Section │ ├── TableSection.swift │ └── TableSectionsHandler.swift │ └── Storage │ └── TableStorage.swift ├── TableCollectionManagerExample ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Common │ ├── TitleCell.swift │ └── TitleHeaderFooter.swift ├── ExampleControllers │ ├── CollectionViewExampleVC.swift │ ├── TableViewExampleVC.swift │ └── TableViewReorderingVC.swift ├── Info.plist ├── MenuVC.swift └── SceneDelegate.swift └── TableCollectionManagerTests ├── Info.plist └── TableCollectionManagerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | *.DS_Store 3 | 4 | # Sonar 5 | .scannerwork/ 6 | sonar-reports/ 7 | .xcodebuild.log 8 | compile_commands.json 9 | 10 | 11 | # Xcode 12 | *.pbxuser 13 | *.mode1v3 14 | *.mode2v3 15 | *.perspectivev3 16 | *.xcuserdatad 17 | 18 | # build 19 | build/ 20 | Build/ 21 | DerivedData/ 22 | vendor/ 23 | .bundle/ 24 | 25 | # temp nibs and swap files 26 | *~.nib 27 | *.swp 28 | *.orig 29 | 30 | # CocoaPods 31 | Pods/ 32 | 33 | # AppCode 34 | .idea 35 | atlassian-ide-plugin.xml 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmytro Mishchenko 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 | # TableCollectionManager 2 | 3 | `TableCollectionManager` is lightweight generic library that allows you to build table and collection views in a declarative type-safe style. 4 | 5 | - [x] Type-safe generic cells and models 6 | 7 | - [x] Functional programming style 8 | - [x] No need for delegates and data sources 9 | - [x] Automatic update for View with Model 10 | 11 | ## Table of content 12 | 13 | - [Installation](#Installation) 14 | - [Usage](#Usage) 15 | - [TableView](#TableView) 16 | - [Quick start](#Table-Quick-start) 17 | - [Cell](#TableCell) 18 | - [Row](#TableRow) 19 | - [Header/Footer](#TableHeaderFooter) 20 | - [Section](#TableSection) 21 | - [Storage](#TableStorage) 22 | - [Manager](#TableManager) 23 | - [CollectionView](#CollectionView) 24 | - [Quick start](#Collection-Quick-start) 25 | - [Cell](#CollectionCell) 26 | - [Row](#CollectionRow) 27 | - [Header/Footer](#CollectionHeaderFooter) 28 | - [Section](#CollectionSection) 29 | - [Storage](#CollectionStorage) 30 | - [Manager](#CollectionManager) 31 | - [License](#License) 32 | 33 | ## Installation 34 | 35 | - add following to `Podfile` 36 | ``` ruby 37 | pod 'TableCollectionManager' 38 | ``` 39 | - Run `pod install`. 40 | - Add to files: 41 | ``` swift 42 | import TableCollectionManager 43 | ``` 44 | 45 | ## Usage 46 | 47 | The main idea is that you have manager to manage table/collection view. And the manager have storage that you can update with items. After that magic happens and view will updates and reloads automatically. Also you can easily handle actions in functional style. 48 | 49 | ### TableView 50 | 51 | #### Table Quick start 52 | 53 | ```swift 54 | import TableCollectionManager 55 | 56 | // create cell and implement TableConfigurable 57 | 58 | class ExampleCell: UITableViewCell, TableConfigurable { 59 | 60 | func update(with model: String) { 61 | textLabel?.text = model 62 | } 63 | } 64 | 65 | class VC: UIViewController { 66 | 67 | // init manager 68 | let manager = TableManager() 69 | var tableView = UITableView(frame: .zero, style: .plain) 70 | 71 | override func viewDidLoad() { 72 | super.viewDidLoad() 73 | 74 | // set tableView to manager 75 | manager.tableView = tableView 76 | 77 | // create rows 78 | 79 | let firstRow = TableRow("First").didSelect { result in 80 | // result.model - cell model 81 | // result.cell - cell 82 | // result.indexPath - indexPath 83 | print("first") 84 | } 85 | 86 | let secondRow = TableRow("Second").didSelect { _ in 87 | print("second") 88 | } 89 | 90 | // add rows to storage 91 | manager.storage.append([firstRow, secondRow]) 92 | } 93 | } 94 | 95 | ``` 96 | 97 | #### TableCell 98 | 99 | To use cells with TableManager you have to implement `TableConfigurable` protocol. 100 | 101 | You have to implement `update(with:)` func to provide model type. 102 | 103 | Imagine you have `Pet` object and you want to create cell to show data of this item. In this case cell will look like this. 104 | 105 | ```swift 106 | struct Pet { 107 | 108 | let name: String 109 | let age: Int 110 | } 111 | 112 | class PetCell: UITableViewCell, TableConfigurable { 113 | 114 | func update(with model: Pet) { 115 | // fill cell with data 116 | textLabel?.text = "\(model.name) is \(model.age) years old" 117 | } 118 | } 119 | ``` 120 | 121 | You can also provide cell `height` and `estimatedHeight`. By default they equals `UITableView.automaticDimension`. 122 | 123 | `````swift 124 | class Cell: UITableViewCell, TableConfigurable { 125 | 126 | // custom height 127 | static var height: CGFloat { 128 | 44 129 | } 130 | 131 | // custom estimatedHeight 132 | static var estimatedHeight: CGFloat { 133 | 44 134 | } 135 | 136 | func update(with model: String) { 137 | 138 | } 139 | } 140 | ````` 141 | 142 | You also don't have to register cell. `TableManager` will do this for you. 143 | 144 | #### TableRow 145 | 146 | `TableRow` is a wrapper around cell. So you only work with rows and models (viewModel) for this row. 147 | 148 | To create `TableRow` provide cell type and model instance. 149 | 150 | ```swift 151 | let pet = Pet(name: "Elon", age: 2) 152 | let petRow = TableRow(pet) 153 | ``` 154 | 155 | You can also subscribe on some actions from `UITableViewDelegate/UITableViewDataSource`. For example you want `didSelect` for this row. In this case just call `didSelect`. 156 | 157 | ```swift 158 | petRow.didSelect { result in 159 | // do something 160 | // result.model - cell model 161 | // result.cell - cell 162 | // result.indexPath - indexPath 163 | } 164 | ``` 165 | 166 | `TableRow` supports this actions: 167 | 168 | - `didSelect` 169 | 170 | - `didDeselect` 171 | 172 | - `willSelect` 173 | 174 | - `willDeselect` 175 | 176 | - `willDisplay` 177 | 178 | - `didEndDisplaying` 179 | 180 | - `moveTo` 181 | 182 | - `shouldHighlight` 183 | 184 | - `editingStyle` 185 | 186 | - `leadingSwipeActions` 187 | 188 | - `trailingSwipeActions` 189 | 190 | - `canEdit` 191 | 192 | - `canMove` 193 | - ` commitStyle` 194 | - ` move` 195 | 196 | You can also chain actions. 197 | 198 | ```swift 199 | let row = TableRow(model).didSelect { _ in 200 | print("selected") 201 | }.willDisplay { _ in 202 | print("willDisplay") 203 | }.canEdit { _ in 204 | return true 205 | } 206 | ``` 207 | 208 | #### TableHeaderFooter 209 | 210 | Headers and footer works almost like cells. The only difference is that `estimatedHeight` not optional. 211 | 212 | ```swift 213 | class HeaderFooter: UITableViewHeaderFooterView, TableConfigurable { 214 | 215 | // required 216 | static var estimatedHeight: CGFloat { 217 | 44 218 | } 219 | 220 | // optional (UITableView.automaticDimension by default) 221 | static var height: CGFloat { 222 | 44 223 | } 224 | 225 | func update(with model: String) { 226 | textLabel?.text = model 227 | } 228 | } 229 | ``` 230 | 231 | `TableHeaderFooter` is a wrapper for headers and footers. To add them to table view just do this. 232 | 233 | ```swift 234 | let header = TableHeaderFooter("Header") 235 | // add header to section at index 236 | manager.storage.setHeader(header, to: 0) 237 | 238 | let footer = TableHeaderFooter("Footer") 239 | // add header to section at index 240 | manager.storage.setFooter(footer, to: 1) 241 | ``` 242 | 243 | #### TableSection 244 | 245 | `TableSection` is a wrapper around `UITableView` sections. If you have more then one section in you table view you should use `TableSection`. You can set rows, header and footer to each section. 246 | 247 | ```swift 248 | let rows = (1...10).map { TableRow("\($0)") } 249 | let header = TableHeaderFooter("Header") 250 | let footer = TableHeaderFooter("Footer") 251 | 252 | let section = TableSection( 253 | rows: rows, 254 | header: header, 255 | footer: footer 256 | ) 257 | 258 | manager.storage.append(section) 259 | ``` 260 | 261 | #### TableStorage 262 | 263 | `TableStorage` is a place where all sections and rows store. Each `TableManager` has storage. And if pass data to this storage table view will updates automatically. 264 | 265 | ```swift 266 | manager.storage.append(sections) 267 | manager.storage.delete(row) 268 | manager.storage.reload(rows) 269 | // and ect. 270 | ``` 271 | 272 | You can also get index of section, indexPath of row, section at index or row at indexPath. 273 | 274 | ```swift 275 | let sectionIndex = manager.storage.index(for: section) 276 | let rowIndexPath = manager.storage.indexPath(for: row) 277 | 278 | let section = manager.storage.section(at: index) 279 | let row = manager.storage.row(at: indexPath) 280 | ``` 281 | 282 | Storage has `performWithoutAnimation` block. Every change you call inside this block will perform without animation. 283 | 284 | ```swift  285 | manager.storage.performWithoutAnimation { 286 | manager.storage.append(sections) 287 | manager.storage.reload(rows) 288 | } 289 | ``` 290 | 291 | Storage has `performBatchUpdate` block. You can use this method in cases where you want to make multiple changes in one single animated operation. 292 | 293 | ```swift 294 | manager.storage.performBatchUpdate({ 295 | manager.storage.append(sections) 296 | manager.storage.delete(row) 297 | }, completion: { isFinished in 298 | // completion 299 | }) 300 | ``` 301 | 302 | All storage functionality 303 | 304 | - `isEmpty` 305 | 306 | - `numberOfSections` 307 | 308 | - `numberOfAllRows` 309 | 310 | - `index(for section:)` 311 | 312 | - `indexPath(for row:)` 313 | 314 | - `section(at index:)` 315 | 316 | - `row(at indexPath:)` 317 | 318 | - `performWithoutAnimation(_ block:)` 319 | 320 | - `performBatchUpdate(_ block:, completion:)` 321 | 322 | - `reload()` 323 | 324 | - `reload(_ row:)` 325 | 326 | - `reload(_ rows:)` 327 | 328 | - `reload(_ section:)` 329 | 330 | - `reload(_ sections:)` 331 | 332 | - `append(_ row:)` 333 | 334 | - `append(_ rows:)` 335 | 336 | - `append(_ row:, to sectionIndex:)` 337 | 338 | - `append(_ rows:, to sectionIndex:)` 339 | 340 | - `append(_ section:)` 341 | 342 | - `append(_ sections:)` 343 | 344 | - `insert(_ row:, at indexPath:)` 345 | 346 | - `insert(_ section:, at index:)` 347 | 348 | - `delete(_ row:)` 349 | 350 | - `delete(_ rows:)` 351 | 352 | - `delete(_ section:)` 353 | 354 | - `delete(_ sections:)` 355 | 356 | - `deleteAllItems()` 357 | 358 | - `moveRowWithoutUpdate(from source:, to destination:)` 359 | 360 | - `moveRow(from source:, to destination:)` 361 | 362 | - `moveSection(from source:, to destination:)` 363 | 364 | #### TableManager 365 | 366 | `TableManager` is object that manage all the delegates and data sources. Manager holds `storage` and `tableView`. You can change both of them. 367 | 368 | Manager also has `scrollHandler` which handles `UIScrollViewDelegate`. 369 | 370 | ```swift 371 | manager.scrollHandler.didScroll { 372 | print("didScroll") 373 | }.shouldScrollToTop { 374 | return true 375 | } 376 | ``` 377 | 378 | ### CollectionView 379 | 380 | #### Collection Quick start 381 | 382 | ```swift 383 | import TableCollectionManager 384 | 385 | // create cell and implement CollectionConfigurable 386 | 387 | class ExampleCell: UICollectionViewCell, CollectionConfigurable { 388 | 389 | func update(with model: String) { 390 | textLabel?.text = model 391 | } 392 | } 393 | 394 | class VC: UIViewController { 395 | 396 | // init manager 397 | let manager = CollectionManager() 398 | var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 399 | 400 | override func viewDidLoad() { 401 | super.viewDidLoad() 402 | 403 | // set collectionView to manager 404 | manager.collectionView = collectionView 405 | 406 | // create rows 407 | 408 | let firstRow = CollectionRow("First").didSelect { result in 409 | // result.model - cell model 410 | // result.cell - cell 411 | // result.indexPath - indexPath 412 | print("first") 413 | } 414 | 415 | let secondRow = CollectionRow("Second").didSelect { _ in 416 | print("second") 417 | } 418 | 419 | // add rows to storage 420 | manager.storage.append([firstRow, secondRow]) 421 | } 422 | } 423 | 424 | ``` 425 | 426 | #### CollectionCell 427 | 428 | To use cells with TableManager you have to implement `CollectionConfigurable` protocol. 429 | 430 | You have to implement `update(with:)` func to provide model type. 431 | 432 | Imagine you have `Pet` object and you want to create cell to show data of this item. In this case cell will look like this. 433 | 434 | ```swift 435 | struct Pet { 436 | 437 | let name: String 438 | let age: Int 439 | } 440 | 441 | class PetCell: UICollectionViewCell, CollectionConfigurable { 442 | 443 | func update(with model: Pet) { 444 | // fill cell with data 445 | } 446 | } 447 | ``` 448 | 449 | You can also provide cell `size`. It's optional you can also provide height using `UICollectionViewLayout` or using `layoutHandler` of [ `CollectionManager`](#CollectionManager). 450 | 451 | `````swift 452 | class Cell: UICollectionViewCell, CollectionConfigurable { 453 | 454 | // custom size 455 | static var size: CGSize? { 456 | CGSize(width: 44, height: 44) 457 | } 458 | 459 | func update(with model: String) { 460 | 461 | } 462 | } 463 | ````` 464 | 465 | You also don't have to register cell. `CollectionManager` will do this for you. 466 | 467 | #### CollectionRow 468 | 469 | `CollectionRow` is a wrapper around cell. So you only work with rows and models (viewModel) for this row. 470 | 471 | To create `CollectionRow` provide cell type and model instance. 472 | 473 | ```swift 474 | let pet = Pet(name: "Elon", age: 2) 475 | let petRow = CollectionRow(pet) 476 | ``` 477 | 478 | You can also subscribe on some actions from `UICollectionViewDelegate/UICollectionViewDataSource`. For example you want `didSelect` for this row. In this case just call `didSelect`. 479 | 480 | ```swift 481 | petRow.didSelect { result in 482 | // do something 483 | // result.model - cell model 484 | // result.cell - cell 485 | // result.indexPath - indexPath 486 | } 487 | ``` 488 | 489 | `CollectionRow` supports this actions: 490 | 491 | - `didSelect` 492 | 493 | - `didDeselect` 494 | 495 | - `willDisplay` 496 | 497 | - `didEndDisplaying` 498 | 499 | - `moveTo` 500 | 501 | - `shouldHighlight` 502 | 503 | - `canMove` 504 | 505 | - `move` 506 | 507 | You can also chain actions. 508 | 509 | ```swift 510 | let row = CollectionRow(model).didSelect { _ in 511 | print("selected") 512 | }.willDisplay { _ in 513 | print("willDisplay") 514 | } 515 | ``` 516 | 517 | #### CollectionHeaderFooter 518 | 519 | Headers and footer works almost like cells. Size handles same way as `CollectionRow`. 520 | 521 | ```swift 522 | class HeaderFooter: UICollectionReusableView, CollectionConfigurable { 523 | 524 | // custom size 525 | static var size: CGSize? { 526 | CGSize(width: 44, height: 44) 527 | } 528 | 529 | func update(with model: String) { 530 | 531 | } 532 | } 533 | ``` 534 | 535 | `CollectionHeaderFooter` is a wrapper for headers and footers. You can add headers/footers using sections. 536 | 537 | ```swift 538 | let header = CollectionHeaderFooter("Header") 539 | let footer = CollectionHeaderFooter("Footer") 540 | let section = CollectionSection( 541 | rows: [], 542 | header: header, 543 | footer: footer 544 | ) 545 | ``` 546 | 547 | #### CollectionSection 548 | 549 | `CollectionSection` is a wrapper around `UICollectioView` sections. If you have more then one section in you table view you should use `CollectionSection`. You can set rows, header and footer to each section. 550 | 551 | ```swift 552 | let rows = (1...10).map { CollectioRow("\($0)") } 553 | let header = CollectionHeaderFooter("Header") 554 | let footer = CollectionHeaderFooter("Footer") 555 | 556 | let section = CollectionSection( 557 | rows: rows, 558 | header: header, 559 | footer: footer 560 | ) 561 | 562 | manager.storage.append(section) 563 | ``` 564 | 565 | #### CollectionStorage 566 | 567 | `CollectionStorage` is a place where all sections and rows store. Each `CollectionManager` has storage. And if pass data to this storage collectio view will updates automatically. 568 | 569 | ```swift 570 | manager.storage.append(sections) 571 | manager.storage.delete(row) 572 | manager.storage.reload(rows) 573 | // and ect. 574 | ``` 575 | 576 | You can also get index of section, indexPath of row, section at index or row at indexPath. 577 | 578 | ```swift 579 | let sectionIndex = manager.storage.index(for: section) 580 | let rowIndexPath = manager.storage.indexPath(for: row) 581 | 582 | let section = manager.storage.section(at: index) 583 | let row = manager.storage.row(at: indexPath) 584 | ``` 585 | 586 | Storage has `performWithoutAnimation` block. Every change you call inside this block will perform without animation. 587 | 588 | ```swift  589 | manager.storage.performWithoutAnimation { 590 | manager.storage.append(sections) 591 | manager.storage.reload(rows) 592 | } 593 | ``` 594 | 595 | Storage has `performBatchUpdate` block. You can use this method in cases where you want to make multiple changes in one single animated operation. 596 | 597 | ```swift 598 | manager.storage.performBatchUpdate({ 599 | manager.storage.append(sections) 600 | manager.storage.delete(row) 601 | }, completion: { isFinished in 602 | // completion 603 | }) 604 | ``` 605 | 606 | All storage functionality - same as [`TableStorage`](#TableStorage). 607 | 608 | #### CollectionManager 609 | 610 | `CollectionManager` is object that manage all the delegates and data sources. Manager holds `storage` and `collectionView`. You can change both of them. 611 | 612 | Manager also has `scrollHandler` which handles `UIScrollViewDelegate`. 613 | 614 | ```swift 615 | manager.scrollHandler.didScroll { 616 | print("didScroll") 617 | }.shouldScrollToTop { 618 | return true 619 | } 620 | ``` 621 | 622 | Manager also has `layoutHandler` which handles `UICollectionViewDelegateFlowLayout` and `collectionView(_ :transitionLayoutForOldLayout:newLayout:)` delegate method. 623 | 624 | You can for example provide sizes for items. 625 | 626 | ```swift 627 | manager.layoutHandler.sizeForItem { indexPath in 628 | return CGSize(width: 44, height: 44) 629 | }.sizeForHeader { sectionIndex in 630 | return CGSize(width: 44, height: 44) 631 | } 632 | ``` 633 | 634 | ## License 635 | 636 | **TableCollectionManager** is under MIT license. See the `LICENSE` file for more info. 637 | -------------------------------------------------------------------------------- /TableCollectionManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'TableCollectionManager' 3 | s.version = '1.0.2' 4 | s.summary = 'TableCollectionManager' 5 | 6 | s.description = 'Manager for UITableView and UICollectionView' 7 | 8 | s.homepage = 'https://github.com/DimaMishchenko/TableCollectionManager' 9 | s.author = { 'Dmytro Mishchenko' => 'narmdv5@gmail.com' } 10 | s.source = { :git => 'https://github.com/DimaMishchenko/TableCollectionManager.git', :tag => s.version.to_s } 11 | 12 | s.license = { :type => 'MIT', :text => <<-LICENSE 13 | MIT License 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | LICENSE 33 | } 34 | 35 | s.ios.deployment_target = '11.0' 36 | s.swift_version = '5.0' 37 | s.source_files = 'TableCollectionManager/TableCollectionManager/**/*' 38 | s.exclude_files = 'TableCollectionManager/TableCollectionManager/*.plist' 39 | 40 | end -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Actions/CollectionActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionActions.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol CollectionRowActions: CollectionDataSourceActions, CollectionDelegateActions {} 28 | 29 | // MARK: - CollectionDataSourceActionsHandler 30 | 31 | public protocol CollectionDataSourceActions { 32 | 33 | typealias BoolAction = (IndexPath) -> Bool 34 | typealias MoveAction = (IndexPath, IndexPath) -> Void 35 | 36 | var canMove: BoolAction? { get } 37 | var move: MoveAction? { get } 38 | } 39 | 40 | // MARK: - CollectionDelegateActionsHandler 41 | 42 | public protocol CollectionDelegateActions { 43 | 44 | typealias VoidAction = (UICollectionViewCell?, IndexPath) -> Void 45 | typealias ShouldHighlightAction = (UICollectionViewCell?, IndexPath) -> Bool 46 | typealias MoveToAction = (IndexPath, IndexPath) -> IndexPath 47 | 48 | var willDisplay: VoidAction? { get } 49 | var didEndDisplaying: VoidAction? { get } 50 | 51 | var didSelect: VoidAction? { get } 52 | var didDeselect: VoidAction? { get } 53 | 54 | var shouldHighlight: ShouldHighlightAction? { get } 55 | 56 | var moveTo: MoveToAction? { get } 57 | } 58 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Actions/CollectionLayoutHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionLayoutHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class CollectionLayoutHandler { 28 | 29 | // MARK: - Typealias 30 | 31 | public typealias TransitionLayoutAction = (UICollectionViewLayout, UICollectionViewLayout) -> UICollectionViewTransitionLayout 32 | 33 | public typealias ItemSizeAction = (IndexPath) -> CGSize 34 | public typealias ViewSizeAction = (Int) -> CGSize 35 | public typealias InsetAction = (Int) -> UIEdgeInsets 36 | public typealias SpacingAction = (Int) -> CGFloat 37 | 38 | // MARK: - Properties 39 | 40 | // common 41 | 42 | internal var transitionLayout: TransitionLayoutAction? 43 | 44 | // flow layout 45 | 46 | internal var sizeForItem: ItemSizeAction? 47 | internal var sizeForHeader: ViewSizeAction? 48 | internal var sizeForFooter: ViewSizeAction? 49 | internal var insetForSection: InsetAction? 50 | internal var minimumLineSpacing: SpacingAction? 51 | internal var minimumInteritemSpacing: SpacingAction? 52 | 53 | // MARK: - Public 54 | 55 | // common 56 | 57 | @discardableResult public func transitionLayout(_ handler: @escaping TransitionLayoutAction) -> Self { 58 | transitionLayout = handler 59 | return self 60 | } 61 | 62 | // flow layout 63 | 64 | @discardableResult public func sizeForItem(_ handler: @escaping ItemSizeAction) -> Self { 65 | sizeForItem = handler 66 | return self 67 | } 68 | @discardableResult public func sizeForHeader(_ handler: @escaping ViewSizeAction) -> Self { 69 | sizeForHeader = handler 70 | return self 71 | } 72 | @discardableResult public func sizeForFooter(_ handler: @escaping ViewSizeAction) -> Self { 73 | sizeForFooter = handler 74 | return self 75 | } 76 | @discardableResult public func insetForSection(_ handler: @escaping InsetAction) -> Self { 77 | insetForSection = handler 78 | return self 79 | } 80 | @discardableResult public func minimumLineSpacing(_ handler: @escaping SpacingAction) -> Self { 81 | minimumLineSpacing = handler 82 | return self 83 | } 84 | @discardableResult public func minimumInteritemSpacing(_ handler: @escaping SpacingAction) -> Self { 85 | minimumInteritemSpacing = handler 86 | return self 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/CellRegisterer/CollectionCellRegisterer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionCellRegisterer.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | internal final class CollectionCellRegisterer { 28 | 29 | // MARK: - Constants 30 | 31 | private static let nib = "nib" 32 | 33 | // MARK: - Properties 34 | 35 | private var registeredCellIds = Set() 36 | private var registeredHeaderFooterIds = Set() 37 | private weak var collectionView: UICollectionView? 38 | 39 | // MARK: - Init 40 | 41 | internal init(collectionView: UICollectionView?) { 42 | self.collectionView = collectionView 43 | } 44 | 45 | // MARK: - Public 46 | 47 | internal func register(cell type: AnyClass, for reuseIdentifier: String) { 48 | guard !registeredCellIds.contains(reuseIdentifier) else { 49 | return 50 | } 51 | 52 | let bundle = Bundle(for: type) 53 | 54 | if bundle.path(forResource: reuseIdentifier, ofType: Self.nib) != nil { 55 | collectionView?.register( 56 | UINib(nibName: reuseIdentifier, bundle: bundle), 57 | forCellWithReuseIdentifier: reuseIdentifier 58 | ) 59 | } else { 60 | collectionView?.register(type, forCellWithReuseIdentifier: reuseIdentifier) 61 | } 62 | 63 | registeredCellIds.insert(reuseIdentifier) 64 | } 65 | 66 | internal func register(headerFooter type: AnyClass, kind: String, for reuseIdentifier: String) { 67 | guard !registeredHeaderFooterIds.contains(reuseIdentifier + kind) else { 68 | return 69 | } 70 | 71 | let bundle = Bundle(for: type) 72 | 73 | if bundle.path(forResource: reuseIdentifier, ofType: Self.nib) != nil { 74 | collectionView?.register( 75 | UINib(nibName: reuseIdentifier, bundle: bundle), 76 | forSupplementaryViewOfKind: kind, 77 | withReuseIdentifier: reuseIdentifier 78 | ) 79 | } else { 80 | collectionView?.register( 81 | type, 82 | forSupplementaryViewOfKind: kind, 83 | withReuseIdentifier: reuseIdentifier 84 | ) 85 | } 86 | 87 | registeredHeaderFooterIds.insert(reuseIdentifier + kind) 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Configurable/CollectionConfigurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionConfigurable.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol CollectionConfigurable: Configurable { 28 | 29 | static var size: CGSize? { get } 30 | } 31 | 32 | public extension CollectionConfigurable { 33 | 34 | static var size: CGSize? { 35 | nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/HeaderFooter/CollectionHeaderFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionHeaderFooter.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class CollectionHeaderFooter: CollectionHeaderFooterItem { 28 | 29 | // MARK: - Properties 30 | 31 | public var model: View.Model 32 | 33 | // MARK: - Init 34 | 35 | public init(_ model: View.Model) { 36 | self.model = model 37 | } 38 | 39 | // MARK: - TableHeaderFooterItem 40 | 41 | public var reuseIdentifier: String { 42 | View.identifier 43 | } 44 | 45 | public var size: CGSize? { 46 | View.size 47 | } 48 | 49 | public var headerFooterType: AnyClass { 50 | View.self 51 | } 52 | 53 | public func configure(_ view: UICollectionReusableView) { 54 | (view as? View)?.update(with: model) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/HeaderFooter/CollectionHeaderFooterItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionHeaderFooterItem.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol CollectionHeaderFooterItem { 28 | 29 | var reuseIdentifier: String { get } 30 | var size: CGSize? { get } 31 | 32 | var headerFooterType: AnyClass { get } 33 | 34 | func configure(_ view: UICollectionReusableView) 35 | } 36 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Manager/CollectionManager+UICollectionViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionManager+UICollectionViewDataSource.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension CollectionManager: UICollectionViewDataSource { 28 | 29 | // MARK: - Number of items 30 | 31 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 32 | storage.section(at: section)?.rows.count ?? .zero 33 | } 34 | 35 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 36 | storage.numberOfSections 37 | } 38 | 39 | // MARK: - Cell/view for item 40 | 41 | public func collectionView( 42 | _ collectionView: UICollectionView, 43 | cellForItemAt indexPath: IndexPath 44 | ) -> UICollectionViewCell { 45 | guard let row = storage.row(at: indexPath) else { 46 | return UICollectionViewCell() 47 | } 48 | 49 | registerer.register(cell: row.cellType, for: row.reuseIdentifier) 50 | 51 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: row.reuseIdentifier, for: indexPath) 52 | row.configure(cell) 53 | 54 | return cell 55 | } 56 | 57 | public func collectionView( 58 | _ collectionView: UICollectionView, 59 | viewForSupplementaryElementOfKind kind: String, 60 | at indexPath: IndexPath 61 | ) -> UICollectionReusableView { 62 | guard let item = headerFooterItem(for: kind, at: indexPath.section) else { 63 | return UICollectionReusableView() 64 | } 65 | 66 | registerer.register(headerFooter: item.headerFooterType, kind: kind, for: item.reuseIdentifier) 67 | 68 | let view = collectionView.dequeueReusableSupplementaryView( 69 | ofKind: kind, 70 | withReuseIdentifier: item.reuseIdentifier, 71 | for: indexPath 72 | ) 73 | 74 | item.configure(view) 75 | 76 | return view 77 | } 78 | 79 | private func headerFooterItem(for kind: String, at index: Int) -> CollectionHeaderFooterItem? { 80 | switch kind { 81 | case UICollectionView.elementKindSectionHeader: 82 | return storage.section(at: index)?.header 83 | case UICollectionView.elementKindSectionFooter: 84 | return storage.section(at: index)?.footer 85 | default: 86 | return nil 87 | } 88 | } 89 | 90 | // MARK: - Movement 91 | 92 | public func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 93 | storage.row(at: indexPath)?.actionsHandler.canMove?(indexPath) ?? false 94 | } 95 | 96 | public func collectionView( 97 | _ collectionView: UICollectionView, 98 | moveItemAt sourceIndexPath: IndexPath, 99 | to destinationIndexPath: IndexPath 100 | ) { 101 | storage.row(at: sourceIndexPath)?.actionsHandler.move?(sourceIndexPath, destinationIndexPath) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Manager/CollectionManager+UICollectionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionManager+UICollectionViewDelegate.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension CollectionManager: UICollectionViewDelegate { 28 | 29 | // MARK: - Display 30 | 31 | public func collectionView( 32 | _ collectionView: UICollectionView, 33 | willDisplay cell: UICollectionViewCell, 34 | forItemAt indexPath: IndexPath 35 | ) { 36 | storage.row(at: indexPath)?.actionsHandler.willDisplay?(cell, indexPath) 37 | } 38 | 39 | public func collectionView( 40 | _ collectionView: UICollectionView, 41 | didEndDisplaying cell: UICollectionViewCell, 42 | forItemAt indexPath: IndexPath 43 | ) { 44 | storage.row(at: indexPath)?.actionsHandler.didEndDisplaying?(cell, indexPath) 45 | } 46 | 47 | // MARK: - Highlight 48 | 49 | public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 50 | storage.row(at: indexPath)?.actionsHandler.shouldHighlight?( 51 | collectionView.cellForItem(at: indexPath), 52 | indexPath 53 | ) ?? true 54 | } 55 | 56 | // MARK: - Selection 57 | 58 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 59 | guard let action = storage.row(at: indexPath)?.actionsHandler.didSelect else { 60 | return 61 | } 62 | 63 | action(collectionView.cellForItem(at: indexPath), indexPath) 64 | collectionView.deselectItem(at: indexPath, animated: true) 65 | } 66 | 67 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 68 | storage.row(at: indexPath)?.actionsHandler.didDeselect?(collectionView.cellForItem(at: indexPath), indexPath) 69 | } 70 | 71 | // MARK: - Movement 72 | 73 | public func collectionView( 74 | _ collectionView: UICollectionView, 75 | targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, 76 | toProposedIndexPath proposedIndexPath: IndexPath 77 | ) -> IndexPath { 78 | storage.row(at: originalIndexPath)?.actionsHandler.moveTo?( 79 | originalIndexPath, 80 | proposedIndexPath 81 | ) ?? proposedIndexPath 82 | } 83 | 84 | // MARK: - Layout 85 | 86 | public func collectionView( 87 | _ collectionView: UICollectionView, 88 | transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, 89 | newLayout toLayout: UICollectionViewLayout 90 | ) -> UICollectionViewTransitionLayout { 91 | layoutHandler.transitionLayout?(fromLayout, toLayout) ?? UICollectionViewTransitionLayout( 92 | currentLayout: fromLayout, 93 | nextLayout: toLayout 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Manager/CollectionManager+UICollectionViewDelegateFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionManager+UICollectionViewDelegateFlowLayout.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension CollectionManager: UICollectionViewDelegateFlowLayout { 28 | 29 | // MARK: - Size 30 | 31 | public func collectionView( 32 | _ collectionView: UICollectionView, 33 | layout collectionViewLayout: UICollectionViewLayout, 34 | sizeForItemAt indexPath: IndexPath 35 | ) -> CGSize { 36 | layoutHandler.sizeForItem?(indexPath) ?? 37 | storage.row(at: indexPath)?.size ?? 38 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize ?? 39 | .zero 40 | } 41 | 42 | public func collectionView( 43 | _ collectionView: UICollectionView, 44 | layout collectionViewLayout: UICollectionViewLayout, 45 | referenceSizeForHeaderInSection section: Int 46 | ) -> CGSize { 47 | layoutHandler.sizeForHeader?(section) ?? 48 | storage.section(at: section)?.header?.size ?? 49 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.headerReferenceSize ?? 50 | .zero 51 | } 52 | 53 | public func collectionView( 54 | _ collectionView: UICollectionView, 55 | layout collectionViewLayout: UICollectionViewLayout, 56 | referenceSizeForFooterInSection section: Int 57 | ) -> CGSize { 58 | layoutHandler.sizeForHeader?(section) ?? 59 | storage.section(at: section)?.footer?.size ?? 60 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.footerReferenceSize ?? 61 | .zero 62 | } 63 | 64 | // MARK: - Spacing 65 | 66 | public func collectionView( 67 | _ collectionView: UICollectionView, 68 | layout collectionViewLayout: UICollectionViewLayout, 69 | insetForSectionAt section: Int 70 | ) -> UIEdgeInsets { 71 | layoutHandler.insetForSection?(section) ?? 72 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? 73 | .zero 74 | } 75 | 76 | public func collectionView( 77 | _ collectionView: UICollectionView, 78 | layout collectionViewLayout: UICollectionViewLayout, 79 | minimumLineSpacingForSectionAt section: Int 80 | ) -> CGFloat { 81 | layoutHandler.minimumLineSpacing?(section) ?? 82 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing ?? 83 | .zero 84 | } 85 | 86 | public func collectionView( 87 | _ collectionView: UICollectionView, 88 | layout collectionViewLayout: UICollectionViewLayout, 89 | minimumInteritemSpacingForSectionAt section: Int 90 | ) -> CGFloat { 91 | layoutHandler.minimumInteritemSpacing?(section) ?? 92 | (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumInteritemSpacing ?? 93 | .zero 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Manager/CollectionManager+UIScrollViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionManager+UIScrollViewDelegate.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension CollectionManager: UIScrollViewDelegate { 28 | 29 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 30 | scrollHandler.didScroll?() 31 | } 32 | 33 | public func scrollViewDidZoom(_ scrollView: UIScrollView) { 34 | scrollHandler.didZoom?() 35 | } 36 | 37 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 38 | scrollHandler.willBeginDragging?() 39 | } 40 | 41 | public func scrollViewWillEndDragging( 42 | _ scrollView: UIScrollView, 43 | withVelocity velocity: CGPoint, 44 | targetContentOffset: UnsafeMutablePointer 45 | ) { 46 | scrollHandler.willEndDragging?(velocity, targetContentOffset) 47 | } 48 | 49 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 50 | scrollHandler.didEndDragging?(decelerate) 51 | } 52 | 53 | public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 54 | scrollHandler.willBeginDecelerating?() 55 | } 56 | 57 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 58 | scrollHandler.didEndDecelerating?() 59 | } 60 | 61 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 62 | scrollHandler.didEndScrollingAnimation?() 63 | } 64 | 65 | public func viewForZooming(in scrollView: UIScrollView) -> UIView? { 66 | scrollHandler.viewForZooming?() ?? nil 67 | } 68 | 69 | public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { 70 | scrollHandler.willBeginZooming?(view) 71 | } 72 | 73 | public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { 74 | scrollHandler.didEndZooming?(view, scale) 75 | } 76 | 77 | public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { 78 | scrollHandler.shouldScrollToTop?() ?? true 79 | } 80 | 81 | public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { 82 | scrollHandler.didScrollToTop?() 83 | } 84 | 85 | public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { 86 | scrollHandler.didChangeAdjustedContentInset?() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Manager/CollectionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionManager.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class CollectionManager: NSObject { 28 | 29 | // MARK: - Properties 30 | 31 | public var storage: CollectionStorage { 32 | didSet { 33 | subscribeOnStorage() 34 | } 35 | } 36 | 37 | public weak var collectionView: UICollectionView? { 38 | didSet { 39 | registerer = CollectionCellRegisterer(collectionView: collectionView) 40 | subscribeOnCollectionView() 41 | } 42 | } 43 | 44 | public let scrollHandler = ScrollDelegateActionsHandler() 45 | public let layoutHandler = CollectionLayoutHandler() 46 | 47 | private(set) var registerer: CollectionCellRegisterer 48 | 49 | // MARK: - Init 50 | 51 | public init(collectionView: UICollectionView? = nil, storage: CollectionStorage? = nil) { 52 | self.collectionView = collectionView 53 | self.storage = storage ?? CollectionStorage() 54 | registerer = CollectionCellRegisterer(collectionView: collectionView) 55 | super.init() 56 | 57 | subscribeOnStorage() 58 | subscribeOnCollectionView() 59 | } 60 | 61 | // MARK: - Private 62 | 63 | private func subscribeOnStorage() { 64 | storage.delegate = self 65 | } 66 | 67 | private func subscribeOnCollectionView() { 68 | collectionView?.dataSource = self 69 | collectionView?.delegate = self 70 | } 71 | 72 | private func performUpdate(_ update: StorageUpdate) { 73 | switch update { 74 | case .reloadData: 75 | collectionView?.reloadData() 76 | case .reloadSections(let indices): 77 | collectionView?.reloadSections(IndexSet(indices)) 78 | case .reloadRows(let indexPaths): 79 | collectionView?.reloadItems(at: indexPaths) 80 | case .insertSections(let indices): 81 | collectionView?.insertSections(IndexSet(indices)) 82 | case .insertRows(let indexPaths): 83 | collectionView?.insertItems(at: indexPaths) 84 | case .deleteSections(let indices): 85 | collectionView?.deleteSections(IndexSet(indices)) 86 | case .deleteRows(let indexPaths): 87 | collectionView?.deleteItems(at: indexPaths) 88 | case .moveSection(let source, let destination): 89 | collectionView?.moveSection(source, toSection: destination) 90 | case .moveRow(let source, let destination): 91 | collectionView?.moveItem(at: source, to: destination) 92 | } 93 | } 94 | } 95 | 96 | // MARK: - StorageDelegate 97 | 98 | extension CollectionManager: StorageDelegate { 99 | 100 | internal func performUpdates(_ updates: [StorageUpdate], animated: Bool, completion: @escaping (Bool) -> Void) { 101 | collectionView?.performBatchUpdates({ 102 | updates.forEach { 103 | performUpdate($0, animated: animated) 104 | } 105 | }, completion: { 106 | completion($0) 107 | }) 108 | } 109 | 110 | internal func performUpdate(_ update: StorageUpdate, animated: Bool) { 111 | if animated { 112 | performUpdate(update) 113 | } else { 114 | UIView.performWithoutAnimation { 115 | performUpdate(update) 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Row/CollectionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionRow.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class CollectionRow: CollectionRowItem { 28 | 29 | // MARK: - Properties 30 | 31 | public var model: Cell.Model 32 | private let rowActionsHandler = CollectionRowActionsHandler() 33 | 34 | // MARK: - Init 35 | 36 | public init(_ model: Cell.Model) { 37 | self.model = model 38 | } 39 | 40 | // MARK: - TableRowItem 41 | 42 | public var reuseIdentifier: String { 43 | Cell.identifier 44 | } 45 | 46 | public var size: CGSize? { 47 | return Cell.size 48 | } 49 | 50 | public var cellType: AnyClass { 51 | Cell.self 52 | } 53 | 54 | public var actionsHandler: CollectionRowActions { 55 | rowActionsHandler 56 | } 57 | 58 | public func configure(_ cell: UICollectionViewCell) { 59 | (cell as? Cell)?.update(with: model) 60 | } 61 | } 62 | 63 | // MARK: - CollectionDataSourceActions 64 | 65 | public extension CollectionRow { 66 | 67 | @discardableResult func canMove(_ handler: @escaping CollectionRowActions.BoolAction) -> Self { 68 | rowActionsHandler.canMove = handler 69 | return self 70 | } 71 | 72 | @discardableResult func move(_ handler: @escaping CollectionRowActions.MoveAction) -> Self { 73 | rowActionsHandler.move = handler 74 | return self 75 | } 76 | } 77 | 78 | // MARK: - CollectionDelegateActions 79 | 80 | public extension CollectionRow { 81 | 82 | typealias VoidAction = (CollectionRowActionResult?) -> Void 83 | typealias ResultAction = (CollectionRowActionResult?) -> T 84 | 85 | @discardableResult func didSelect(_ handler: @escaping VoidAction) -> Self { 86 | rowActionsHandler.didSelect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 87 | return self 88 | } 89 | 90 | @discardableResult func didDeselect(_ handler: @escaping VoidAction) -> Self { 91 | rowActionsHandler.didDeselect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 92 | return self 93 | } 94 | 95 | @discardableResult func willDisplay(_ handler: @escaping VoidAction) -> Self { 96 | rowActionsHandler.willDisplay = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 97 | return self 98 | } 99 | 100 | @discardableResult func didEndDisplaying(_ handler: @escaping VoidAction) -> Self { 101 | rowActionsHandler.didEndDisplaying = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 102 | return self 103 | } 104 | 105 | @discardableResult func moveTo(_ handler: @escaping CollectionRowActions.MoveToAction) -> Self { 106 | rowActionsHandler.moveTo = handler 107 | return self 108 | } 109 | 110 | @discardableResult func shouldHighlight(_ handler: @escaping ResultAction) -> Self { 111 | rowActionsHandler.shouldHighlight = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 112 | return self 113 | } 114 | } 115 | 116 | // MARK: - Private 117 | 118 | extension CollectionRow { 119 | 120 | private func actionResult( 121 | for cell: UICollectionViewCell?, 122 | at indexPath: IndexPath 123 | ) -> CollectionRowActionResult { 124 | CollectionRowActionResult(model: model, cell: cell as? Cell, indexPath: indexPath) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Row/CollectionRowActionResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionRowActionResult.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class CollectionRowActionResult { 28 | 29 | public let model: Cell.Model 30 | public let cell: Cell? 31 | public let indexPath: IndexPath 32 | 33 | public init(model: Cell.Model, cell: Cell?, indexPath: IndexPath) { 34 | self.model = model 35 | self.cell = cell 36 | self.indexPath = indexPath 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Row/CollectionRowActionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionRowActionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class CollectionRowActionsHandler: CollectionRowActions { 28 | 29 | // MARK: - CollectionDataSourceActionsHandler 30 | 31 | public var canMove: CollectionRowActions.BoolAction? 32 | public var move: CollectionRowActions.MoveAction? 33 | 34 | // MARK: - CollectionDelegateActionsHandler 35 | 36 | public var willDisplay: CollectionRowActions.VoidAction? 37 | public var didEndDisplaying: CollectionRowActions.VoidAction? 38 | public var didSelect: CollectionRowActions.VoidAction? 39 | public var didDeselect: CollectionRowActions.VoidAction? 40 | public var shouldHighlight: CollectionRowActions.ShouldHighlightAction? 41 | public var moveTo: CollectionRowActions.MoveToAction? 42 | } 43 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Row/CollectionRowItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionRowItem.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol CollectionRowItem: HashableItem { 28 | 29 | var reuseIdentifier: String { get } 30 | var size: CGSize? { get } 31 | 32 | var cellType: AnyClass { get } 33 | 34 | var actionsHandler: CollectionRowActions { get } 35 | 36 | func configure(_ cell: UICollectionViewCell) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Section/CollectionSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionSection.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class CollectionSection: Section { 28 | 29 | // MARK: - Properties 30 | 31 | public var rows: [CollectionRowItem] 32 | 33 | public var header: CollectionHeaderFooterItem? 34 | public var footer: CollectionHeaderFooterItem? 35 | 36 | // MARK: - Init 37 | 38 | public init(rows: [CollectionRowItem] = []) { 39 | self.rows = rows 40 | } 41 | 42 | public convenience init( 43 | header: CollectionHeaderFooterItem? = nil, 44 | footer: CollectionHeaderFooterItem? = nil, 45 | rows: [CollectionRowItem] = [] 46 | ) { 47 | self.init(rows: rows) 48 | 49 | self.header = header 50 | self.footer = footer 51 | } 52 | 53 | // MARK: - Public 54 | 55 | public func index(for row: CollectionRowItem) -> Int? { 56 | guard let index = rows.firstIndex(where: { $0.hashValue == row.hashValue }) else { 57 | return nil 58 | } 59 | 60 | return index 61 | } 62 | 63 | public func contains(_ row: CollectionRowItem) -> Bool { 64 | rows.contains(where: { row.hashValue == $0.hashValue }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Section/CollectionSectionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionSectionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class CollectionSectionsHandler: SectionsHandler { 28 | 29 | // MARK: - Properties 30 | 31 | public var sections: [CollectionSection] 32 | 33 | // MARK: - Init 34 | 35 | public init(sections: [CollectionSection]) { 36 | self.sections = sections 37 | } 38 | 39 | // MARK: - Public 40 | 41 | public func emptySection() -> CollectionSection { 42 | CollectionSection() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/CollectionManager/Storage/CollectionStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionStorage.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class CollectionStorage: Storage { 28 | 29 | // MARK: - Properties 30 | 31 | public let sectionsHandler: CollectionSectionsHandler 32 | 33 | // MARK: - Init 34 | 35 | public init() { 36 | sectionsHandler = CollectionSectionsHandler(sections: []) 37 | } 38 | 39 | public init(sections: [CollectionSection] = []) { 40 | sectionsHandler = CollectionSectionsHandler(sections: sections) 41 | } 42 | 43 | public init(rows: [CollectionRowItem] = []) { 44 | sectionsHandler = CollectionSectionsHandler(sections: [CollectionSection(rows: rows)]) 45 | } 46 | 47 | // MARK: - Public 48 | 49 | // to fix UICollectionView bug. Crash when inserting first section. 50 | public func append(_ rows: [CollectionSectionsHandler.SectionItem.RowItem]) { 51 | let section = lastSection() 52 | check(rows, in: section) 53 | section.append(rows) 54 | 55 | if section.numberOfRows == rows.count { 56 | performUpdate(.reloadData) 57 | } else { 58 | performUpdate(.insertRows(at: indexPaths(for: rows))) 59 | } 60 | } 61 | 62 | // to fix UICollectionView bug. Crash when inserting first section. 63 | public func append(_ sections: [CollectionSectionsHandler.SectionItem]) { 64 | check(sections) 65 | sectionsHandler.append(sections) 66 | 67 | if numberOfSections == sections.count { 68 | performUpdate(.reloadData) 69 | } else { 70 | performUpdate(.insertSections(at: indices(for: sections))) 71 | } 72 | } 73 | 74 | // to fix UICollectionView bug. Crash when inserting first section. 75 | public func insert(_ section: CollectionSectionsHandler.SectionItem, at index: Int) { 76 | check([section]) 77 | sectionsHandler.insert(section, at: index) 78 | 79 | if numberOfSections == 1 { 80 | performUpdate(.reloadData) 81 | } else { 82 | performUpdate(.insertSections(at: [index])) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Actions/ScrollDelegateActionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollDelegateActionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class ScrollDelegateActionsHandler { 28 | 29 | // MARK: - Typealias 30 | 31 | public typealias VoidAction = () -> Void 32 | public typealias BoolAction = () -> Bool 33 | public typealias WillEndDraggingAction = (CGPoint, UnsafeMutablePointer) -> Void 34 | public typealias DidEndDraggingAction = (Bool) -> Void 35 | public typealias ViewForZoomingAction = () -> UIView? 36 | public typealias WillBeginZooming = (UIView?) -> Void 37 | public typealias DidEndZooming = (UIView?, CGFloat) -> Void 38 | 39 | // MARK: - Properties 40 | 41 | internal var didScroll: VoidAction? 42 | internal var didZoom: VoidAction? 43 | internal var willBeginDragging: VoidAction? 44 | internal var willEndDragging: WillEndDraggingAction? 45 | internal var didEndDragging: DidEndDraggingAction? 46 | internal var willBeginDecelerating: VoidAction? 47 | internal var didEndDecelerating: VoidAction? 48 | internal var didEndScrollingAnimation: VoidAction? 49 | internal var viewForZooming: ViewForZoomingAction? 50 | internal var willBeginZooming: WillBeginZooming? 51 | internal var didEndZooming: DidEndZooming? 52 | internal var shouldScrollToTop: BoolAction? 53 | internal var didScrollToTop: VoidAction? 54 | internal var didChangeAdjustedContentInset: VoidAction? 55 | 56 | // MARK: - Public 57 | 58 | @discardableResult public func didScroll(_ handler: @escaping VoidAction) -> Self { 59 | didScroll = handler 60 | return self 61 | } 62 | 63 | @discardableResult public func didZoom(_ handler: @escaping VoidAction) -> Self { 64 | didZoom = handler 65 | return self 66 | } 67 | 68 | @discardableResult public func willBeginDragging(_ handler: @escaping VoidAction) -> Self { 69 | willBeginDragging = handler 70 | return self 71 | } 72 | 73 | @discardableResult public func willEndDragging(_ handler: @escaping WillEndDraggingAction) -> Self { 74 | willEndDragging = handler 75 | return self 76 | } 77 | 78 | @discardableResult public func didEndDragging(_ handler: @escaping DidEndDraggingAction) -> Self { 79 | didEndDragging = handler 80 | return self 81 | } 82 | 83 | @discardableResult public func willBeginDecelerating(_ handler: @escaping VoidAction) -> Self { 84 | willBeginDecelerating = handler 85 | return self 86 | } 87 | 88 | @discardableResult public func didEndDecelerating(_ handler: @escaping VoidAction) -> Self { 89 | didEndDecelerating = handler 90 | return self 91 | } 92 | 93 | @discardableResult public func didEndScrollingAnimation(_ handler: @escaping VoidAction) -> Self { 94 | didEndScrollingAnimation = handler 95 | return self 96 | } 97 | 98 | @discardableResult public func viewForZooming(_ handler: @escaping ViewForZoomingAction) -> Self { 99 | viewForZooming = handler 100 | return self 101 | } 102 | 103 | @discardableResult public func willBeginZooming(_ handler: @escaping WillBeginZooming) -> Self { 104 | willBeginZooming = handler 105 | return self 106 | } 107 | 108 | @discardableResult public func didEndZooming(_ handler: @escaping DidEndZooming) -> Self { 109 | didEndZooming = handler 110 | return self 111 | } 112 | 113 | @discardableResult public func shouldScrollToTop(_ handler: @escaping BoolAction) -> Self { 114 | shouldScrollToTop = handler 115 | return self 116 | } 117 | 118 | @discardableResult public func didScrollToTop(_ handler: @escaping VoidAction) -> Self { 119 | didScrollToTop = handler 120 | return self 121 | } 122 | 123 | @discardableResult public func didChangeAdjustedContentInset(_ handler: @escaping VoidAction) -> Self { 124 | didChangeAdjustedContentInset = handler 125 | return self 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Configurable/Configurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurable.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol Configurable { 28 | 29 | static var identifier: String { get } 30 | 31 | associatedtype Model 32 | func update(with model: Model) 33 | } 34 | 35 | public extension Configurable { 36 | 37 | static var identifier: String { 38 | String(describing: self) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Section/Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Section.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol Section: class, HashableItem { 28 | 29 | associatedtype RowItem 30 | associatedtype HeaderItem 31 | associatedtype FooterItem 32 | 33 | var rows: [RowItem] { get set } 34 | 35 | var header: HeaderItem? { get set } 36 | var footer: FooterItem? { get set } 37 | 38 | var numberOfRows: Int { get } 39 | var isEmpty: Bool { get } 40 | 41 | func row(at index: Int) -> RowItem? 42 | func index(for row: RowItem) -> Int? 43 | func contains(_ row: RowItem) -> Bool 44 | 45 | func clear() -> Self 46 | 47 | @discardableResult func append(_ row: RowItem) -> Self 48 | @discardableResult func append(_ rows: [RowItem]) -> Self 49 | 50 | @discardableResult func insert(_ row: RowItem, at index: Int) -> Self 51 | @discardableResult func insert(_ rows: [RowItem], at index: Int) -> Self 52 | 53 | @discardableResult func replace(at index: Int, with row: RowItem) -> Self 54 | @discardableResult func swap(from: Int, to: Int) -> Self 55 | 56 | @discardableResult func delete(at index: Int) -> Self 57 | 58 | @discardableResult func add(header: HeaderItem) -> Self 59 | @discardableResult func deleteHeader() -> Self 60 | 61 | @discardableResult func add(footer: FooterItem) -> Self 62 | @discardableResult func deleteFooter() -> Self 63 | } 64 | 65 | // MARK: - Public 66 | 67 | public extension Section { 68 | 69 | var numberOfRows: Int { 70 | rows.count 71 | } 72 | 73 | var isEmpty: Bool { 74 | rows.isEmpty 75 | } 76 | 77 | func row(at index: Int) -> RowItem? { 78 | guard rows.indices.contains(index) else { 79 | return nil 80 | } 81 | 82 | return rows[index] 83 | } 84 | 85 | @discardableResult func clear() -> Self { 86 | rows.removeAll() 87 | return self 88 | } 89 | 90 | @discardableResult func append(_ row: RowItem) -> Self { 91 | rows.append(row) 92 | return self 93 | } 94 | 95 | @discardableResult func append(_ rows: [RowItem]) -> Self { 96 | self.rows.append(contentsOf: rows) 97 | return self 98 | } 99 | 100 | @discardableResult func insert(_ row: RowItem, at index: Int) -> Self { 101 | rows.insert(row, at: index) 102 | return self 103 | } 104 | 105 | @discardableResult func insert(_ rows: [RowItem], at index: Int) -> Self { 106 | self.rows.insert(contentsOf: rows, at: index) 107 | return self 108 | } 109 | 110 | @discardableResult func replace(at index: Int, with row: RowItem) -> Self { 111 | rows[index] = row 112 | return self 113 | } 114 | 115 | @discardableResult func swap(from: Int, to: Int) -> Self { 116 | rows.swapAt(from, to) 117 | return self 118 | } 119 | 120 | @discardableResult func delete(at index: Int) -> Self { 121 | rows.remove(at: index) 122 | return self 123 | } 124 | 125 | @discardableResult func add(header: HeaderItem) -> Self { 126 | self.header = header 127 | return self 128 | } 129 | 130 | @discardableResult func deleteHeader() -> Self { 131 | header = nil 132 | return self 133 | } 134 | 135 | @discardableResult func add(footer: FooterItem) -> Self { 136 | self.footer = footer 137 | return self 138 | } 139 | 140 | @discardableResult func deleteFooter() -> Self { 141 | footer = nil 142 | return self 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Section/SectionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol SectionsHandler: class, HashableItem { 28 | 29 | associatedtype SectionItem: Section 30 | 31 | var sections: [SectionItem] { get set } 32 | 33 | var isEmpty: Bool { get } 34 | 35 | func index(for section: SectionItem) -> Int? 36 | func indexPath(for row: SectionItem.RowItem) -> IndexPath? 37 | 38 | func section(at index: Int) -> SectionItem? 39 | func row(at indexPath: IndexPath) -> SectionItem.RowItem? 40 | 41 | func contains(_ section: SectionItem) -> Bool 42 | 43 | @discardableResult func append(_ section: SectionItem) -> Self 44 | @discardableResult func append(_ sections: [SectionItem]) -> Self 45 | 46 | @discardableResult func insert(_ section: SectionItem, at index: Int) -> Self 47 | 48 | @discardableResult func replaceSection(at index: Int, with section: SectionItem) -> Self 49 | @discardableResult func swap(from: Int, to: Int) -> Self 50 | 51 | @discardableResult func delete(at index: Int) -> Self 52 | 53 | @discardableResult func clear() -> Self 54 | 55 | func emptySection() -> SectionItem 56 | } 57 | 58 | // MARK: - Public 59 | 60 | public extension SectionsHandler { 61 | 62 | var isEmpty: Bool { 63 | sections.isEmpty 64 | } 65 | 66 | func index(for section: SectionItem) -> Int? { 67 | sections.firstIndex { $0.hashValue == section.hashValue } 68 | } 69 | 70 | func contains(_ section: SectionItem) -> Bool { 71 | sections.contains(where: { section.hashValue == $0.hashValue }) 72 | } 73 | 74 | func section(at index: Int) -> SectionItem? { 75 | guard sections.indices.contains(index) else { 76 | return nil 77 | } 78 | 79 | return sections[index] 80 | } 81 | 82 | func indexPath(for row: SectionItem.RowItem) -> IndexPath? { 83 | for (sectionIndex, section) in sections.enumerated() { 84 | guard let rowIndex = section.index(for: row) else { 85 | continue 86 | } 87 | 88 | return IndexPath(item: rowIndex, section: sectionIndex) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func row(at indexPath: IndexPath) -> SectionItem.RowItem? { 95 | section(at: indexPath.section)?.row(at: indexPath.row) 96 | } 97 | 98 | @discardableResult func append(_ section: SectionItem) -> Self { 99 | sections.append(section) 100 | return self 101 | } 102 | 103 | @discardableResult func append(_ sections: [SectionItem]) -> Self { 104 | self.sections.append(contentsOf: sections) 105 | return self 106 | } 107 | 108 | @discardableResult func insert(_ section: SectionItem, at index: Int) -> Self { 109 | sections.insert(section, at: index) 110 | return self 111 | } 112 | 113 | @discardableResult func replaceSection(at index: Int, with section: SectionItem) -> Self { 114 | if index < sections.count { 115 | sections[index] = section 116 | } 117 | return self 118 | } 119 | 120 | @discardableResult func swap(from: Int, to: Int) -> Self { 121 | sections.swapAt(from, to) 122 | return self 123 | } 124 | 125 | @discardableResult func delete(at index: Int) -> Self { 126 | sections.remove(at: index) 127 | return self 128 | } 129 | 130 | @discardableResult func clear() -> Self { 131 | sections.removeAll() 132 | return self 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Storage/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storage.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | internal protocol StorageDelegate: class { 28 | 29 | func performUpdates(_ updates: [StorageUpdate], animated: Bool, completion: @escaping (Bool) -> Void) 30 | func performUpdate(_ update: StorageUpdate, animated: Bool) 31 | } 32 | 33 | public protocol Storage: class { 34 | 35 | associatedtype SectionsHandlerItem: SectionsHandler 36 | var sectionsHandler: SectionsHandlerItem { get } 37 | 38 | var isEmpty: Bool { get } 39 | var numberOfSections: Int { get } 40 | var numberOfAllRows: Int { get } 41 | 42 | func index(for section: SectionsHandlerItem.SectionItem) -> Int? 43 | func indexPath(for row: SectionsHandlerItem.SectionItem.RowItem) -> IndexPath? 44 | 45 | func section(at index: Int) -> SectionsHandlerItem.SectionItem? 46 | func row(at indexPath: IndexPath) -> SectionsHandlerItem.SectionItem.RowItem? 47 | 48 | func performWithoutAnimation(_ block: () -> Void) 49 | func performBatchUpdate(_ block: () -> Void, completion: @escaping (Bool) -> Void) 50 | 51 | func reload() 52 | func reload(_ row: SectionsHandlerItem.SectionItem.RowItem) 53 | func reload(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) 54 | func reload(_ section: SectionsHandlerItem.SectionItem) 55 | func reload(_ sections: [SectionsHandlerItem.SectionItem]) 56 | 57 | func append(_ row: SectionsHandlerItem.SectionItem.RowItem) 58 | func append(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) 59 | func append(_ row: SectionsHandlerItem.SectionItem.RowItem, to sectionIndex: Int) 60 | func append(_ rows: [SectionsHandlerItem.SectionItem.RowItem], to sectionIndex: Int) 61 | func append(_ section: SectionsHandlerItem.SectionItem) 62 | func append(_ sections: [SectionsHandlerItem.SectionItem]) 63 | 64 | func insert(_ row: SectionsHandlerItem.SectionItem.RowItem, at indexPath: IndexPath) 65 | func insert(_ section: SectionsHandlerItem.SectionItem, at index: Int) 66 | 67 | func delete(_ row: SectionsHandlerItem.SectionItem.RowItem) 68 | func delete(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) 69 | func delete(_ section: SectionsHandlerItem.SectionItem) 70 | func delete(_ sections: [SectionsHandlerItem.SectionItem]) 71 | func deleteAllItems() 72 | 73 | func moveRowWithoutUpdate(from source: IndexPath, to destination: IndexPath) 74 | func moveRow(from source: IndexPath, to destination: IndexPath) 75 | func moveSection(from source: Int, to destination: Int) 76 | } 77 | 78 | // MARK: - Internal 79 | 80 | internal struct StorageAssociatedKeys { 81 | 82 | static var delegate: UInt8 = 0 83 | static var animated: UInt8 = 1 84 | static var batchUpdate: UInt8 = 2 85 | static var batchUpdates: UInt8 = 3 86 | static var checkUniqueItems: UInt8 = 4 87 | } 88 | 89 | internal extension Storage { 90 | 91 | weak var delegate: StorageDelegate? { 92 | get { objc_getAssociatedObject(self, &StorageAssociatedKeys.delegate) as? StorageDelegate } 93 | set { objc_setAssociatedObject(self, &StorageAssociatedKeys.delegate, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 94 | } 95 | 96 | private var animated: Bool { 97 | get { objc_getAssociatedObject(self, &StorageAssociatedKeys.animated) as? Bool ?? true } 98 | set { objc_setAssociatedObject(self, &StorageAssociatedKeys.animated, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 99 | } 100 | 101 | private var batchUpdate: Bool { 102 | get { objc_getAssociatedObject(self, &StorageAssociatedKeys.batchUpdate) as? Bool ?? false } 103 | set { objc_setAssociatedObject(self, &StorageAssociatedKeys.batchUpdate, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 104 | } 105 | 106 | private var batchUpdates: [StorageUpdate] { 107 | get { objc_getAssociatedObject(self, &StorageAssociatedKeys.batchUpdates) as? [StorageUpdate] ?? [] } 108 | set { objc_setAssociatedObject(self, &StorageAssociatedKeys.batchUpdates, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 109 | } 110 | 111 | #if DEBUG 112 | private var _checkUniqueItems: Bool { 113 | get { objc_getAssociatedObject(self, &StorageAssociatedKeys.checkUniqueItems) as? Bool ?? true } 114 | set { objc_setAssociatedObject(self, &StorageAssociatedKeys.checkUniqueItems, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 115 | } 116 | #endif 117 | } 118 | 119 | // MARK: - Public 120 | 121 | public extension Storage { 122 | 123 | #if DEBUG 124 | /// You can only use unique rows/sections inside storage. Storage has unique items check to make sure you don't append/insert same item multiple times. But it can cause small performance issues since Storage should iterate through items. This only works in DEBUG mode but if you want so turn of it just call storage.checkUniqueItems = false 125 | var checkUniqueItems: Bool { 126 | get { _checkUniqueItems } 127 | set { _checkUniqueItems = newValue } 128 | } 129 | #endif 130 | 131 | var isEmpty: Bool { 132 | sectionsHandler.isEmpty 133 | } 134 | 135 | var numberOfSections: Int { 136 | sectionsHandler.sections.count 137 | } 138 | 139 | var numberOfAllRows: Int { 140 | sectionsHandler.sections.reduce(0) { sum, section in 141 | sum + section.rows.count 142 | } 143 | } 144 | 145 | func index(for section: SectionsHandlerItem.SectionItem) -> Int? { 146 | sectionsHandler.index(for: section) 147 | } 148 | 149 | func indexPath(for row: SectionsHandlerItem.SectionItem.RowItem) -> IndexPath? { 150 | sectionsHandler.indexPath(for: row) 151 | } 152 | 153 | func section(at index: Int) -> SectionsHandlerItem.SectionItem? { 154 | sectionsHandler.section(at: index) 155 | } 156 | 157 | func row(at indexPath: IndexPath) -> SectionsHandlerItem.SectionItem.RowItem? { 158 | sectionsHandler.row(at: indexPath) 159 | } 160 | 161 | // MARK: - Updates 162 | 163 | func performWithoutAnimation(_ block: () -> Void) { 164 | animated = false 165 | block() 166 | animated = true 167 | } 168 | 169 | func performBatchUpdate(_ block: () -> Void, completion: @escaping (Bool) -> Void = { _ in }) { 170 | batchUpdate = true 171 | block() 172 | batchUpdate = false 173 | delegate?.performUpdates(batchUpdates, animated: animated) { 174 | completion($0) 175 | } 176 | batchUpdates = [] 177 | } 178 | 179 | // MARK: - Reloading 180 | 181 | func reload() { 182 | performUpdate(.reloadData) 183 | } 184 | 185 | func reload(_ row: SectionsHandlerItem.SectionItem.RowItem) { 186 | reload([row]) 187 | } 188 | 189 | func reload(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) { 190 | performUpdate(.reloadRows(at: indexPaths(for: rows))) 191 | } 192 | 193 | func reload(_ section: SectionsHandlerItem.SectionItem) { 194 | reload([section]) 195 | } 196 | 197 | func reload(_ sections: [SectionsHandlerItem.SectionItem]) { 198 | performUpdate(.reloadSections(at: indices(for: sections))) 199 | } 200 | 201 | // MARK: - Append 202 | 203 | func append(_ row: SectionsHandlerItem.SectionItem.RowItem) { 204 | append([row]) 205 | } 206 | 207 | func append(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) { 208 | let section = lastSection() 209 | check(rows, in: section) 210 | section.append(rows) 211 | performUpdate(.insertRows(at: indexPaths(for: rows))) 212 | } 213 | 214 | func append(_ row: SectionsHandlerItem.SectionItem.RowItem, to sectionIndex: Int) { 215 | append([row], to: sectionIndex) 216 | } 217 | 218 | func append(_ rows: [SectionsHandlerItem.SectionItem.RowItem], to sectionIndex: Int) { 219 | guard let section = section(at: sectionIndex) else { 220 | logError("Failed to append. Invalid section index \(sectionIndex)") 221 | return 222 | } 223 | check(rows, in: section) 224 | section.append(rows) 225 | performUpdate(.insertRows(at: indexPaths(for: rows))) 226 | } 227 | 228 | func append(_ section: SectionsHandlerItem.SectionItem) { 229 | append([section]) 230 | } 231 | 232 | func append(_ sections: [SectionsHandlerItem.SectionItem]) { 233 | check(sections) 234 | sectionsHandler.append(sections) 235 | performUpdate(.insertSections(at: indices(for: sections))) 236 | } 237 | 238 | // MARK: - Insertion 239 | 240 | func insert(_ row: SectionsHandlerItem.SectionItem.RowItem, at indexPath: IndexPath) { 241 | guard let section = self.section(at: indexPath.section) else { 242 | if numberOfSections == .zero && indexPath.section == .zero && indexPath.row == .zero { 243 | append(row) 244 | } else { 245 | logError("Failed to insert. Invalid indexPath \(indexPath) to insert row \(row)") 246 | } 247 | return 248 | } 249 | check([row], in: section) 250 | section.insert(row, at: indexPath.row) 251 | performUpdate(.insertRows(at: [indexPath])) 252 | } 253 | 254 | func insert(_ section: SectionsHandlerItem.SectionItem, at index: Int) { 255 | check([section]) 256 | sectionsHandler.insert(section, at: index) 257 | performUpdate(.insertSections(at: [index])) 258 | } 259 | 260 | // MARK: - Deleting 261 | 262 | func delete(_ row: SectionsHandlerItem.SectionItem.RowItem) { 263 | delete([row]) 264 | } 265 | 266 | func delete(_ rows: [SectionsHandlerItem.SectionItem.RowItem]) { 267 | let indexPaths = self.indexPaths(for: rows) 268 | indexPaths.forEach { section(at: $0.section)?.delete(at: $0.row) } 269 | performUpdate(.deleteRows(at: indexPaths)) 270 | 271 | let emptySections = sectionsHandler.sections.filter { $0.isEmpty } 272 | delete(emptySections) 273 | } 274 | 275 | func delete(_ section: SectionsHandlerItem.SectionItem) { 276 | delete([section]) 277 | } 278 | 279 | func delete(_ sections: [SectionsHandlerItem.SectionItem]) { 280 | let indices = self.indices(for: sections) 281 | indices.forEach { sectionsHandler.delete(at: $0) } 282 | performUpdate(.deleteSections(at: indices)) 283 | } 284 | 285 | func deleteAllItems() { 286 | sectionsHandler.clear() 287 | performUpdate(.reloadData) 288 | } 289 | 290 | // MARK: - Reordering 291 | 292 | func moveRowWithoutUpdate(from source: IndexPath, to destination: IndexPath) { 293 | guard let row = row(at: source) else { 294 | logError("Failed to move. Invalid source row at \(source)") 295 | return 296 | } 297 | section(at: source.section)?.delete(at: source.row) 298 | section(at: destination.section)?.insert(row, at: destination.row) 299 | } 300 | 301 | func moveRow(from source: IndexPath, to destination: IndexPath) { 302 | guard let row = row(at: source) else { 303 | logError("Failed to move. Invalid source row at \(source)") 304 | return 305 | } 306 | section(at: source.section)?.delete(at: source.row) 307 | section(at: destination.section)?.insert(row, at: destination.row) 308 | performUpdate(.moveRow(from: source, to: destination)) 309 | } 310 | 311 | func moveSection(from source: Int, to destination: Int) { 312 | guard let section = section(at: source) else { 313 | logError("Failed to move. Invalid source section at \(source)") 314 | return 315 | } 316 | sectionsHandler.delete(at: source) 317 | sectionsHandler.insert(section, at: destination) 318 | performUpdate(.moveSection(from: source, to: destination)) 319 | } 320 | } 321 | 322 | // MARK: - Internal 323 | 324 | internal extension Storage { 325 | 326 | func check(_ rows: [SectionsHandlerItem.SectionItem.RowItem], in section: SectionsHandlerItem.SectionItem) { 327 | #if DEBUG 328 | guard checkUniqueItems else { return } 329 | 330 | 331 | guard rows.count == (rows as [AnyObject]).unique.count else { 332 | fatalError("Rows \(rows) contains same objects, please create unique instance for each row") 333 | } 334 | 335 | rows.forEach { 336 | if section.contains($0) { 337 | fatalError("Section already contains \($0) row, please create new instance") 338 | } 339 | } 340 | #endif 341 | } 342 | 343 | func check(_ sections: [SectionsHandlerItem.SectionItem]) { 344 | #if DEBUG 345 | guard checkUniqueItems else { return } 346 | 347 | guard sections.count == sections.unique.count else { 348 | fatalError("Sections \(sections) contains same objects, please create unique instance for each section") 349 | } 350 | 351 | sections.forEach { 352 | if sectionsHandler.contains($0) { 353 | fatalError("Storage already contains \($0) section, please create new instance") 354 | } 355 | } 356 | #endif 357 | } 358 | 359 | func performUpdate(_ update: StorageUpdate) { 360 | guard !batchUpdate else { 361 | batchUpdates.append(update) 362 | return 363 | } 364 | 365 | delegate?.performUpdate(update, animated: animated) 366 | } 367 | 368 | func indices(for sections: [SectionsHandlerItem.SectionItem]) -> [Int] { 369 | sections.compactMap { index(for: $0) } 370 | } 371 | 372 | func indexPaths(for rows: [SectionsHandlerItem.SectionItem.RowItem]) -> [IndexPath] { 373 | rows.compactMap { indexPath(for: $0) } 374 | } 375 | 376 | func lastSection() -> SectionsHandlerItem.SectionItem { 377 | guard let section = sectionsHandler.sections.last else { 378 | let section = sectionsHandler.emptySection() 379 | sectionsHandler.append(section) 380 | return section 381 | } 382 | 383 | return section 384 | } 385 | 386 | func logError(_ text: String, line: Int = #line) { 387 | print("TableCollectionManager error: \(text). In \(Self.self) line \(line)") 388 | } 389 | } 390 | 391 | fileprivate extension Sequence where Iterator.Element: AnyObject { 392 | 393 | var unique: [Element] { 394 | var uniqueValues: [Element] = [] 395 | forEach { item in 396 | if !uniqueValues.contains(where: { ObjectIdentifier($0) == ObjectIdentifier(item) }) { 397 | uniqueValues += [item] 398 | } 399 | } 400 | return uniqueValues 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Storage/StorageUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageUpdate.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | internal enum StorageUpdate { 28 | 29 | case reloadData 30 | case reloadSections(at: [Int]) 31 | case reloadRows(at: [IndexPath]) 32 | 33 | case insertSections(at: [Int]) 34 | case insertRows(at: [IndexPath]) 35 | 36 | case deleteSections(at: [Int]) 37 | case deleteRows(at: [IndexPath]) 38 | 39 | case moveSection(from: Int, to: Int) 40 | case moveRow(from: IndexPath, to: IndexPath) 41 | } 42 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/Common/Utils/HashableItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashableItem.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public protocol HashableItem { 28 | 29 | var hashValue: Int { get } 30 | } 31 | 32 | public extension HashableItem where Self: AnyObject { 33 | 34 | var hashValue: Int { 35 | ObjectIdentifier(self).hashValue 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableCollectionManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // TableCollectionManager.h 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | #import 26 | 27 | //! Project version number for TableCollectionManager. 28 | FOUNDATION_EXPORT double TableCollectionManagerVersionNumber; 29 | 30 | //! Project version string for TableCollectionManager. 31 | FOUNDATION_EXPORT const unsigned char TableCollectionManagerVersionString[]; 32 | 33 | // In this header, you should import all the public headers of your framework using statements like #import 34 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Actions/TableActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableActions.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol TableRowActions: TableDataSourceActions, TableDelegateActions {} 28 | 29 | // MARK: - TableDataSourceActionsHandler 30 | 31 | public protocol TableDataSourceActions { 32 | 33 | typealias BoolAction = (IndexPath) -> Bool 34 | typealias CommitAction = (UITableViewCell.EditingStyle, IndexPath) -> Void 35 | typealias MoveAction = (IndexPath, IndexPath) -> Void 36 | 37 | var canEdit: BoolAction? { get } 38 | var canMove: BoolAction? { get } 39 | 40 | var commitStyle: CommitAction? { get } 41 | var move: MoveAction? { get } 42 | } 43 | 44 | // MARK: - TableDelegateActionsHandler 45 | 46 | public protocol TableDelegateActions { 47 | 48 | typealias VoidAction = (UITableViewCell?, IndexPath) -> Void 49 | typealias ShouldHighlightAction = (UITableViewCell?, IndexPath) -> Bool 50 | typealias MoveToAction = (IndexPath, IndexPath) -> IndexPath 51 | typealias EditingStyleAction = (UITableViewCell?, IndexPath) -> UITableViewCell.EditingStyle 52 | typealias SwipeConfigurationAction = (UITableViewCell?, IndexPath) -> UISwipeActionsConfiguration? 53 | 54 | var didSelect: VoidAction? { get } 55 | var didDeselect: VoidAction? { get } 56 | 57 | var willSelect: VoidAction? { get } 58 | var willDeselect: VoidAction? { get } 59 | 60 | var willDisplay: VoidAction? { get } 61 | var didEndDisplaying: VoidAction? { get } 62 | 63 | var moveTo: MoveToAction? { get } 64 | 65 | var shouldHighlight: ShouldHighlightAction? { get } 66 | 67 | var editingStyle: EditingStyleAction? { get } 68 | 69 | var leadingSwipeActions: SwipeConfigurationAction? { get } 70 | var trailingSwipeActions: SwipeConfigurationAction? { get } 71 | } 72 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/CellRegisterer/TableCellRegisterer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableCellRegisterer.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | internal final class TableCellRegisterer { 28 | 29 | // MARK: - Constants 30 | 31 | private static let nib = "nib" 32 | 33 | // MARK: - Properties 34 | 35 | private var registeredCellIds = Set() 36 | private var registeredHeaderFooterIds = Set() 37 | private weak var tableView: UITableView? 38 | 39 | // MARK: - Init 40 | 41 | internal init(tableView: UITableView?) { 42 | self.tableView = tableView 43 | } 44 | 45 | // MARK: - Public 46 | 47 | internal func register(cell type: AnyClass, for reuseIdentifier: String) { 48 | guard !registeredCellIds.contains(reuseIdentifier) else { 49 | return 50 | } 51 | 52 | guard tableView?.dequeueReusableCell(withIdentifier: reuseIdentifier) == nil else { 53 | registeredCellIds.insert(reuseIdentifier) 54 | return 55 | } 56 | 57 | let bundle = Bundle(for: type) 58 | 59 | if bundle.path(forResource: reuseIdentifier, ofType: Self.nib) != nil { 60 | tableView?.register(UINib(nibName: reuseIdentifier, bundle: bundle), forCellReuseIdentifier: reuseIdentifier) 61 | } else { 62 | tableView?.register(type, forCellReuseIdentifier: reuseIdentifier) 63 | } 64 | 65 | registeredCellIds.insert(reuseIdentifier) 66 | } 67 | 68 | internal func register(headerFooter type: AnyClass, for reuseIdentifier: String) { 69 | guard !registeredHeaderFooterIds.contains(reuseIdentifier) else { 70 | return 71 | } 72 | 73 | guard tableView?.dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) == nil else { 74 | registeredHeaderFooterIds.insert(reuseIdentifier) 75 | return 76 | } 77 | 78 | let bundle = Bundle(for: type) 79 | 80 | if bundle.path(forResource: reuseIdentifier, ofType: Self.nib) != nil { 81 | tableView?.register( 82 | UINib(nibName: reuseIdentifier, bundle: bundle), 83 | forHeaderFooterViewReuseIdentifier: reuseIdentifier 84 | ) 85 | } else { 86 | tableView?.register(type, forHeaderFooterViewReuseIdentifier: reuseIdentifier) 87 | } 88 | 89 | registeredHeaderFooterIds.insert(reuseIdentifier) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Configurable/TableConfigurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableConfigurable.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol TableConfigurable: Configurable { 28 | 29 | static var height: CGFloat { get } 30 | static var estimatedHeight: CGFloat { get } 31 | } 32 | 33 | public extension TableConfigurable where Self: UITableViewCell { 34 | 35 | static var height: CGFloat { 36 | UITableView.automaticDimension 37 | } 38 | 39 | static var estimatedHeight: CGFloat { 40 | UITableView.automaticDimension 41 | } 42 | } 43 | 44 | public extension TableConfigurable where Self: UITableViewHeaderFooterView { 45 | 46 | static var height: CGFloat { 47 | UITableView.automaticDimension 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/HeaderFooter/TableHeaderFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableHeaderFooter.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class TableHeaderFooter: TableHeaderFooterItem { 28 | 29 | // MARK: - Properties 30 | 31 | public var model: View.Model 32 | 33 | // MARK: - Init 34 | 35 | public init(_ model: View.Model) { 36 | self.model = model 37 | } 38 | 39 | // MARK: - TableHeaderFooterItem 40 | 41 | public var reuseIdentifier: String { 42 | View.identifier 43 | } 44 | 45 | public var height: CGFloat { 46 | View.height 47 | } 48 | 49 | public var estimatedHeight: CGFloat { 50 | View.estimatedHeight 51 | } 52 | 53 | public var headerFooterType: AnyClass { 54 | View.self 55 | } 56 | 57 | public func configure(_ view: UIView) { 58 | (view as? View)?.update(with: model) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/HeaderFooter/TableHeaderFooterItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableHeaderFooterItem.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol TableHeaderFooterItem { 28 | 29 | var reuseIdentifier: String { get } 30 | var height: CGFloat { get } 31 | var estimatedHeight: CGFloat { get } 32 | 33 | var headerFooterType: AnyClass { get } 34 | 35 | func configure(_ view: UIView) 36 | } 37 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Manager/TableManager+UIScrollViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableManager+UIScrollViewDelegate.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension TableManager: UIScrollViewDelegate { 28 | 29 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 30 | scrollHandler.didScroll?() 31 | } 32 | 33 | public func scrollViewDidZoom(_ scrollView: UIScrollView) { 34 | scrollHandler.didZoom?() 35 | } 36 | 37 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 38 | scrollHandler.willBeginDragging?() 39 | } 40 | 41 | public func scrollViewWillEndDragging( 42 | _ scrollView: UIScrollView, 43 | withVelocity velocity: CGPoint, 44 | targetContentOffset: UnsafeMutablePointer 45 | ) { 46 | scrollHandler.willEndDragging?(velocity, targetContentOffset) 47 | } 48 | 49 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 50 | scrollHandler.didEndDragging?(decelerate) 51 | } 52 | 53 | public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 54 | scrollHandler.willBeginDecelerating?() 55 | } 56 | 57 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 58 | scrollHandler.didEndDecelerating?() 59 | } 60 | 61 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 62 | scrollHandler.didEndScrollingAnimation?() 63 | } 64 | 65 | public func viewForZooming(in scrollView: UIScrollView) -> UIView? { 66 | scrollHandler.viewForZooming?() ?? nil 67 | } 68 | 69 | public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { 70 | scrollHandler.willBeginZooming?(view) 71 | } 72 | 73 | public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { 74 | scrollHandler.didEndZooming?(view, scale) 75 | } 76 | 77 | public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { 78 | scrollHandler.shouldScrollToTop?() ?? true 79 | } 80 | 81 | public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { 82 | scrollHandler.didScrollToTop?() 83 | } 84 | 85 | public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { 86 | scrollHandler.didChangeAdjustedContentInset?() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Manager/TableManager+UITableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableManager+UITableViewDataSource.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension TableManager: UITableViewDataSource { 28 | 29 | // MARK: - Number of items 30 | 31 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 32 | storage.section(at: section)?.rows.count ?? .zero 33 | } 34 | 35 | public func numberOfSections(in tableView: UITableView) -> Int { 36 | storage.numberOfSections 37 | } 38 | 39 | // MARK: - Cell for row 40 | 41 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | guard let row = storage.row(at: indexPath) else { 43 | return UITableViewCell() 44 | } 45 | 46 | registerer.register(cell: row.cellType, for: row.reuseIdentifier) 47 | 48 | let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) 49 | row.configure(cell) 50 | 51 | return cell 52 | } 53 | 54 | // MARK: - Editing 55 | 56 | public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 57 | storage.row(at: indexPath)?.actionsHandler.canEdit?(indexPath) ?? false 58 | } 59 | 60 | public func tableView( 61 | _ tableView: UITableView, 62 | commit editingStyle: UITableViewCell.EditingStyle, 63 | forRowAt indexPath: IndexPath 64 | ) { 65 | storage.row(at: indexPath)?.actionsHandler.commitStyle?(editingStyle, indexPath) 66 | } 67 | 68 | // MARK: - Movement 69 | 70 | public func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 71 | storage.row(at: indexPath)?.actionsHandler.canMove?(indexPath) ?? false 72 | } 73 | 74 | public func tableView( 75 | _ tableView: UITableView, 76 | moveRowAt sourceIndexPath: IndexPath, 77 | to destinationIndexPath: IndexPath 78 | ) { 79 | storage.row(at: sourceIndexPath)?.actionsHandler.move?(sourceIndexPath, destinationIndexPath) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Manager/TableManager+UITableViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableManager+UITableViewDelegate.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | extension TableManager: UITableViewDelegate { 28 | 29 | // MARK: - Height 30 | 31 | public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 32 | storage.row(at: indexPath)?.height ?? .zero 33 | } 34 | 35 | public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 36 | storage.row(at: indexPath)?.estimatedHeight ?? .zero 37 | } 38 | 39 | public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 40 | storage.section(at: section)?.header?.height ?? .zero 41 | } 42 | 43 | public func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat { 44 | storage.section(at: section)?.header?.estimatedHeight ?? .zero 45 | } 46 | 47 | public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 48 | storage.section(at: section)?.footer?.height ?? .zero 49 | } 50 | 51 | public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat { 52 | storage.section(at: section)?.footer?.estimatedHeight ?? .zero 53 | } 54 | 55 | // MARK: - Header Footer View 56 | 57 | public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 58 | guard let item = storage.section(at: section)?.header else { 59 | return nil 60 | } 61 | 62 | return headerFooterView(for: item, tableView: tableView) 63 | } 64 | 65 | public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 66 | guard let item = storage.section(at: section)?.footer else { 67 | return nil 68 | } 69 | 70 | return headerFooterView(for: item, tableView: tableView) 71 | } 72 | 73 | private func headerFooterView(for item: TableHeaderFooterItem, tableView: UITableView) -> UIView? { 74 | 75 | registerer.register(headerFooter: item.headerFooterType, for: item.reuseIdentifier) 76 | 77 | guard let headerFooterView = tableView.dequeueReusableHeaderFooterView(withIdentifier: item.reuseIdentifier) else { 78 | return nil 79 | } 80 | 81 | item.configure(headerFooterView) 82 | 83 | return headerFooterView 84 | } 85 | 86 | // MARK: - Display 87 | 88 | public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 89 | storage.row(at: indexPath)?.actionsHandler.willDisplay?(cell, indexPath) 90 | } 91 | 92 | public func tableView( 93 | _ tableView: UITableView, 94 | didEndDisplaying cell: UITableViewCell, 95 | forRowAt indexPath: IndexPath 96 | ) { 97 | storage.row(at: indexPath)?.actionsHandler.didEndDisplaying?(cell, indexPath) 98 | } 99 | 100 | // MARK: - Highlight 101 | 102 | public func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 103 | storage.row(at: indexPath)?.actionsHandler.shouldHighlight?(tableView.cellForRow(at: indexPath), indexPath) ?? true 104 | } 105 | 106 | // MARK: - Selection 107 | 108 | public func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 109 | storage.row(at: indexPath)?.actionsHandler.willSelect?(tableView.cellForRow(at: indexPath), indexPath) 110 | return indexPath 111 | } 112 | 113 | public func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? { 114 | storage.row(at: indexPath)?.actionsHandler.willDeselect?(tableView.cellForRow(at: indexPath), indexPath) 115 | return indexPath 116 | } 117 | 118 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 119 | guard let action = storage.row(at: indexPath)?.actionsHandler.didSelect else { 120 | return 121 | } 122 | 123 | action(tableView.cellForRow(at: indexPath), indexPath) 124 | tableView.deselectRow(at: indexPath, animated: true) 125 | } 126 | 127 | public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { 128 | storage.row(at: indexPath)?.actionsHandler.didDeselect?(tableView.cellForRow(at: indexPath), indexPath) 129 | } 130 | 131 | // MARK: - Movement 132 | 133 | public func tableView( 134 | _ tableView: UITableView, 135 | targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, 136 | toProposedIndexPath proposedDestinationIndexPath: IndexPath 137 | ) -> IndexPath { 138 | storage.row(at: sourceIndexPath)?.actionsHandler.moveTo?( 139 | sourceIndexPath, 140 | proposedDestinationIndexPath 141 | ) ?? proposedDestinationIndexPath 142 | } 143 | 144 | // MARK: - Editing Style 145 | 146 | public func tableView( 147 | _ tableView: UITableView, 148 | editingStyleForRowAt indexPath: IndexPath 149 | ) -> UITableViewCell.EditingStyle { 150 | storage.row(at: indexPath)?.actionsHandler.editingStyle?(tableView.cellForRow(at: indexPath), indexPath) ?? .none 151 | } 152 | 153 | // MARK: - Swipe Actions 154 | 155 | public func tableView( 156 | _ tableView: UITableView, 157 | leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath 158 | ) -> UISwipeActionsConfiguration? { 159 | storage.row(at: indexPath)?.actionsHandler.leadingSwipeActions?( 160 | tableView.cellForRow(at: indexPath), 161 | indexPath 162 | ) ?? nil 163 | } 164 | 165 | public func tableView( 166 | _ tableView: UITableView, 167 | trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath 168 | ) -> UISwipeActionsConfiguration? { 169 | storage.row(at: indexPath)?.actionsHandler.trailingSwipeActions?( 170 | tableView.cellForRow(at: indexPath), 171 | indexPath 172 | ) ?? nil 173 | } 174 | 175 | // MARK: - Default 176 | 177 | public func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { 178 | false 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Manager/TableManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableManager.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class TableManager: NSObject { 28 | 29 | // MARK: - Properties 30 | 31 | public var storage: TableStorage { 32 | didSet { 33 | subscribeOnStorage() 34 | } 35 | } 36 | 37 | public weak var tableView: UITableView? { 38 | didSet { 39 | registerer = TableCellRegisterer(tableView: tableView) 40 | subscribeOnTableView() 41 | } 42 | } 43 | 44 | public var scrollHandler = ScrollDelegateActionsHandler() 45 | private(set) var registerer: TableCellRegisterer 46 | 47 | // MARK: - Init 48 | 49 | public init(tableView: UITableView? = nil, storage: TableStorage? = nil) { 50 | self.tableView = tableView 51 | self.storage = storage ?? TableStorage() 52 | registerer = TableCellRegisterer(tableView: tableView) 53 | super.init() 54 | 55 | subscribeOnStorage() 56 | subscribeOnTableView() 57 | } 58 | 59 | // MARK: - Private 60 | 61 | private func subscribeOnStorage() { 62 | storage.delegate = self 63 | } 64 | 65 | private func subscribeOnTableView() { 66 | tableView?.dataSource = self 67 | tableView?.delegate = self 68 | } 69 | 70 | private func performUpdate(_ update: StorageUpdate, style: UITableView.RowAnimation) { 71 | switch update { 72 | case .reloadData: 73 | tableView?.reloadData() 74 | case .reloadSections(let indices): 75 | tableView?.reloadSections(IndexSet(indices), with: style) 76 | case .reloadRows(let indexPaths): 77 | tableView?.reloadRows(at: indexPaths, with: style) 78 | case .insertSections(let indices): 79 | tableView?.insertSections(IndexSet(indices), with: style) 80 | case .insertRows(let indexPaths): 81 | tableView?.insertRows(at: indexPaths, with: style) 82 | case .deleteSections(let indices): 83 | tableView?.deleteSections(IndexSet(indices), with: style) 84 | case .deleteRows(let indexPaths): 85 | tableView?.deleteRows(at: indexPaths, with: style) 86 | case .moveSection(let source, let destination): 87 | tableView?.moveSection(source, toSection: destination) 88 | case .moveRow(let source, let destination): 89 | tableView?.moveRow(at: source, to: destination) 90 | } 91 | } 92 | } 93 | 94 | // MARK: - StorageDelegate 95 | 96 | extension TableManager: StorageDelegate { 97 | 98 | internal func performUpdates(_ updates: [StorageUpdate], animated: Bool, completion: @escaping (Bool) -> Void) { 99 | tableView?.performBatchUpdates({ 100 | updates.forEach { 101 | performUpdate($0, animated: animated) 102 | } 103 | }, completion: { 104 | completion($0) 105 | }) 106 | } 107 | 108 | internal func performUpdate(_ update: StorageUpdate, animated: Bool) { 109 | let style: UITableView.RowAnimation = animated ? .automatic : .none 110 | if animated { 111 | performUpdate(update, style: style) 112 | } else { 113 | UIView.performWithoutAnimation { 114 | performUpdate(update, style: style) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Row/TableRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRow.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class TableRow: TableRowItem { 28 | 29 | // MARK: - Properties 30 | 31 | public var model: Cell.Model 32 | private let rowActionsHandler = TableRowActionsHandler() 33 | 34 | // MARK: - Init 35 | 36 | public init(_ model: Cell.Model) { 37 | self.model = model 38 | } 39 | 40 | // MARK: - TableRowItem 41 | 42 | public var reuseIdentifier: String { 43 | Cell.identifier 44 | } 45 | 46 | public var height: CGFloat { 47 | Cell.height 48 | } 49 | 50 | public var estimatedHeight: CGFloat { 51 | Cell.estimatedHeight 52 | } 53 | 54 | public var cellType: AnyClass { 55 | Cell.self 56 | } 57 | 58 | public var actionsHandler: TableRowActions { 59 | rowActionsHandler 60 | } 61 | 62 | public func configure(_ cell: UITableViewCell) { 63 | (cell as? Cell)?.update(with: model) 64 | } 65 | } 66 | 67 | // MARK: - TableDataSourceActions 68 | 69 | public extension TableRow { 70 | 71 | @discardableResult func canEdit(_ handler: @escaping TableRowActions.BoolAction) -> Self { 72 | rowActionsHandler.canEdit = handler 73 | return self 74 | } 75 | 76 | @discardableResult func canMove(_ handler: @escaping TableRowActions.BoolAction) -> Self { 77 | rowActionsHandler.canMove = handler 78 | return self 79 | } 80 | 81 | @discardableResult func commitStyle(_ handler: @escaping TableRowActions.CommitAction) -> Self { 82 | rowActionsHandler.commitStyle = handler 83 | return self 84 | } 85 | 86 | @discardableResult func move(_ handler: @escaping TableRowActions.MoveAction) -> Self { 87 | rowActionsHandler.move = handler 88 | return self 89 | } 90 | } 91 | 92 | // MARK: - TableDelegateActions 93 | 94 | public extension TableRow { 95 | 96 | typealias VoidAction = (TableRowActionResult?) -> Void 97 | typealias ResultAction = (TableRowActionResult?) -> T 98 | 99 | @discardableResult func didSelect(_ handler: @escaping VoidAction) -> Self { 100 | rowActionsHandler.didSelect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 101 | return self 102 | } 103 | 104 | @discardableResult func didDeselect(_ handler: @escaping VoidAction) -> Self { 105 | rowActionsHandler.didDeselect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 106 | return self 107 | } 108 | 109 | @discardableResult func willSelect(_ handler: @escaping VoidAction) -> Self { 110 | rowActionsHandler.willSelect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 111 | return self 112 | } 113 | 114 | @discardableResult func willDeselect(_ handler: @escaping VoidAction) -> Self { 115 | rowActionsHandler.willDeselect = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 116 | return self 117 | } 118 | 119 | @discardableResult func willDisplay(_ handler: @escaping VoidAction) -> Self { 120 | rowActionsHandler.willDisplay = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 121 | return self 122 | } 123 | 124 | @discardableResult func didEndDisplaying(_ handler: @escaping VoidAction) -> Self { 125 | rowActionsHandler.didEndDisplaying = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 126 | return self 127 | } 128 | 129 | @discardableResult func moveTo(_ handler: @escaping TableRowActions.MoveToAction) -> Self { 130 | rowActionsHandler.moveTo = handler 131 | return self 132 | } 133 | 134 | @discardableResult func shouldHighlight(_ handler: @escaping ResultAction) -> Self { 135 | rowActionsHandler.shouldHighlight = { [weak self] in handler(self?.actionResult(for: $0, at: $1)) } 136 | return self 137 | } 138 | 139 | @discardableResult func editingStyle(_ handler: @escaping ResultAction) -> Self { 140 | rowActionsHandler.editingStyle = { [weak self] in return handler(self?.actionResult(for: $0, at: $1)) } 141 | return self 142 | } 143 | 144 | @discardableResult func leadingSwipeActions(_ handler: @escaping ResultAction) -> Self { 145 | rowActionsHandler.leadingSwipeActions = { [weak self] in return handler(self?.actionResult(for: $0, at: $1)) } 146 | return self 147 | } 148 | 149 | @discardableResult func trailingSwipeActions(_ handler: @escaping ResultAction) -> Self { 150 | rowActionsHandler.trailingSwipeActions = { [weak self] in return handler(self?.actionResult(for: $0, at: $1)) } 151 | return self 152 | } 153 | } 154 | 155 | // MARK: - Private 156 | 157 | extension TableRow { 158 | 159 | private func actionResult(for cell: UITableViewCell?, at indexPath: IndexPath) -> TableRowActionResult { 160 | TableRowActionResult(model: model, cell: cell as? Cell, indexPath: indexPath) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Row/TableRowActionResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRowActionResult.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public final class TableRowActionResult { 28 | 29 | public let model: Cell.Model 30 | public let cell: Cell? 31 | public let indexPath: IndexPath 32 | 33 | public init(model: Cell.Model, cell: Cell?, indexPath: IndexPath) { 34 | self.model = model 35 | self.cell = cell 36 | self.indexPath = indexPath 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Row/TableRowActionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRowActionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class TableRowActionsHandler: TableRowActions { 28 | 29 | // MARK: - TableDataSourceActionsHandler 30 | 31 | public var canEdit: TableRowActions.BoolAction? 32 | public var canMove: TableRowActions.BoolAction? 33 | public var commitStyle: TableRowActions.CommitAction? 34 | public var move: TableRowActions.MoveAction? 35 | 36 | // MARK: - TableDelegateActionsHandler 37 | 38 | public var didSelect: TableRowActions.VoidAction? 39 | public var didDeselect: TableRowActions.VoidAction? 40 | public var willSelect: TableRowActions.VoidAction? 41 | public var willDeselect: TableRowActions.VoidAction? 42 | public var willDisplay: TableRowActions.VoidAction? 43 | public var didEndDisplaying: TableRowActions.VoidAction? 44 | public var shouldHighlight: TableRowActions.ShouldHighlightAction? 45 | public var moveTo: TableRowActions.MoveToAction? 46 | public var editingStyle: TableRowActions.EditingStyleAction? 47 | public var leadingSwipeActions: TableRowActions.SwipeConfigurationAction? 48 | public var trailingSwipeActions: TableRowActions.SwipeConfigurationAction? 49 | } 50 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Row/TableRowItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRowItem.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import UIKit 26 | 27 | public protocol TableRowItem: HashableItem { 28 | 29 | var reuseIdentifier: String { get } 30 | var height: CGFloat { get } 31 | var estimatedHeight: CGFloat { get } 32 | 33 | var cellType: AnyClass { get } 34 | 35 | var actionsHandler: TableRowActions { get } 36 | 37 | func configure(_ cell: UITableViewCell) 38 | } 39 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Section/TableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSection.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class TableSection: Section { 28 | 29 | // MARK: - Properties 30 | 31 | public var rows: [TableRowItem] 32 | public var header: TableHeaderFooterItem? 33 | public var footer: TableHeaderFooterItem? 34 | 35 | // MARK: - Init 36 | 37 | public init(rows: [TableRowItem] = []) { 38 | self.rows = rows 39 | } 40 | 41 | public convenience init( 42 | header: TableHeaderFooterItem? = nil, 43 | footer: TableHeaderFooterItem? = nil, 44 | rows: [TableRowItem] = [] 45 | ) { 46 | self.init(rows: rows) 47 | 48 | self.header = header 49 | self.footer = footer 50 | } 51 | 52 | // MARK: - Public 53 | 54 | public func index(for row: TableRowItem) -> Int? { 55 | guard let index = rows.firstIndex(where: { $0.hashValue == row.hashValue }) else { 56 | return nil 57 | } 58 | 59 | return index 60 | } 61 | 62 | public func contains(_ row: TableRowItem) -> Bool { 63 | rows.contains(where: { row.hashValue == $0.hashValue }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Section/TableSectionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSectionsHandler.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class TableSectionsHandler: SectionsHandler { 28 | 29 | // MARK: - Properties 30 | 31 | public var sections: [TableSection] 32 | 33 | // MARK: - Init 34 | 35 | public init(sections: [TableSection]) { 36 | self.sections = sections 37 | } 38 | 39 | // MARK: - Public 40 | 41 | public func emptySection() -> TableSection { 42 | TableSection() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManager/TableManager/Storage/TableStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableStorage.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | public final class TableStorage: Storage { 28 | 29 | // MARK: - Properties 30 | 31 | public let sectionsHandler: TableSectionsHandler 32 | 33 | // MARK: - Init 34 | 35 | public init() { 36 | sectionsHandler = TableSectionsHandler(sections: []) 37 | } 38 | 39 | public init(sections: [TableSection] = []) { 40 | sectionsHandler = TableSectionsHandler(sections: sections) 41 | } 42 | 43 | public init(rows: [TableRowItem] = []) { 44 | sectionsHandler = TableSectionsHandler(sections: [TableSection(rows: rows)]) 45 | } 46 | 47 | // MARK: - Header/Footer 48 | 49 | public func setHeader(_ header: SectionsHandlerItem.SectionItem.HeaderItem, to sectionIndex: Int) { 50 | guard let section = section(at: sectionIndex) else { 51 | logError("Failed to add header. Invalid section index \(sectionIndex)") 52 | return 53 | } 54 | section.header = header 55 | performUpdate(.reloadSections(at: [sectionIndex])) 56 | } 57 | 58 | public func setFooter(_ footer: SectionsHandlerItem.SectionItem.FooterItem, to sectionIndex: Int) { 59 | guard let section = section(at: sectionIndex) else { 60 | logError("Failed to add footer. Invalid section index \(sectionIndex)") 61 | return 62 | } 63 | section.footer = footer 64 | performUpdate(.reloadSections(at: [sectionIndex])) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 25.02.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | func application( 15 | _ application: UIApplication, 16 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 17 | ) -> Bool { 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application( 24 | _ application: UIApplication, 25 | configurationForConnecting connectingSceneSession: UISceneSession, 26 | options: UIScene.ConnectionOptions 27 | ) -> UISceneConfiguration { 28 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Common/TitleCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleCell.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 04.03.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class TitleCell: UITableViewCell, TableConfigurable { 13 | 14 | func update(with model: String) { 15 | textLabel?.text = model 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/Common/TitleHeaderFooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleHeaderFooter.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 04.03.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class TitleHeaderFooter: UITableViewHeaderFooterView, TableConfigurable { 13 | 14 | static var estimatedHeight: CGFloat { 15 | 44 16 | } 17 | 18 | func update(with model: String) { 19 | textLabel?.text = model 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/ExampleControllers/CollectionViewExampleVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewExampleVC.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 28.02.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class CollectionViewExampleVC: UIViewController { 13 | 14 | let manager = CollectionManager() 15 | var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setupUI() 20 | } 21 | 22 | // MARK: - Actions 23 | 24 | @objc private func addSection() { 25 | addSectionAlert { [weak self] headerTitle, footerTitle, numberOfItems in 26 | 27 | guard numberOfItems > 0 else { return } 28 | 29 | let section = CollectionSection(rows: (1...numberOfItems).map { 30 | CollectionRow("Row #\($0)") 31 | }) 32 | 33 | if let title = headerTitle, !title.isEmpty { 34 | section.header = CollectionHeaderFooter(title) 35 | } 36 | 37 | if let title = footerTitle, !title.isEmpty { 38 | section.footer = CollectionHeaderFooter(title) 39 | } 40 | 41 | self?.manager.storage.append(section) 42 | } 43 | } 44 | 45 | @objc private func deleteSection() { 46 | indexAlert("Delete section") { [weak self] sectionIndex in 47 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 48 | self?.manager.storage.delete(section) 49 | } 50 | } 51 | 52 | @objc private func addRow() { 53 | indexAlert("Add row to section") { [weak self] sectionIndex in 54 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 55 | 56 | let row = CollectionRow("Row #\(section.numberOfRows + 1)") 57 | self?.manager.storage.append(row, to: sectionIndex) 58 | } 59 | } 60 | 61 | @objc private func deleteRow() { 62 | indexAlert("Delete last row at section") { [weak self] sectionIndex in 63 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 64 | guard let row = section.rows.last else { return } 65 | self?.manager.storage.delete(row) 66 | } 67 | } 68 | 69 | // MARK: - Private 70 | 71 | private func setupUI() { 72 | 73 | navigationItem.rightBarButtonItems = [ 74 | UIBarButtonItem(title: "➕Section", style: .plain, target: self, action: #selector(addSection)), 75 | UIBarButtonItem(title: "❌Section", style: .plain, target: self, action: #selector(deleteSection)), 76 | UIBarButtonItem(title: "➕Row", style: .plain, target: self, action: #selector(addRow)), 77 | UIBarButtonItem(title: "❌Row", style: .plain, target: self, action: #selector(deleteRow)) 78 | ] 79 | 80 | setupCollectionView() 81 | } 82 | 83 | private func setupCollectionView() { 84 | view.addSubview(collectionView) 85 | 86 | collectionView.backgroundColor = .white 87 | collectionView.translatesAutoresizingMaskIntoConstraints = false 88 | collectionView.alwaysBounceVertical = true 89 | 90 | NSLayoutConstraint.activate([ 91 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 92 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 93 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 94 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 95 | ]) 96 | 97 | manager.collectionView = collectionView 98 | } 99 | 100 | private func addSectionAlert(_ completion: @escaping (String?, String?, Int) -> Void) { 101 | let alert = UIAlertController(title: "Add Section", message: "", preferredStyle: .alert) 102 | 103 | alert.addTextField { $0.placeholder = "Header text" } 104 | alert.addTextField { $0.placeholder = "Footer text" } 105 | alert.addTextField { $0.placeholder = "Number of rows" } 106 | 107 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] _ in 108 | completion( 109 | alert?.textFields?[0].text, 110 | alert?.textFields?[1].text, 111 | Int(alert?.textFields?[2].text ?? "0") ?? 0 112 | ) 113 | })) 114 | 115 | present(alert, animated: true, completion: nil) 116 | } 117 | 118 | private func indexAlert(_ title: String, _ completion: @escaping (Int) -> Void) { 119 | let alert = UIAlertController(title: title, message: "", preferredStyle: .alert) 120 | 121 | alert.addTextField { $0.placeholder = "At index" } 122 | 123 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] _ in 124 | completion(Int(alert?.textFields?[0].text ?? "0") ?? 0) 125 | })) 126 | 127 | present(alert, animated: true, completion: nil) 128 | } 129 | } 130 | 131 | extension CollectionViewExampleVC { 132 | 133 | class Cell: UICollectionViewCell, CollectionConfigurable { 134 | 135 | static var size: CGSize? { 136 | CGSize(width: 100, height: 100) 137 | } 138 | 139 | private let label = UILabel() 140 | 141 | override init(frame: CGRect) { 142 | super.init(frame: frame) 143 | 144 | contentView.backgroundColor = .lightGray 145 | 146 | contentView.addSubview(label) 147 | 148 | label.translatesAutoresizingMaskIntoConstraints = false 149 | 150 | label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true 151 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 152 | } 153 | 154 | required init?(coder: NSCoder) { 155 | fatalError("init(coder:) has not been implemented") 156 | } 157 | 158 | func update(with model: String) { 159 | label.text = model 160 | } 161 | } 162 | 163 | class HeaderFooter: UICollectionReusableView, CollectionConfigurable { 164 | 165 | static var size: CGSize? { 166 | CGSize(width: .zero, height: 44) 167 | } 168 | 169 | private let label = UILabel() 170 | 171 | override init(frame: CGRect) { 172 | super.init(frame: frame) 173 | 174 | addSubview(label) 175 | 176 | label.translatesAutoresizingMaskIntoConstraints = false 177 | 178 | label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 179 | label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 180 | } 181 | 182 | required init?(coder: NSCoder) { 183 | fatalError("init(coder:) has not been implemented") 184 | } 185 | 186 | func update(with model: String) { 187 | label.text = model 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/ExampleControllers/TableViewExampleVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewExampleVC.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 28.02.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class TableViewExampleVC: UIViewController { 13 | 14 | let manager = TableManager() 15 | var tableView = UITableView(frame: .zero, style: .plain) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | setupUI() 20 | } 21 | 22 | // MARK: - Actions 23 | 24 | @objc private func addSection() { 25 | addSectionAlert { [weak self] headerTitle, footerTitle, numberOfItems in 26 | 27 | guard numberOfItems > 0 else { return } 28 | 29 | let section = TableSection(rows: (1...numberOfItems).map { 30 | TableRow("Row #\($0)") 31 | }) 32 | 33 | if let title = headerTitle, !title.isEmpty { 34 | section.header = TableHeaderFooter(title) 35 | } 36 | 37 | if let title = footerTitle, !title.isEmpty { 38 | section.footer = TableHeaderFooter(title) 39 | } 40 | 41 | self?.manager.storage.append(section) 42 | } 43 | } 44 | 45 | @objc private func deleteSection() { 46 | indexAlert("Delete section") { [weak self] sectionIndex in 47 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 48 | self?.manager.storage.delete(section) 49 | } 50 | } 51 | 52 | @objc private func addRow() { 53 | indexAlert("Add row to section") { [weak self] sectionIndex in 54 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 55 | 56 | let row = TableRow("Row #\(section.numberOfRows + 1)") 57 | self?.manager.storage.append(row, to: sectionIndex) 58 | } 59 | } 60 | 61 | @objc private func deleteRow() { 62 | indexAlert("Delete last row at section") { [weak self] sectionIndex in 63 | guard let section = self?.manager.storage.section(at: sectionIndex) else { return } 64 | guard let row = section.rows.last else { return } 65 | self?.manager.storage.delete(row) 66 | } 67 | } 68 | 69 | // MARK: - Private 70 | 71 | private func setupUI() { 72 | 73 | navigationItem.rightBarButtonItems = [ 74 | UIBarButtonItem(title: "➕Section", style: .plain, target: self, action: #selector(addSection)), 75 | UIBarButtonItem(title: "❌Section", style: .plain, target: self, action: #selector(deleteSection)), 76 | UIBarButtonItem(title: "➕Row", style: .plain, target: self, action: #selector(addRow)), 77 | UIBarButtonItem(title: "❌Row", style: .plain, target: self, action: #selector(deleteRow)) 78 | ] 79 | 80 | setupTableView() 81 | } 82 | 83 | private func setupTableView() { 84 | view.addSubview(tableView) 85 | 86 | tableView.translatesAutoresizingMaskIntoConstraints = false 87 | 88 | NSLayoutConstraint.activate([ 89 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 90 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 91 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 92 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 93 | ]) 94 | 95 | manager.tableView = tableView 96 | } 97 | 98 | private func addSectionAlert(_ completion: @escaping (String?, String?, Int) -> Void) { 99 | let alert = UIAlertController(title: "Add Section", message: "", preferredStyle: .alert) 100 | 101 | alert.addTextField { $0.placeholder = "Header text" } 102 | alert.addTextField { $0.placeholder = "Footer text" } 103 | alert.addTextField { $0.placeholder = "Number of rows" } 104 | 105 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] _ in 106 | completion( 107 | alert?.textFields?[0].text, 108 | alert?.textFields?[1].text, 109 | Int(alert?.textFields?[2].text ?? "0") ?? 0 110 | ) 111 | })) 112 | 113 | present(alert, animated: true, completion: nil) 114 | } 115 | 116 | private func indexAlert(_ title: String, _ completion: @escaping (Int) -> Void) { 117 | let alert = UIAlertController(title: title, message: "", preferredStyle: .alert) 118 | 119 | alert.addTextField { $0.placeholder = "At index" } 120 | 121 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] _ in 122 | completion(Int(alert?.textFields?[0].text ?? "0") ?? 0) 123 | })) 124 | 125 | present(alert, animated: true, completion: nil) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/ExampleControllers/TableViewReorderingVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewReorderingVC.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 04.03.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class TableViewReorderingVC: UIViewController { 13 | 14 | let manager = TableManager() 15 | var tableView = UITableView(frame: .zero, style: .plain) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | title = "Reordering" 21 | 22 | setupTableView() 23 | 24 | let items = [ 25 | ["Section #1 Row #1", "Section #1 Row #2"], 26 | ["Section #2 Row #1"], 27 | ["Section #3 Row #1", "Section #3 Row #2", "Section #3 Row #3", "Section #3 Row #4"] 28 | ] 29 | 30 | manager.storage.append( 31 | items.map { 32 | TableSection(rows: $0.map { 33 | row(for: $0) 34 | }) 35 | } 36 | ) 37 | 38 | navigationItem.rightBarButtonItem = editButtonItem 39 | } 40 | 41 | override func setEditing(_ editing: Bool, animated: Bool) { 42 | super.setEditing(editing, animated: animated) 43 | tableView.setEditing(editing, animated: animated) 44 | } 45 | 46 | // MARK: - Private 47 | 48 | private func row(for title: String) -> TableRow { 49 | TableRow(title).canEdit { _ in true }.canMove { _ in true }.editingStyle { _ in .delete }.move { [weak self] in 50 | self?.manager.storage.moveRowWithoutUpdate(from: $0, to: $1) 51 | }.commitStyle { [weak self] in 52 | guard $0 == .delete, let rowToDelete = self?.manager.storage.row(at: $1) else { return } 53 | self?.manager.storage.delete(rowToDelete) 54 | } 55 | } 56 | 57 | private func setupTableView() { 58 | view.addSubview(tableView) 59 | 60 | tableView.translatesAutoresizingMaskIntoConstraints = false 61 | 62 | NSLayoutConstraint.activate([ 63 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 64 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 65 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 66 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 67 | ]) 68 | 69 | manager.tableView = tableView 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/MenuVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 25.02.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TableCollectionManager 11 | 12 | class MenuVC: UIViewController { 13 | 14 | let manager = TableManager() 15 | var tableView = UITableView(frame: .zero, style: .plain) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | title = "TableCollectionManager" 21 | 22 | setupTableView() 23 | 24 | manager.storage.append([ 25 | TableRow("TableView").didSelect { [weak self] _ in 26 | self?.navigationController?.pushViewController(TableViewExampleVC(), animated: true) 27 | }, 28 | TableRow("TableView Reordering").didSelect { [weak self] _ in 29 | self?.navigationController?.pushViewController(TableViewReorderingVC(), animated: true) 30 | }, 31 | TableRow("CollectionView").didSelect { [weak self] _ in 32 | self?.navigationController?.pushViewController(CollectionViewExampleVC(), animated: true) 33 | } 34 | ]) 35 | 36 | manager.storage.setHeader(TableHeaderFooter("Examples"), to: .zero) 37 | } 38 | 39 | // MARK: - Private 40 | 41 | private func setupTableView() { 42 | 43 | view.addSubview(tableView) 44 | 45 | tableView.translatesAutoresizingMaskIntoConstraints = false 46 | 47 | NSLayoutConstraint.activate([ 48 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 49 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 50 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 51 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 52 | ]) 53 | 54 | manager.tableView = tableView 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerExample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // TableCollectionManagerExample 4 | // 5 | // Created by Dima Mishchenko on 25.02.2020. 6 | // Copyright © 2020 Dmytro Mishchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | guard let _ = (scene as? UIWindowScene) else { return } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TableCollectionManager/TableCollectionManagerTests/TableCollectionManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableCollectionManagerTests.swift 3 | // 4 | // Copyright (c) 2020 Dmytro Mishchenko (https://github.com/DimaMishchenko) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import XCTest 26 | @testable import TableCollectionManager 27 | 28 | class TableCollectionManagerTests: XCTestCase { 29 | 30 | override func setUp() { 31 | // Put setup code here. This method is called before the invocation of each test method in the class. 32 | } 33 | 34 | override func tearDown() { 35 | // Put teardown code here. This method is called after the invocation of each test method in the class. 36 | } 37 | 38 | func testExample() { 39 | // This is an example of a functional test case. 40 | // Use XCTAssert and related functions to verify your tests produce the correct results. 41 | } 42 | 43 | func testPerformanceExample() { 44 | // This is an example of a performance test case. 45 | self.measure { 46 | // Put the code you want to measure the time of here. 47 | } 48 | } 49 | 50 | } 51 | --------------------------------------------------------------------------------