├── .github ├── codecov.yml └── workflows │ ├── ci.yml │ ├── generate-docs.yml │ └── pod-lint.yml ├── .gitignore ├── .jazzy.yaml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CHANGELOG.md ├── Cartfile ├── DTTableViewManager.podspec ├── DTTableViewManager.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ ├── xcbaselines │ ├── 9A6CED5B1BE570750091B0AF.xcbaseline │ │ ├── 54946D62-95C0-45DA-8093-C40D67816E4A.plist │ │ ├── EF4EBE41-A313-40F5-BF94-0050C7DA8AF3.plist │ │ └── Info.plist │ └── 9AF003191A5ABE7000ABFC90.xcbaseline │ │ ├── 0BA00215-5FA3-4E34-9991-38A949912C7A.plist │ │ ├── 57C13DCB-C084-4801-A3DF-4AF613702629.plist │ │ ├── 86EC97F0-9D90-46AC-8385-571A93CEFF5F.plist │ │ ├── C49EB807-F831-4102-B309-BD3DE6F89A55.plist │ │ ├── D57F76B3-54EA-4235-ACAE-A6C5A56EE7B6.plist │ │ ├── DADEF45D-3D90-4B49-AF9F-38FAE5D06F85.plist │ │ └── Info.plist │ └── xcschemes │ ├── DTTableViewManager.xcscheme │ ├── Tests-iOS.xcscheme │ └── Tests-tvOS.xcscheme ├── Documentation ├── Anomalies.md ├── Conditional mappings.md ├── Datasources.md ├── Events.md ├── Mapping.md ├── Migration guides │ ├── 4.0 Migration Guide.md │ ├── 5.0 migration guide.md │ ├── 6.0 Migration Guide.md │ ├── 7.0 Migration Guide.md │ └── 8.0 Migration Guide.md ├── Registration.md ├── SwiftUI.md ├── TableViewConfiguration.md ├── TableViewUpdater.md └── Why.md ├── Example ├── App │ ├── AppDelegate.swift │ ├── Info.plist │ └── SceneDelegate.swift ├── Cells │ ├── BankCell.swift │ ├── BankCell.xib │ ├── CustomStringCell.swift │ └── CustomStringCell.xib ├── Controllers │ ├── AddRemoveViewController.swift │ ├── AutoDiffSearchViewController.swift │ ├── CoreDataSearchViewController.swift │ ├── CustomViewsController.swift │ ├── DiffableCoreDataViewController.swift │ ├── ExamplesListViewController.swift │ ├── MultiSectionDiffingTableViewController.swift │ └── ReorderViewController.swift ├── CoreData │ ├── Bank.swift │ ├── Banks.xcdatamodeld │ │ └── Banks.xcdatamodel │ │ │ └── contents │ └── CoreDataManager.swift ├── Example.entitlements ├── Extensions │ └── UIViewController+Extensions.swift ├── Headers:Footers │ ├── CustomHeaderFooterView.swift │ └── CustomHeaderFooterView.xib ├── Models │ └── Example.swift └── Resources │ ├── Base.lproj │ └── LaunchScreen.xib │ └── Resources.xcassets │ ├── AppIcon.appiconset │ └── Contents.json │ ├── Banks.dataset │ ├── Banks.json │ └── Contents.json │ ├── Contents.json │ ├── LaunchImage.launchimage │ ├── Contents.json │ ├── Default-568h@2x.png │ ├── iPhone 6Plus.png │ └── iPhone6.png │ ├── mochaGrunge.imageset │ ├── Contents.json │ └── mochaGrunge.png │ ├── students.dataset │ ├── Contents.json │ └── students.json │ └── textured_paper.imageset │ ├── Contents.json │ └── textured_paper.png ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── DTTableViewManager │ ├── DTTableViewDataSource.swift │ ├── DTTableViewDelegate.swift │ ├── DTTableViewDelegateWrapper.swift │ ├── DTTableViewDiffableDataSource.swift │ ├── DTTableViewDragDelegate.swift │ ├── DTTableViewDropDelegate.swift │ ├── DTTableViewDropPlaceholderContext.swift │ ├── DTTableViewManager+DataSource.swift │ ├── DTTableViewManager+Delegate.swift │ ├── DTTableViewManager+Deprecated.swift │ ├── DTTableViewManager+Drag.swift │ ├── DTTableViewManager+Drop.swift │ ├── DTTableViewManager+Prefetch.swift │ ├── DTTableViewManager+Registration.swift │ ├── DTTableViewManager+SwiftUIRegistration.swift │ ├── DTTableViewManager.swift │ ├── DTTableViewManagerAnomalyHandler.swift │ ├── DTTableViewPrefetchDataSource.swift │ ├── Exports.swift │ ├── HostingCellViewModelMapping.swift │ ├── HostingConfigurationViewModelMapping.swift │ ├── HostingTableViewCell.swift │ ├── TableViewCellModelMapping.swift │ ├── TableViewConfiguration.swift │ ├── TableViewFactory.swift │ ├── TableViewHeaderFooterViewModelMapping.swift │ └── TableViewUpdater.swift └── Tests │ ├── AnomaliesTestCase.swift │ ├── BaseTestCase.swift │ ├── CreationTestCase.swift │ ├── DatasourceTestCase.swift │ ├── DiffableDatasourcesTestCase.swift │ ├── Fixtures │ ├── DTTestTableViewController.swift │ ├── FixtureCells.swift │ ├── FixtureStoryboard.storyboard │ ├── FixtureViews.swift │ ├── OptionalCells.swift │ ├── ReactingHeaderFooters.swift │ ├── ReactingTableCell.swift │ ├── StoryboardCell.swift │ ├── XibTableViewController.swift │ ├── iOS │ │ ├── CustomNibCell.xib │ │ ├── EmptyXib.xib │ │ ├── NibCell.xib │ │ ├── NibHeaderFooterView.xib │ │ ├── NibView.xib │ │ ├── RandomNameWrongReuseIdentifierCell.xib │ │ ├── RandomNibNameCell.xib │ │ ├── ReactingHeaderFooterView.xib │ │ ├── WrongReuseIdentifierCell.xib │ │ └── XibTableViewController.xib │ └── tvOS │ │ ├── CustomNibCell.xib │ │ ├── EmptyXib.xib │ │ ├── FixtureStoryboard.storyboard │ │ ├── NibCell.xib │ │ ├── NibHeaderFooterView.xib │ │ ├── NibView.xib │ │ ├── RandomNameWrongReuseIdentifierCell.xib │ │ ├── RandomNibNameCell.xib │ │ ├── ReactingHeaderFooterView.xib │ │ ├── WrongReuseIdentifierCell.xib │ │ └── XibTableViewController.xib │ ├── MappingConditionsTestCase.swift │ ├── MappingTestCase.swift │ ├── NSIndexPath+Constructor.swift │ ├── ReactingToEventsTestCase.swift │ ├── StoryboardMappingTestCase.swift │ ├── StoryboardViewController.swift │ ├── TableViewController+UnitTests.swift │ ├── TableViewFactoryTestCase.swift │ └── ViewModelMappingCustomizableTestCase.swift ├── Supporting files ├── Framework.plist └── Tests.plist ├── docs ├── Classes.html ├── Classes │ ├── DTTableViewDataSource.html │ ├── DTTableViewDelegate.html │ ├── DTTableViewDelegateWrapper.html │ ├── DTTableViewDragDelegate.html │ ├── DTTableViewDropDelegate.html │ ├── DTTableViewDropPlaceholderContext.html │ ├── DTTableViewManager.html │ ├── DTTableViewManagerAnomalyHandler.html │ └── TableViewUpdater.html ├── Enums.html ├── Enums │ ├── DTTableViewManagerAnomaly.html │ └── SupplementarySectionStyle.html ├── Extensions.html ├── Extensions │ ├── SupplementaryAccessible.html │ ├── UIView.html │ └── ViewModelMapping.html ├── Protocols.html ├── Protocols │ ├── DTTableViewManageable.html │ └── DTTableViewOptionalManageable.html ├── Structs.html ├── Structs │ └── TableViewConfiguration.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── DTTableViewManager.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── DTTableViewDataSource.html │ │ │ │ ├── DTTableViewDelegate.html │ │ │ │ ├── DTTableViewDelegateWrapper.html │ │ │ │ ├── DTTableViewDragDelegate.html │ │ │ │ ├── DTTableViewDropDelegate.html │ │ │ │ ├── DTTableViewDropPlaceholderContext.html │ │ │ │ ├── DTTableViewManager.html │ │ │ │ ├── DTTableViewManagerAnomalyHandler.html │ │ │ │ └── TableViewUpdater.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ ├── DTTableViewManagerAnomaly.html │ │ │ │ └── SupplementarySectionStyle.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ ├── SupplementaryAccessible.html │ │ │ │ ├── UIView.html │ │ │ │ └── ViewModelMapping.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── DTTableViewManageable.html │ │ │ │ └── DTTableViewOptionalManageable.html │ │ │ ├── Structs.html │ │ │ ├── Structs │ │ │ │ └── TableViewConfiguration.html │ │ │ ├── badge.svg │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ ├── search.json │ │ │ └── undocumented.json │ │ │ └── docSet.dsidx │ ├── DTTableViewManager.tgz │ └── DTTableViewManager.xml ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json └── fastlane ├── Fastfile ├── Pluginfile ├── README.md └── Scanfile /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: header, changes, diff 3 | coverage: 4 | ignore: 5 | - Sources/Tests 6 | status: 7 | patch: false 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | env-details-Xcode-13: 13 | name: Environment details Xcode 15 14 | runs-on: macOS-14 15 | env: 16 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 17 | steps: 18 | - name: xcode version 19 | run: xcodebuild -version -sdk 20 | 21 | - name: list simulators 22 | run: | 23 | xcrun simctl delete unavailable 24 | xcrun simctl list 25 | env-details-Xcode-14: 26 | name: Environment details Xcode 14 27 | runs-on: macOS-14 28 | env: 29 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 30 | steps: 31 | - name: xcode version 32 | run: xcodebuild -version -sdk 33 | 34 | - name: list simulators 35 | run: | 36 | xcrun simctl delete unavailable 37 | xcrun simctl list 38 | 39 | # Xcode-13: 40 | # name: Xcode 13 41 | # runs-on: macOS-12 42 | # env: 43 | # DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer 44 | # strategy: 45 | # matrix: 46 | # destination: ["test_ios15", "test_tvos15", "test_catalyst"] 47 | # steps: 48 | # - name: git checkout 49 | # uses: actions/checkout@v2 50 | # - name: ruby setup 51 | # uses: ruby/setup-ruby@v1 52 | # with: 53 | # ruby-version: 3.1.2 54 | # bundler-cache: true 55 | # - name: ${{ matrix.destination }} 56 | # run: bundle exec fastlane ${{ matrix.destination }} 57 | # - name: Generate code coverage 58 | # if: matrix.destination != 'test_catalyst' 59 | # run: bundle exec fastlane generate_code_coverage 60 | # - name: Codecov 61 | # uses: codecov/codecov-action@v3 62 | 63 | Xcode-15: 64 | name: Xcode 15.4 65 | runs-on: macOS-14 66 | env: 67 | DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer 68 | strategy: 69 | matrix: 70 | destination: ["test_ios17", "test_tvos17", "test_catalyst"] 71 | steps: 72 | - name: git checkout 73 | uses: actions/checkout@v2 74 | - name: ruby setup 75 | uses: ruby/setup-ruby@v1 76 | with: 77 | ruby-version: 3.1.2 78 | bundler-cache: true 79 | - name: ${{ matrix.destination }} 80 | run: bundle exec fastlane ${{ matrix.destination }} 81 | - name: Generate code coverage 82 | if: matrix.destination != 'test_catalyst' 83 | run: bundle exec fastlane generate_code_coverage 84 | - name: Codecov 85 | uses: codecov/codecov-action@v2 86 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Documentation 2 | 3 | on: 4 | - workflow_dispatch 5 | 6 | env: 7 | DEVELOPER_DIR: /Applications/Xcode_14.0.app/Contents/Developer 8 | 9 | jobs: 10 | main: 11 | name: Create docs PR 12 | runs-on: macOS-12 13 | steps: 14 | - name: git checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: ruby setup 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 3.1.2 21 | bundler-cache: true 22 | 23 | - name: generate docs 24 | run: bundle exec jazzy 25 | 26 | - name: create pull request 27 | uses: peter-evans/create-pull-request@v3 28 | with: 29 | commit-message: "[automated] generate docs" 30 | title: "[automated] generate docs" 31 | body: Documentation automatically generated via GitHub Actions. 32 | branch: gh-pages 33 | delete-branch: true 34 | labels: documentation 35 | assignees: ${{ github.actor }} 36 | reviewers: ${{ github.actor }} 37 | draft: false 38 | -------------------------------------------------------------------------------- /.github/workflows/pod-lint.yml: -------------------------------------------------------------------------------- 1 | name: Pod Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/workflows/pod-lint.yml' 12 | - '*.podspec' 13 | - 'Gemfile*' 14 | - 'Source/**/*.*' 15 | 16 | env: 17 | DEVELOPER_DIR: /Applications/Xcode_14.0.app/Contents/Developer 18 | 19 | jobs: 20 | main: 21 | name: Pod Lint 22 | runs-on: macOS-12 23 | steps: 24 | - name: git checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: ruby versions 28 | run: | 29 | ruby --version 30 | gem --version 31 | bundler --version 32 | - name: ruby setup 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: 3.1.2 36 | bundler-cache: true 37 | 38 | - name: pod lint 39 | run: bundle exec fastlane pod_lint 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/swift 2 | 3 | ### Swift ### 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | .build/ 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 51 | 52 | fastlane/report.xml 53 | fastlane/Preview.html 54 | fastlane/screenshots 55 | fastlane/test_output 56 | 57 | ### Swift.CocoaPods Stack ### 58 | ## CocoaPods GitIgnore Template 59 | 60 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 61 | # - Also handy if you have a lage number of dependant pods 62 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGONRE THE LOCK FILE 63 | Pods/ 64 | 65 | ### Swift.Carthage Stack ### 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | Carthage/Checkouts 70 | 71 | Carthage/Build 72 | 73 | # End of https://www.gitignore.io/api/swift 74 | 75 | # Ignore Ruby CI setup 76 | vendor/bundle 77 | .bundle/config 78 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | author: Denys Telezhkin 2 | author_url: https://github.com/DenTelezhkin 3 | github_url: https://github.com/DenTelezhkin/DTTableViewManager 4 | root_url: https://dentelezhkin.github.io/DTTableViewManager/ 5 | module: DTTableViewManager 6 | xcodebuild_arguments: [-scheme, DTTableViewManager, -sdk, iphonesimulator] 7 | output: docs 8 | theme: fullwidth 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - colon 3 | - opening_brace 4 | - trailing_whitespace 5 | - vertical_whitespace 6 | - unused_optional_binding 7 | - identifier_name 8 | - redundant_string_enum_value 9 | - vertical_parameter_alignment 10 | - switch_case_alignment 11 | - attributes 12 | - xctfail_message 13 | opt_in_rules: # some rules are only opt-in 14 | - force_unwrapping 15 | - overridden_super_call 16 | - closure_spacing 17 | - contains_over_first_not_nil 18 | - first_where 19 | - closure_end_indentation 20 | - sorted_first_last 21 | # Find all the available rules by running: 22 | # swiftlint rules 23 | included: 24 | - Sources 25 | excluded: 26 | - Sources/Tests 27 | 28 | # configurable rules can be customized from this configuration file 29 | # binary rules can set their severity level 30 | force_cast: warning # implicitly 31 | force_try: 32 | severity: warning # explicitly 33 | 34 | cyclomatic_complexity: 35 | ignores_case_statements: true 36 | function_body_length: 37 | - 300 #warning 38 | type_body_length: 39 | - 350 # warning 40 | - 500 # error 41 | # or they can set both explicitly 42 | line_length: 43 | - 300 44 | file_length: 45 | warning: 500 46 | error: 1200 47 | ignore_comment_only_lines: true 48 | # naming rules can set warnings/errors for min_length and max_length 49 | # additionally they can set excluded names 50 | type_name: 51 | min_length: 3 # only warning 52 | max_length: # warning and error 53 | warning: 40 54 | error: 50 55 | excluded: 56 | - iPhone 57 | - API 58 | - E 59 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) 60 | 61 | custom_rules: 62 | missing_docs: 63 | included: ".*.swift" 64 | regex: '\n *(?!\/\/\/)(\/\/)?[^\n\/]*\n *(?:@\S+ )*(?:public|open)' 65 | name: "Missing Docs" 66 | message: "Types, properties and methods with public or open access level should be documented." 67 | severity: warning 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "DenTelezhkin/DTModelStorage" >= 11.0.0 2 | -------------------------------------------------------------------------------- /DTTableViewManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DTTableViewManager' 3 | s.version = "11.0.1" 4 | s.license = 'MIT' 5 | s.summary = 'Protocol-oriented UITableView management, powered by generics and associated types.' 6 | s.homepage = 'https://github.com/DenTelezhkin/DTTableViewManager' 7 | s.authors = { 'Denys Telezhkin' => 'denys.telezhkin.oss@gmail.com' } 8 | s.social_media_url = 'https://twitter.com/DenTelezhkin' 9 | s.source = { :git => 'https://github.com/DenTelezhkin/DTTableViewManager.git', :tag => s.version.to_s } 10 | s.source_files = 'Sources/DTTableViewManager/*.swift' 11 | s.swift_versions = ['5.3', '5.4', '5.5'] 12 | s.ios.deployment_target = '11.0' 13 | s.tvos.deployment_target = '11.0' 14 | s.frameworks = 'UIKit', 'Foundation' 15 | s.dependency 'DTModelStorage' , '~> 11.0.0' 16 | end 17 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "be4f6867adfebf7a2a613fab8cbb79ea7c62c436f1182c9af68110069060528d", 3 | "pins" : [ 4 | { 5 | "identity" : "changeset", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/DenTelezhkin/Changeset", 8 | "state" : { 9 | "branch" : "swiftpm-platforms", 10 | "revision" : "76ee0cdc4f46a5129229e36530384e1bee7d34e5" 11 | } 12 | }, 13 | { 14 | "identity" : "dtmodelstorage", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/DenTelezhkin/DTModelStorage", 17 | "state" : { 18 | "branch" : "main", 19 | "revision" : "1473c7ed4970b9572e93cf0305c80d54855b3cdf" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9A6CED5B1BE570750091B0AF.xcbaseline/54946D62-95C0-45DA-8093-C40D67816E4A.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.012439 15 | baselineIntegrationDisplayName 16 | Jul 6, 2019 at 4:23:53 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 7.4356e-05 25 | baselineIntegrationDisplayName 26 | Jul 6, 2019 at 4:23:53 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.00017727 38 | baselineIntegrationDisplayName 39 | Jul 6, 2019 at 4:23:53 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9A6CED5B1BE570750091B0AF.xcbaseline/EF4EBE41-A313-40F5-BF94-0050C7DA8AF3.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.01259 15 | baselineIntegrationDisplayName 16 | May 14, 2019 at 5:01:06 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 3.5677e-05 25 | baselineIntegrationDisplayName 26 | May 14, 2019 at 5:01:06 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.00019277 38 | baselineIntegrationDisplayName 39 | May 14, 2019 at 5:01:06 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9A6CED5B1BE570750091B0AF.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 54946D62-95C0-45DA-8093-C40D67816E4A 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2900 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro13,3 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | AppleTV6,2 34 | platformIdentifier 35 | com.apple.platform.appletvsimulator 36 | 37 | 38 | EF4EBE41-A313-40F5-BF94-0050C7DA8AF3 39 | 40 | localComputer 41 | 42 | busSpeedInMHz 43 | 100 44 | cpuCount 45 | 1 46 | cpuKind 47 | Intel Core i7 48 | cpuSpeedInMHz 49 | 3500 50 | logicalCPUCoresPerPackage 51 | 8 52 | modelCode 53 | iMac14,2 54 | physicalCPUCoresPerPackage 55 | 4 56 | platformIdentifier 57 | com.apple.platform.macosx 58 | 59 | targetArchitecture 60 | x86_64 61 | targetDevice 62 | 63 | modelCode 64 | AppleTV6,2 65 | platformIdentifier 66 | com.apple.platform.appletvsimulator 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/0BA00215-5FA3-4E34-9991-38A949912C7A.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.019256 15 | baselineIntegrationDisplayName 16 | Jul 6, 2019 at 4:24:00 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 8.4147e-05 25 | baselineIntegrationDisplayName 26 | Jul 6, 2019 at 4:24:00 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.00033409 38 | baselineIntegrationDisplayName 39 | Jul 6, 2019 at 4:24:00 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/57C13DCB-C084-4801-A3DF-4AF613702629.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.017995 15 | baselineIntegrationDisplayName 16 | May 14, 2019 at 5:30:49 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 3.1722e-05 25 | baselineIntegrationDisplayName 26 | May 14, 2019 at 5:30:49 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.0001761 38 | baselineIntegrationDisplayName 39 | May 14, 2019 at 5:30:49 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/86EC97F0-9D90-46AC-8385-571A93CEFF5F.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.020199 15 | baselineIntegrationDisplayName 16 | May 14, 2019 at 5:00:15 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 7.1243e-05 25 | baselineIntegrationDisplayName 26 | May 14, 2019 at 5:00:15 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.00021903 38 | baselineIntegrationDisplayName 39 | May 14, 2019 at 5:00:15 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/C49EB807-F831-4102-B309-BD3DE6F89A55.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.026539 15 | baselineIntegrationDisplayName 16 | Jul 23, 2019 at 7:33:48 PM 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 4.3824e-05 25 | baselineIntegrationDisplayName 26 | Jul 23, 2019 at 7:33:48 PM 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.0002999 38 | baselineIntegrationDisplayName 39 | Jul 23, 2019 at 7:33:48 PM 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/D57F76B3-54EA-4235-ACAE-A6C5A56EE7B6.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.012693 15 | baselineIntegrationDisplayName 16 | 8 июля 2017 г., 15:48:58 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 5.8104e-05 25 | baselineIntegrationDisplayName 26 | 8 июля 2017 г., 15:48:58 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.00026169 38 | baselineIntegrationDisplayName 39 | 8 июля 2017 г., 15:48:58 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcbaselines/9AF003191A5ABE7000ABFC90.xcbaseline/DADEF45D-3D90-4B49-AF9F-38FAE5D06F85.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | ReactingToEventsFastTestCase 8 | 9 | testEventsRegistrationPerfomance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.01 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | testSearchForEventPerfomance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.002 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | 30 | ReactingToEventsTestCase 31 | 32 | testCellSelectionPerfomance() 33 | 34 | com.apple.XCTPerformanceMetric_WallClockTime 35 | 36 | baselineAverage 37 | 0.003 38 | baselineIntegrationDisplayName 39 | Local Baseline 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcschemes/DTTableViewManager.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcschemes/Tests-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /DTTableViewManager.xcodeproj/xcshareddata/xcschemes/Tests-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Documentation/Anomalies.md: -------------------------------------------------------------------------------- 1 | ### Anomaly handler 2 | 3 | `DTTableViewManager` is built on some conventions. For example, your cell needs to have reuseIdentifier that matches the name of your class, XIB files need to be named also identical to the name of your class(to work with default mapping without customization). However when those conventions are not followed, or something unexpected happens, your app may crash or behave inconsistently. Most of the errors are reported by `UITableView` API, but there's space to improve. 4 | 5 | `DTTableViewManager` as well as `DTCollectionViewManager` and `DTModelStorage` have dedicated anomaly analyzers, that try to find inconsistencies and programmer errors when using those frameworks. They detect stuff like missing mappings, inconsistencies in xib files, and even unused events. By default, detected anomalies will be printed in console while you are debugging your app. For example, if you try to register an empty xib to use for your cell, here's what you'll see in console: 6 | 7 | ``` 8 | ⚠️[DTTableViewManager] Attempted to register xib EmptyXib for PostCell, but this xib does not contain any views. 9 | ``` 10 | 11 | Messages are prefixed, so for `DTTableViewManager` messages will have `[DTTableViewManager]` prefix. 12 | 13 | By default, anomaly handler only prints information into console and does not do anything beyond that, but you can change it's behavior by assigning a custom handler for anomalies: 14 | 15 | ```swift 16 | manager.anomalyHandler.anomalyAction = { anomaly in 17 | // invoke custom action 18 | } 19 | ``` 20 | 21 | For example, you may want to send all detected anomalies to analytics you have in your app. For this case anomalies implement shorter description, that is more suitable for analytics, that often have limits for amount of data you can put in. To do that globally for all instances of `DTTableViewManager` that will be created during runtime of your app, set default action: 22 | 23 | ```swift 24 | DTTableViewManagerAnomalyHandler.defaultAction = { anomaly in 25 | print(anomaly.debugDescription) 26 | 27 | analytics.postEvent("DTTableViewManager", anomaly.description) 28 | } 29 | ``` 30 | 31 | If you use `DTTableViewManager` and `DTCollectionViewManager`, you can override 3 default actions for both manager frameworks and `DTModelStorage`, presumably during app initialization, before any views are loaded: 32 | 33 | ```swift 34 | DTTableViewManagerAnomalyHandler.defaultAction = { anomaly in } 35 | DTCollectionViewManagerAnomalyHandler.defaultAction = { anomaly in } 36 | MemoryStorageAnomalyHandler.defaultAction = { anomaly in } 37 | ``` 38 | 39 | ### Silencing anomalies 40 | 41 | If you feel, that anomaly reported actually is not an anomaly, you can silence it for this specific case: 42 | 43 | ```swift 44 | // silence single anomaly case 45 | manager.anomalyHandler.silenceAnomaly(.nilCellModel(IndexPath(item: 0, section: 0))) 46 | 47 | // silence several anomalies: 48 | manager.anomalyHandler.silenceAnomaly { anomaly -> Bool in 49 | switch anomaly { 50 | case .nilCellModel, .unusedEventDetected: return true 51 | default: return false 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /Documentation/Conditional mappings.md: -------------------------------------------------------------------------------- 1 | # Conditional mappings 2 | 3 | Conditional mappings is a feature, that allows you to specify, where this mapping will be active. It's useful in cases, where you want the same data model to be displayed in different cells or using different layouts. 4 | 5 | ## Section condition 6 | 7 | Section condition allows you to limit mapping to a single section: 8 | 9 | ```swift 10 | manager.register(PostCell.self) { mapping in 11 | // This mapping will only be used in PostCell.ModelType is displayed in first section 12 | mapping.condition = .section(0) 13 | } 14 | ``` 15 | 16 | ## Model condition 17 | 18 | Model condition gives you fine-grained control over which mapping is used: 19 | 20 | ```swift 21 | manager.register(OddCell.self) { mapping in 22 | mapping.condition = mapping.modelCondition { model, indexPath in 23 | return indexPath.item.isOdd 24 | } 25 | } 26 | ``` 27 | 28 | ## Limitations 29 | 30 | Keep in mind, that while `DTTableViewManager` implements conditional mappings, `UITableView` does not have a clue, that this is happening. This may cause issues in several cases, shown below: 31 | 32 | ##### Matching reuse identifiers 33 | 34 | ```swift 35 | manager.register(PostCell.self) { mapping in 36 | mapping.condition = .section(0) 37 | } 38 | 39 | mapping.register(PostCell.self) { mapping in 40 | mapping.condition = .section(1) 41 | mapping.xibName = "CustomPostCell" 42 | } 43 | ``` 44 | 45 | In this example we are trying to show two different designs of cells in first and second section. The issue is, even if we have different xib names, reuse identifier for both of those registrations is the same - "PostCell". So when the cell is dequeued, the last registration would be used by `UITableView`, breaking the first mapping. Appropriate fix for this situation would be setting custom reuseIdentifier: 46 | 47 | ```swift 48 | manager.register(PostCell.self) { mapping in 49 | mapping.condition = .section(0) 50 | } 51 | 52 | mapping.register(PostCell.self) { mapping in 53 | mapping.condition = .section(1) 54 | mapping.xibName = "CustomPostCell" 55 | mapping.reuseIdentifier = "custom-post-cell" 56 | } 57 | ``` 58 | 59 | This way cells would get registered under different reuse identifiers, and dequeue will work correctly. 60 | 61 | ##### Intersecting model conditions 62 | 63 | ```swift 64 | manager.register(PostCell.self) 65 | 66 | manager.register(VideoPostCell.self) { mapping in 67 | mapping.condition = mapping.modelCondition { indexPath, model in 68 | return model.containsVideo 69 | } 70 | } 71 | ``` 72 | 73 | In this case, we have two mappings for the same data model - `Post`. Second mapping needs to work when post contains video, and first mapping - in other cases. The issue here is that first mapping does not have condition, and therefore when `DTTableViewManager` starts searching for mapping for post with Video, it finds two mappings instead of one. The first mapping would be used, which, in this case, is incorrect. 74 | 75 | The fix to this would be to use mapping conditions, that don't intersect between each other: 76 | 77 | ```swift 78 | manager.register(PostCell.self) { mapping in 79 | mapping.condition = mapping.modelCondition { indexPath, model in 80 | return !model.containsVideo 81 | } 82 | } 83 | 84 | manager.register(VideoPostCell.self) { mapping in 85 | mapping.condition = mapping.modelCondition { indexPath, model in 86 | return model.containsVideo 87 | } 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /Documentation/Datasources.md: -------------------------------------------------------------------------------- 1 | ## Datasources 2 | 3 | ### MemoryStorage 4 | 5 | By default, `DTTableViewManager` uses `MemoryStorage` - a storage wrapper, representing storage of data models in memory. 6 | 7 | ```swift 8 | manager.memoryStorage.setItems([1,2,3]) 9 | ``` 10 | 11 | `MemoryStorage` has a lot of methods, allowing you to modify storage contents - adding, inserting, removing, replacing, moving, searching items in storage. For a complete list, head on to [MemoryStorage documentation](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Documentation/Memory%20storage.md). 12 | 13 | ### Diffable datasources (iOS/tvOS 13+) 14 | 15 | Setting up diffable datasources is similar to how you would setup them without `DTTableViewManager`. The only difference is that instead of creating diffable datasource object using it's initializer, you instead use method provided by `DTTableViewManager`: 16 | 17 | ```swift 18 | dataSource = manager.configureDiffableDataSource { indexPath, model in 19 | model 20 | } 21 | ``` 22 | 23 | You can find working example of multi-section diffable datasources integration [here](https://github.com/DenTelezhkin/DTTableViewManager/blob/master/Example/Controllers/MultiSectionDiffingTableViewController.swift). 24 | 25 | More documentation on diffable datasources integration can be found [here](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Documentation/Diffable%20datasource%20storage.md). 26 | 27 | ### SingleSectionEquatableStorage 28 | 29 | `SingleSectionEquatableStorage` is a storage for a single section, that calculates UI updates using provided differ. 30 | 31 | No differ is provided by `DTModelStorage`, but you really need to build a thin adapter(5-6 lines of code) between your differ of choice and `DTModelStorage` for update calculation. `DTModelStorage` provides example of already built adapters for [Dwifft](https://github.com/jflinter/Dwifft), [HeckelDiff](https://github.com/mcudich/HeckelDiff) and [Changeset](https://github.com/osteslag/Changeset) differs. 32 | 33 | Setting up this kind of storage is simple: 34 | 35 | ```swift 36 | let storage = SingleSectionEquatableStorage(items: arrayOfPosts, differ: ChangesetDiffer()) 37 | storage.setItems(startingItems) 38 | manager.storage = storage 39 | 40 | storage.addItems(newItems) 41 | ``` 42 | 43 | For more specific documentation on this storage, head on to [this document](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Documentation/Single%20section%20diffable%20storage.md). 44 | 45 | ### CoreDataStorage 46 | 47 | `CoreDataStorage` is designed to work with `NSFetchedResultsController`, and automatically animate all changes happening in the database. 48 | 49 | ```swift 50 | manager.storage = CoreDataStorage(fetchedResultsController: controller) 51 | ``` 52 | 53 | For more information, read documentation on [CoreDataStorage](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Documentation/CoreData%20storage.md). 54 | 55 | ### RealmStorage 56 | 57 | `RealmStorage` is a single-section storage for data models from [Realm](https://realm.io) 58 | 59 | 60 | ``` 61 | let results = try! Realm().objects(Dog) 62 | 63 | let storage = RealmStorage() 64 | storage.addSection(with:results) 65 | ``` 66 | 67 | For more details, please read [documentation on RealmStorage](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Documentation/Realm%20storage.md). 68 | 69 | ### Custom storage 70 | 71 | If you are not happy with provided options, you can build custom storage, that fits your needs. You can either subclass any of 5 storages described above, or implement your own. The only requirement is [Storage protocol](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Sources/DTModelStorage/StorageProtocols.swift#L30-L40). Additionally, you may implement [SupplementaryStorage protocol](https://github.com/DenTelezhkin/DTModelStorage/blob/master/Sources/DTModelStorage/StorageProtocols.swift#L43-L59), that allows storage to implement supplementary models / header-footer models. 72 | 73 | For convenience, `DTTableViewManager` provides optional access to supplementaryStorage: 74 | 75 | ```swift 76 | manager.supplementaryStorage?.setSectionHeaderModels([1]) 77 | ``` 78 | 79 | Please note, that this accessor works also for any of the 5 storages described above, as well as for any storage, that implements `SupplementaryStorage` protocol. 80 | -------------------------------------------------------------------------------- /Documentation/Mapping.md: -------------------------------------------------------------------------------- 1 | # Mapping 2 | 3 | DTTableViewManager provides two ways two establish mapping between UITableViewCell/UITableViewHeaderFooterView and data model: 4 | 5 | 1. Through `ModelTransfer` protocol, provided by DTModelStorage framework. 6 | 2. Explicitly specifying both types at registration. 7 | 8 | ## ModelTransfer 9 | 10 | `ModelTransfer` is a protocol, that your reusable views can conform to, which consists of a single method, that transfers data model to your view, providing an opportunity to update it's interface: 11 | 12 | ```swift 13 | class FoodTableViewCell : UITableViewCell, ModelTransfer { 14 | func update(with model: Food) { 15 | // Display food in a cell 16 | } 17 | } 18 | ``` 19 | 20 | Model can be of any type, value type, reference type, or even protocol: 21 | 22 | ```swift 23 | protocol Food {} 24 | class Apple : Food {} 25 | class Carrot: Food {} 26 | ``` 27 | 28 | Registering such mapping is easy: 29 | 30 | ```swift 31 | manager.register(FoodTableViewCell.self) 32 | ``` 33 | 34 | By doing so, `DTTableViewManager` establishes mapping between Food type and FoodTableViewCell. 35 | 36 | Displaying two cells for those data models: 37 | 38 | ```swift 39 | manager.memoryStorage.setItems([Apple(),Carrot()]) 40 | ``` 41 | 42 | It's important to note, that when mapping is resolved, cell type is not available, storage only contains `Any` model. Therefore, when `DTTableViewManager` searches for mapping for this model, it tries to cast this model to any types that have been registered, and find appropriate match. 43 | 44 | If you need same model type to resolve to different cells, you can use conditional mappings: 45 | 46 | ```swift 47 | manager.register(VeganFoodTableViewCell.self) { mapping in 48 | mapping.condition = mapping.modelCondition { food, indexPath in 49 | food.isVegan 50 | } 51 | } 52 | manager.register(MeatContainingFoodTableViewCell.self) { mapping in 53 | mapping.condition = mapping.modelCondition { food, indexPath in 54 | food.containsMeat 55 | } 56 | } 57 | ``` 58 | 59 | In both cases, those cells model is Food, but by providing specific mapping conditions, we can make sure, that only one mapping candidate is found when dequeueing cell for this model. 60 | 61 | For more information on conditional mappings, head on to [Conditional mappings](Conditional%20mappings.md). To find out how mappings interact with delegate event closures, read more in [Events](Events.md). 62 | 63 | ## Without ModelTransfer 64 | 65 | Although usage of `ModelTransfer` protocol is recommended, it's not required. You can register cells without explicitly transferring it's model, which is useful for simple cells(for example UITableViewCell with default styles /accessories): 66 | 67 | ```swift 68 | manager.register(UITableViewCell.self, for: MenuItem.self) { mapping in 69 | mapping.didSelect { cell, model, indexPath in 70 | // did select menu item \(model) at \(indexPath) 71 | } 72 | } handler: { cell, model, indexPath in 73 | cell.textLabel.text = model 74 | } 75 | ``` 76 | 77 | In this kind of registration method, `handler` parameter is required, because there's no other way to update cell with it's data model. 78 | 79 | ## Headers/Footers 80 | 81 | Headers/Footers support both kinds of mapping, with `ModelTransfer` protocol and without it: 82 | 83 | ```swift 84 | manager.registerHeader(HeaderFooterView.self, ofKind: "Header") 85 | manager.registerHeader(UITableViewHeaderFooterView.self, for: String.self) { header, model, indexPath in 86 | 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /Documentation/TableViewConfiguration.md: -------------------------------------------------------------------------------- 1 | ## TableViewConfiguration 2 | 3 | `TableViewConfiguration` is a class, available on `DTTableViewManager` instance through `configuration` property, that allows you to customize several aspects of `UITableView` and `DTTableViewManager` behaviors. 4 | 5 | #### Displaying headers/footers on empty sections 6 | 7 | If you don't want table view section headers to show, if data model for those sections is nil, set `displayHeaderOnEmptySections` property to false: 8 | 9 | ```swift 10 | manager.configuration.displayHeaderOnEmptySections = false 11 | manager.configuration.displayFooterOnEmptySections = false 12 | ``` 13 | 14 | #### Section styles 15 | 16 | `UITableView` has two ways of displaying section headers and footers - as a view and as a title. In the latter case, `String` model is used to show section title. 17 | 18 | `DTTableViewManager` defaults to using `.title` style, but automatically switches to `.view` style if you register a view for header or footer. You can control those behaviors: 19 | 20 | ```swift 21 | manager.configuration.sectionHeaderStyle = .title 22 | manager.configuration.sectionFooterStyle = .view 23 | ``` 24 | 25 | #### Semantic heights 26 | 27 | `DTTableViewManager` implements `tableView(_:heightForHeaderInSection:)` and `tableView(_:heightForFooterInSection:)` delegate methods with several behaviors: 28 | 29 | 1. Checks .displayHeader/displayFooterOnEmptySections property on `TableViewConfiguration` instance 30 | 2. Checks whether header/footer model is not nil 31 | 3. Checks whether TableViewConfiguration.sectionHeader/FooterStyle is title or view. 32 | 4. Checks whether UITableView.Style is .plain or .grouped. 33 | 34 | Depending on all of those, `DTTableViewManager` attempts to automatically return appropriate height for header/footer. 35 | 36 | This semantic height calculation can be nice, but can be an obstacle, especially if you have a lot of sections, and therefore a lot of section headers/footers, which may hurt performance, if you are not using self-sized headers/footers. In order to give you control, this behavior can be turned off: 37 | 38 | ```swift 39 | manager.configuration.semanticHeaderHeight = false 40 | manager.configuration.semanticFooterHeight = false 41 | ``` 42 | 43 | When those properties are turned off, `DTTableViewManager` will pretend that `tableView(_:heightForHeaderInSection:)` and `tableView(_:heightForFooterInSection:)` delegate methods are not implemented(unless you specify reaction closure through `heightForHeader/Footer` methods or implement delegate methods yourself). 44 | 45 | #### Minimal heights 46 | 47 | If the height needs to be .zero, `UITableView` actually expects different values for .grouped(CGFloat.leastNormalMagnitude) and .plain(CGFloat.zero) styles. Those zero heights can be customized through `minimalHeader/FooterHeightForTableView` properties. 48 | -------------------------------------------------------------------------------- /Documentation/TableViewUpdater.md: -------------------------------------------------------------------------------- 1 | # TableViewUpdater 2 | 3 | `TableViewUpdater` is a class, responsible for animating datasource updates. 4 | 5 | ### Reacting to content updates 6 | 7 | Sometimes it's convenient to know, when data is updated, for example to hide UITableView, if there's no data. `TableViewUpdater` has `willUpdateContent` and `didUpdateContent` properties, that can help: 8 | 9 | ```swift 10 | updater.willUpdateContent = { update in 11 | print("UI update is about to begin") 12 | } 13 | 14 | updater.didUpdateContent = { update in 15 | print("UI update finished") 16 | } 17 | ``` 18 | 19 | Please keep in mind, that those closures will not be called if you directly invoke `tableView.reloadData()`. If you need to call `reloadData` and trigger those closures, please call: 20 | 21 | ```swift 22 | manager.tableViewUpdater?.storageNeedsReloading() 23 | ``` 24 | 25 | ### Animations 26 | 27 | You can customize section and row animations: 28 | 29 | ```swift 30 | updater.insertSectionAnimation = .automatic 31 | updater.deleteSectionAnimation = .fade 32 | updater.reloadSectionAnimation = .none 33 | 34 | updater.insertRowAnimation = .automatic 35 | updater.deleteRowAnimation = .fade 36 | updater.reloadRowAnimation = .none 37 | ``` 38 | 39 | ### Customizing UITableView updates 40 | 41 | `DTTableViewManager` uses `TableViewUpdater` class by default. While usually, you don't need to configure anything additional with `TableViewUpdater`, one exception to this rule is CoreData and `CoreDataStorage`. 42 | 43 | When setting up CoreDataStorage with `DTTableViewManager` and `DTCollectionViewManager`, consider using special CoreData updater: 44 | 45 | ```swift 46 | manager.collectionViewUpdater = manager.coreDataUpdater() 47 | 48 | manager.tableViewUpdater = manager.coreDataUpdater() 49 | ``` 50 | 51 | This special version of updater has two important differences from default behavior: 52 | 53 | 1. Moving items is animated as insert and delete 54 | 2. When data model changes, `update(with:)` method and `handler` closure are called to update visible cells without explicitly reloading them. 55 | 56 | Those are [recommended by Apple](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate) approaches to handle `NSFetchedResultsControllerDelegate` updates with `UITableView` and `UICollectionView`. 57 | 58 | If your `UITableView` is not on screen, it's updates are not required to be animated. For performance reasons you may want to disable offscreen animations: 59 | 60 | ```swift 61 | manager.tableViewUpdater.animateChangesOffScreen = false 62 | ``` 63 | -------------------------------------------------------------------------------- /Example/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 26.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool { 17 | CoreDataManager.sharedInstance.preloadData() 18 | return true 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Example/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIApplicationSceneManifest 32 | 33 | UIApplicationSupportsMultipleScenes 34 | 35 | UISceneConfigurations 36 | 37 | UIWindowSceneSessionRoleApplication 38 | 39 | 40 | UISceneConfigurationName 41 | Default 42 | UISceneDelegateClassName 43 | $(PRODUCT_MODULE_NAME).SceneDelegate 44 | 45 | 46 | 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Example/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 23.08.2020. 6 | // Copyright © 2020 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | // MARK: - UIWindowSceneDelegate 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | guard let windowScene = scene as? UIWindowScene else { return } 19 | window = UIWindow(windowScene: windowScene) 20 | window?.rootViewController = UINavigationController(rootViewController: ExamplesListViewController(style: .plain)) 21 | window?.makeKeyAndVisible() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/Cells/BankCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BankCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 20.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import DTTableViewManager 12 | 13 | class BankCell : UITableViewCell, ModelTransfer 14 | { 15 | func update(with model: Bank) { 16 | self.textLabel?.text = model.name 17 | self.detailTextLabel?.text = model.city 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/Cells/BankCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Example/Cells/CustomStringCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStringCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 03.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class CustomStringCell: UITableViewCell, ModelTransfer { 13 | 14 | @IBOutlet weak var label: UILabel! 15 | 16 | func update(with model: String) { 17 | self.label.text = model 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/Cells/CustomStringCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/Controllers/AddRemoveViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddRemoveViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 02.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class AddRemoveViewController: UITableViewController, DTTableViewManageable { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | manager.register(UITableViewCell.self, for: String.self) { [weak self] mapping in 18 | mapping.didSelect { _, model, indexPath in 19 | let alert = UIAlertController(title: "Selected cell", 20 | message: "with model: \(model) at indexPath: \(indexPath)", 21 | preferredStyle: .alert) 22 | let action = UIAlertAction(title: "Ok", style: .cancel, handler: nil) 23 | alert.addAction(action) 24 | self?.present(alert, animated: true, completion: nil) 25 | } 26 | mapping.commitEditingStyle { _, _, _, indexPath in 27 | self?.manager.memoryStorage.removeItems(at: [indexPath]) 28 | } 29 | mapping.heightForCell { _, _ in 80 } 30 | } handler: { cell, model, _ in 31 | cell.textLabel?.text = model 32 | } 33 | 34 | navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .add, primaryAction: UIAction { [weak manager] _ in 35 | manager?.memoryStorage.addItem("Row # \(manager?.memoryStorage.section(atIndex: 0)?.numberOfItems ?? 0)") 36 | }, menu: nil) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/Controllers/AutoDiffSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoDiffSearchViewController.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 22.09.2018. 6 | // Copyright © 2018 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | import Changeset 12 | 13 | struct ChangesetDiffer : EquatableDiffingAlgorithm { 14 | func diff(from: [T], to: [T]) -> [SingleSectionOperation] where T : EntityIdentifiable, T : Equatable { 15 | let changeset = Changeset.edits(from: from, to: to) 16 | return changeset.map { 17 | switch $0.operation { 18 | case .deletion: return SingleSectionOperation.delete($0.destination) 19 | case .insertion: return SingleSectionOperation.insert($0.destination) 20 | case .substitution: return SingleSectionOperation.update($0.destination) 21 | case .move(origin: let offset): return SingleSectionOperation.move(from: offset, to: $0.destination) 22 | } 23 | } 24 | } 25 | } 26 | 27 | extension String: EntityIdentifiable { 28 | public var identifier: AnyHashable { return self } 29 | } 30 | 31 | class AutoDiffSearchViewController: UITableViewController, DTTableViewManageable, UISearchResultsUpdating { 32 | 33 | let searchController = UISearchController(searchResultsController: nil) 34 | 35 | let spells = [ 36 | "Riddikulus", "Obliviate", "Sectumsempra", "Avada Kedavra", 37 | "Alohomora", "Lumos", "Expelliarmus", "Wingardium Leviosa", 38 | "Accio", "Expecto Patronum" 39 | ] 40 | 41 | lazy var storage = SingleSectionEquatableStorage(items: spells, differ: ChangesetDiffer()) 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | manager = DTTableViewManager(storage: storage) 47 | manager.register(UITableViewCell.self, for: String.self) { cell, model, _ in 48 | cell.textLabel?.text = model 49 | } 50 | searchController.searchResultsUpdater = self 51 | navigationItem.hidesSearchBarWhenScrolling = false 52 | navigationItem.searchController = searchController 53 | } 54 | 55 | func updateSearchResults(for searchController: UISearchController) { 56 | guard let query = searchController.searchBar.text, !query.isEmpty else { 57 | storage.setItems(spells) 58 | return 59 | } 60 | storage.setItems(spells.filter { $0.lowercased().contains(searchController.searchBar.text?.lowercased() ?? "") }) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Example/Controllers/CoreDataSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataSearchViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 20.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | import CoreData 12 | 13 | class CoreDataSearchViewController: UIViewController, DTTableViewManageable { 14 | 15 | var tableView: UITableView! 16 | var noContentLabel: UILabel! 17 | let searchController = UISearchController(searchResultsController: nil) 18 | let fetchResultsController = CoreDataManager.sharedInstance.banksFetchController() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | view.backgroundColor = .white 23 | configureSubviews() 24 | 25 | manager = DTTableViewManager(storage: CoreDataStorage(fetchedResultsController: fetchResultsController)) 26 | manager.register(BankCell.self) 27 | manager.tableViewUpdater?.didUpdateContent = { [weak self] _ in 28 | // In some cases it makes sense to show no content view underneath tableView 29 | self?.tableView.isHidden = self?.tableView.numberOfSections == 0 30 | self?.noContentLabel.isHidden = self?.tableView.numberOfSections != 0 31 | } 32 | 33 | searchController.searchResultsUpdater = self 34 | navigationItem.searchController = searchController 35 | navigationItem.hidesSearchBarWhenScrolling = false 36 | } 37 | 38 | func configureSubviews() { 39 | noContentLabel = UILabel() 40 | noContentLabel.translatesAutoresizingMaskIntoConstraints = false 41 | noContentLabel.text = "No banks found" 42 | noContentLabel.isHidden = true 43 | tableView = UITableView(frame: view.bounds, style: .plain) 44 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 45 | view.addSubview(tableView) 46 | view.addSubview(noContentLabel) 47 | noContentLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 48 | noContentLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 49 | } 50 | } 51 | 52 | extension CoreDataSearchViewController : UISearchResultsUpdating 53 | { 54 | func updateSearchResults(for searchController: UISearchController) { 55 | let searchString = searchController.searchBar.text ?? "" 56 | if searchString == "" { 57 | self.fetchResultsController.fetchRequest.predicate = nil 58 | } else { 59 | let predicate = NSPredicate(format: "name contains[c] %@ OR city contains[c] %@ OR state contains[c] %@",searchString,searchString,searchString) 60 | self.fetchResultsController.fetchRequest.predicate = predicate 61 | } 62 | try! fetchResultsController.performFetch() 63 | manager.tableViewUpdater?.storageNeedsReloading() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/Controllers/CustomViewsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomViewsController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 03.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class CustomViewsController: UITableViewController, DTTableViewManageable { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | manager.register(CustomStringCell.self) 17 | manager.registerHeader(CustomHeaderFooterView.self, handler: { header, _,_ in 18 | header.backgroundPatternView.backgroundColor = UIColor(patternImage: UIImage(named: "textured_paper")!) 19 | }) 20 | manager.registerFooter(CustomHeaderFooterView.self, handler: { footer,_,_ in 21 | footer.backgroundPatternView.backgroundColor = UIColor(patternImage: UIImage(named: "mochaGrunge")!) 22 | }) 23 | 24 | manager.memoryStorage.setSectionHeaderModels(["Awesome custom header"]) 25 | manager.memoryStorage.setSectionFooterModels(["Not so awesome custom footer"]) 26 | manager.memoryStorage.setItems(["Custom cell", "Custom cell 2"]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Controllers/DiffableCoreDataViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableCoreDataViewController.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 7/28/19. 6 | // Copyright © 2019 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | import CoreData 12 | 13 | class DiffableCoreDataViewController: UITableViewController, DTTableViewManageable, NSFetchedResultsControllerDelegate { 14 | 15 | let fetchController = CoreDataManager.sharedInstance.banksFetchController() 16 | var dataSource: UITableViewDiffableDataSource! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | manager.register(BankCell.self) { mapping in 21 | mapping.trailingSwipeActionsConfiguration { _, model, _ in 22 | UISwipeActionsConfiguration(actions: [UIContextualAction(style: .destructive, title: "Delete", handler: { _, _, _ in 23 | CoreDataManager.sharedInstance.managedObjectContext.delete(model) 24 | try? CoreDataManager.sharedInstance.managedObjectContext.save() 25 | })]) 26 | } 27 | } 28 | dataSource = manager.configureDiffableDataSource(modelProvider: { [weak self] indexPath, identifier in 29 | self?.fetchController.object(at: indexPath) as Any 30 | }) 31 | manager.supplementaryStorage?.headerModelProvider = { [weak self] in 32 | if let sections = self?.fetchController.sections { 33 | if $0 >= sections.count { return nil } 34 | return sections[$0].name 35 | } 36 | return nil 37 | } 38 | updateSnapshot(animatingDifferences: false) 39 | fetchController.delegate = self 40 | navigationItem.rightBarButtonItems = [ 41 | barButton(title: "Add", action: { vc in vc.addRecordButtonTapped()} ), 42 | barButton(title: "Reset", action: { vc in vc.resetDataButtonTapped() }) 43 | ] 44 | } 45 | 46 | func updateSnapshot(animatingDifferences: Bool) { 47 | guard let sections = fetchController.sections else { return } 48 | var snapshot = NSDiffableDataSourceSnapshot() 49 | for section in sections { 50 | snapshot.appendSections([section.name]) 51 | snapshot.appendItems((section.objects ?? []).compactMap { $0 as? Bank }, toSection: section.name) 52 | } 53 | dataSource.apply(snapshot, animatingDifferences: animatingDifferences) 54 | } 55 | 56 | func resetDataButtonTapped() { 57 | // This functionality is currently bugged as NSFetchedResultsController returns duplicated records each time 58 | // Which leads to displaying wrong data, even though database was cleared. 59 | // So we just pop to rootViewController. 60 | CoreDataManager.sharedInstance.resetData() 61 | navigationController?.popViewController(animated: true) 62 | } 63 | 64 | func addRecordButtonTapped() { 65 | let context = CoreDataManager.sharedInstance.managedObjectContext 66 | _ = Bank(info: [ 67 | "name": "Random bank name", 68 | "city": "Random city", 69 | "zip": ["111","222","333","4444"].randomElement() ?? "", 70 | "state": manager.supplementaryStorage?.headerModelProvider?((0..<(manager.storage.numberOfSections())).randomElement() ?? 0) as Any 71 | ], inContext: context) 72 | try? context.save() 73 | } 74 | 75 | func controllerDidChangeContent(_ controller: NSFetchedResultsController) { 76 | updateSnapshot(animatingDifferences: true) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/Controllers/ExamplesListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExamplesListViewController.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 24.08.2020. 6 | // Copyright © 2020 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | import SwiftUI 12 | 13 | class ExamplesListViewController: UITableViewController, DTTableViewManageable { 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | navigationItem.title = "Examples" 18 | manager.registerHostingConfiguration(for: Example.self) { _, model, _ in 19 | UIHostingConfiguration { 20 | HStack { 21 | Text(model.title) 22 | .font(.system(size: 20)) 23 | Spacer() 24 | Image(systemName: "chevron.forward") 25 | .font(.footnote.bold()) 26 | .foregroundColor(Color(UIColor.tertiaryLabel)) 27 | } 28 | } 29 | } mapping: { [weak self] in 30 | $0.didSelect { _, model, _ in 31 | self?.navigationController?.pushViewController(model.controller, animated: true) 32 | } 33 | } 34 | manager.memoryStorage.setItems(Example.allCases) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Example/Controllers/MultiSectionDiffingTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiSectionDiffingTableViewController.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 7/20/19. 6 | // Copyright © 2019 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class MultiSectionDiffingTableViewController: UITableViewController, DTTableViewManageable { 13 | 14 | lazy var students: [String: [String]] = { 15 | (try? JSONDecoder().decode([String:[String]].self, 16 | from: NSDataAsset(name: "students")?.data ?? .init())) ?? [:] 17 | }() 18 | 19 | enum Section: String, CaseIterable { 20 | case gryffindor 21 | case ravenclaw 22 | case hufflepuff 23 | case slytherin 24 | } 25 | let searchController = UISearchController(searchResultsController: nil) 26 | var diffableDataSource : UITableViewDiffableDataSource? 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | manager.register(UITableViewCell.self, for: String.self) { cell, model, _ in 31 | cell.textLabel?.text = model 32 | } 33 | diffableDataSource = manager.configureDiffableDataSource { indexPath, item in 34 | item 35 | } 36 | manager.supplementaryStorage?.setSectionHeaderModels(Section.allCases.map { $0.rawValue.capitalized }) 37 | manager.tableViewUpdater?.deleteRowAnimation = .fade 38 | searchController.searchResultsUpdater = self 39 | navigationItem.searchController = searchController 40 | navigationItem.hidesSearchBarWhenScrolling = false 41 | 42 | updateUI(searchTerm: "", animated: false) 43 | } 44 | 45 | func updateUI(searchTerm: String, animated: Bool) { 46 | var snapshot : NSDiffableDataSourceSnapshot = .init() 47 | for section in Section.allCases { 48 | let studentsInClass = students[section.rawValue.capitalized]?.filter { $0.lowercased().contains(searchTerm.lowercased()) || searchTerm.isEmpty } ?? [] 49 | if !studentsInClass.isEmpty { 50 | snapshot.appendSections([section]) 51 | snapshot.appendItems(studentsInClass) 52 | } 53 | } 54 | diffableDataSource?.apply(snapshot, animatingDifferences: animated) 55 | } 56 | } 57 | 58 | // MARK: - UISearchResultsUpdating 59 | @available(iOS 13, *) 60 | extension MultiSectionDiffingTableViewController : UISearchResultsUpdating { 61 | func updateSearchResults(for searchController: UISearchController) { 62 | updateUI(searchTerm: searchController.searchBar.text ?? "", animated: true) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/Controllers/ReorderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReorderViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 02.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class ReorderViewController: UITableViewController, DTTableViewManageable { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | manager.register(UITableViewCell.self, for: String.self) { [weak self] in 17 | $0.canMove { _,_,_ in true } 18 | $0.editingStyle { _,_ in .none } 19 | $0.moveRowTo { destination, _, _, source in 20 | self?.manager.memoryStorage.moveItemWithoutAnimation(from: source, to: destination) 21 | } 22 | } handler: { cell, model, _ in 23 | cell.textLabel?.text = model 24 | } 25 | manager.memoryStorage.addItems(["Section 1 cell", "Section 1 cell"], toSection: 0) 26 | manager.memoryStorage.addItems(["Section 2 cell"], toSection: 1) 27 | manager.memoryStorage.addItems(["Section 3 cell", "Section 3 cell", "Section 3 cell"], toSection: 2) 28 | manager.memoryStorage.setSectionHeaderModels(["Section 1", "Section 2", "Section 3"]) 29 | navigationItem.rightBarButtonItem = editButtonItem 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/CoreData/Bank.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bank.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 20.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Bank) 13 | class Bank : NSManagedObject 14 | { 15 | @NSManaged var name : String 16 | @NSManaged var city : String 17 | @NSManaged var zip : Int 18 | @NSManaged var state : String 19 | 20 | convenience init(info : [String:Any], inContext context: NSManagedObjectContext) 21 | { 22 | let entity = NSEntityDescription.entity(forEntityName: "Bank", in: context) 23 | self.init(entity: entity!, insertInto: context) 24 | name = info["name"] as? String ?? "" 25 | city = info["city"] as? String ?? "" 26 | zip = info["zip"] as? Int ?? 0 27 | state = info["state"] as? String ?? "" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/CoreData/Banks.xcdatamodeld/Banks.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Example/CoreData/CoreDataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataManager.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 20.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import UIKit 12 | 13 | class CoreDataManager 14 | { 15 | static let sharedInstance = CoreDataManager() 16 | fileprivate init(){ 17 | let storeURL = self.applicationDocumentsDirectory.appendingPathComponent("Banks.sqlite") 18 | persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) 19 | try! persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil) 20 | managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 21 | managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator 22 | } 23 | 24 | fileprivate var banksPreloaded : Bool { 25 | get { 26 | return UserDefaults.standard.bool(forKey: "UserDefaultsBanksPreloaded") 27 | } 28 | set { 29 | UserDefaults.standard.set(newValue, forKey: "UserDefaultsBanksPreloaded") 30 | } 31 | } 32 | 33 | fileprivate let applicationDocumentsDirectory = FileManager.default.urls(for :.documentDirectory, in: .userDomainMask).last! 34 | 35 | fileprivate let managedObjectModel : NSManagedObjectModel = { 36 | let modelURL = Bundle.main.url(forResource: "Banks", withExtension: "momd")! 37 | return NSManagedObjectModel(contentsOf: modelURL)! 38 | }() 39 | 40 | fileprivate let persistentStoreCoordinator : NSPersistentStoreCoordinator 41 | 42 | let managedObjectContext : NSManagedObjectContext 43 | 44 | func preloadData() 45 | { 46 | if banksPreloaded { return } 47 | loadData() 48 | } 49 | 50 | private func loadData() { 51 | if let data = NSDataAsset(name: "Banks")?.data, 52 | let banks = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String:AnyObject]] 53 | { 54 | for bankInfo in banks { 55 | let _ = Bank(info: bankInfo, inContext: managedObjectContext) 56 | } 57 | try! managedObjectContext.save() 58 | banksPreloaded = true 59 | } 60 | } 61 | 62 | func resetData() { 63 | let fetchRequest = NSFetchRequest(entityName: "Bank") 64 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 65 | 66 | do { 67 | try persistentStoreCoordinator.execute(deleteRequest, with: managedObjectContext) 68 | } catch { 69 | print("Failed to reset the database") 70 | } 71 | banksPreloaded = false 72 | loadData() 73 | } 74 | 75 | func banksFetchController() -> NSFetchedResultsController { 76 | let context = CoreDataManager.sharedInstance.managedObjectContext 77 | let request = NSFetchRequest() 78 | request.entity = NSEntityDescription.entity(forEntityName: "Bank", in: context) 79 | request.fetchBatchSize = 20 80 | request.sortDescriptors = [NSSortDescriptor(key: "zip", ascending: true)] 81 | 82 | let fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "state", cacheName: nil) 83 | try! fetchResultsController.performFetch() 84 | return fetchResultsController 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Extensions/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extensions.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 25.08.2020. 6 | // Copyright © 2020 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol BarButtonCreatable {} 13 | extension BarButtonCreatable where Self: UIViewController { 14 | func barButton(title: String, action: @escaping (Self) -> Void) -> UIBarButtonItem { 15 | UIBarButtonItem(title: title, image: nil, primaryAction: UIAction(handler: { [weak self] _ in 16 | guard let self = self else { return } 17 | action(self) 18 | }), menu: nil) 19 | } 20 | } 21 | extension UIViewController : BarButtonCreatable {} 22 | -------------------------------------------------------------------------------- /Example/Headers:Footers/CustomHeaderFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomHeaderFooterView.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 03.08.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class CustomHeaderFooterView: UITableViewHeaderFooterView, ModelTransfer { 13 | 14 | @IBOutlet weak var backgroundPatternView: UIView! 15 | @IBOutlet weak var label: UILabel! 16 | 17 | func update(with model: String) { 18 | label.text = model 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Example/Headers:Footers/CustomHeaderFooterView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/Models/Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Example.swift 3 | // Example 4 | // 5 | // Created by Denys Telezhkin on 24.08.2020. 6 | // Copyright © 2020 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Example: CaseIterable { 12 | 13 | case addRemoveSelect 14 | case reorder 15 | case customViews 16 | case coreDataSearch 17 | case diffableCoreDataSearch 18 | case singleSectionDiffing 19 | case multiSectionDiffing 20 | 21 | var title: String { 22 | switch self { 23 | case .addRemoveSelect: return "Add/remove/select items" 24 | case .reorder: return "Editing/reorder" 25 | case .customViews: return "Custom views" 26 | case .coreDataSearch: return "CoreData search" 27 | case .diffableCoreDataSearch: return "Diffable CoreData search" 28 | case .singleSectionDiffing: return "Single section diffing" 29 | case .multiSectionDiffing: return "Multi section diffing" 30 | } 31 | } 32 | 33 | var controller: UIViewController { 34 | let viewController: UIViewController 35 | switch self { 36 | case .addRemoveSelect: viewController = AddRemoveViewController() 37 | case .reorder: viewController = ReorderViewController() 38 | case .customViews: viewController = CustomViewsController() 39 | case .coreDataSearch: viewController = CoreDataSearchViewController() 40 | case .diffableCoreDataSearch: viewController = DiffableCoreDataViewController() 41 | case .singleSectionDiffing: viewController = AutoDiffSearchViewController() 42 | case .multiSectionDiffing: viewController = MultiSectionDiffingTableViewController() 43 | } 44 | viewController.navigationItem.title = title 45 | return viewController 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/Resources/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Example/Resources/Resources.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 | } -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/Banks.dataset/Banks.json: -------------------------------------------------------------------------------- 1 | [{ "name": "JP Morgan Chase", "city": "Tuscaloosa", "state": "Alabama", "zip": 11111}, 2 | { "name": "Bank of America", "city": "Kansas city", "state": "Kansas", "zip": 22222}, 3 | { "name": "Citigroup", "city": "Wichita", "state": "Kansas", "zip": 33333}, 4 | { "name": "Wells Fargo", "city": "Santa Fe", "state": "New mexico", "zip": 33333}, 5 | { "name": "Wells Fargo", "city": "Clovis", "state": "New mexico", "zip": 33333}, 6 | { "name": "Bank of America", "city": "Truth or Consequences", "state": "New mexico", "zip": 33333}, 7 | { "name": "Goldman Sachs", "city": "Omaha", "state": "Nebraska", "zip": 33333}, 8 | { "name": "Goldman Sachs", "city": "Las Vegas", "state": "Nevada", "zip": 33333}, 9 | { "name": "Wells Fargo", "city": "Tulsa", "state": "Oklahoma", "zip": 44444}] -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/Banks.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "Banks.json", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "736h", 7 | "filename" : "iPhone 6Plus.png", 8 | "minimum-system-version" : "8.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "667h", 16 | "filename" : "iPhone6.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "portrait", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "orientation" : "portrait", 23 | "idiom" : "iphone", 24 | "extent" : "full-screen", 25 | "minimum-system-version" : "7.0", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "extent" : "full-screen", 30 | "idiom" : "iphone", 31 | "subtype" : "retina4", 32 | "filename" : "Default-568h@2x.png", 33 | "minimum-system-version" : "7.0", 34 | "orientation" : "portrait", 35 | "scale" : "2x" 36 | } 37 | ], 38 | "info" : { 39 | "version" : 1, 40 | "author" : "xcode" 41 | } 42 | } -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/LaunchImage.launchimage/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/Example/Resources/Resources.xcassets/LaunchImage.launchimage/Default-568h@2x.png -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/LaunchImage.launchimage/iPhone 6Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/Example/Resources/Resources.xcassets/LaunchImage.launchimage/iPhone 6Plus.png -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/LaunchImage.launchimage/iPhone6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/Example/Resources/Resources.xcassets/LaunchImage.launchimage/iPhone6.png -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/mochaGrunge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "mochaGrunge.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/mochaGrunge.imageset/mochaGrunge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/Example/Resources/Resources.xcassets/mochaGrunge.imageset/mochaGrunge.png -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/students.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "students.json", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/students.dataset/students.json: -------------------------------------------------------------------------------- 1 | { 2 | "Gryffindor": [ 3 | "Harry Potter", "Ronald Weasley", "Neville Longbottom", "Dean Thomas", "Seamus Finnigan", "Hermione Granger", "Lavender Brown", "Parvati Patil" 4 | ], 5 | "Hufflepuff": [ 6 | "Ernie Macmillan", "Justin Finch-Fletchley", "Hannah Abbott", "Susan Bones" 7 | ], 8 | "Ravenclaw": [ 9 | "Terry Boot", "Mandy Brocklehurst", "Michael Corner", "Anthony Goldstein", "Padma Patil", "Lisa Turpin" 10 | ], 11 | "Slytherin": [ 12 | "Draco Malfoy", "Vincent Crabbe", "Gregory Goyle", "Blaise Zabini", "Theodore Nott", "Pansy Parkinson", "Millicent Bulstrode", "Daphne Greengrass" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/textured_paper.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "textured_paper.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Resources/Resources.xcassets/textured_paper.imageset/textured_paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/Example/Resources/Resources.xcassets/textured_paper.imageset/textured_paper.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gem "fastlane" 5 | gem 'octokit' 6 | gem 'netrc' 7 | gem 'cocoapods' 8 | gem 'jazzy' 9 | gem 'mime-types' 10 | gem 'cocoapods-trunk' 11 | gem 'slather' 12 | 13 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 14 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Denys Telezhkin (http://github.com/DenTelezhkin/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DTModelStorage", 6 | "repositoryURL": "https://github.com/DenTelezhkin/DTModelStorage", 7 | "state": { 8 | "branch": null, 9 | "revision": "1473c7ed4970b9572e93cf0305c80d54855b3cdf", 10 | "version": "11.0.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // 3 | // Package.swift 4 | // DTTableViewManager 5 | // 6 | // Created by Denys Telezhkin on 17.07.2019. 7 | // Copyright © 2019 Denys Telezhkin. All rights reserved. 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import PackageDescription 28 | 29 | let package = Package( 30 | name: "DTTableViewManager", 31 | platforms: [ 32 | .iOS(.v11), 33 | .tvOS(.v11) 34 | ], 35 | products: [ 36 | .library(name: "DTTableViewManager", targets: ["DTTableViewManager"]) 37 | ], 38 | dependencies: [ 39 | .package(url: "https://github.com/DenTelezhkin/DTModelStorage", .upToNextMajor(from: "11.0.0")) 40 | ], 41 | targets: [ 42 | .target(name: "DTTableViewManager", dependencies: ["DTModelStorage"]) 43 | ], 44 | swiftLanguageVersions: [.v5] 45 | ) 46 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/DTTableViewDropPlaceholderContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTTableViewDropPlaceholderContext.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 27.08.17. 6 | // Copyright © 2017 Denys Telezhkin. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | import UIKit 28 | import DTModelStorage 29 | 30 | #if os(iOS) 31 | 32 | /// Thin wrapper around `UITableViewDropPlaceholderContext`, which automates insertion of `dragItems` if you are using `MemoryStorage`. 33 | /// Typically, you would not create this class directly, but use `DTTableViewManager.drop(_:to:with:)` convenience method. 34 | open class DTTableViewDropPlaceholderContext { 35 | 36 | /// Drop context 37 | public let context : UITableViewDropPlaceholderContext 38 | 39 | /// Storage, on which drop operation is performed 40 | weak var storage: Storage? 41 | 42 | /// Creates `DTTableViewDropPlaceholderContext` with `context` and `storage` 43 | public init(context: UITableViewDropPlaceholderContext, storage: Storage?) { 44 | self.context = context 45 | self.storage = storage 46 | } 47 | 48 | /// Commits insertion of item, using `UITableViewDropPlaceholderContext.commitInsertion(_:)` method. Both commit and `insertionIndexPathClosure` will be automatically dispatched to `DispatchQueue.main`. 49 | /// If you are using `MemoryStorage`, model will be automatically inserted, and no additional actions are required. 50 | open func commitInsertion(ofItem item: Model, _ insertionIndexPathClosure: ((IndexPath) -> Void)? = nil) { 51 | DispatchQueue.main.async { [weak self] in 52 | self?.context.commitInsertion { insertionIndexPath in 53 | guard let storage = self?.storage else { return } 54 | if let storage = storage as? MemoryStorage, 55 | let section = storage.section(atIndex: insertionIndexPath.section), 56 | section.items.count >= insertionIndexPath.item 57 | { 58 | section.items.insert(item, at: insertionIndexPath.row) 59 | } 60 | insertionIndexPathClosure?(insertionIndexPath) 61 | } 62 | } 63 | } 64 | 65 | @discardableResult 66 | /// Convenience method to call `context.deletePlaceholder`. 67 | open func deletePlaceholder() -> Bool { 68 | return context.deletePlaceholder() 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/DTTableViewManager+Prefetch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTTableViewManager+Prefetch.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 01.10.2022. 6 | // Copyright © 2022 Denys Telezhkin. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | 28 | /// Extension for prefetch events (UITableViewDataSourcePrefetching) 29 | public extension CellViewModelMappingProtocolGeneric { 30 | 31 | /// Registers `closure` to be executed when `UITableViewDataSourcePrefetching.tableView(_:prefetchRowsAt:)` method is called, and indexPaths contains indexPath of Model in a storage. 32 | func prefetch(_ closure: @escaping (Model, IndexPath) -> Void) { 33 | reactions.append(EventReaction(modelType: Model.self, signature: EventMethodSignature.prefetchRowsAtIndexPaths.rawValue, closure)) 34 | } 35 | 36 | /// Registers `closure` to be executed when `UITableViewDataSourcePrefetching.tableView(_:cancelPrefetchingForRowsAt:)` method is called, and indexPaths contains indexPath of Model in a storage. 37 | func cancelPrefetch(_ closure: @escaping (Model, IndexPath) -> Void) { 38 | reactions.append(EventReaction(modelType: Model.self, signature: EventMethodSignature.cancelPrefetchingForRowsAtIndexPaths.rawValue, closure)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/DTTableViewManager+SwiftUIRegistration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTTableViewManager+SwiftUIRegistration.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 22.06.2022. 6 | // Copyright © 2022 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import SwiftUI 12 | 13 | // swiftlint:disable line_length 14 | 15 | @available(iOS 13, tvOS 13, *) 16 | /// Extension for registering SwiftUI views. 17 | public extension DTTableViewManager { 18 | 19 | /// Register mapping from `model` type to SwiftUI view `Content`, presented in `HostingTableViewCell`. 20 | /// 21 | /// When `HostingTableViewCell` is first dequeued, `Content` view will be created and added to view hierarchy. This will also add hosting controller, that hosts this cell, as a child view controller for parent view controller, containing tableView. This is required for proper sizing and appearance events of SwiftUI view. 22 | /// 23 | /// However, adding SwiftUI hosting controller as a child may produce some unintended effects, for example showing navigation bar even though `Content` view has nothing to do with navigation stack. To avoid this problem, hosting controller may be customized. Read more about this in [Documentation](Documentation/SwiftUI.md) 24 | /// - Parameters: 25 | /// - model: data model, mapped to cell 26 | /// - content: SwiftUI view, rendered inside UITableViewCell 27 | /// - mapping: mapping configuration closure, executed before any registration or dequeue is performed. 28 | func registerHostingCell(for model: Model.Type, content: @escaping (Model, IndexPath) -> Content, mapping: ((HostingCellViewModelMapping) -> Void)? = nil) { 29 | viewFactory.registerHostingCell(content, parentViewController: delegate as? UIViewController, mapping: mapping) 30 | } 31 | 32 | #if swift(>=5.7) && !canImport(AppKit) || (canImport(AppKit) && swift(>=5.7.1)) // Xcode 14.0 AND macCatalyst on Xcode 14.1 (which will have swift> 5.7.1) 33 | 34 | @available(iOS 16, tvOS 16, *) 35 | /// Registers mapping from `model` to `UIHostingConfiguration`, that will be created and set to `contentConfiguration` property of `UITableViewCell` once dequeued. 36 | /// - Parameters: 37 | /// - model: model type 38 | /// - configuration: hosting configuration for a cell 39 | /// - mapping: mapping customization closure 40 | func registerHostingConfiguration( 41 | for model: Model.Type, 42 | configuration: @escaping (UITableViewCell, Model, IndexPath) -> UIHostingConfiguration, 43 | mapping: ((HostingConfigurationViewModelMapping) -> Void)? = nil) { 44 | viewFactory.registerHostingConfiguration(configuration: configuration, mapping: mapping) 45 | } 46 | 47 | @available(iOS 16, tvOS 16, *) 48 | /// Registers mapping from `model` to `UIHostingConfiguration`, that will be created and set to `contentConfiguration` property of `UITableViewCell` inside of `UITableViewCell.configurationUpdateHandler` property to manage state. 49 | /// - Parameters: 50 | /// - model: model type 51 | /// - configuration: hosting configuration for a cell 52 | /// - mapping: mapping customization closure 53 | func registerHostingConfiguration( 54 | for model: Model.Type, 55 | configuration: @escaping (UICellConfigurationState, UITableViewCell, Model, IndexPath) -> UIHostingConfiguration, 56 | mapping: ((HostingConfigurationViewModelMapping) -> Void)? = nil) { 57 | viewFactory.registerHostingConfiguration(configuration: configuration, mapping: mapping) 58 | } 59 | #endif 60 | } 61 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/DTTableViewPrefetchDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTTableViewPrefetchDataSource.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 01.10.2022. 6 | // Copyright © 2022 Denys Telezhkin. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | import UIKit 28 | 29 | /// Object, that implements `UITableViewDataSourcePrefetching` methods for `DTTableViewManager`. 30 | open class DTTableViewPrefetchDataSource: DTTableViewDelegateWrapper, UITableViewDataSourcePrefetching { 31 | 32 | override func delegateWasReset() { 33 | tableView?.prefetchDataSource = nil 34 | tableView?.prefetchDataSource = self 35 | } 36 | 37 | /// Implementation for `UITableViewDataSourcePrefetching` protocol 38 | public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { 39 | indexPaths.forEach { 40 | _ = performCellReaction(.prefetchRowsAtIndexPaths, location: $0, provideCell: false) 41 | } 42 | (delegate as? UITableViewDataSourcePrefetching)?.tableView(tableView, prefetchRowsAt: indexPaths) 43 | } 44 | 45 | /// Implementation for `UITableViewDataSourcePrefetching` protocol 46 | public func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { 47 | indexPaths.forEach { 48 | _ = performCellReaction(.cancelPrefetchingForRowsAtIndexPaths, location: $0, provideCell: false) 49 | } 50 | (delegate as? UITableViewDataSourcePrefetching)?.tableView?(tableView, cancelPrefetchingForRowsAt: indexPaths) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 29.04.2020. 6 | // Copyright © 2020 Denys Telezhkin. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | @_exported import DTModelStorage 27 | -------------------------------------------------------------------------------- /Sources/DTTableViewManager/HostingTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostingTableViewCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 22.06.2022. 6 | // Copyright © 2022 Denys Telezhkin. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | import UIKit 28 | import SwiftUI 29 | 30 | @available(iOS 13, tvOS 13, *) 31 | /// Cell subclass, that allows hosting SwiftUI content inside UITableViewCell. 32 | open class HostingTableViewCell: UITableViewCell { 33 | 34 | private var hostingController: UIHostingController? 35 | 36 | /// Updates cell with new SwiftUI view. If the cell is being reused, it's hosting controller will also be reused. 37 | /// - Parameters: 38 | /// - rootView: SwiftUI view 39 | /// - configuration: configuration to use while updating 40 | open func updateWith(rootView: Content, configuration: HostingTableViewCellConfiguration) { 41 | 42 | if let existingHosting = hostingController { 43 | existingHosting.rootView = rootView 44 | hostingController?.view.invalidateIntrinsicContentSize() 45 | configuration.configureCell(self) 46 | } else { 47 | let hosting = configuration.hostingControllerMaker(rootView) 48 | hostingController = hosting 49 | if let backgroundColor = configuration.backgroundColor { 50 | self.backgroundColor = backgroundColor 51 | } 52 | if let hostingBackgroundColor = configuration.hostingViewBackgroundColor { 53 | hostingController?.view.backgroundColor = hostingBackgroundColor 54 | } 55 | if let contentViewBackgroundColor = configuration.contentViewBackgroundColor { 56 | contentView.backgroundColor = contentViewBackgroundColor 57 | } 58 | selectionStyle = configuration.selectionStyle 59 | 60 | hostingController?.view.invalidateIntrinsicContentSize() 61 | 62 | hosting.willMove(toParent: configuration.parentController) 63 | configuration.parentController?.addChild(hosting) 64 | contentView.addSubview(hosting.view) 65 | 66 | hosting.view.translatesAutoresizingMaskIntoConstraints = false 67 | NSLayoutConstraint.activate([ 68 | hosting.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 69 | hosting.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 70 | hosting.view.topAnchor.constraint(equalTo: contentView.topAnchor), 71 | hosting.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) 72 | ]) 73 | 74 | hosting.didMove(toParent: configuration.parentController) 75 | 76 | configuration.configureCell(self) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Tests/AnomaliesTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnomaliesTestCase.swift 3 | // Tests-iOS 4 | // 5 | // Created by Denys Telezhkin on 02.05.2018. 6 | // Copyright © 2018 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DTTableViewManager 11 | 12 | enum TestAnomaly : Equatable, CustomDebugStringConvertible { 13 | case itemEventCalledWithCellType(ObjectIdentifier) 14 | case weirdIndexPathAction(IndexPath) 15 | 16 | var debugDescription: String { return "" } 17 | } 18 | 19 | class DTTestAnomalyHandler : AnomalyHandler { 20 | var anomalyAction : (TestAnomaly) -> Void = { print($0.debugDescription) } 21 | } 22 | 23 | extension XCTestExpectation { 24 | func expect(anomaly: TestAnomaly) -> (TestAnomaly) -> Void { 25 | return { 26 | guard $0 == anomaly else { return } 27 | self.fulfill() 28 | } 29 | } 30 | 31 | func expect(anomaly: DTTableViewManagerAnomaly) -> (DTTableViewManagerAnomaly) -> Void { 32 | return { 33 | guard $0 == anomaly else { return } 34 | self.fulfill() 35 | } 36 | } 37 | } 38 | 39 | class AnomaliesTestCase: XCTestCase { 40 | 41 | var sut: DTTestAnomalyHandler! 42 | 43 | override func setUp() { 44 | super.setUp() 45 | sut = DTTestAnomalyHandler() 46 | } 47 | 48 | func testAnomaliesCanBePositivelyValidated() { 49 | let exp = expectation(description: "Should receive item event anomaly") 50 | sut.anomalyAction = exp.expect(anomaly: .itemEventCalledWithCellType(ObjectIdentifier(MemoryStorage.self))) 51 | sut.reportAnomaly(.itemEventCalledWithCellType(ObjectIdentifier(MemoryStorage.self))) 52 | waitForExpectations(timeout: 0.1) 53 | } 54 | 55 | func testWrongAnomalyFailsTheTest() { 56 | let exp = expectation(description: "Should receive item event anomaly") 57 | exp.isInverted = true 58 | sut.anomalyAction = exp.expect(anomaly: .itemEventCalledWithCellType(ObjectIdentifier(MemoryStorage.self))) 59 | sut.reportAnomaly(.weirdIndexPathAction(indexPath(0, 0))) 60 | waitForExpectations(timeout: 0.1) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Tests/BaseTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseTestCase.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 7/23/19. 6 | // Copyright © 2019 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class BaseTestCase: XCTestCase { 12 | 13 | var controller : DTTestTableViewController! 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | controller = DTTestTableViewController() 19 | controller.tableView = AlwaysVisibleTableView() 20 | let _ = controller.view 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Tests/CreationTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreationTestCase.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 13.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import DTTableViewManager 12 | 13 | class FooCell : UITableViewCell, ModelTransfer 14 | { 15 | func update(with model: Int) { 16 | 17 | } 18 | } 19 | 20 | class OptionalTableViewController : UIViewController, DTTableViewManageable { 21 | var optionalTableView: UITableView? 22 | } 23 | 24 | class CreationTestCase: XCTestCase { 25 | 26 | func testManagingWithOptionalTableViewWorks() { 27 | let controller = OptionalTableViewController() 28 | controller.optionalTableView = UITableView() 29 | 30 | XCTAssert(controller.manager.isManagingTableView) 31 | } 32 | 33 | func testCreatingTableControllerFromCode() 34 | { 35 | let controller = DTTestTableViewController() 36 | controller.manager.register(FooCell.self) 37 | } 38 | 39 | func testDelegateIsNotNil() { 40 | let controller = DTTestTableViewController() 41 | XCTAssertNotNil((controller.manager.storage as? BaseUpdateDeliveringStorage)?.delegate) 42 | } 43 | 44 | func testDelegateIsNotNilForMemoryStorage() { 45 | let controller = DTTestTableViewController() 46 | XCTAssertNotNil(controller.manager.memoryStorage.delegate) 47 | } 48 | 49 | func testSwitchingStorages() { 50 | let controller = DTTestTableViewController() 51 | let first = MemoryStorage() 52 | let second = MemoryStorage() 53 | controller.manager.storage = first 54 | XCTAssert(first.delegate === controller.manager.tableViewUpdater) 55 | 56 | controller.manager.storage = second 57 | 58 | XCTAssertNil(first.delegate) 59 | XCTAssert(second.delegate === controller.manager.tableViewUpdater) 60 | } 61 | 62 | func testCreatingTableControllerFromXIB() 63 | { 64 | let controller = XibTableViewController(nibName: "XibTableViewController", bundle: Bundle(for: type(of: self))) 65 | let _ = controller.view 66 | controller.manager.register(FooCell.self) 67 | } 68 | 69 | func testConfigurationAssociation() 70 | { 71 | let foo = DTTestTableViewController(nibName: nil, bundle: nil) 72 | 73 | XCTAssertNotNil(foo.manager) 74 | XCTAssert(foo.manager === foo.manager) // Test if lazily instantiating using associations works correctly 75 | } 76 | 77 | func testManagerSetter() 78 | { 79 | let manager = DTTableViewManager() 80 | let foo = DTTestTableViewController(nibName: nil, bundle: nil) 81 | foo.manager = manager 82 | 83 | XCTAssert(foo.manager === manager) 84 | } 85 | 86 | func testCallingStartManagingMethodIsNotRequired() { 87 | let controller = DTTestTableViewController() 88 | controller.manager.register(NibCell.self) 89 | controller.manager.memoryStorage.addItem(3) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/DTTestTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTTestTableViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 22.08.15. 6 | // Copyright © 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class DTTestTableViewController: UIViewController, DTTableViewManageable { 13 | 14 | @IBOutlet var tableView : UITableView! = UITableView(frame: .zero) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/FixtureCells.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiblessCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 15.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class BaseTestCell : UITableViewCell, ModelTransfer, ModelRetrievable 13 | { 14 | var model : Any! = nil 15 | var awakedFromNib = false 16 | var inittedWithStyle = false 17 | 18 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 19 | super.init(style: style, reuseIdentifier: reuseIdentifier) 20 | self.inittedWithStyle = true 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | } 26 | 27 | override func awakeFromNib() { 28 | super.awakeFromNib() 29 | self.awakedFromNib = true 30 | } 31 | 32 | func update(with model: Int) { 33 | self.model = model 34 | } 35 | } 36 | 37 | class NiblessCell: BaseTestCell {} 38 | 39 | class NibCell: BaseTestCell { 40 | @IBOutlet weak var customLabel: UILabel? 41 | } 42 | 43 | class StringCell : UITableViewCell, ModelTransfer 44 | { 45 | func update(with model: String) { 46 | 47 | } 48 | } 49 | 50 | class WrongReuseIdentifierCell : BaseTestCell {} 51 | 52 | import SwiftUI 53 | 54 | @available(iOS 13, tvOS 13, *) 55 | struct SwiftUICell: View { 56 | var text: String 57 | 58 | var body: some View { 59 | Text(text) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/FixtureViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FixtureViews.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 18.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import DTTableViewManager 12 | 13 | class NibView : UIView, ModelTransfer 14 | { 15 | func update(with model: Int) { 16 | } 17 | } 18 | 19 | class NibHeaderFooterView : UITableViewHeaderFooterView, ModelTransfer 20 | { 21 | func update(with model: Int) { 22 | } 23 | } 24 | 25 | class NiblessHeaderFooterView : UITableViewHeaderFooterView, ModelTransfer 26 | { 27 | func update(with model: Int) { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/OptionalCells.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalCells.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 17.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import DTTableViewManager 12 | 13 | class OptionalIntCell : UITableViewCell, ModelTransfer, ModelRetrievable 14 | { 15 | var model: Any! 16 | func update(with model: Int?) { 17 | self.model = model 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/ReactingHeaderFooters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactingHeaderFooters.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 23.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import DTTableViewManager 12 | 13 | class ReactingHeaderFooterView : UITableViewHeaderFooterView, ModelTransfer 14 | { 15 | var sectionIndex: Int? 16 | var model : String? 17 | 18 | func update(with model: String) { 19 | self.model = model 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/ReactingTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactingTableCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 19.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class ReactingTableCell: UITableViewCell, ModelTransfer { 13 | 14 | func update(with model: Int) { 15 | 16 | } 17 | 18 | } 19 | 20 | class SelectionReactingTableCell: ReactingTableCell 21 | { 22 | var indexPath: IndexPath? 23 | var cell: SelectionReactingTableCell? 24 | var model : Int? 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/StoryboardCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardCell.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 10.01.16. 6 | // Copyright © 2016 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class StoryboardCell : UITableViewCell, ModelTransfer 13 | { 14 | @IBOutlet weak var storyboardLabel: UILabel! 15 | func update(with model: Int) { 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/XibTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XibTableViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 13.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class XibTableViewController: DTTestTableViewController { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/CustomNibCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/EmptyXib.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/NibCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/NibHeaderFooterView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/NibView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/RandomNameWrongReuseIdentifierCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/RandomNibNameCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/ReactingHeaderFooterView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/WrongReuseIdentifierCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/iOS/XibTableViewController.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/CustomNibCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/EmptyXib.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/NibCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/NibHeaderFooterView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/NibView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/RandomNameWrongReuseIdentifierCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/RandomNibNameCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/ReactingHeaderFooterView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/WrongReuseIdentifierCell.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/Fixtures/tvOS/XibTableViewController.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Sources/Tests/MappingConditionsTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappingConditionsTestCase.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 16.07.17. 6 | // Copyright © 2017 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DTTableViewManager 11 | 12 | class MappingConditionsTestCase: BaseTestCase { 13 | 14 | func testMappingCanBeSwitchedBetweenSections() { 15 | controller.manager.register(NibCell.self) { mapping in 16 | mapping.condition = .section(0) 17 | } 18 | controller.manager.register(AnotherIntCell.self) { mapping in 19 | mapping.condition = .section(1) 20 | } 21 | 22 | controller.manager.memoryStorage.addItem(1) 23 | controller.manager.memoryStorage.addItem(2, toSection: 1) 24 | 25 | let nibCell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 0)) 26 | XCTAssert(nibCell is NibCell) 27 | 28 | let cell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 1)) 29 | 30 | XCTAssert(cell is AnotherIntCell) 31 | } 32 | 33 | func testCustomMappingIsRevolvableForTheSameModel() { 34 | controller.manager.register(NibCell.self) { mapping in 35 | mapping.condition = .custom({ indexPath, model in 36 | guard let model = model as? Int else { return false } 37 | return model > 2 38 | }) 39 | } 40 | controller.manager.register(AnotherIntCell.self) { mapping in 41 | mapping.condition = .custom({ indexPath, model -> Bool in 42 | guard let model = model as? Int else { return false } 43 | return model <= 2 44 | }) 45 | } 46 | 47 | controller.manager.memoryStorage.addItem(3) 48 | let cell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 0)) 49 | XCTAssert(cell is NibCell) 50 | 51 | controller.manager.memoryStorage.addItem(1) 52 | let anotherCell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(1, 0)) 53 | XCTAssert(anotherCell is AnotherIntCell) 54 | } 55 | 56 | func testMappingCanBeSwitchedForNibNames() { 57 | controller.manager.register(NibCell.self) { mapping in 58 | mapping.condition = .section(0) 59 | mapping.reuseIdentifier = "NibCell One" 60 | } 61 | controller.manager.register(NibCell.self) { mapping in 62 | mapping.condition = .section(1) 63 | mapping.xibName = "CustomNibCell" 64 | mapping.reuseIdentifier = "NibCell Two" 65 | } 66 | 67 | controller.manager.memoryStorage.addItem(1) 68 | controller.manager.memoryStorage.addItem(2, toSection: 1) 69 | 70 | let nibCell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 0)) as? NibCell 71 | XCTAssertNil(nibCell?.customLabel) 72 | 73 | let customNibCell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 1)) as? NibCell 74 | 75 | XCTAssertNotNil(customNibCell?.customLabel) 76 | } 77 | 78 | func testSwiftUICellCanBeLoaded() throws { 79 | guard #available(iOS 13, tvOS 13, *) else { throw XCTSkip() } 80 | controller.manager.registerHostingCell(for: String.self) { model, _ in 81 | SwiftUICell(text: model) 82 | } 83 | controller.manager.memoryStorage.addItem("Hello SwiftUI") 84 | let hostingCell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 0)) as? HostingTableViewCell 85 | 86 | XCTAssertNotNil(hostingCell) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Tests/NSIndexPath+Constructor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSIndexPath+Constructor.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 15.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func indexPath(_ item: Int, _ section: Int) -> IndexPath 12 | { 13 | return IndexPath(item: item, section: section) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Tests/StoryboardMappingTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardMappingTestCase.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 10.01.16. 6 | // Copyright © 2016 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import DTTableViewManager 12 | 13 | class StoryboardMappingTestCase: XCTestCase { 14 | 15 | var controller : StoryboardViewController! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | let storyboard = UIStoryboard(name: "FixtureStoryboard", bundle: Bundle(for: type(of: self))) 20 | controller = storyboard.instantiateInitialViewController() as? StoryboardViewController 21 | _ = controller.view 22 | } 23 | 24 | func testCellIsMappedAndOutletsAreCreated() { 25 | controller.manager.register(StoryboardCell.self) 26 | controller.manager.memoryStorage.addItem(1) 27 | 28 | let cell: StoryboardCell 29 | if #available(tvOS 11, *) { 30 | cell = controller.tableView.cellForRow(at: indexPath(0, 0)) as! StoryboardCell 31 | } else { 32 | cell = controller.manager.tableDataSource?.tableView(controller.tableView, cellForRowAt: indexPath(0, 0)) as! StoryboardCell 33 | } 34 | 35 | XCTAssertNotNil(cell.storyboardLabel) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Tests/StoryboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoryboardViewController.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 10.01.16. 6 | // Copyright © 2016 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DTTableViewManager 11 | 12 | class StoryboardViewController: UIViewController, DTTableViewManageable { 13 | 14 | @IBOutlet weak var tableView: UITableView! 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Tests/TableViewController+UnitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController+UnitTests.swift 3 | // DTTableViewManager 4 | // 5 | // Created by Denys Telezhkin on 14.07.15. 6 | // Copyright (c) 2015 Denys Telezhkin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DTTableViewManager 11 | import UIKit 12 | 13 | protocol ModelRetrievable 14 | { 15 | var model : Any! { get } 16 | } 17 | 18 | func recursiveForceUnwrap(_ any: T) -> T 19 | { 20 | let mirror = Mirror(reflecting: any) 21 | if mirror.displayStyle != .optional 22 | { 23 | return any 24 | } 25 | let (_,some) = mirror.children.first! 26 | return recursiveForceUnwrap(some) as! T 27 | } 28 | 29 | extension DTTestTableViewController 30 | { 31 | func verifyItem(_ item: Model, atIndexPath indexPath: IndexPath) -> Bool 32 | { 33 | let itemTable = (self.manager.tableDataSource?.tableView(self.tableView, cellForRowAt: indexPath) as! ModelRetrievable).model as! Model 34 | let itemDatasource = recursiveForceUnwrap(self.manager.storage.item(at: indexPath)!) as! Model 35 | 36 | if !(item == itemDatasource) 37 | { 38 | return false 39 | } 40 | 41 | if !(item == itemTable) 42 | { 43 | return false 44 | } 45 | 46 | return true 47 | } 48 | 49 | func verifySection(_ section: [Int], withSectionNumber sectionNumber: Int) -> Bool 50 | { 51 | for itemNumber in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 11.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Supporting files/Tests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 82% 23 | 24 | 25 | 82% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | /* Credit to https://gist.github.com/wataru420/2048287 */ 6 | .highlight .c { 7 | color: #999988; 8 | font-style: italic; } 9 | 10 | .highlight .err { 11 | color: #a61717; 12 | background-color: #e3d2d2; } 13 | 14 | .highlight .k { 15 | color: #000000; 16 | font-weight: bold; } 17 | 18 | .highlight .o { 19 | color: #000000; 20 | font-weight: bold; } 21 | 22 | .highlight .cm { 23 | color: #999988; 24 | font-style: italic; } 25 | 26 | .highlight .cp { 27 | color: #999999; 28 | font-weight: bold; } 29 | 30 | .highlight .c1 { 31 | color: #999988; 32 | font-style: italic; } 33 | 34 | .highlight .cs { 35 | color: #999999; 36 | font-weight: bold; 37 | font-style: italic; } 38 | 39 | .highlight .gd { 40 | color: #000000; 41 | background-color: #ffdddd; } 42 | 43 | .highlight .gd .x { 44 | color: #000000; 45 | background-color: #ffaaaa; } 46 | 47 | .highlight .ge { 48 | color: #000000; 49 | font-style: italic; } 50 | 51 | .highlight .gr { 52 | color: #aa0000; } 53 | 54 | .highlight .gh { 55 | color: #999999; } 56 | 57 | .highlight .gi { 58 | color: #000000; 59 | background-color: #ddffdd; } 60 | 61 | .highlight .gi .x { 62 | color: #000000; 63 | background-color: #aaffaa; } 64 | 65 | .highlight .go { 66 | color: #888888; } 67 | 68 | .highlight .gp { 69 | color: #555555; } 70 | 71 | .highlight .gs { 72 | font-weight: bold; } 73 | 74 | .highlight .gu { 75 | color: #aaaaaa; } 76 | 77 | .highlight .gt { 78 | color: #aa0000; } 79 | 80 | .highlight .kc { 81 | color: #000000; 82 | font-weight: bold; } 83 | 84 | .highlight .kd { 85 | color: #000000; 86 | font-weight: bold; } 87 | 88 | .highlight .kp { 89 | color: #000000; 90 | font-weight: bold; } 91 | 92 | .highlight .kr { 93 | color: #000000; 94 | font-weight: bold; } 95 | 96 | .highlight .kt { 97 | color: #445588; } 98 | 99 | .highlight .m { 100 | color: #009999; } 101 | 102 | .highlight .s { 103 | color: #d14; } 104 | 105 | .highlight .na { 106 | color: #008080; } 107 | 108 | .highlight .nb { 109 | color: #0086B3; } 110 | 111 | .highlight .nc { 112 | color: #445588; 113 | font-weight: bold; } 114 | 115 | .highlight .no { 116 | color: #008080; } 117 | 118 | .highlight .ni { 119 | color: #800080; } 120 | 121 | .highlight .ne { 122 | color: #990000; 123 | font-weight: bold; } 124 | 125 | .highlight .nf { 126 | color: #990000; } 127 | 128 | .highlight .nn { 129 | color: #555555; } 130 | 131 | .highlight .nt { 132 | color: #000080; } 133 | 134 | .highlight .nv { 135 | color: #008080; } 136 | 137 | .highlight .ow { 138 | color: #000000; 139 | font-weight: bold; } 140 | 141 | .highlight .w { 142 | color: #bbbbbb; } 143 | 144 | .highlight .mf { 145 | color: #009999; } 146 | 147 | .highlight .mh { 148 | color: #009999; } 149 | 150 | .highlight .mi { 151 | color: #009999; } 152 | 153 | .highlight .mo { 154 | color: #009999; } 155 | 156 | .highlight .sb { 157 | color: #d14; } 158 | 159 | .highlight .sc { 160 | color: #d14; } 161 | 162 | .highlight .sd { 163 | color: #d14; } 164 | 165 | .highlight .s2 { 166 | color: #d14; } 167 | 168 | .highlight .se { 169 | color: #d14; } 170 | 171 | .highlight .sh { 172 | color: #d14; } 173 | 174 | .highlight .si { 175 | color: #d14; } 176 | 177 | .highlight .sx { 178 | color: #d14; } 179 | 180 | .highlight .sr { 181 | color: #009926; } 182 | 183 | .highlight .s1 { 184 | color: #d14; } 185 | 186 | .highlight .ss { 187 | color: #990073; } 188 | 189 | .highlight .bp { 190 | color: #999999; } 191 | 192 | .highlight .vc { 193 | color: #008080; } 194 | 195 | .highlight .vg { 196 | color: #008080; } 197 | 198 | .highlight .vi { 199 | color: #008080; } 200 | 201 | .highlight .il { 202 | color: #009999; } 203 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.dttableviewmanager 7 | CFBundleName 8 | DTTableViewManager 9 | DocSetPlatformFamily 10 | dttableviewmanager 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | DashDocSetFallbackURL 20 | https://dentelezhkin.github.io/DTTableViewManager/ 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 82% 23 | 24 | 25 | 82% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /*! Jazzy - https://github.com/realm/jazzy 2 | * Copyright Realm Inc. 3 | * SPDX-License-Identifier: MIT 4 | */ 5 | /* Credit to https://gist.github.com/wataru420/2048287 */ 6 | .highlight .c { 7 | color: #999988; 8 | font-style: italic; } 9 | 10 | .highlight .err { 11 | color: #a61717; 12 | background-color: #e3d2d2; } 13 | 14 | .highlight .k { 15 | color: #000000; 16 | font-weight: bold; } 17 | 18 | .highlight .o { 19 | color: #000000; 20 | font-weight: bold; } 21 | 22 | .highlight .cm { 23 | color: #999988; 24 | font-style: italic; } 25 | 26 | .highlight .cp { 27 | color: #999999; 28 | font-weight: bold; } 29 | 30 | .highlight .c1 { 31 | color: #999988; 32 | font-style: italic; } 33 | 34 | .highlight .cs { 35 | color: #999999; 36 | font-weight: bold; 37 | font-style: italic; } 38 | 39 | .highlight .gd { 40 | color: #000000; 41 | background-color: #ffdddd; } 42 | 43 | .highlight .gd .x { 44 | color: #000000; 45 | background-color: #ffaaaa; } 46 | 47 | .highlight .ge { 48 | color: #000000; 49 | font-style: italic; } 50 | 51 | .highlight .gr { 52 | color: #aa0000; } 53 | 54 | .highlight .gh { 55 | color: #999999; } 56 | 57 | .highlight .gi { 58 | color: #000000; 59 | background-color: #ddffdd; } 60 | 61 | .highlight .gi .x { 62 | color: #000000; 63 | background-color: #aaffaa; } 64 | 65 | .highlight .go { 66 | color: #888888; } 67 | 68 | .highlight .gp { 69 | color: #555555; } 70 | 71 | .highlight .gs { 72 | font-weight: bold; } 73 | 74 | .highlight .gu { 75 | color: #aaaaaa; } 76 | 77 | .highlight .gt { 78 | color: #aa0000; } 79 | 80 | .highlight .kc { 81 | color: #000000; 82 | font-weight: bold; } 83 | 84 | .highlight .kd { 85 | color: #000000; 86 | font-weight: bold; } 87 | 88 | .highlight .kp { 89 | color: #000000; 90 | font-weight: bold; } 91 | 92 | .highlight .kr { 93 | color: #000000; 94 | font-weight: bold; } 95 | 96 | .highlight .kt { 97 | color: #445588; } 98 | 99 | .highlight .m { 100 | color: #009999; } 101 | 102 | .highlight .s { 103 | color: #d14; } 104 | 105 | .highlight .na { 106 | color: #008080; } 107 | 108 | .highlight .nb { 109 | color: #0086B3; } 110 | 111 | .highlight .nc { 112 | color: #445588; 113 | font-weight: bold; } 114 | 115 | .highlight .no { 116 | color: #008080; } 117 | 118 | .highlight .ni { 119 | color: #800080; } 120 | 121 | .highlight .ne { 122 | color: #990000; 123 | font-weight: bold; } 124 | 125 | .highlight .nf { 126 | color: #990000; } 127 | 128 | .highlight .nn { 129 | color: #555555; } 130 | 131 | .highlight .nt { 132 | color: #000080; } 133 | 134 | .highlight .nv { 135 | color: #008080; } 136 | 137 | .highlight .ow { 138 | color: #000000; 139 | font-weight: bold; } 140 | 141 | .highlight .w { 142 | color: #bbbbbb; } 143 | 144 | .highlight .mf { 145 | color: #009999; } 146 | 147 | .highlight .mh { 148 | color: #009999; } 149 | 150 | .highlight .mi { 151 | color: #009999; } 152 | 153 | .highlight .mo { 154 | color: #009999; } 155 | 156 | .highlight .sb { 157 | color: #d14; } 158 | 159 | .highlight .sc { 160 | color: #d14; } 161 | 162 | .highlight .sd { 163 | color: #d14; } 164 | 165 | .highlight .s2 { 166 | color: #d14; } 167 | 168 | .highlight .se { 169 | color: #d14; } 170 | 171 | .highlight .sh { 172 | color: #d14; } 173 | 174 | .highlight .si { 175 | color: #d14; } 176 | 177 | .highlight .sx { 178 | color: #d14; } 179 | 180 | .highlight .sr { 181 | color: #009926; } 182 | 183 | .highlight .s1 { 184 | color: #d14; } 185 | 186 | .highlight .ss { 187 | color: #990073; } 188 | 189 | .highlight .bp { 190 | color: #999999; } 191 | 192 | .highlight .vc { 193 | color: #008080; } 194 | 195 | .highlight .vg { 196 | color: #008080; } 197 | 198 | .highlight .vi { 199 | color: #008080; } 200 | 201 | .highlight .il { 202 | color: #009999; } 203 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | window.jazzy = {'docset': false} 6 | if (typeof window.dash != 'undefined') { 7 | document.documentElement.className += ' dash' 8 | window.jazzy.docset = true 9 | } 10 | if (navigator.userAgent.match(/xcode/i)) { 11 | document.documentElement.className += ' xcode' 12 | window.jazzy.docset = true 13 | } 14 | 15 | function toggleItem($link, $content) { 16 | var animationDuration = 300; 17 | $link.toggleClass('token-open'); 18 | $content.slideToggle(animationDuration); 19 | } 20 | 21 | function itemLinkToContent($link) { 22 | return $link.parent().parent().next(); 23 | } 24 | 25 | // On doc load + hash-change, open any targetted item 26 | function openCurrentItemIfClosed() { 27 | if (window.jazzy.docset) { 28 | return; 29 | } 30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 31 | $content = itemLinkToContent($link); 32 | if ($content.is(':hidden')) { 33 | toggleItem($link, $content); 34 | } 35 | } 36 | 37 | $(openCurrentItemIfClosed); 38 | $(window).on('hashchange', openCurrentItemIfClosed); 39 | 40 | // On item link ('token') click, toggle its discussion 41 | $('.token').on('click', function(event) { 42 | if (window.jazzy.docset) { 43 | return; 44 | } 45 | var $link = $(this); 46 | toggleItem($link, itemLinkToContent($link)); 47 | 48 | // Keeps the document from jumping to the hash. 49 | var href = $link.attr('href'); 50 | if (history.pushState) { 51 | history.pushState({}, '', href); 52 | } else { 53 | location.hash = href; 54 | } 55 | event.preventDefault(); 56 | }); 57 | 58 | // Clicks on links to the current, closed, item need to open the item 59 | $("a:not('.token')").on('click', function() { 60 | if (location == this.href) { 61 | openCurrentItemIfClosed(); 62 | } 63 | }); 64 | 65 | // KaTeX rendering 66 | if ("katex" in window) { 67 | $($('.math').each( (_, element) => { 68 | katex.render(element.textContent, element, { 69 | displayMode: $(element).hasClass('m-block'), 70 | throwOnError: false, 71 | trust: true 72 | }); 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | $(function(){ 6 | var $typeahead = $('[data-typeahead]'); 7 | var $form = $typeahead.parents('form'); 8 | var searchURL = $form.attr('action'); 9 | 10 | function displayTemplate(result) { 11 | return result.name; 12 | } 13 | 14 | function suggestionTemplate(result) { 15 | var t = '
'; 16 | t += '' + result.name + ''; 17 | if (result.parent_name) { 18 | t += '' + result.parent_name + ''; 19 | } 20 | t += '
'; 21 | return t; 22 | } 23 | 24 | $typeahead.one('focus', function() { 25 | $form.addClass('loading'); 26 | 27 | $.getJSON(searchURL).then(function(searchData) { 28 | const searchIndex = lunr(function() { 29 | this.ref('url'); 30 | this.field('name'); 31 | this.field('abstract'); 32 | for (const [url, doc] of Object.entries(searchData)) { 33 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 34 | } 35 | }); 36 | 37 | $typeahead.typeahead( 38 | { 39 | highlight: true, 40 | minLength: 3, 41 | autoselect: true 42 | }, 43 | { 44 | limit: 10, 45 | display: displayTemplate, 46 | templates: { suggestion: suggestionTemplate }, 47 | source: function(query, sync) { 48 | const lcSearch = query.toLowerCase(); 49 | const results = searchIndex.query(function(q) { 50 | q.term(lcSearch, { boost: 100 }); 51 | q.term(lcSearch, { 52 | boost: 10, 53 | wildcard: lunr.Query.wildcard.TRAILING 54 | }); 55 | }).map(function(result) { 56 | var doc = searchData[result.ref]; 57 | doc.url = result.ref; 58 | return doc; 59 | }); 60 | sync(results); 61 | } 62 | } 63 | ); 64 | $form.removeClass('loading'); 65 | $typeahead.trigger('focus'); 66 | }); 67 | }); 68 | 69 | var baseURL = searchURL.slice(0, -"search.json".length); 70 | 71 | $typeahead.on('typeahead:select', function(e, result) { 72 | window.location = baseURL + result.url; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/docsets/DTTableViewManager.tgz -------------------------------------------------------------------------------- /docs/docsets/DTTableViewManager.xml: -------------------------------------------------------------------------------- 1 | 9.0.0-beta.1https://dentelezhkin.github.io/DTTableViewManager/docsets/DTTableViewManager.tgz 2 | -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenTelezhkin/DTTableViewManager/ebb417e4dc3024baf6eca53fc580c84b63b77453/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | window.jazzy = {'docset': false} 6 | if (typeof window.dash != 'undefined') { 7 | document.documentElement.className += ' dash' 8 | window.jazzy.docset = true 9 | } 10 | if (navigator.userAgent.match(/xcode/i)) { 11 | document.documentElement.className += ' xcode' 12 | window.jazzy.docset = true 13 | } 14 | 15 | function toggleItem($link, $content) { 16 | var animationDuration = 300; 17 | $link.toggleClass('token-open'); 18 | $content.slideToggle(animationDuration); 19 | } 20 | 21 | function itemLinkToContent($link) { 22 | return $link.parent().parent().next(); 23 | } 24 | 25 | // On doc load + hash-change, open any targetted item 26 | function openCurrentItemIfClosed() { 27 | if (window.jazzy.docset) { 28 | return; 29 | } 30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 31 | $content = itemLinkToContent($link); 32 | if ($content.is(':hidden')) { 33 | toggleItem($link, $content); 34 | } 35 | } 36 | 37 | $(openCurrentItemIfClosed); 38 | $(window).on('hashchange', openCurrentItemIfClosed); 39 | 40 | // On item link ('token') click, toggle its discussion 41 | $('.token').on('click', function(event) { 42 | if (window.jazzy.docset) { 43 | return; 44 | } 45 | var $link = $(this); 46 | toggleItem($link, itemLinkToContent($link)); 47 | 48 | // Keeps the document from jumping to the hash. 49 | var href = $link.attr('href'); 50 | if (history.pushState) { 51 | history.pushState({}, '', href); 52 | } else { 53 | location.hash = href; 54 | } 55 | event.preventDefault(); 56 | }); 57 | 58 | // Clicks on links to the current, closed, item need to open the item 59 | $("a:not('.token')").on('click', function() { 60 | if (location == this.href) { 61 | openCurrentItemIfClosed(); 62 | } 63 | }); 64 | 65 | // KaTeX rendering 66 | if ("katex" in window) { 67 | $($('.math').each( (_, element) => { 68 | katex.render(element.textContent, element, { 69 | displayMode: $(element).hasClass('m-block'), 70 | throwOnError: false, 71 | trust: true 72 | }); 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | // Jazzy - https://github.com/realm/jazzy 2 | // Copyright Realm Inc. 3 | // SPDX-License-Identifier: MIT 4 | 5 | $(function(){ 6 | var $typeahead = $('[data-typeahead]'); 7 | var $form = $typeahead.parents('form'); 8 | var searchURL = $form.attr('action'); 9 | 10 | function displayTemplate(result) { 11 | return result.name; 12 | } 13 | 14 | function suggestionTemplate(result) { 15 | var t = '
'; 16 | t += '' + result.name + ''; 17 | if (result.parent_name) { 18 | t += '' + result.parent_name + ''; 19 | } 20 | t += '
'; 21 | return t; 22 | } 23 | 24 | $typeahead.one('focus', function() { 25 | $form.addClass('loading'); 26 | 27 | $.getJSON(searchURL).then(function(searchData) { 28 | const searchIndex = lunr(function() { 29 | this.ref('url'); 30 | this.field('name'); 31 | this.field('abstract'); 32 | for (const [url, doc] of Object.entries(searchData)) { 33 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 34 | } 35 | }); 36 | 37 | $typeahead.typeahead( 38 | { 39 | highlight: true, 40 | minLength: 3, 41 | autoselect: true 42 | }, 43 | { 44 | limit: 10, 45 | display: displayTemplate, 46 | templates: { suggestion: suggestionTemplate }, 47 | source: function(query, sync) { 48 | const lcSearch = query.toLowerCase(); 49 | const results = searchIndex.query(function(q) { 50 | q.term(lcSearch, { boost: 100 }); 51 | q.term(lcSearch, { 52 | boost: 10, 53 | wildcard: lunr.Query.wildcard.TRAILING 54 | }); 55 | }).map(function(result) { 56 | var doc = searchData[result.ref]; 57 | doc.url = result.ref; 58 | return doc; 59 | }); 60 | sync(results); 61 | } 62 | } 63 | ); 64 | $form.removeClass('loading'); 65 | $typeahead.trigger('focus'); 66 | }); 67 | }); 68 | 69 | var baseURL = searchURL.slice(0, -"search.json".length); 70 | 71 | $typeahead.on('typeahead:select', function(e, result) { 72 | window.location = baseURL + result.url; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "1.98.0" 2 | 3 | lane :test_ios17 do 4 | scan(devices: ["iPhone 15 Pro (17.5)"], scheme: 'Tests-iOS') 5 | end 6 | 7 | lane :test_ios15 do 8 | scan(devices: ["iPhone 13 Pro Max (15.5)"], scheme: 'Tests-iOS') 9 | end 10 | 11 | lane :test_tvos17 do 12 | scan(device: "Apple TV (17.5)", scheme: 'Tests-tvOS') 13 | end 14 | 15 | lane :test_tvos15 do 16 | scan(device: "Apple TV (15.4)", scheme: 'Tests-tvOS') 17 | end 18 | 19 | lane :pod_lint do 20 | sh "bundle exec pod repo update" 21 | pod_lib_lint(allow_warnings: true, verbose: true) 22 | end 23 | 24 | lane :generate_code_coverage do 25 | slather( 26 | proj: "DTTableViewManager.xcodeproj", 27 | scheme: "DTTableViewManager", 28 | verbose: true, 29 | cobertura_xml: true 30 | ) 31 | end 32 | 33 | lane :test_catalyst do 34 | scan(destination: "platform=macOS,variant=Mac Catalyst", scheme: 'Tests-iOS', prelaunch_simulator: false, disable_slide_to_type: false) 35 | end 36 | 37 | lane :release do |params| 38 | version = params[:version] 39 | 40 | abort "You must specify a version in semver format." if version.nil? || version.scan(/\d+\.\d+\.\d+(-\w+\.\d+)?/).length == 0 41 | 42 | puts "Setting Framework version" 43 | increment_version_number_in_plist( 44 | version_number: version, 45 | target: "DTTableViewManager" 46 | ) 47 | 48 | Dir.chdir("..") do 49 | 50 | puts "Updating podspec." 51 | filename = "DTTableViewManager.podspec" 52 | contents = File.read(filename) 53 | contents.gsub!(/s\.version\s*=\s"\d+\.\d+\.\d+(-\w+\.\d)?"/, "s.version = \"#{version}\"") 54 | File.open(filename, 'w') { |file| file.puts contents } 55 | 56 | puts "Updating changelog." 57 | changelog_filename = "CHANGELOG.md" 58 | changelog = File.read(changelog_filename) 59 | changelog.gsub!(/# Next/, "# Next\n\n## [#{version}](https://github.com/DenTelezhkin/DTTableViewManager/releases/tag/#{version})") 60 | File.open(changelog_filename, 'w') { |file| file.puts changelog } 61 | 62 | puts "Comitting, tagging, and pushing." 63 | message = "Releasing version #{version}." 64 | sh "git commit -am '#{message}'" 65 | sh "git tag #{version} -m '#{message}'" 66 | sh "git push --follow-tags" 67 | 68 | puts "Updating Specs repo" 69 | sh "bundle exec pod repo update" 70 | 71 | puts "Pushing to CocoaPods trunk." 72 | sh "bundle exec pod trunk push DTTableViewManager.podspec --allow-warnings" 73 | 74 | puts "Pushing as a GitHub Release." 75 | fastlane_require 'octokit' 76 | stripped_changelog = changelog.split(/^## /)[1].split("\n")[1..-1].join("\n").strip 77 | client = Octokit::Client.new(netrc: true) 78 | client.create_release('DenTelezhkin/DTTableViewManager', 79 | version, 80 | name: version, 81 | body: stripped_changelog) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-versioning' 6 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### test_ios16 17 | 18 | ```sh 19 | [bundle exec] fastlane test_ios16 20 | ``` 21 | 22 | 23 | 24 | ### test_ios15 25 | 26 | ```sh 27 | [bundle exec] fastlane test_ios15 28 | ``` 29 | 30 | 31 | 32 | ### test_tvos16 33 | 34 | ```sh 35 | [bundle exec] fastlane test_tvos16 36 | ``` 37 | 38 | 39 | 40 | ### test_tvos15 41 | 42 | ```sh 43 | [bundle exec] fastlane test_tvos15 44 | ``` 45 | 46 | 47 | 48 | ### pod_lint 49 | 50 | ```sh 51 | [bundle exec] fastlane pod_lint 52 | ``` 53 | 54 | 55 | 56 | ### generate_code_coverage 57 | 58 | ```sh 59 | [bundle exec] fastlane generate_code_coverage 60 | ``` 61 | 62 | 63 | 64 | ### test_catalyst 65 | 66 | ```sh 67 | [bundle exec] fastlane test_catalyst 68 | ``` 69 | 70 | 71 | 72 | ### release 73 | 74 | ```sh 75 | [bundle exec] fastlane release 76 | ``` 77 | 78 | 79 | 80 | ---- 81 | 82 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 83 | 84 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 85 | 86 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 87 | -------------------------------------------------------------------------------- /fastlane/Scanfile: -------------------------------------------------------------------------------- 1 | clean true 2 | output_types "" 3 | skip_slack true 4 | --------------------------------------------------------------------------------