├── .codecov.yml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .swiftlint.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── Dangerfile ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── close.imageset │ │ ├── Contents.json │ │ ├── closeIcon.png │ │ ├── closeIcon@2x.png │ │ └── closeIcon@3x.png │ ├── grid-selected.imageset │ │ ├── 1076-grid-4-selected.png │ │ ├── 1076-grid-4-selected@2x.png │ │ ├── 1076-grid-4-selected@3x.png │ │ └── Contents.json │ ├── grid.imageset │ │ ├── 1076-grid-4.png │ │ ├── 1076-grid-4@2x.png │ │ ├── 1076-grid-4@3x.png │ │ └── Contents.json │ ├── list-selected.imageset │ │ ├── 1099-list-1-selected.png │ │ ├── 1099-list-1-selected@2x.png │ │ ├── 1099-list-1-selected@3x.png │ │ └── Contents.json │ └── list.imageset │ │ ├── 1099-list-1.png │ │ ├── 1099-list-1@2x.png │ │ ├── 1099-list-1@3x.png │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CollectionToolCell.xib ├── CollectionViewCells.swift ├── CollectionViewController.swift ├── CollectionViewHeaderView.xib ├── Info.plist ├── Models.swift ├── TableViewCellModels.swift ├── TableViewController.swift └── ToolTableViewCell.swift ├── Gemfile ├── Gemfile.lock ├── Guides ├── Getting Started.md └── VISION.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── Podfile ├── Podfile.lock ├── Pods ├── DifferenceKit │ ├── LICENSE │ ├── README.md │ └── Sources │ │ ├── Algorithm.swift │ │ ├── AnyDifferentiable.swift │ │ ├── ArraySection.swift │ │ ├── Changeset.swift │ │ ├── ContentEquatable.swift │ │ ├── Differentiable.swift │ │ ├── DifferentiableSection.swift │ │ ├── ElementPath.swift │ │ ├── Extensions │ │ └── UIKitExtension.swift │ │ └── StagedChangeset.swift ├── Manifest.lock ├── Pods.xcodeproj │ └── project.pbxproj ├── SwiftLint │ ├── LICENSE │ └── swiftlint └── Target Support Files │ ├── DifferenceKit │ ├── DifferenceKit-Info.plist │ ├── DifferenceKit-dummy.m │ ├── DifferenceKit-prefix.pch │ ├── DifferenceKit-umbrella.h │ ├── DifferenceKit.debug.xcconfig │ ├── DifferenceKit.modulemap │ ├── DifferenceKit.release.xcconfig │ ├── DifferenceKit.xcconfig │ └── Info.plist │ ├── Pods-ReactiveLists-ReactiveListsExample │ ├── Info.plist │ ├── Pods-ReactiveLists-ReactiveListsExample-Info.plist │ ├── Pods-ReactiveLists-ReactiveListsExample-acknowledgements.markdown │ ├── Pods-ReactiveLists-ReactiveListsExample-acknowledgements.plist │ ├── Pods-ReactiveLists-ReactiveListsExample-dummy.m │ ├── Pods-ReactiveLists-ReactiveListsExample-frameworks.sh │ ├── Pods-ReactiveLists-ReactiveListsExample-resources.sh │ ├── Pods-ReactiveLists-ReactiveListsExample-umbrella.h │ ├── Pods-ReactiveLists-ReactiveListsExample.debug.xcconfig │ ├── Pods-ReactiveLists-ReactiveListsExample.modulemap │ └── Pods-ReactiveLists-ReactiveListsExample.release.xcconfig │ ├── Pods-ReactiveLists │ ├── Info.plist │ ├── Pods-ReactiveLists-Info.plist │ ├── Pods-ReactiveLists-acknowledgements.markdown │ ├── Pods-ReactiveLists-acknowledgements.plist │ ├── Pods-ReactiveLists-dummy.m │ ├── Pods-ReactiveLists-resources.sh │ ├── Pods-ReactiveLists-umbrella.h │ ├── Pods-ReactiveLists.debug.xcconfig │ ├── Pods-ReactiveLists.modulemap │ └── Pods-ReactiveLists.release.xcconfig │ ├── Pods-ReactiveListsTests │ ├── Info.plist │ ├── Pods-ReactiveListsTests-Info.plist │ ├── Pods-ReactiveListsTests-acknowledgements.markdown │ ├── Pods-ReactiveListsTests-acknowledgements.plist │ ├── Pods-ReactiveListsTests-dummy.m │ ├── Pods-ReactiveListsTests-frameworks.sh │ ├── Pods-ReactiveListsTests-resources.sh │ ├── Pods-ReactiveListsTests-umbrella.h │ ├── Pods-ReactiveListsTests.debug.xcconfig │ ├── Pods-ReactiveListsTests.modulemap │ └── Pods-ReactiveListsTests.release.xcconfig │ └── SwiftLint │ ├── SwiftLint.debug.xcconfig │ ├── SwiftLint.release.xcconfig │ └── SwiftLint.xcconfig ├── README.md ├── ReactiveLists.podspec ├── ReactiveLists.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Example.xcscheme │ └── ReactiveLists.xcscheme ├── ReactiveLists.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDETemplateMacros.plist │ └── IDEWorkspaceChecks.plist ├── ReactiveLists └── 0.8.1.beta.1 │ └── ReactiveLists.podspec ├── Resources └── logo.png ├── Sources ├── AccessibilityFormats.swift ├── CellContainerViewProtocol.swift ├── CollectionCellViewModelDataSource.swift ├── CollectionViewDriver.swift ├── CollectionViewModel.swift ├── Diffing.swift ├── Info.plist ├── ReactiveLists.h ├── ReusableCellViewModelProtocol.swift ├── SupplementaryViewInfo.swift ├── TableCellViewModelDataSource.swift ├── TableViewDriver.swift ├── TableViewModel.swift ├── Typealiases.swift ├── UICollectionView+Extensions.swift ├── UITableView+Extensions.swift └── ViewRegistrationInfo.swift ├── Tests ├── CollectionView │ ├── CollectionViewDriverDiffingTests.swift │ ├── CollectionViewDriverTests.swift │ ├── CollectionViewLazyDiffingTests.swift │ ├── CollectionViewMocks.swift │ ├── CollectionViewModelTests.swift │ └── TestCollectionViewModels.swift ├── Info.plist ├── TableView │ ├── TableViewDiffingTests.swift │ ├── TableViewDriverTests.swift │ ├── TableViewLazyDiffingTest.swift │ ├── TableViewMocks.swift │ ├── TableViewModelTests.swift │ └── TestTableViewModels.swift └── Utils │ └── XCTest+Parameterized.swift ├── docs ├── Classes.html ├── Classes │ ├── CollectionViewDriver.html │ ├── TableViewDriver.html │ └── TableViewDriver │ │ └── TableRefreshContext.html ├── Enums.html ├── Enums │ ├── SupplementaryViewKind.html │ └── ViewRegistrationMethod.html ├── Guides.html ├── Protocols.html ├── Protocols │ ├── CollectionCellViewModel.html │ ├── CollectionSupplementaryViewModel.html │ ├── DiffableViewModel.html │ ├── ReusableCellProtocol.html │ ├── ReusableCellViewModelProtocol.html │ ├── ReusableSupplementaryViewModelProtocol.html │ ├── TableCellViewModel.html │ ├── TableSectionHeaderFooterViewModel.html │ └── TableViewCellModelEditActions.html ├── Structs.html ├── Structs │ ├── AnyDiffableViewModel.html │ ├── CollectionSectionViewModel.html │ ├── CollectionViewModel.html │ ├── SupplementaryViewInfo.html │ ├── TableSectionViewModel.html │ ├── TableViewModel.html │ └── ViewRegistrationInfo.html ├── Typealiases.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── ReactiveLists.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── CollectionViewDriver.html │ │ │ │ ├── TableViewDriver.html │ │ │ │ └── TableViewDriver │ │ │ │ │ └── TableRefreshContext.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ ├── SupplementaryViewKind.html │ │ │ │ └── ViewRegistrationMethod.html │ │ │ ├── Guides.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── CollectionCellViewModel.html │ │ │ │ ├── CollectionSupplementaryViewModel.html │ │ │ │ ├── DiffableViewModel.html │ │ │ │ ├── ReusableCellProtocol.html │ │ │ │ ├── ReusableCellViewModelProtocol.html │ │ │ │ ├── ReusableSupplementaryViewModelProtocol.html │ │ │ │ ├── TableCellViewModel.html │ │ │ │ ├── TableSectionHeaderFooterViewModel.html │ │ │ │ └── TableViewCellModelEditActions.html │ │ │ ├── Structs.html │ │ │ ├── Structs │ │ │ │ ├── AnyDiffableViewModel.html │ │ │ │ ├── CollectionSectionViewModel.html │ │ │ │ ├── CollectionViewModel.html │ │ │ │ ├── SupplementaryViewInfo.html │ │ │ │ ├── TableSectionViewModel.html │ │ │ │ ├── TableViewModel.html │ │ │ │ └── ViewRegistrationInfo.html │ │ │ ├── Typealiases.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── getting-started.html │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ └── gh.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ └── jquery.min.js │ │ │ ├── search.json │ │ │ └── vision.html │ │ │ └── docSet.dsidx │ └── ReactiveLists.tgz ├── getting-started.html ├── img │ ├── carat.png │ ├── dash.png │ └── gh.png ├── index.html ├── js │ ├── jazzy.js │ └── jquery.min.js ├── search.json ├── undocumented.json └── vision.html └── scripts └── gen_docs.sh /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | 4 | coverage: 5 | precision: 2 6 | round: nearest 7 | range: "60...100" 8 | ignore: 9 | - Tests/* 10 | - Example/* 11 | - Pods/* 12 | 13 | status: 14 | project: 15 | default: 16 | target: auto 17 | branches: 18 | - master 19 | - dev 20 | 21 | comment: 22 | layout: "header, diff, changes, sunburst, uncovered, tree" 23 | behavior: default 24 | branches: 25 | - master 26 | - dev 27 | 28 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | *Thanks to [IGListKit for writing a great CONTRIBUTING.md](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md), upon which our guidelines are based.* 4 | 5 | ---- 6 | 7 | We're excited about new contributors and want to make it easy for you to help improve this project. If you run into problems, please open a GitHub issue. 8 | 9 | If you want to contribute a fix, or a minor change that doesn't change the API, go ahead and open a pull requests, see details on pull requests below. 10 | 11 | If you're interested in making a larger change, we recommend you to open an issue for discussion first. That way we can ensure that the change is within the scope of this project (see our [Vision document](https://github.com/plangrid/ReactiveLists/blob/master/Guides/VISION.md) for details on the scope of the project) before you put a lot of effort into it. 12 | 13 | ## Pull Requests 14 | 15 | 1. Fork the repo and create your branch from `master`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes. 19 | 5. Make sure your code lints. 20 | 6. Add an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. 21 | 22 | ## Issues 23 | 24 | We use GitHub issues to track public bugs. Please use the provided issue template and ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 25 | 26 | ## License 27 | 28 | By contributing to `ReactiveLists`, you agree that your contributions will be licensed under the [LICENSE](https://github.com/plangrid/ReactiveLists/blob/master/LICENSE) file in the root directory of this source tree. 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## New issue checklist 2 | 3 | - [ ] I have reviewed the [`README`](https://github.com/plangrid/ReactiveLists/blob/master/README.md) and [documentation](https://plangrid.github.io/ReactiveLists) 4 | - [ ] I have searched [existing issues](https://github.com/plangrid/ReactiveLists/issues) and this is not a duplicate 5 | 6 | ### General information 7 | 8 | - `ReactiveLists` version: 9 | - iOS version(s): 10 | - CocoaPods version: 11 | - Xcode version: 12 | - Devices/Simulators affected: 13 | - Reproducible in the demo project? (Yes/No): 14 | - Related issues: 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Changes in this pull request 2 | 3 | Issue fixed: # 4 | 5 | ### Checklist 6 | 7 | - [ ] All tests pass. Demo project builds and runs. 8 | - [ ] I added tests, an experiment, or detailed why my change isn't tested. 9 | - [ ] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. 10 | - [ ] I have reviewed the [contributing guide](https://github.com/plangrid/ReactiveLists/blob/master/.github/CONTRIBUTING.md) 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer 7 | WORKSPACE: ReactiveLists.xcworkspace 8 | LIBRARY_SCHEME: ReactiveLists 9 | EXAMPLE_APP_SCHEME: Example 10 | IOS_SDK: iphonesimulator14.5 11 | 12 | jobs: 13 | iOS: 14 | name: iOS Test 15 | runs-on: macOS-latest 16 | strategy: 17 | matrix: 18 | destination: ["OS=15.0,name=iPhone 13"] 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Select Xcode version 22 | run: sudo xcode-select -s '/Applications/Xcode_14.2.app/Contents/Developer' 23 | - name: Bundle Install 24 | run: bundle install 25 | - uses: actions/cache@v1 26 | with: 27 | path: Pods 28 | key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pods- 31 | - name: Install Pods 32 | run: bundle exec pod install 33 | - name: Run Tests 34 | run: | 35 | set -o pipefail 36 | xcodebuild clean 37 | xcodebuild test -workspace "$WORKSPACE" -scheme "$LIBRARY_SCHEME" -destination "${{ matrix.destination }}" -configuration Debug -enableCodeCoverage YES ONLY_ACTIVE_ARCH=NO | bundle exec xcpretty -c 38 | xcodebuild test -workspace "$WORKSPACE" -scheme "$EXAMPLE_APP_SCHEME" -destination "${{ matrix.destination }}" -configuration Debug -enableCodeCoverage YES ONLY_ACTIVE_ARCH=NO | bundle exec xcpretty -c 39 | - name: Run Code Coverage 40 | run: bash <(curl -s https://codecov.io/bash) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## macOS specific 10 | .DS_Store 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 | # Bundler 29 | .bundle 30 | vendor 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # 44 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | # Package.resolved 48 | .build/ 49 | 50 | # CocoaPods 51 | # 52 | # We recommend against adding the Pods directory to your .gitignore. However 53 | # you should judge for yourself, the pros and cons are mentioned at: 54 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 55 | # 56 | # Pods/ 57 | 58 | # Carthage 59 | # 60 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 61 | # Carthage/Checkouts 62 | 63 | Carthage/Build 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots 75 | fastlane/test_output 76 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # paths to ignore 2 | excluded: 3 | - Carthage 4 | - Pods 5 | - Guides 6 | - fastlane 7 | - vendor 8 | - docs 9 | - scripts 10 | 11 | disabled_rules: 12 | - identifier_name 13 | - nesting 14 | - type_name 15 | - trailing_comma 16 | - void_return 17 | 18 | opt_in_rules: 19 | # performance 20 | - empty_count 21 | - first_where 22 | - sorted_first_last 23 | - contains_over_first_not_nil 24 | - last_where 25 | 26 | 27 | # idiomatic 28 | - fatal_error_message 29 | - xctfail_message 30 | - legacy_random 31 | - no_extension_access_modifier 32 | - redundant_type_annotation 33 | - toggle_bool 34 | 35 | # style 36 | - number_separator 37 | - operator_usage_whitespace 38 | - sorted_imports 39 | - redundant_objc_attribute 40 | - closure_spacing 41 | - collection_alignment 42 | - modifier_order 43 | - vertical_whitespace_closing_braces 44 | - multiline_arguments 45 | - multiline_parameters 46 | - unneeded_parentheses_in_closure_argument 47 | - explicit_self 48 | 49 | # lint 50 | - overridden_super_call 51 | - yoda_condition 52 | - weak_computed_property 53 | - anyobject_protocol 54 | - array_init 55 | - empty_xctest_method 56 | - identical_operands 57 | - prohibited_super_call 58 | - unused_import 59 | - unused_private_declaration 60 | 61 | file_length: 650 62 | line_length: 200 63 | 64 | type_body_length: 400 65 | 66 | function_body_length: 150 67 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # review_type: Random 2 | * @plangrid/ios-developers 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at **benji@plangrid.com**. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | has_source_changes = !git.modified_files.grep(/Source/).empty? 2 | 3 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 4 | warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" 5 | 6 | # Warn when there is a big PR 7 | warn("Big PR") if git.lines_of_code > 500 8 | 9 | # Milestones are required to track what's included in each release 10 | if has_source_changes 11 | has_milestone = !github.pr_json['milestone'].nil? 12 | warn('There is no milestone for this PR. Please add one.', sticky: false) unless has_milestone 13 | end 14 | 15 | # Changelog entries are required for changes to library files 16 | no_changelog_entry = !git.modified_files.include?("CHANGELOG.md") 17 | if has_source_changes && no_changelog_entry && git.lines_of_code > 10 18 | warn("Source code changes (in APIs or behaviors) should have an entry in CHANGELOG.md.", sticky: false) 19 | end 20 | 21 | # Docs are regenerated when releasing 22 | has_doc_changes = !git.modified_files.grep(/docs\//).empty? 23 | has_doc_gen_title = github.pr_title.include? "#docgen" 24 | if has_doc_changes && !has_doc_gen_title 25 | fail("Docs are regenerated when creating new releases.") 26 | message("Docs are generated by using [Jazzy](https://github.com/realm/jazzy). If you want to contribute, please update the [markdown guides](https://github.com/plangrid/ReactiveLists/tree/master/Guides) or doc comments.") 27 | end 28 | 29 | swiftlint.verbose = true 30 | swiftlint.binary_path = './Pods/SwiftLint/swiftlint' 31 | swiftlint.config_file = './.swiftlint.yml' 32 | swiftlint.lint_files(inline_mode: true) 33 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import UIKit 18 | 19 | @UIApplicationMain 20 | final class AppDelegate: UIResponder, UIApplicationDelegate { 21 | 22 | var window: UIWindow? 23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "closeIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "closeIcon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "closeIcon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/close.imageset/closeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/close.imageset/closeIcon.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/close.imageset/closeIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/close.imageset/closeIcon@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/close.imageset/closeIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/close.imageset/closeIcon@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid-selected.imageset/1076-grid-4-selected@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid-selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "1076-grid-4-selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "1076-grid-4-selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "1076-grid-4-selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid.imageset/1076-grid-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid.imageset/1076-grid-4.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid.imageset/1076-grid-4@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid.imageset/1076-grid-4@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid.imageset/1076-grid-4@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/grid.imageset/1076-grid-4@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/grid.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "1076-grid-4.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "1076-grid-4@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "1076-grid-4@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list-selected.imageset/1099-list-1-selected@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list-selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "1099-list-1-selected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "1099-list-1-selected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "1099-list-1-selected@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/list.imageset/1099-list-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list.imageset/1099-list-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list.imageset/1099-list-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list.imageset/1099-list-1@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list.imageset/1099-list-1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Example/Assets.xcassets/list.imageset/1099-list-1@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/list.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "1099-list-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "1099-list-1@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "1099-list-1@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/CollectionToolCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Example/CollectionViewCells.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | import ReactiveLists 19 | import UIKit 20 | 21 | final class CollectionToolCell: UICollectionViewCell { 22 | @IBOutlet weak var toolNameLabel: UILabel! 23 | @IBOutlet weak var emojiLabel: UILabel! 24 | @IBOutlet weak var closeButton: UIButton! 25 | } 26 | 27 | final class CollectionToolCellModel: CollectionCellViewModel, DiffableViewModel { 28 | 29 | let accessibilityFormat: CellAccessibilityFormat = "CollectionToolCell" 30 | let registrationInfo = ViewRegistrationInfo(classType: CollectionToolCell.self, nibName: "CollectionToolCell") 31 | let itemSize: CGSize? = CGSize(width: 150, height: 100) 32 | let commitEditingStyle: CommitEditingStyleClosure? 33 | let editingStyle: UITableViewCell.EditingStyle = .delete 34 | 35 | let tool: Tool 36 | let onDeleteClosure: (Tool) -> Void 37 | 38 | init(tool: Tool, onDeleteClosure: @escaping (Tool) -> Void) { 39 | self.tool = tool 40 | self.onDeleteClosure = onDeleteClosure 41 | self.commitEditingStyle = { style in 42 | if style == .delete { 43 | onDeleteClosure(tool) 44 | } 45 | } 46 | } 47 | 48 | @objc 49 | func deleteTapped() { 50 | self.onDeleteClosure(self.tool) 51 | } 52 | 53 | func applyViewModelToCell(_ cell: UICollectionViewCell) { 54 | guard let collectionToolCell = cell as? CollectionToolCell else { return } 55 | collectionToolCell.toolNameLabel.text = self.tool.type.name 56 | collectionToolCell.emojiLabel.text = self.tool.type.emoji 57 | collectionToolCell.closeButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside) 58 | } 59 | 60 | var diffingKey: String { 61 | return self.tool.uuid.uuidString 62 | } 63 | } 64 | 65 | final class CollectionViewHeaderView: UICollectionReusableView { 66 | @IBOutlet weak var headerLabel: UILabel! 67 | } 68 | 69 | struct CollectionViewHeaderModel: CollectionSupplementaryViewModel { 70 | var title: String? 71 | var height: CGFloat? 72 | var viewInfo: SupplementaryViewInfo? 73 | 74 | init(title: String?, height: CGFloat?, viewInfo: SupplementaryViewInfo? = nil) { 75 | self.title = title 76 | self.height = height 77 | self.viewInfo = viewInfo 78 | } 79 | 80 | func applyViewModelToView(_ view: UICollectionReusableView) { 81 | guard let collectionHeaderView = view as? CollectionViewHeaderView else { return } 82 | collectionHeaderView.headerLabel.text = self.title 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Example/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import ReactiveLists 18 | import UIKit 19 | 20 | final class CollectionViewController: UICollectionViewController { 21 | 22 | var collectionViewDriver: CollectionViewDriver? 23 | var groups: [ToolGroup] = [] { 24 | didSet { 25 | let model = CollectionViewController.viewModel( 26 | forState: groups, 27 | onDeleteClosure: { deletedTool in 28 | // Iterate through the user groups and find the deleted user. 29 | for (index, group) in self.groups.enumerated() { 30 | self.groups[index].tools = group.tools.filter { $0.uuid != deletedTool.uuid } 31 | } 32 | } 33 | ) 34 | self.collectionViewDriver?.collectionViewModel = model 35 | } 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | self.collectionViewDriver = CollectionViewDriver(collectionView: collectionView!) 41 | 42 | self.groups = [ 43 | ToolGroup( 44 | name: "OLD TOOLS", 45 | tools: [Tool(type: .wrench), Tool(type: .hammer), Tool(type: .clamp), Tool(type: .nutBolt), Tool(type: .crane)] 46 | ), 47 | ToolGroup( 48 | name: "NEW TOOLS", 49 | tools: [Tool(type: .wrench), Tool(type: .hammer), Tool(type: .clamp), Tool(type: .nutBolt), Tool(type: .crane)] 50 | ), 51 | ] 52 | } 53 | 54 | @IBAction func swapSections(_ sender: Any) { 55 | let group0 = self.groups[0] 56 | self.groups[0] = self.groups[1] 57 | self.groups[1] = group0 58 | } 59 | 60 | @IBAction func addTool(_ sender: Any) { 61 | self.groups[0].tools.append(Tool.randomTool()) 62 | } 63 | } 64 | 65 | // MARK: View Model Provider 66 | 67 | extension CollectionViewController { 68 | /// Pure function mapping new state to a new `CollectionViewModel`. This is invoked each time the state updates 69 | /// in order for ReactiveLists to update the UI. 70 | static func viewModel(forState groups: [ToolGroup], onDeleteClosure: @escaping (Tool) -> Void) -> CollectionViewModel { 71 | let sections: [CollectionSectionViewModel] = groups.map { group in 72 | let cellViewModels = group.tools.map { CollectionToolCellModel(tool: $0, onDeleteClosure: onDeleteClosure) } 73 | let headerViewModel = CollectionViewHeaderModel( 74 | title: group.name, 75 | height: 44, 76 | viewInfo: SupplementaryViewInfo( 77 | registrationInfo: ViewRegistrationInfo(classType: CollectionViewHeaderView.self, nibName: "CollectionViewHeaderView"), 78 | kind: .header, 79 | accessibilityFormat: "CollectionViewHeaderView" 80 | ) 81 | ) 82 | return CollectionSectionViewModel( 83 | diffingKey: group.name, 84 | cellViewModels: cellViewModels, 85 | headerViewModel: headerViewModel 86 | ) 87 | } 88 | return CollectionViewModel(sectionModels: sections) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Example/CollectionViewHeaderView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | 19 | struct ToolGroup { 20 | let name: String 21 | var tools: [Tool] 22 | } 23 | 24 | struct Tool { 25 | let type: ToolType 26 | let uuid = UUID() 27 | 28 | static func randomTool() -> Tool { 29 | let randomNumber = UInt32.random(in: 0.. Void) { 30 | self.tool = tool 31 | self.commitEditingStyle = { style in 32 | if style == .delete { 33 | onDeleteClosure(tool) 34 | } 35 | } 36 | } 37 | 38 | func applyViewModelToCell(_ cell: UITableViewCell) { 39 | cell.textLabel?.text = "\(self.tool.type.emoji) \(self.tool.type.name)" 40 | } 41 | 42 | var diffingKey: String { 43 | return self.tool.uuid.uuidString 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Example/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import ReactiveLists 18 | import UIKit 19 | 20 | final class TableViewController: UITableViewController { 21 | 22 | var tableViewDriver: TableViewDriver? 23 | var groups: [ToolGroup] = [] { 24 | didSet { 25 | self.tableViewDriver?.tableViewModel = TableViewController.viewModel( 26 | forState: groups, 27 | onDeleteClosure: { deletedTool in 28 | // Iterate through the user groups and find the deleted user. 29 | for (index, group) in self.groups.enumerated() { 30 | self.groups[index].tools = group.tools.filter { $0.uuid != deletedTool.uuid } 31 | } 32 | } 33 | ) 34 | } 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | self.tableViewDriver = TableViewDriver(tableView: self.tableView) 40 | 41 | self.groups = [ 42 | ToolGroup( 43 | name: "OLD TOOLS", 44 | tools: [Tool(type: .wrench), Tool(type: .hammer), Tool(type: .clamp), Tool(type: .nutBolt), Tool(type: .crane)] 45 | ), 46 | ToolGroup( 47 | name: "NEW TOOLS", 48 | tools: [Tool(type: .wrench), Tool(type: .hammer), Tool(type: .clamp), Tool(type: .nutBolt), Tool(type: .crane)] 49 | ), 50 | ] 51 | } 52 | 53 | @IBAction func swapSections(_ sender: Any) { 54 | let group0 = self.groups[0] 55 | self.groups[0] = self.groups[1] 56 | self.groups[1] = group0 57 | } 58 | 59 | @IBAction func addTool(_ sender: Any) { 60 | self.groups[0].tools.append(Tool.randomTool()) 61 | } 62 | } 63 | 64 | // MARK: View Model Provider 65 | 66 | extension TableViewController { 67 | 68 | /// Pure function mapping new state to a new `TableViewModel`. This is invoked each time the state updates 69 | /// in order for ReactiveLists to update the UI. 70 | static func viewModel(forState groups: [ToolGroup], onDeleteClosure: @escaping (Tool) -> Void) -> TableViewModel { 71 | let sections: [TableSectionViewModel] = groups.map { group in 72 | let cellViewModels = group.tools.map { ToolTableCellModel(tool: $0, onDeleteClosure: onDeleteClosure) } 73 | return TableSectionViewModel( 74 | diffingKey: group.name, 75 | headerTitle: group.name, 76 | headerHeight: 44, 77 | cellViewModels: cellViewModels 78 | ) 79 | } 80 | return TableViewModel(sectionModels: sections) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Example/ToolTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import UIKit 18 | 19 | final class ToolTableViewCell: UITableViewCell { 20 | } 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', '~> 1.9' 4 | gem 'danger', '~> 8.0' 5 | gem 'danger-swiftlint', '~> 0.12.1' 6 | gem 'xcpretty' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (5.2.4.4) 6 | concurrent-ruby (~> 1.0, >= 1.0.2) 7 | i18n (>= 0.7, < 2) 8 | minitest (~> 5.1) 9 | tzinfo (~> 1.1) 10 | addressable (2.8.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.5) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | claide (1.0.3) 17 | claide-plugins (0.9.2) 18 | cork 19 | nap 20 | open4 (~> 1.3) 21 | cocoapods (1.10.0) 22 | addressable (~> 2.6) 23 | claide (>= 1.0.2, < 2.0) 24 | cocoapods-core (= 1.10.0) 25 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 26 | cocoapods-downloader (>= 1.4.0, < 2.0) 27 | cocoapods-plugins (>= 1.0.0, < 2.0) 28 | cocoapods-search (>= 1.0.0, < 2.0) 29 | cocoapods-trunk (>= 1.4.0, < 2.0) 30 | cocoapods-try (>= 1.1.0, < 2.0) 31 | colored2 (~> 3.1) 32 | escape (~> 0.0.4) 33 | fourflusher (>= 2.3.0, < 3.0) 34 | gh_inspector (~> 1.0) 35 | molinillo (~> 0.6.6) 36 | nap (~> 1.0) 37 | ruby-macho (~> 1.4) 38 | xcodeproj (>= 1.19.0, < 2.0) 39 | cocoapods-core (1.10.0) 40 | activesupport (> 5.0, < 6) 41 | addressable (~> 2.6) 42 | algoliasearch (~> 1.0) 43 | concurrent-ruby (~> 1.1) 44 | fuzzy_match (~> 2.0.4) 45 | nap (~> 1.0) 46 | netrc (~> 0.11) 47 | public_suffix 48 | typhoeus (~> 1.0) 49 | cocoapods-deintegrate (1.0.4) 50 | cocoapods-downloader (1.4.0) 51 | cocoapods-plugins (1.0.0) 52 | nap 53 | cocoapods-search (1.0.0) 54 | cocoapods-trunk (1.5.0) 55 | nap (>= 0.8, < 2.0) 56 | netrc (~> 0.11) 57 | cocoapods-try (1.2.0) 58 | colored2 (3.1.2) 59 | concurrent-ruby (1.1.7) 60 | cork (0.3.0) 61 | colored2 (~> 3.1) 62 | danger (8.0.5) 63 | claide (~> 1.0) 64 | claide-plugins (>= 0.9.2) 65 | colored2 (~> 3.1) 66 | cork (~> 0.1) 67 | faraday (>= 0.9.0, < 2.0) 68 | faraday-http-cache (~> 2.0) 69 | git (~> 1.7) 70 | kramdown (~> 2.3) 71 | kramdown-parser-gfm (~> 1.0) 72 | no_proxy_fix 73 | octokit (~> 4.7) 74 | terminal-table (~> 1) 75 | danger-swiftlint (0.12.1) 76 | danger 77 | rake (> 10) 78 | thor (~> 0.19) 79 | escape (0.0.4) 80 | ethon (0.12.0) 81 | ffi (>= 1.3.0) 82 | faraday (1.0.1) 83 | multipart-post (>= 1.2, < 3) 84 | faraday-http-cache (2.2.0) 85 | faraday (>= 0.8) 86 | ffi (1.13.1) 87 | fourflusher (2.3.1) 88 | fuzzy_match (2.0.4) 89 | gh_inspector (1.1.3) 90 | git (1.7.0) 91 | rchardet (~> 1.8) 92 | httpclient (2.8.3) 93 | i18n (1.8.5) 94 | concurrent-ruby (~> 1.0) 95 | json (2.3.1) 96 | kramdown (2.3.1) 97 | rexml 98 | kramdown-parser-gfm (1.1.0) 99 | kramdown (~> 2.0) 100 | minitest (5.14.2) 101 | molinillo (0.6.6) 102 | multipart-post (2.1.1) 103 | nanaimo (0.3.0) 104 | nap (1.1.0) 105 | netrc (0.11.0) 106 | no_proxy_fix (0.1.2) 107 | octokit (4.18.0) 108 | faraday (>= 0.9) 109 | sawyer (~> 0.8.0, >= 0.5.3) 110 | open4 (1.3.4) 111 | public_suffix (4.0.6) 112 | rake (13.0.1) 113 | rchardet (1.8.0) 114 | rexml (3.2.5) 115 | rouge (2.0.7) 116 | ruby-macho (1.4.0) 117 | sawyer (0.8.2) 118 | addressable (>= 2.3.5) 119 | faraday (> 0.8, < 2.0) 120 | terminal-table (1.8.0) 121 | unicode-display_width (~> 1.1, >= 1.1.1) 122 | thor (0.20.3) 123 | thread_safe (0.3.6) 124 | typhoeus (1.4.0) 125 | ethon (>= 0.9.0) 126 | tzinfo (1.2.8) 127 | thread_safe (~> 0.1) 128 | unicode-display_width (1.7.0) 129 | xcodeproj (1.19.0) 130 | CFPropertyList (>= 2.3.3, < 4.0) 131 | atomos (~> 0.1.3) 132 | claide (>= 1.0.2, < 2.0) 133 | colored2 (~> 3.1) 134 | nanaimo (~> 0.3.0) 135 | xcpretty (0.3.0) 136 | rouge (~> 2.0.7) 137 | 138 | PLATFORMS 139 | ruby 140 | 141 | DEPENDENCIES 142 | cocoapods (~> 1.9) 143 | danger (~> 8.0) 144 | danger-swiftlint (~> 0.12.1) 145 | xcpretty 146 | 147 | BUNDLED WITH 148 | 2.1.4 149 | -------------------------------------------------------------------------------- /Guides/VISION.md: -------------------------------------------------------------------------------- 1 | # Vision 2 | 3 | The goal of `ReactiveLists` is to provide a [React](https://reactjs.org/)-like API for `UITableView` and `UICollectionView`. It is currently in use across large parts of the [PlanGrid](https://www.plangrid.com/) iOS app. 4 | 5 | The APIs provided by `ReactiveLists` aim to be declarative, therefore they are significantly different from the `UIKit` APIs that favor providing data via the delegate pattern. From the [React](https://github.com/facebook/react) repo: 6 | 7 | > React is a JavaScript library for building user interfaces. 8 | > 9 | > [...] 10 | > 11 | > - **Declarative:** React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes. Declarative views make your code more predictable, simpler to understand, and easier to debug. 12 | 13 | `ReactiveLists` provides automated diffing. This means that whenever your application data changes, you only need to map that new data to a new view model to update the UI. 14 | 15 | Anything other than providing declarative APIs on top of existing `UITableView` and `UICollectionView` APIs is currently considered out of scope. 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present PlanGrid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DifferenceKit", 6 | "repositoryURL": "https://github.com/ra1028/DifferenceKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "14c66681e12a38b81045f44c6c29724a0d4b0e72", 10 | "version": "1.1.5" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ReactiveLists", 6 | platforms: [ 7 | .iOS(.v11), 8 | ], 9 | products: [ 10 | .library(name: "ReactiveLists", targets: ["ReactiveLists"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/ra1028/DifferenceKit.git", .upToNextMinor(from: "1.2.0")), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "ReactiveLists", 18 | dependencies: ["DifferenceKit"], 19 | path: "Sources" 20 | ), 21 | .testTarget( 22 | name: "ReactiveListsTests", 23 | dependencies: ["ReactiveLists"], 24 | path: "Tests" 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://cdn.cocoapods.org/' 2 | 3 | platform :ios, '11.0' 4 | use_frameworks! 5 | inhibit_all_warnings! 6 | 7 | target 'ReactiveLists' do 8 | project 'ReactiveLists.xcodeproj' 9 | 10 | pod 'DifferenceKit', '1.1.3' 11 | pod 'SwiftLint' 12 | 13 | target 'ReactiveListsExample' do 14 | project 'ReactiveLists.xcodeproj' 15 | end 16 | end 17 | 18 | target 'ReactiveListsTests' do 19 | project 'ReactiveLists.xcodeproj' 20 | end 21 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DifferenceKit (1.1.3): 3 | - DifferenceKit/Core (= 1.1.3) 4 | - DifferenceKit/UIKitExtension (= 1.1.3) 5 | - DifferenceKit/Core (1.1.3) 6 | - DifferenceKit/UIKitExtension (1.1.3): 7 | - DifferenceKit/Core 8 | - SwiftLint (0.39.2) 9 | 10 | DEPENDENCIES: 11 | - DifferenceKit (= 1.1.3) 12 | - SwiftLint 13 | 14 | SPEC REPOS: 15 | trunk: 16 | - DifferenceKit 17 | - SwiftLint 18 | 19 | SPEC CHECKSUMS: 20 | DifferenceKit: 5018791b6c1fc839921a3c171a0a539ace6ea60c 21 | SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 22 | 23 | PODFILE CHECKSUM: ad1a9d103cb5e7d9f38226030275a10ba608dbd2 24 | 25 | COCOAPODS: 1.10.0 26 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/AnyDifferentiable.swift: -------------------------------------------------------------------------------- 1 | /// A type-erased differentiable value. 2 | /// 3 | /// The `AnyDifferentiable` type hides the specific underlying types. 4 | /// Associated type `DifferenceIdentifier` is erased by `AnyHashable`. 5 | /// The comparisons of whether has updated is forwards to an underlying differentiable value. 6 | /// 7 | /// You can store mixed-type elements in collection that require `Differentiable` conformance by 8 | /// wrapping mixed-type elements in `AnyDifferentiable`: 9 | /// 10 | /// extension String: Differentiable {} 11 | /// extension Int: Differentiable {} 12 | /// 13 | /// let source = [ 14 | /// AnyDifferentiable("ABC"), 15 | /// AnyDifferentiable(100) 16 | /// ] 17 | /// let target = [ 18 | /// AnyDifferentiable("ABC"), 19 | /// AnyDifferentiable(100), 20 | /// AnyDifferentiable(200) 21 | /// ] 22 | /// 23 | /// let changeset = StagedChangeset(source: source, target: target) 24 | /// print(changeset.isEmpty) // prints "false" 25 | public struct AnyDifferentiable: Differentiable { 26 | /// The value wrapped by this instance. 27 | @inlinable 28 | public var base: Any { 29 | return box.base 30 | } 31 | 32 | /// A type-erased identifier value for difference calculation. 33 | @inlinable 34 | public var differenceIdentifier: AnyHashable { 35 | return box.differenceIdentifier 36 | } 37 | 38 | @usableFromInline 39 | internal let box: AnyDifferentiableBox 40 | 41 | /// Creates a type-erased differentiable value that wraps the given instance. 42 | /// 43 | /// - Parameters: 44 | /// - base: A differentiable value to wrap. 45 | @inlinable 46 | public init(_ base: D) { 47 | if let anyDifferentiable = base as? AnyDifferentiable { 48 | self = anyDifferentiable 49 | } 50 | else { 51 | box = DifferentiableBox(base) 52 | } 53 | } 54 | 55 | /// Indicate whether the content of `base` is equals to the content of the given source value. 56 | /// 57 | /// - Parameters: 58 | /// - source: A source value to be compared. 59 | /// 60 | /// - Returns: A Boolean value indicating whether the content of `base` is equals 61 | /// to the content of `base` of the given source value. 62 | @inlinable 63 | public func isContentEqual(to source: AnyDifferentiable) -> Bool { 64 | return box.isContentEqual(to: source.box) 65 | } 66 | } 67 | 68 | extension AnyDifferentiable: CustomDebugStringConvertible { 69 | public var debugDescription: String { 70 | return "AnyDifferentiable(\(String(reflecting: base)))" 71 | } 72 | } 73 | 74 | @usableFromInline 75 | internal protocol AnyDifferentiableBox { 76 | var base: Any { get } 77 | var differenceIdentifier: AnyHashable { get } 78 | 79 | func isContentEqual(to source: AnyDifferentiableBox) -> Bool 80 | } 81 | 82 | @usableFromInline 83 | internal struct DifferentiableBox: AnyDifferentiableBox { 84 | @usableFromInline 85 | internal let baseComponent: Base 86 | 87 | @inlinable 88 | internal var base: Any { 89 | return baseComponent 90 | } 91 | 92 | @inlinable 93 | internal var differenceIdentifier: AnyHashable { 94 | return baseComponent.differenceIdentifier 95 | } 96 | 97 | @inlinable 98 | internal init(_ base: Base) { 99 | baseComponent = base 100 | } 101 | 102 | @inlinable 103 | internal func isContentEqual(to source: AnyDifferentiableBox) -> Bool { 104 | guard let sourceBase = source.base as? Base else { 105 | return false 106 | } 107 | return baseComponent.isContentEqual(to: sourceBase) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ArraySection.swift: -------------------------------------------------------------------------------- 1 | /// A differentiable section with model and array of elements. 2 | /// 3 | /// Arrays are can not be identify each one and comparing whether has updated from other one. 4 | /// ArraySection is a generic wrapper to hold a model to allow it. 5 | public struct ArraySection: DifferentiableSection { 6 | /// The model of section for differentiated with other section. 7 | public var model: Model 8 | /// The array of element in the section. 9 | public var elements: [Element] 10 | 11 | /// An identifier value that of model for difference calculation. 12 | @inlinable 13 | public var differenceIdentifier: Model.DifferenceIdentifier { 14 | return model.differenceIdentifier 15 | } 16 | 17 | /// Creates a section with the model and the elements. 18 | /// 19 | /// - Parameters: 20 | /// - model: A differentiable model of section. 21 | /// - elements: The collection of element in the section. 22 | @inlinable 23 | public init(model: Model, elements: C) where C.Element == Element { 24 | self.model = model 25 | self.elements = Array(elements) 26 | } 27 | 28 | /// Creates a new section reproducing the given source section with replacing the elements. 29 | /// 30 | /// - Parameters: 31 | /// - source: A source section to reproduce. 32 | /// - elements: The collection of elements for the new section. 33 | @inlinable 34 | public init(source: ArraySection, elements: C) where C.Element == Element { 35 | self.init(model: source.model, elements: elements) 36 | } 37 | 38 | /// Indicate whether the content of `self` is equals to the content of 39 | /// the given source section. 40 | /// 41 | /// - Note: It's compared by the model of `self` and the specified section. 42 | /// 43 | /// - Parameters: 44 | /// - source: A source section to compare. 45 | /// 46 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 47 | /// to the content of the given source section. 48 | @inlinable 49 | public func isContentEqual(to source: ArraySection) -> Bool { 50 | return model.isContentEqual(to: source.model) 51 | } 52 | } 53 | 54 | extension ArraySection: Equatable where Model: Equatable, Element: Equatable { 55 | public static func == (lhs: ArraySection, rhs: ArraySection) -> Bool { 56 | return lhs.model == rhs.model && lhs.elements == rhs.elements 57 | } 58 | } 59 | 60 | extension ArraySection: CustomDebugStringConvertible { 61 | public var debugDescription: String { 62 | return """ 63 | ArraySection( 64 | model: \(model), 65 | elements: \(elements) 66 | ) 67 | """ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ContentEquatable.swift: -------------------------------------------------------------------------------- 1 | /// Represents a value that can compare whether the content are equal. 2 | public protocol ContentEquatable { 3 | /// Indicate whether the content of `self` is equals to the content of 4 | /// the given source value. 5 | /// 6 | /// - Parameters: 7 | /// - source: A source value to be compared. 8 | /// 9 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 10 | /// to the content of the given source value. 11 | func isContentEqual(to source: Self) -> Bool 12 | } 13 | 14 | public extension ContentEquatable where Self: Equatable { 15 | /// Indicate whether the content of `self` is equals to the content of the given source value. 16 | /// Compared using `==` operator of `Equatable'. 17 | /// 18 | /// - Parameters: 19 | /// - source: A source value to be compared. 20 | /// 21 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 22 | /// to the content of the given source value. 23 | @inlinable 24 | func isContentEqual(to source: Self) -> Bool { 25 | return self == source 26 | } 27 | } 28 | 29 | extension Optional: ContentEquatable where Wrapped: ContentEquatable { 30 | /// Indicate whether the content of `self` is equals to the content of the given source value. 31 | /// Returns `true` if both values compared are nil. 32 | /// The result of comparison between nil and non-nil values is `false`. 33 | /// 34 | /// - Parameters: 35 | /// - source: An optional source value to be compared. 36 | /// 37 | /// - Returns: A Boolean value indicating whether the content of `self` is equals 38 | /// to the content of the given source value. 39 | @inlinable 40 | public func isContentEqual(to source: Wrapped?) -> Bool { 41 | switch (self, source) { 42 | case let (lhs?, rhs?): 43 | return lhs.isContentEqual(to: rhs) 44 | 45 | case (.none, .none): 46 | return true 47 | 48 | case (.none, .some), (.some, .none): 49 | return false 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/Differentiable.swift: -------------------------------------------------------------------------------- 1 | /// Represents the value that identified for differentiate. 2 | public protocol Differentiable: ContentEquatable { 3 | /// A type representing the identifier. 4 | associatedtype DifferenceIdentifier: Hashable 5 | 6 | /// An identifier value for difference calculation. 7 | var differenceIdentifier: DifferenceIdentifier { get } 8 | } 9 | 10 | public extension Differentiable where Self: Hashable { 11 | /// The `self` value as an identifier for difference calculation. 12 | @inlinable 13 | var differenceIdentifier: Self { 14 | return self 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/DifferentiableSection.swift: -------------------------------------------------------------------------------- 1 | /// Represents the section of collection that can be identified and compared to whether has updated. 2 | public protocol DifferentiableSection: Differentiable { 3 | /// A type representing the elements in section. 4 | associatedtype Collection: Swift.Collection where Collection.Element: Differentiable 5 | 6 | /// The collection of element in the section. 7 | var elements: Collection { get } 8 | 9 | /// Creates a new section reproducing the given source section with replacing the elements. 10 | /// 11 | /// - Parameters: 12 | /// - source: A source section to reproduce. 13 | /// - elements: The collection of elements for the new section. 14 | init(source: Self, elements: C) where C.Element == Collection.Element 15 | } 16 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/ElementPath.swift: -------------------------------------------------------------------------------- 1 | /// Represents the path to a specific element in a tree of nested collections. 2 | /// 3 | /// - Note: `Foundation.IndexPath` is disadvantageous in performance. 4 | public struct ElementPath: Hashable { 5 | /// The element index (or offset) of this path. 6 | public var element: Int 7 | /// The section index (or offset) of this path. 8 | public var section: Int 9 | 10 | /// Creates a new `ElementPath`. 11 | /// 12 | /// - Parameters: 13 | /// - element: The element index (or offset). 14 | /// - section: The section index (or offset). 15 | @inlinable 16 | public init(element: Int, section: Int) { 17 | self.element = element 18 | self.section = section 19 | } 20 | } 21 | 22 | extension ElementPath: CustomDebugStringConvertible { 23 | public var debugDescription: String { 24 | return "[element: \(element), section: \(section)]" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pods/DifferenceKit/Sources/StagedChangeset.swift: -------------------------------------------------------------------------------- 1 | /// An ordered collection of `Changeset` as staged set of changes in the sectioned collection. 2 | /// 3 | /// The order is representing the stages of changesets. 4 | /// 5 | /// We know that there are combination of changes that crash when applied simultaneously 6 | /// in batch-updates of UI such as UITableView or UICollectionView. 7 | /// The `StagedChangeset` created from the two collection is split at the minimal stages 8 | /// that can be perform batch-updates with no crashes. 9 | /// 10 | /// Example for calculating differences between the two linear collections. 11 | /// 12 | /// extension String: Differentiable {} 13 | /// 14 | /// let source = ["A", "B", "C"] 15 | /// let target = ["B", "C", "D"] 16 | /// 17 | /// let changeset = StagedChangeset(source: source, target: target) 18 | /// print(changeset.isEmpty) // prints "false" 19 | /// 20 | /// Example for calculating differences between the two sectioned collections. 21 | /// 22 | /// let source = [ 23 | /// Section(model: "A", elements: ["😉"]), 24 | /// ] 25 | /// let target = [ 26 | /// Section(model: "A", elements: ["😉, 😺"]), 27 | /// Section(model: "B", elements: ["😪"]) 28 | /// ] 29 | /// 30 | /// let changeset = StagedChangeset(source: sectionedSource, target: sectionedTarget) 31 | /// print(changeset.isEmpty) // prints "false" 32 | public struct StagedChangeset { 33 | @usableFromInline 34 | internal var changesets: ContiguousArray> 35 | 36 | /// Creates a new `StagedChangeset`. 37 | /// 38 | /// - Parameters: 39 | /// - changesets: The collection of `Changeset`. 40 | @inlinable 41 | public init(_ changesets: C) where C.Element == Changeset { 42 | self.changesets = ContiguousArray(changesets) 43 | } 44 | } 45 | 46 | extension StagedChangeset: RandomAccessCollection, RangeReplaceableCollection, MutableCollection { 47 | public typealias Element = Changeset 48 | 49 | @inlinable 50 | public init() { 51 | self.init([]) 52 | } 53 | 54 | @inlinable 55 | public var startIndex: Int { 56 | return changesets.startIndex 57 | } 58 | 59 | @inlinable 60 | public var endIndex: Int { 61 | return changesets.endIndex 62 | } 63 | 64 | @inlinable 65 | public func index(after i: Int) -> Int { 66 | return changesets.index(after: i) 67 | } 68 | 69 | @inlinable 70 | public subscript(position: Int) -> Changeset { 71 | get { return changesets[position] } 72 | set { changesets[position] = newValue } 73 | } 74 | 75 | @inlinable 76 | public mutating func replaceSubrange(_ subrange: R, with newElements: C) where C.Element == Changeset, R.Bound == Int { 77 | changesets.replaceSubrange(subrange, with: newElements) 78 | } 79 | } 80 | 81 | extension StagedChangeset: Equatable where Collection: Equatable { 82 | @inlinable 83 | public static func == (lhs: StagedChangeset, rhs: StagedChangeset) -> Bool { 84 | return lhs.changesets == rhs.changesets 85 | } 86 | } 87 | 88 | extension StagedChangeset: ExpressibleByArrayLiteral { 89 | @inlinable 90 | public init(arrayLiteral elements: Changeset...) { 91 | self.init(elements) 92 | } 93 | } 94 | 95 | extension StagedChangeset: CustomDebugStringConvertible { 96 | public var debugDescription: String { 97 | guard !isEmpty else { return "[]" } 98 | 99 | return "[\n\(map { " \($0.debugDescription.split(separator: "\n").joined(separator: "\n "))" }.joined(separator: ",\n"))\n]" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DifferenceKit (1.1.3): 3 | - DifferenceKit/Core (= 1.1.3) 4 | - DifferenceKit/UIKitExtension (= 1.1.3) 5 | - DifferenceKit/Core (1.1.3) 6 | - DifferenceKit/UIKitExtension (1.1.3): 7 | - DifferenceKit/Core 8 | - SwiftLint (0.39.2) 9 | 10 | DEPENDENCIES: 11 | - DifferenceKit (= 1.1.3) 12 | - SwiftLint 13 | 14 | SPEC REPOS: 15 | trunk: 16 | - DifferenceKit 17 | - SwiftLint 18 | 19 | SPEC CHECKSUMS: 20 | DifferenceKit: 5018791b6c1fc839921a3c171a0a539ace6ea60c 21 | SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 22 | 23 | PODFILE CHECKSUM: ad1a9d103cb5e7d9f38226030275a10ba608dbd2 24 | 25 | COCOAPODS: 1.10.0 26 | -------------------------------------------------------------------------------- /Pods/SwiftLint/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Realm Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/SwiftLint/swiftlint: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Pods/SwiftLint/swiftlint -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_DifferenceKit : NSObject 3 | @end 4 | @implementation PodsDummy_DifferenceKit 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double DifferenceKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char DifferenceKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 5 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/DifferenceKit 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module DifferenceKit { 2 | umbrella header "DifferenceKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 5 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings 6 | PODS_BUILD_DIR = ${BUILD_DIR} 7 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 8 | PODS_ROOT = ${SRCROOT} 9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/DifferenceKit 10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 14 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/DifferenceKit.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "UIKit" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/DifferenceKit 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/DifferenceKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ReactiveLists_ReactiveListsExample : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ReactiveLists_ReactiveListsExample 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ReactiveLists_ReactiveListsExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ReactiveLists_ReactiveListsExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 8 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ReactiveLists_ReactiveListsExample { 2 | umbrella header "Pods-ReactiveLists-ReactiveListsExample-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists-ReactiveListsExample/Pods-ReactiveLists-ReactiveListsExample.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 8 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ReactiveLists : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ReactiveLists 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ReactiveListsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ReactiveListsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' '@executable_path/../../Frameworks' 6 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 7 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 14 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 15 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ReactiveLists { 2 | umbrella header "Pods-ReactiveLists-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' '@executable_path/../../Frameworks' 6 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 7 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 14 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 15 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_ReactiveListsTests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_ReactiveListsTests 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ReactiveListsTestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ReactiveListsTestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 8 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_ReactiveListsTests { 2 | umbrella header "Pods-ReactiveListsTests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" 6 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 7 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit" 8 | OTHER_LDFLAGS = $(inherited) -framework "DifferenceKit" -framework "UIKit" 9 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 10 | PODS_BUILD_DIR = ${BUILD_DIR} 11 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 12 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 13 | PODS_ROOT = ${SRCROOT}/Pods 14 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 15 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 16 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint 8 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint 8 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftLint/SwiftLint.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_ROOT = ${SRCROOT} 6 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint 7 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 8 | SKIP_INSTALL = YES 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | *React-like API for `UITableView` & `UICollectionView`* 4 | 5 | [![Build Status](https://travis-ci.org/plangrid/ReactiveLists.svg?branch=master)](https://travis-ci.org/plangrid/ReactiveLists) [![Version Status](https://img.shields.io/cocoapods/v/ReactiveLists.svg)][podLink] [![license MIT](https://img.shields.io/cocoapods/l/ReactiveLists.svg)][mitLink] [![codecov](https://codecov.io/gh/plangrid/ReactiveLists/branch/master/graph/badge.svg)](https://codecov.io/gh/plangrid/ReactiveLists) [![Platform](https://img.shields.io/cocoapods/p/ReactiveLists.svg)][docsLink] 6 | 7 | `ReactiveLists` provides a React-like API for `UITableView` and `UICollectionView` that makes it easy to write stateless code that generates user interfaces. 8 | 9 | In our experience this can make UI code significantly easier to read and maintain. Instead of spreading the definition of your content over various data source methods, you can define the content concisely. The table or collection content and layout are immediately obvious by scanning over the source code. 10 | 11 | You can read more about the origins of this library in our [announcement blog post](https://medium.com/plangrid-technology/open-sourcing-reactivelists-for-ios-3abdf41b770a). 12 | 13 | ## Features 14 | 15 | - React-like declarative API for `UITableView` and `UICollectionView` 16 | - Automatic UI updates, whenever your models change 17 | 18 | ## Example 19 | 20 | ```swift 21 | // Given a view controller with a table view 22 | 23 | // 1. create cell models 24 | let cell0 = ExampleTableCellModel(...) 25 | let cell1 = ExampleTableCellModel(...) 26 | let cell2 = ExampleTableCellModel(...) 27 | 28 | // 2. create section models 29 | let section0 = ExampleTableSectionViewModel(cellViewModels: [cell0, cell1, cell2]) 30 | 31 | // 3. create table model 32 | let tableModel = TableViewModel(sectionModels: [section0]) 33 | 34 | // 4. create driver 35 | self.driver = TableViewDriver(tableView: self.tableView, tableViewModel: tableModel) 36 | 37 | // 5. update driver with new table model as it changes 38 | let updatedTableModel = self.doSomethingToChangeModels() 39 | self.driver.tableViewModel = updatedTableModel 40 | 41 | // self.tableView will update automatically 42 | ``` 43 | 44 | ## Project Status 45 | 46 | An early version of the `UITableView` support has been shipping in the [PlanGrid app](https://itunes.apple.com/us/app/plangrid-construction-software/id498795789?mt=8) since late 2015 and is now used accross wide parts of the app. The support for `UICollectionView` is less mature as we only use `UICollectionView` in very few places. 47 | 48 | | Feature | Status | 49 | | -------------------------- | :-------------: | 50 | | `UITableView` support | ✅ | 51 | | `UICollectionView` support | ⚠️ Experimental | 52 | 53 | ## Vision 54 | 55 | For long-term goals and direction, please see [`VISION.md`](https://github.com/plangrid/ReactiveLists/blob/master/Guides/VISION.md). 56 | 57 | ## Getting Started 58 | 59 | Read our [Getting Started Guide](https://github.com/plangrid/ReactiveLists/blob/master/Guides/Getting%20Started.md) to learn how to use `ReactiveLists`. 60 | 61 | ## Documentation 62 | 63 | Read our [documentation here][docsLink]. Generated with [jazzy](https://github.com/realm/jazzy). Hosted by [GitHub Pages](https://pages.github.com). 64 | 65 | #### Generating docs 66 | 67 | ```bash 68 | $ ./scripts/gen_docs.sh 69 | ``` 70 | 71 | ## Requirements 72 | 73 | * Xcode 10.2+ 74 | * Swift 4.2 or 5.0 75 | * iOS 11+ 76 | 77 | ## Installation 78 | 79 | ### [CocoaPods](https://cocoapods.org/) (recommended) 80 | 81 | ```ruby 82 | use_frameworks! 83 | 84 | # For latest release in CocoaPods 85 | pod 'ReactiveLists' 86 | 87 | # Latest on master branch 88 | pod 'ReactiveLists', :git => 'https://github.com/plangrid/ReactiveLists.git', :branch => 'master' 89 | ``` 90 | 91 | ### [Swift Package Manager](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) 92 | 93 | Select Xcode menu File > Swift Packages > Add Package Dependency... and enter repository URL with GUI. 94 | 95 | Repository: https://github.com/plangrid/ReactiveLists 96 | 97 | 98 | ## Contribute 99 | 100 | Please read and follow our [Contributing Guide](https://github.com/plangrid/ReactiveLists/blob/master/.github/CONTRIBUTING.md) and our [Code of Conduct](https://github.com/plangrid/ReactiveLists/blob/master/CODE_OF_CONDUCT.md). 101 | 102 | ## License 103 | 104 | `ReactiveLists` is released under an [MIT License][mitLink]. See `LICENSE` for details. 105 | 106 | > **Copyright © 2018-present PlanGrid, Inc.** 107 | 108 | [docsLink]:https://plangrid.github.io/ReactiveLists 109 | [podLink]:https://cocoapods.org/pods/ReactiveLists 110 | [mitLink]:https://opensource.org/licenses/MIT 111 | -------------------------------------------------------------------------------- /ReactiveLists.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveLists" 3 | s.version = "0.8.5" 4 | 5 | s.summary = "React-like API for UITableView and UICollectionView" 6 | s.homepage = "https://github.com/plangrid/ReactiveLists" 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | 9 | s.author = "PlanGrid" 10 | s.documentation_url = "https://plangrid.github.io/ReactiveLists" 11 | s.social_media_url = "https://medium.com/plangrid-technology" 12 | 13 | s.source = { :git => "https://github.com/plangrid/ReactiveLists.git", :tag => s.version.to_s } 14 | s.source_files = 'Sources/**/*.swift' 15 | s.ios.deployment_target = '11.0' 16 | s.swift_versions = ['4.2', '5.0'] 17 | s.requires_arc = true 18 | 19 | s.dependency 'DifferenceKit', '~> 1.2.0' 20 | end 21 | -------------------------------------------------------------------------------- /ReactiveLists.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReactiveLists.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReactiveLists.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /ReactiveLists.xcodeproj/xcshareddata/xcschemes/ReactiveLists.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ReactiveLists.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ReactiveLists.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // PlanGrid 8 | // https://www.plangrid.com 9 | // https://medium.com/plangrid-technology 10 | // 11 | // Documentation 12 | // https://plangrid.github.io/ReactiveLists 13 | // 14 | // GitHub 15 | // https://github.com/plangrid/ReactiveLists 16 | // 17 | // License 18 | // Copyright © 2018-present PlanGrid, Inc. 19 | // Released under an MIT license: https://opensource.org/licenses/MIT 20 | // 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReactiveLists.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReactiveLists/0.8.1.beta.1/ReactiveLists.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveLists" 3 | s.version = "0.8.1.beta.1" 4 | 5 | s.summary = "React-like API for UITableView and UICollectionView" 6 | s.homepage = "https://github.com/plangrid/ReactiveLists" 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | 9 | s.author = "PlanGrid" 10 | s.documentation_url = "https://plangrid.github.io/ReactiveLists" 11 | s.social_media_url = "https://medium.com/plangrid-technology" 12 | 13 | s.source = { :git => "https://github.com/plangrid/ReactiveLists.git", :tag => s.version.to_s } 14 | s.source_files = 'Sources/**/*.swift' 15 | s.ios.deployment_target = '11.0' 16 | s.swift_versions = ['4.2', '5.0'] 17 | s.requires_arc = true 18 | 19 | s.dependency 'DifferenceKit', '~> 1.1.0' 20 | end 21 | -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/Resources/logo.png -------------------------------------------------------------------------------- /Sources/AccessibilityFormats.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | 19 | // Note: The accessibility types below are not documented as they are not intended to be part 20 | // of the `ReactiveLists` project in the long term. See https://github.com/plangrid/ReactiveLists/issues/77 21 | 22 | /// :nodoc: 23 | public struct CellAccessibilityFormat: ExpressibleByStringLiteral, Equatable { 24 | private let _format: String 25 | 26 | /// :nodoc: 27 | public init(_ format: String) { 28 | self._format = format 29 | } 30 | 31 | /// :nodoc: 32 | public init(stringLiteral value: StringLiteralType) { 33 | self._format = value 34 | } 35 | 36 | /// :nodoc: 37 | public init(extendedGraphemeClusterLiteral value: String) { 38 | self._format = value 39 | } 40 | 41 | /// :nodoc: 42 | public init(unicodeScalarLiteral value: String) { 43 | self._format = value 44 | } 45 | 46 | /// :nodoc: 47 | public func accessibilityIdentifierForIndexPath(_ indexPath: IndexPath) -> String { 48 | return self._format.replacingOccurrences(of: "%{section}", with: String(indexPath.section)) 49 | .replacingOccurrences(of: "%{item}", with: String(indexPath.item)) 50 | .replacingOccurrences(of: "%{row}", with: String(indexPath.row)) 51 | } 52 | } 53 | 54 | /// :nodoc: 55 | public struct SupplementaryAccessibilityFormat: ExpressibleByStringLiteral, Equatable { 56 | private let _format: String 57 | 58 | /// :nodoc: 59 | public init(_ format: String) { 60 | self._format = format 61 | } 62 | 63 | /// :nodoc: 64 | public init(stringLiteral value: StringLiteralType) { 65 | self._format = value 66 | } 67 | 68 | /// :nodoc: 69 | public init(extendedGraphemeClusterLiteral value: String) { 70 | self._format = value 71 | } 72 | 73 | /// :nodoc: 74 | public init(unicodeScalarLiteral value: String) { 75 | self._format = value 76 | } 77 | 78 | /// :nodoc: 79 | public func accessibilityIdentifierForSection(_ section: Int) -> String { 80 | return self._format.replacingOccurrences(of: "%{section}", with: String(section)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/CellContainerViewProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | /** 21 | This protocol unifies `UICollectionView` and `UITableView` by providing a common dequeue method for cells. 22 | It describes a view that is the "parent" view for a cell. 23 | For `UICollectionViewCell`, this would be `UICollectionView`. 24 | For `UITableViewCell`, this would be `UITableView`. 25 | */ 26 | protocol CellContainerViewProtocol { 27 | 28 | /// The type of cell for this parent view. 29 | associatedtype CellType: UIView 30 | 31 | /// The type of supplementary view for this parent view. 32 | associatedtype SupplementaryType: UIView 33 | 34 | func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType 35 | 36 | func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType? 37 | 38 | func registerCellClass(_ cellClass: AnyClass?, identifier: String) 39 | func registerCellNib(_ cellNib: UINib?, identifier: String) 40 | 41 | func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String) 42 | func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String) 43 | } 44 | 45 | extension CellContainerViewProtocol { 46 | func registerCellViewModels(_ cellViewModels: S) where S.Element == ReusableCellViewModelProtocol { 47 | cellViewModels.forEach { 48 | self.registerCellViewModel($0) 49 | } 50 | } 51 | 52 | func registerCellViewModel(_ model: ReusableCellViewModelProtocol) { 53 | let info = model.registrationInfo 54 | let identifier = info.reuseIdentifier 55 | let method = info.registrationMethod 56 | 57 | switch method { 58 | case let .fromClass(classType): 59 | self.registerCellClass(classType, identifier: identifier) 60 | case .fromNib: 61 | self.registerCellNib(method.nib, identifier: identifier) 62 | } 63 | } 64 | 65 | func registerSupplementaryViewModel(_ model: ReusableSupplementaryViewModelProtocol) { 66 | guard let info = model.viewInfo else { return } 67 | let identifier = info.registrationInfo.reuseIdentifier 68 | let method = info.registrationInfo.registrationMethod 69 | let kind = info.kind 70 | 71 | switch method { 72 | case let .fromClass(classType): 73 | self.registerSupplementaryClass(classType, kind: kind, identifier: identifier) 74 | case .fromNib: 75 | self.registerSupplementaryNib(method.nib, kind: kind, identifier: identifier) 76 | } 77 | } 78 | } 79 | 80 | extension UICollectionView: CellContainerViewProtocol { 81 | typealias CellType = UICollectionViewCell 82 | typealias SupplementaryType = UICollectionReusableView 83 | 84 | func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType { 85 | return self.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) 86 | } 87 | 88 | func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType? { 89 | return self.dequeueReusableSupplementaryView(ofKind: kind.collectionElementKind, withReuseIdentifier: identifier, for: indexPath) 90 | } 91 | 92 | func registerCellClass(_ cellClass: AnyClass?, identifier: String) { 93 | self.register(cellClass, forCellWithReuseIdentifier: identifier) 94 | } 95 | 96 | func registerCellNib(_ cellNib: UINib?, identifier: String) { 97 | self.register(cellNib, forCellWithReuseIdentifier: identifier) 98 | } 99 | 100 | func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String) { 101 | self.register(supplementaryClass, forSupplementaryViewOfKind: kind.collectionElementKind, withReuseIdentifier: identifier) 102 | } 103 | 104 | func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String) { 105 | self.register(supplementaryNib, forSupplementaryViewOfKind: kind.collectionElementKind, withReuseIdentifier: identifier) 106 | } 107 | } 108 | 109 | extension UITableView: CellContainerViewProtocol { 110 | typealias CellType = UITableViewCell 111 | typealias SupplementaryType = UITableViewHeaderFooterView 112 | 113 | func dequeueReusableCellFor(identifier: String, indexPath: IndexPath) -> CellType { 114 | return self.dequeueReusableCell(withIdentifier: identifier, for: indexPath) 115 | } 116 | 117 | func dequeueReusableSupplementaryViewFor(kind: SupplementaryViewKind, identifier: String, indexPath: IndexPath) -> SupplementaryType? { 118 | return self.dequeueReusableHeaderFooterView(withIdentifier: identifier) 119 | } 120 | 121 | func registerCellClass(_ cellClass: AnyClass?, identifier: String) { 122 | self.register(cellClass, forCellReuseIdentifier: identifier) 123 | } 124 | 125 | func registerCellNib(_ cellNib: UINib?, identifier: String) { 126 | self.register(cellNib, forCellReuseIdentifier: identifier) 127 | } 128 | 129 | func registerSupplementaryClass(_ supplementaryClass: AnyClass?, kind: SupplementaryViewKind, identifier: String) { 130 | self.register(supplementaryClass, forHeaderFooterViewReuseIdentifier: identifier) 131 | } 132 | 133 | func registerSupplementaryNib(_ supplementaryNib: UINib?, kind: SupplementaryViewKind, identifier: String) { 134 | self.register(supplementaryNib, forHeaderFooterViewReuseIdentifier: identifier) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/CollectionCellViewModelDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import DifferenceKit 18 | import Foundation 19 | 20 | /// Protocol for providing `CollectionViewModel`s to a `CollectionSectionViewModel` 21 | /// 22 | /// It is itself the `Collection` of `CollectionCellViewModel` and 23 | /// also provides hooks for pre-fetching data 24 | /// 25 | /// - Note: `[TableCellViewModel]` has a default implementation 26 | public protocol CollectionCellViewModelDataSourceProtocol: RandomAccessCollection where Element == CollectionCellViewModel, Index == Int { 27 | 28 | /// Called by the equivalent `UITableViewDataSourcePrefetching` method 29 | /// - Parameter indices: The indices in the section, for which to prefetch the models 30 | func prefetchRowsAt(indices: S) where S.Element == Int 31 | 32 | /// Called by the equivalent `UITableViewDataSourcePrefetching` method 33 | /// - Parameter indices: The indices in the section, for which to cancel prefetchign the models 34 | func cancelPrefetchingRowsAt(indices: S) where S.Element == Int 35 | 36 | /// The `ViewRegistrationInfo` for the cells represented by this datasource 37 | var cellRegistrationInfo: [ViewRegistrationInfo] { get } 38 | } 39 | 40 | /// The concrete data source that wraps a provided `TableCellViewModelDataSourceProtocol` implementation 41 | public struct CollectionCellViewModelDataSource: RandomAccessCollection { 42 | 43 | // MARK: `CollectionCellViewModelDataSourceProtocol` wrapper blocks for type erasure 44 | 45 | /// :nodoc: 46 | private let _subscriptBlock: (Int) -> CollectionCellViewModel 47 | 48 | /// :nodoc: 49 | private let _prefetchBlock: (AnySequence) -> Void 50 | 51 | /// :nodoc: 52 | private let _prefetchCancelBlock: (AnySequence) -> Void 53 | 54 | /// Initializes the `CollectionCellViewModelDataSource` with the provided `CollectionCellViewModelDataSourceProtocol` implementation 55 | public init(_ dataSource: DataSource) { 56 | self.init(dataSource, cellRegistrationInfo: dataSource.cellRegistrationInfo) 57 | } 58 | 59 | /// Used internally by the public init and during diffing 60 | /// when cached ``ViewRegistrationInfo` is available 61 | init(_ dataSource: DataSource, cellRegistrationInfo: [ViewRegistrationInfo]) { 62 | self._prefetchBlock = dataSource.prefetchRowsAt 63 | self._prefetchCancelBlock = dataSource.cancelPrefetchingRowsAt 64 | self._subscriptBlock = { dataSource[$0] } 65 | self.startIndex = dataSource.startIndex 66 | self.endIndex = dataSource.endIndex 67 | self.cellRegistrationInfo = cellRegistrationInfo 68 | } 69 | 70 | // MARK: - Protocol Implementation 71 | 72 | /// :nodoc: 73 | public let cellRegistrationInfo: [ViewRegistrationInfo] 74 | 75 | /// :nodoc: 76 | func prefetchRowsAt(indices: S) where S.Element == Int { 77 | self._prefetchBlock(AnySequence(indices)) 78 | } 79 | 80 | /// :nodoc: 81 | func cancelPrefetchingRowsAt(indices: S) where S.Element == Int { self._prefetchCancelBlock(AnySequence(indices)) } 82 | 83 | /// :nodoc: 84 | public typealias Element = CollectionCellViewModel 85 | 86 | /// :nodoc: 87 | public typealias Index = Int 88 | 89 | /// :nodoc: 90 | public subscript(position: Int) -> CollectionCellViewModel { 91 | self._subscriptBlock(position) 92 | } 93 | 94 | /// :nodoc: 95 | public let startIndex: Int 96 | 97 | /// :nodoc: 98 | public let endIndex: Int 99 | } 100 | 101 | extension Array: CollectionCellViewModelDataSourceProtocol where Element == CollectionCellViewModel { 102 | 103 | /// :nodoc: 104 | public func prefetchRowsAt(indices: S) where S.Element == Int {} 105 | 106 | /// :nodoc: 107 | public func cancelPrefetchingRowsAt(indices: S) where S.Element == Int {} 108 | 109 | /// :nodoc: 110 | public var cellRegistrationInfo: [ViewRegistrationInfo] { 111 | self.map { 112 | $0.registrationInfo 113 | } 114 | } 115 | } 116 | 117 | //extension Array where Element == IndexPath {j 118 | // 119 | // /// Helper that transforms `[IndexPath]` to sequence of pairs of sections and row sequences 120 | // func indicesBySection() -> AnySequence<(Int, AnySequence)> { 121 | // let indexPathsBySection = [Int: [IndexPath]](grouping: self) { $0.section } 122 | // return AnySequence(indexPathsBySection.lazy.map { section, indexPaths in 123 | // return (section, AnySequence(indexPaths.lazy.map { $0.row })) 124 | // }) 125 | // } 126 | //} 127 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/ReactiveLists.h: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | #import 18 | 19 | FOUNDATION_EXPORT double ReactiveListsVersionNumber; 20 | FOUNDATION_EXPORT const unsigned char ReactiveListsVersionString[]; 21 | -------------------------------------------------------------------------------- /Sources/ReusableCellViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | 19 | /// Describes a cell view model. 20 | /// Unifies table cell and collection cell view models. 21 | public protocol ReusableCellViewModelProtocol { 22 | 23 | /// The registration info for the cell. 24 | var registrationInfo: ViewRegistrationInfo { get } 25 | } 26 | 27 | /// Describes a supplementary view model. 28 | /// Unifies table supplementary and collection supplementary view models. 29 | public protocol ReusableSupplementaryViewModelProtocol { 30 | 31 | /// The registration info for the supplementary view. 32 | var viewInfo: SupplementaryViewInfo? { get } 33 | } 34 | 35 | struct AnyReusableCellViewModel: ReusableCellViewModelProtocol { 36 | 37 | /// The registration info for the cell. 38 | let registrationInfo: ViewRegistrationInfo 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SupplementaryViewInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | /// Metadata thats required for setting up a supplementary view. 21 | public struct SupplementaryViewInfo: Equatable { 22 | 23 | /// The registration info for the supplementary view. 24 | public let registrationInfo: ViewRegistrationInfo 25 | 26 | /// The kind of supplementary view (e.g. `header` or `footer`) 27 | public let kind: SupplementaryViewKind 28 | 29 | /// `TableViewDataSource` and `CollectionViewDataSource` will automatically apply 30 | /// an `accessibilityIdentifier` to the supplementary view based on this format. 31 | public let accessibilityFormat: SupplementaryAccessibilityFormat 32 | 33 | /// Initializes the metadata for a supplementary view. 34 | /// 35 | /// - Parameters: 36 | /// - registrationInfo: The registration info for the view. 37 | /// - kind: The kind of supplementary view (e.g. `header` or `footer`) 38 | /// - accessibilityFormat: A format string that generates an accessibility identifier for 39 | /// the view that will be mapped to this view model. 40 | public init( 41 | registrationInfo: ViewRegistrationInfo, 42 | kind: SupplementaryViewKind, 43 | accessibilityFormat: SupplementaryAccessibilityFormat 44 | ) { 45 | self.registrationInfo = registrationInfo 46 | self.kind = kind 47 | self.accessibilityFormat = accessibilityFormat 48 | } 49 | } 50 | 51 | /// Defines the kind of a supplementary view. 52 | /// 53 | /// - header: indicates that the view is a header 54 | /// - footer: indicates that the view is a footer 55 | public enum SupplementaryViewKind: Equatable { 56 | 57 | /// A header view. 58 | case header 59 | 60 | /// A footer view. 61 | case footer 62 | 63 | init?(collectionElementKindString: String) { 64 | switch collectionElementKindString { 65 | case UICollectionView.elementKindSectionHeader: 66 | self = .header 67 | case UICollectionView.elementKindSectionFooter: 68 | self = .footer 69 | default: 70 | return nil 71 | } 72 | } 73 | 74 | var collectionElementKind: String { 75 | switch self { 76 | case .header: return UICollectionView.elementKindSectionHeader 77 | case .footer: return UICollectionView.elementKindSectionFooter 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/TableCellViewModelDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import DifferenceKit 18 | import Foundation 19 | 20 | /// Protocol for providing `TableCellViewModel`s to a `TableSectionViewModel` 21 | /// 22 | /// It is itself the `Collection` of `TableCellViewModel` and 23 | /// also provides hooks for pre-fetching data 24 | /// 25 | /// - Note: `[TableCellViewModel]` has a default implementation 26 | public protocol TableCellViewModelDataSourceProtocol: RandomAccessCollection where Element == TableCellViewModel, Index == Int { 27 | 28 | /// Called by the equivalent `UITableViewDataSourcePrefetching` method 29 | /// - Parameter indices: The indices in the section, for which to prefetch the models 30 | func prefetchRowsAt(indices: S) where S.Element == Int 31 | 32 | /// Called by the equivalent `UITableViewDataSourcePrefetching` method 33 | /// - Parameter indices: The indices in the section, for which to cancel prefetchign the models 34 | func cancelPrefetchingRowsAt(indices: S) where S.Element == Int 35 | 36 | /// The `ViewRegistrationInfo` for the cells represented by this datasource 37 | var cellRegistrationInfo: [ViewRegistrationInfo] { get } 38 | } 39 | 40 | /// The concrete data source that wraps a provided `TableCellViewModelDataSourceProtocol` implementation 41 | public struct TableCellViewModelDataSource: RandomAccessCollection { 42 | 43 | // MARK: `TableCellViewModelDataSourceProtocol` wrapper blocks for type erasure 44 | 45 | /// :nodoc: 46 | private let _subscriptBlock: (Int) -> TableCellViewModel 47 | 48 | /// :nodoc: 49 | private let _prefetchBlock: (AnySequence) -> Void 50 | 51 | /// :nodoc: 52 | private let _prefetchCancelBlock: (AnySequence) -> Void 53 | 54 | /// Initializes the `TableCellViewModelDataSource` with the provided `TableCellViewModelDataSourceProtocol` implementation 55 | public init(_ dataSource: DataSource) { 56 | self.init(dataSource, cellRegistrationInfo: dataSource.cellRegistrationInfo) 57 | } 58 | 59 | /// Used internally by the public init and during diffing 60 | /// when cached ``ViewRegistrationInfo` is available 61 | init(_ dataSource: DataSource, cellRegistrationInfo: [ViewRegistrationInfo]) { 62 | self._prefetchBlock = dataSource.prefetchRowsAt 63 | self._prefetchCancelBlock = dataSource.cancelPrefetchingRowsAt 64 | self._subscriptBlock = { dataSource[$0] } 65 | self.startIndex = dataSource.startIndex 66 | self.endIndex = dataSource.endIndex 67 | self.cellRegistrationInfo = cellRegistrationInfo 68 | } 69 | 70 | // MARK: - Protocol Implementation 71 | 72 | /// :nodoc: 73 | public let cellRegistrationInfo: [ViewRegistrationInfo] 74 | 75 | /// :nodoc: 76 | func prefetchRowsAt(indices: S) where S.Element == Int { 77 | self._prefetchBlock(AnySequence(indices)) 78 | } 79 | 80 | /// :nodoc: 81 | func cancelPrefetchingRowsAt(indices: S) where S.Element == Int { self._prefetchCancelBlock(AnySequence(indices)) } 82 | 83 | /// :nodoc: 84 | public typealias Element = TableCellViewModel 85 | 86 | /// :nodoc: 87 | public typealias Index = Int 88 | 89 | /// :nodoc: 90 | public subscript(position: Int) -> TableCellViewModel { 91 | self._subscriptBlock(position) 92 | } 93 | 94 | /// :nodoc: 95 | public let startIndex: Int 96 | 97 | /// :nodoc: 98 | public let endIndex: Int 99 | } 100 | 101 | extension Array: TableCellViewModelDataSourceProtocol where Element == TableCellViewModel { 102 | 103 | /// :nodoc: 104 | public func prefetchRowsAt(indices: S) where S.Element == Int {} 105 | 106 | /// :nodoc: 107 | public func cancelPrefetchingRowsAt(indices: S) where S.Element == Int {} 108 | 109 | /// :nodoc: 110 | public var cellRegistrationInfo: [ViewRegistrationInfo] { 111 | self.map { $0.registrationInfo } 112 | } 113 | } 114 | 115 | extension Array where Element == IndexPath { 116 | 117 | /// Helper that transforms `[IndexPath]` to sequence of pairs of sections and row sequences 118 | func indicesBySection() -> AnySequence<(Int, AnySequence)> { 119 | let indexPathsBySection = [Int: [IndexPath]](grouping: self) { $0.section } 120 | return AnySequence(indexPathsBySection.lazy.map { section, indexPaths in 121 | return (section, AnySequence(indexPaths.lazy.map { $0.row })) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Typealiases.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | /// :nodoc: 21 | public typealias CommitEditingStyleClosure = (UITableViewCell.EditingStyle) -> Void 22 | /// :nodoc: 23 | public typealias DidSelectClosure = () -> Void 24 | /// :nodoc: 25 | public typealias DidDeleteClosure = () -> Void 26 | /// :nodoc: 27 | public typealias DidDeselectClosure = () -> Void 28 | /// :nodoc: 29 | public typealias WillBeginEditingClosure = () -> Void 30 | /// :nodoc: 31 | public typealias DidEndEditingClosure = () -> Void 32 | /// :nodoc: 33 | public typealias AccessoryButtonTappedClosure = () -> Void 34 | /// :nodoc: 35 | public typealias DidScrollClosure = (UIScrollView) -> Void 36 | -------------------------------------------------------------------------------- /Sources/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import UIKit 18 | 19 | extension UICollectionView { 20 | 21 | func registerViews(for model: CollectionViewModel) { 22 | model.sectionModels.forEach { 23 | if let dataSource = $0.cellViewModelDataSource { 24 | self.registerCellViewModels(dataSource.cellRegistrationInfo.lazy.map { 25 | AnyReusableCellViewModel(registrationInfo: $0) 26 | }) 27 | } else { 28 | self.registerCellViewModels($0.cellViewModels) 29 | } 30 | 31 | if let header = $0.headerViewModel { 32 | self.registerSupplementaryViewModel(header) 33 | } 34 | 35 | if let footer = $0.footerViewModel { 36 | self.registerSupplementaryViewModel(footer) 37 | } 38 | } 39 | } 40 | 41 | func configuredCell(for model: CollectionCellViewModel, at indexPath: IndexPath) -> UICollectionViewCell { 42 | let cell = self.dequeueReusableCellFor(identifier: model.registrationInfo.reuseIdentifier, indexPath: indexPath) 43 | model.applyViewModelToCell(cell) 44 | cell.accessibilityIdentifier = model.accessibilityFormat.accessibilityIdentifierForIndexPath(indexPath) 45 | return cell 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/UITableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import UIKit 18 | 19 | extension UITableView { 20 | 21 | func configuredCell(for model: TableCellViewModel, at indexPath: IndexPath) -> UITableViewCell { 22 | let cell = self.dequeueReusableCellFor(identifier: model.registrationInfo.reuseIdentifier, indexPath: indexPath) 23 | model.applyViewModelToCell(cell) 24 | cell.accessibilityIdentifier = model.accessibilityFormat.accessibilityIdentifierForIndexPath(indexPath) 25 | return cell 26 | } 27 | 28 | func registerViews(for model: TableViewModel) { 29 | model.sectionModels.forEach { 30 | self.registerCellViewModels( 31 | $0.cellViewModelDataSource.cellRegistrationInfo.lazy.map { 32 | AnyReusableCellViewModel(registrationInfo: $0) 33 | } 34 | ) 35 | 36 | if let header = $0.headerViewModel { 37 | self.registerSupplementaryViewModel(header) 38 | } 39 | 40 | if let footer = $0.footerViewModel { 41 | self.registerSupplementaryViewModel(footer) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ViewRegistrationInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | import UIKit 19 | 20 | /// Describes a reusable cell and specifies how to register it. 21 | public protocol ReusableCellProtocol { 22 | 23 | /// The registration info for the cell. 24 | var registrationInfo: ViewRegistrationInfo { get } 25 | } 26 | 27 | /// Describes the registration information for a cell or supplementary view. 28 | public struct ViewRegistrationInfo: Equatable { 29 | 30 | /// The reuse identifier for the view. 31 | public let reuseIdentifier: String 32 | 33 | /// The registration method for the view. 34 | public let registrationMethod: ViewRegistrationMethod 35 | 36 | /// Initializes a new `ViewRegistrationInfo` for the provided `classType`. 37 | /// 38 | /// - Note: 39 | /// The class name is used for `reuseIdentifier`. 40 | /// The `registrationMethod` is set to `.fromClass`. 41 | /// 42 | /// - Parameter classType: The cell or supplementary view class. 43 | public init(classType: AnyClass) { 44 | self.reuseIdentifier = "\(classType)" 45 | self.registrationMethod = .fromClass(classType) 46 | } 47 | 48 | /// Initializes a new `ViewRegistrationInfo` for the provided `classType`, `nibName`, and `bundle`. 49 | /// 50 | /// - Note: 51 | /// The class name is used for `reuseIdentifier`. 52 | /// The `registrationMethod` is set to `.fromNib` using the provided `nibName` and `bundle`. 53 | /// 54 | /// - Parameters: 55 | /// - classType: The cell or supplementary view class. 56 | /// - nibName: The name of the nib for the view. 57 | /// - bundle: The bundle in which the nib is located. Pass `nil` to use the main bundle. 58 | public init(classType: AnyClass, nibName: String, bundle: Bundle? = nil) { 59 | self.reuseIdentifier = "\(classType)" 60 | self.registrationMethod = .fromNib(name: nibName, bundle: bundle) 61 | } 62 | } 63 | 64 | /// The method for registering cells and supplementary views 65 | public enum ViewRegistrationMethod { 66 | 67 | /// Class-based views 68 | case fromClass(AnyClass) 69 | 70 | /// Nib-based views 71 | case fromNib(name: String, bundle: Bundle?) 72 | 73 | var nib: UINib? { 74 | switch self { 75 | case let .fromNib(name, bundle): 76 | return UINib(nibName: name, bundle: bundle) 77 | case .fromClass: 78 | return nil 79 | } 80 | } 81 | } 82 | 83 | extension ViewRegistrationMethod: Equatable { 84 | public static func == (lhs: ViewRegistrationMethod, rhs: ViewRegistrationMethod) -> Bool { 85 | switch (lhs, rhs) { 86 | case let (.fromClass(lhsClass), .fromClass(rhsClass)): 87 | return lhsClass == rhsClass 88 | case let (.fromNib(lhsName, lhsBundle), .fromNib(rhsName, rhsBundle)): 89 | return lhsName == rhsName && lhsBundle == rhsBundle 90 | default: 91 | return false 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/CollectionView/CollectionViewDriverDiffingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import XCTest 19 | 20 | final class CollectionViewDriverDiffingTests: XCTestCase { 21 | 22 | var collectionViewDataSource: CollectionViewDriver! 23 | var mockCollectionView: TestCollectionView! 24 | 25 | override func setUp() { 26 | super.setUp() 27 | self.mockCollectionView = TestCollectionView( 28 | frame: .zero, 29 | collectionViewLayout: UICollectionViewFlowLayout() 30 | ) 31 | self.collectionViewDataSource = CollectionViewDriver( 32 | collectionView: self.mockCollectionView, 33 | automaticDiffingEnabled: true 34 | ) 35 | } 36 | 37 | /// Tests that changes to individual rows result in the correct calls to update the 38 | /// collection view. 39 | /// 40 | /// - Note: We're only testing one type of row update since this is sufficient to test the 41 | /// communication between the diffing lib and the collection view. The diffing lib itself has 42 | /// extensive tests for the various diffing scenarios. 43 | func testChangingRows() { 44 | let initialModel = CollectionViewModel( 45 | sectionModels: [ 46 | CollectionSectionViewModel( 47 | diffingKey: "1", 48 | cellViewModels: [] 49 | ) 50 | ] 51 | ) 52 | 53 | self.collectionViewDataSource.collectionViewModel = initialModel 54 | 55 | let updatedModel = CollectionViewModel( 56 | sectionModels: [ 57 | CollectionSectionViewModel( 58 | diffingKey: "1", 59 | cellViewModels: [CollectionUserCellModel(user: User(name: "Mona"))] 60 | ) 61 | ] 62 | ) 63 | 64 | self.collectionViewDataSource.collectionViewModel = updatedModel 65 | 66 | XCTAssertEqual(self.mockCollectionView.callsToInsertItems.count, 0) 67 | XCTAssertEqual(self.mockCollectionView.callsToReloadData, 3) 68 | } 69 | 70 | /// Tests that changes to individual sections result in the correct calls to update the 71 | /// collection view. 72 | /// 73 | /// - Note: We're only testing one type of section update since this is sufficient to test the 74 | /// communication between the diffing lib and the collection view. The diffing lib itself has 75 | /// extensive tests for the various diffing scenarios. 76 | func testChangingSections() { 77 | let section = CollectionSectionViewModel( 78 | diffingKey: "2", 79 | cellViewModels: generateCollectionCellViewModels() 80 | ) 81 | 82 | let initialModel = CollectionViewModel( 83 | sectionModels: [ 84 | CollectionSectionViewModel( 85 | diffingKey: "1", 86 | cellViewModels: generateCollectionCellViewModels() 87 | ), 88 | section, 89 | ] 90 | ) 91 | 92 | self.collectionViewDataSource.collectionViewModel = initialModel 93 | 94 | // Check the number of sections to get around a testing bug where, despite a correct diff, 95 | // the collection view throws an exception claiming that the number of sections before the 96 | // update was 1 97 | XCTAssertEqual(self.collectionViewDataSource.collectionView.numberOfSections, 2) 98 | 99 | let updatedModel = CollectionViewModel(sectionModels: [section]) 100 | 101 | self.collectionViewDataSource.collectionViewModel = updatedModel 102 | 103 | XCTAssertEqual(self.mockCollectionView.callsToDeleteSections.count, 1) 104 | XCTAssertEqual(self.mockCollectionView.callsToDeleteSections[0], IndexSet(integer: 0)) 105 | } 106 | } 107 | 108 | struct CollectionUserCellModel: CollectionCellViewModel, DiffableViewModel { 109 | 110 | var accessibilityFormat: CellAccessibilityFormat = "CollectionUserCell" 111 | var registrationInfo = ViewRegistrationInfo(classType: TestCollectionViewCell.self) 112 | let editingStyle: UITableViewCell.EditingStyle = .delete 113 | 114 | let user: User 115 | 116 | init(user: User) { 117 | self.user = user 118 | } 119 | 120 | func applyViewModelToCell(_ cell: UICollectionViewCell) { } 121 | 122 | var diffingKey: String { 123 | return self.user.uuid.uuidString 124 | } 125 | } 126 | 127 | struct User { 128 | let name: String 129 | let uuid = UUID() 130 | } 131 | 132 | struct UserGroup { 133 | let name: String 134 | var users: [User] 135 | } 136 | -------------------------------------------------------------------------------- /Tests/CollectionView/CollectionViewLazyDiffingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import XCTest 19 | 20 | final class CollectionViewDiffingTests: XCTestCase { 21 | 22 | var collectionViewDataSource: CollectionViewDriver! 23 | var mockCollectionView: TestCollectionView! 24 | 25 | override func setUp() { 26 | super.setUp() 27 | self.mockCollectionView = TestCollectionView( 28 | frame: .zero, 29 | collectionViewLayout: UICollectionViewFlowLayout() 30 | ) 31 | // self.mockCollectionView.indexPathsForVisibleRowsOverride = [ 32 | // IndexPath(row: 0, section: 0), 33 | // ] 34 | self.collectionViewDataSource = CollectionViewDriver( 35 | collectionView: self.mockCollectionView, 36 | shouldDeselectUponSelection: false, 37 | useDataSource: true, 38 | automaticDiffingEnabled: true 39 | ) 40 | } 41 | 42 | /// Tests that changes to individual rows result in the correct calls to update the 43 | /// table view. 44 | /// 45 | /// - Note: We're only testing one type of row update since this is sufficient to test the 46 | /// communication between the diffing lib and the table view. The diffing lib itself has 47 | /// extensive tests for the various diffing scenarios. 48 | func testChangingRows() { 49 | let userCells = [LazyTestUserCell(user: "Name")] 50 | let dataSource = CollectionCellViewModelDataSource(userCells) 51 | let section = CollectionSectionViewModel( 52 | diffingKey: "default_section", 53 | cellViewModels: [], 54 | cellViewModelDataSource: dataSource 55 | ) 56 | let initialModel = CollectionViewModel( 57 | sectionModels: [section] 58 | ) 59 | 60 | self.collectionViewDataSource.collectionViewModel = initialModel 61 | 62 | let testUser1 = [LazyTestUserCell(user: "TestUser1")] 63 | // [LazyUserCell(user: "TestUser1"), LazyUserCell(user: "TestUser2")] 64 | // let testUser2 = LazyUserCell(user: "TestUser2") 65 | let dataSource1 = CollectionCellViewModelDataSource(testUser1) 66 | let section2 = CollectionSectionViewModel( 67 | diffingKey: "default_section", 68 | cellViewModels: [], 69 | cellViewModelDataSource: dataSource1 70 | ) 71 | let updatedModel = CollectionViewModel( 72 | sectionModels: [section2] 73 | ) 74 | 75 | self.collectionViewDataSource.collectionViewModel = updatedModel 76 | 77 | XCTAssertEqual(self.mockCollectionView.callsToInsertItems.count, 1) 78 | XCTAssertEqual(self.mockCollectionView.callsToReloadData, 2) 79 | } 80 | } 81 | 82 | final class LazyTestUserCell: CollectionCellViewModel, DiffableViewModel { 83 | var accessibilityFormat: CellAccessibilityFormat = "" 84 | let registrationInfo = ViewRegistrationInfo(classType: UICollectionViewCell.self) 85 | 86 | let user: String 87 | private(set) var diffingKeyAccessed: Bool = false 88 | 89 | init(user: String) { 90 | self.user = user 91 | } 92 | 93 | func applyViewModelToCell(_ cell: UICollectionViewCell) {} 94 | 95 | func willDisplay(cell: UICollectionViewCell) {} 96 | 97 | var diffingKey: String { 98 | self.diffingKeyAccessed = true 99 | return self.user 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/CollectionView/CollectionViewMocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import UIKit 19 | 20 | typealias _RegisterClassCallInfo = (viewClass: AnyClass?, viewKind: SupplementaryViewKind?, reuseIdentifier: String) 21 | class TestCollectionView: UICollectionView { 22 | 23 | var callsToRegisterClass: [_RegisterClassCallInfo?] = [] 24 | var callsToDeselect = 0 25 | 26 | var callsToReloadData = 0 27 | 28 | var callsToInsertItems = [[IndexPath]]() 29 | var callsToDeleteSections = [IndexSet]() 30 | 31 | override var window: UIWindow? { 32 | return UIWindow() 33 | } 34 | 35 | override func dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath) -> UICollectionViewCell { 36 | return TestCollectionViewCell(identifier: identifier) 37 | } 38 | 39 | override func dequeueReusableSupplementaryView(ofKind elementKind: String, withReuseIdentifier identifier: String, for indexPath: IndexPath) -> UICollectionReusableView { 40 | return TestCollectionReusableView(identifier: identifier) 41 | } 42 | 43 | override func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String) { 44 | if let viewClass = viewClass { 45 | self.callsToRegisterClass.append((viewClass, SupplementaryViewKind(collectionElementKindString: elementKind), identifier)) 46 | } else { 47 | self.callsToRegisterClass.append(nil) 48 | } 49 | super.register(viewClass, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: identifier) 50 | } 51 | 52 | override func deselectItem(at indexPath: IndexPath, animated: Bool) { 53 | self.callsToDeselect += 1 54 | } 55 | 56 | override func insertItems(at indexPaths: [IndexPath]) { 57 | self.callsToInsertItems.append(indexPaths) 58 | } 59 | 60 | override func deleteSections(_ sections: IndexSet) { 61 | self.callsToDeleteSections.append(sections) 62 | } 63 | 64 | override func reloadData() { 65 | self.callsToReloadData += 1 66 | } 67 | 68 | override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { 69 | updates?() 70 | completion?(true) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/CollectionView/CollectionViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import XCTest 19 | 20 | final class CollectionViewModelTests: XCTestCase { 21 | 22 | /// Can be initialized with a custom header and footer view. 23 | func testViewModelInitializerWithCustomHeaderAndFooter() { 24 | let sectionModel = CollectionSectionViewModel( 25 | diffingKey: nil, 26 | cellViewModels: [generateTestCollectionCellViewModel()], 27 | headerViewModel: TestCollectionViewSupplementaryViewModel( 28 | height: 40, 29 | viewKind: .header, 30 | sectionLabel: "A" 31 | ), 32 | footerViewModel: TestCollectionViewSupplementaryViewModel( 33 | height: 50, 34 | viewKind: .footer, 35 | sectionLabel: "A" 36 | ) 37 | ) 38 | 39 | XCTAssertEqual(sectionModel.cellViewModels.count, 1) 40 | 41 | XCTAssertEqual(sectionModel.headerViewModel?.height, 40) 42 | XCTAssertEqual(sectionModel.footerViewModel?.height, 50) 43 | 44 | let headerViewInfo = sectionModel.headerViewModel?.viewInfo 45 | XCTAssertTrue(headerViewInfo?.registrationInfo.registrationMethod == .fromClass(HeaderView.self)) 46 | XCTAssertEqual(headerViewInfo?.registrationInfo.reuseIdentifier, "HeaderView") 47 | XCTAssertEqual(headerViewInfo?.accessibilityFormat.accessibilityIdentifierForSection(84), "access_header+84") 48 | 49 | let footerViewInfo = sectionModel.footerViewModel?.viewInfo 50 | XCTAssertTrue(footerViewInfo?.registrationInfo.registrationMethod == .fromClass(FooterView.self)) 51 | XCTAssertEqual(footerViewInfo?.registrationInfo.reuseIdentifier, "FooterView") 52 | XCTAssertEqual(footerViewInfo?.accessibilityFormat.accessibilityIdentifierForSection(84), "access_footer+84") 53 | } 54 | 55 | /// The table view model allows subscripting into sections and cells. 56 | /// If the section or cell at the index does not exist, the table view 57 | /// model returns `nil`. 58 | func testSubscripts() { 59 | let collectionViewModel = CollectionViewModel(sectionModels: [ 60 | CollectionSectionViewModel(diffingKey: nil, cellViewModels: []), 61 | CollectionSectionViewModel( 62 | diffingKey: nil, 63 | cellViewModels: [ 64 | generateTestCollectionCellViewModel("A"), 65 | generateTestCollectionCellViewModel("B"), 66 | generateTestCollectionCellViewModel("C"), 67 | ]), 68 | ]) 69 | 70 | // Returns `nil` when there's no cell/section at the provided path. 71 | XCTAssertNil(collectionViewModel[ifExists: 9]?.headerViewModel?.height) 72 | XCTAssertNil(collectionViewModel[ifExists: IndexPath(row: 0, section: 0)]) 73 | XCTAssertNil(collectionViewModel[ifExists: IndexPath(row: 0, section: 9)]) 74 | XCTAssertNil(collectionViewModel[ifExists: IndexPath(row: 9, section: 1)]) 75 | 76 | // Returns the section/cell model, if the index path exists within the table view model. 77 | let cell_row_0_section_1 = collectionViewModel[ifExists: IndexPath(row: 0, section: 1)] 78 | as? TestCollectionCellViewModel 79 | XCTAssertEqual(cell_row_0_section_1?.label, "A") 80 | } 81 | 82 | /// The `.isEmpty` property of the collection view. 83 | func testIsEmpty() { 84 | let section0 = CollectionSectionViewModel( 85 | diffingKey: nil, 86 | cellViewModels: generateCollectionCellViewModels() 87 | ) 88 | let sectionEmpty = CollectionSectionViewModel(diffingKey: nil, cellViewModels: []) 89 | let section2 = CollectionSectionViewModel( 90 | diffingKey: nil, 91 | cellViewModels: generateCollectionCellViewModels(count: 1) 92 | ) 93 | 94 | let viewModel1 = CollectionViewModel(sectionModels: []) 95 | XCTAssertTrue(viewModel1.isEmpty) 96 | 97 | let viewModel2 = CollectionViewModel(sectionModels: [sectionEmpty, sectionEmpty, sectionEmpty, sectionEmpty]) 98 | XCTAssertTrue(viewModel2.isEmpty) 99 | 100 | let viewModel3 = CollectionViewModel(sectionModels: [sectionEmpty, section0, section0, section0]) 101 | XCTAssertFalse(viewModel3.isEmpty) 102 | 103 | let viewModel4 = CollectionViewModel(sectionModels: [section2, section0, section0, sectionEmpty]) 104 | XCTAssertFalse(viewModel4.isEmpty) 105 | 106 | let viewModel5 = CollectionViewModel(sectionModels: [section0, sectionEmpty, section2]) 107 | XCTAssertFalse(viewModel5.isEmpty) 108 | } 109 | 110 | /// Verify Collection conformace 111 | func testSectionCollection() { 112 | let section = CollectionSectionViewModel( 113 | diffingKey: nil, 114 | cellViewModels: generateCollectionCellViewModels() 115 | ) 116 | let sectionLabels = section.cellViewModels.compactMap { 117 | ($0 as? TestCollectionCellViewModel)?.label 118 | } 119 | let sectionLabelsViaCollection = section.compactMap { ($0 as? TestCollectionCellViewModel)?.label } 120 | XCTAssertFalse(sectionLabels.isEmpty) 121 | XCTAssertEqual(sectionLabels, sectionLabelsViaCollection) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Tests/CollectionView/TestCollectionViewModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | @testable import ReactiveLists 19 | 20 | struct TestCollectionCellViewModel: CollectionCellViewModel { 21 | let label: String 22 | let didSelect: DidSelectClosure? 23 | let didDeselect: DidDeselectClosure? 24 | 25 | let registrationInfo = ViewRegistrationInfo(classType: TestCollectionViewCell.self) 26 | let accessibilityFormat: CellAccessibilityFormat = "access-%{section}.%{row}" 27 | let shouldHighlight = false 28 | 29 | var diffingKey: DiffingKey { 30 | return self.label 31 | } 32 | 33 | func applyViewModelToCell(_ cell: UICollectionViewCell) { 34 | guard let testCell = cell as? TestCollectionViewCell else { return } 35 | testCell.label = self.label 36 | } 37 | } 38 | 39 | struct TestFlowLayoutCollectionCellViewModel: FlowLayoutCollectionCellViewModel { 40 | let label: String 41 | let itemSize: CGSize 42 | let didSelect: DidSelectClosure? 43 | let didDeselect: DidDeselectClosure? 44 | 45 | let registrationInfo = ViewRegistrationInfo(classType: TestCollectionViewCell.self) 46 | let accessibilityFormat: CellAccessibilityFormat = "access-%{section}.%{row}" 47 | let shouldHighlight = false 48 | 49 | var diffingKey: DiffingKey { 50 | return self.label 51 | } 52 | 53 | func applyViewModelToCell(_ cell: UICollectionViewCell) { 54 | guard let testCell = cell as? TestCollectionViewCell else { return } 55 | testCell.label = self.label 56 | } 57 | 58 | func itemSize( 59 | in collectionView: UICollectionView, 60 | layout: UICollectionViewFlowLayout, 61 | indexPath: IndexPath 62 | ) -> CGSize { 63 | self.itemSize 64 | } 65 | } 66 | 67 | struct TestCollectionViewSupplementaryViewModel: CollectionSupplementaryViewModel { 68 | let label: String? 69 | let height: CGFloat? 70 | let viewInfo: SupplementaryViewInfo? 71 | 72 | init(label: String?, height: CGFloat?, viewInfo: SupplementaryViewInfo? = nil) { 73 | self.label = label 74 | self.height = height 75 | self.viewInfo = viewInfo 76 | } 77 | 78 | init(height: CGFloat?, viewKind: SupplementaryViewKind = .header, sectionLabel: String) { 79 | let kindString = viewKind == .header ? "header" : "footer" 80 | self.label = "label_\(kindString)+\(sectionLabel)" // e.g. title_header+A 81 | self.height = height 82 | self.viewInfo = SupplementaryViewInfo( 83 | registrationInfo: ViewRegistrationInfo(classType: viewKind == .header ? HeaderView.self : FooterView.self), 84 | kind: viewKind, 85 | accessibilityFormat: SupplementaryAccessibilityFormat("access_\(kindString)+%{section}")) // e.g. access_header+%{section} 86 | } 87 | 88 | func applyViewModelToView(_ view: UICollectionReusableView) { 89 | guard let testView = view as? TestCollectionReusableView else { return } 90 | testView.label = self.label 91 | } 92 | } 93 | 94 | class TestCollectionViewCell: UICollectionViewCell { 95 | var identifier: String? 96 | var label: String? 97 | 98 | init(identifier: String) { 99 | self.identifier = identifier 100 | super.init(frame: CGRect.zero) 101 | } 102 | 103 | required init?(coder aDecoder: NSCoder) { 104 | super.init(coder: aDecoder) 105 | } 106 | } 107 | 108 | class TestCollectionReusableView: UICollectionReusableView { 109 | var identifier: String? 110 | var label: String? 111 | 112 | init(identifier: String) { 113 | self.identifier = identifier 114 | super.init(frame: CGRect.zero) 115 | } 116 | 117 | required init?(coder aDecoder: NSCoder) { 118 | super.init(coder: aDecoder) 119 | } 120 | } 121 | 122 | func generateTestCollectionCellViewModel(_ label: String? = nil) -> TestCollectionCellViewModel { 123 | return TestCollectionCellViewModel(label: label ?? UUID().uuidString, 124 | didSelect: nil, 125 | didDeselect: nil 126 | ) 127 | } 128 | 129 | func generateCollectionCellViewModels(count: Int = 4) -> [CollectionCellViewModel] { 130 | var models = [TestCollectionCellViewModel]() 131 | for _ 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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/TableView/TableViewDiffingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import XCTest 19 | 20 | final class TableViewDiffingTests: XCTestCase { 21 | 22 | var tableViewDataSource: TableViewDriver! 23 | var mockTableView: TestTableView! 24 | 25 | override func setUp() { 26 | super.setUp() 27 | self.mockTableView = TestTableView() 28 | self.tableViewDataSource = TableViewDriver( 29 | tableView: self.mockTableView, 30 | shouldDeselectUponSelection: false, 31 | automaticDiffingEnabled: true 32 | ) 33 | } 34 | 35 | /// Tests that changes to individual rows result in the correct calls to update the 36 | /// table view. 37 | /// 38 | /// - Note: We're only testing one type of row update since this is sufficient to test the 39 | /// communication between the diffing lib and the table view. The diffing lib itself has 40 | /// extensive tests for the various diffing scenarios. 41 | func testChangingRows() { 42 | let initialModel = TableViewModel( 43 | cellViewModels: [UserCell(user: "Name")] 44 | ) 45 | 46 | self.tableViewDataSource.tableViewModel = initialModel 47 | 48 | let updatedModel = TableViewModel( 49 | cellViewModels: [UserCell(user: "TestUser")] 50 | ) 51 | 52 | self.tableViewDataSource.tableViewModel = updatedModel 53 | 54 | XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths.count, 1) 55 | XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths[0].indexPaths, [IndexPath(row: 0, section: 0)]) 56 | } 57 | 58 | func testChangingRowsWithEmptyModels() { 59 | let initialModel = TableViewModel( 60 | cellViewModels: [] 61 | ) 62 | 63 | self.tableViewDataSource.tableViewModel = initialModel 64 | 65 | let updatedModel = TableViewModel( 66 | cellViewModels: [UserCell(user: "TestUser")] 67 | ) 68 | 69 | self.tableViewDataSource.tableViewModel = updatedModel 70 | 71 | XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths.count, 0) 72 | XCTAssertEqual(self.mockTableView.callsToReloadData, 3) 73 | } 74 | 75 | /// Tests that changes to individual sections result in the correct calls to update the 76 | /// table view. 77 | /// 78 | /// - Note: We're only testing one type of section update since this is sufficient to test the 79 | /// communication between the diffing lib and the table view. The diffing lib itself has 80 | /// extensive tests for the various diffing scenarios. 81 | func testChangingSections() { 82 | let initialModel = TableViewModel(sectionModels: [ 83 | TableSectionViewModel( 84 | diffingKey: "1", 85 | cellViewModels: generateTableCellViewModels() 86 | ), 87 | TableSectionViewModel( 88 | diffingKey: "2", 89 | cellViewModels: generateTableCellViewModels() 90 | ), 91 | ]) 92 | 93 | self.tableViewDataSource.tableViewModel = initialModel 94 | 95 | let updatedModel = TableViewModel(sectionModels: [ 96 | TableSectionViewModel( 97 | diffingKey: "2", 98 | cellViewModels: generateTableCellViewModels() 99 | ), 100 | ]) 101 | 102 | self.tableViewDataSource.tableViewModel = updatedModel 103 | 104 | XCTAssertEqual(self.mockTableView.callsToDeleteSections.count, 1) 105 | XCTAssertEqual(self.mockTableView.callsToDeleteSections[0].sections, IndexSet(integer: 0)) 106 | } 107 | 108 | func testChangingSectionsThatAreEmpty() { 109 | let initialModel = TableViewModel(sectionModels: [ 110 | TableSectionViewModel( 111 | diffingKey: "1", 112 | cellViewModels: [] 113 | ), 114 | TableSectionViewModel( 115 | diffingKey: "2", 116 | cellViewModels: [] 117 | ), 118 | ]) 119 | 120 | self.tableViewDataSource.tableViewModel = initialModel 121 | 122 | let updatedModel = TableViewModel(sectionModels: [ 123 | TableSectionViewModel( 124 | diffingKey: "2", 125 | cellViewModels: [] 126 | ), 127 | ]) 128 | 129 | self.tableViewDataSource.tableViewModel = updatedModel 130 | 131 | XCTAssertEqual(self.mockTableView.callsToDeleteSections.count, 0) 132 | XCTAssertEqual(self.mockTableView.callsToReloadData, 3) 133 | } 134 | } 135 | 136 | struct UserCell: TableCellViewModel, DiffableViewModel { 137 | var accessibilityFormat: CellAccessibilityFormat = "" 138 | let registrationInfo = ViewRegistrationInfo(classType: UITableViewCell.self) 139 | 140 | let user: String 141 | 142 | init(user: String) { 143 | self.user = user 144 | } 145 | 146 | func applyViewModelToCell(_ cell: UITableViewCell) {} 147 | 148 | func willDisplay(cell: UITableViewCell) {} 149 | 150 | var diffingKey: String { 151 | return self.user 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/TableView/TableViewLazyDiffingTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | import XCTest 19 | 20 | final class TableViewLazyDiffingTests: XCTestCase { 21 | 22 | var tableViewDataSource: TableViewDriver! 23 | var mockTableView: TestTableView! 24 | 25 | override func setUp() { 26 | super.setUp() 27 | self.mockTableView = TestTableView() 28 | self.mockTableView.indexPathsForVisibleRowsOverride = [ 29 | IndexPath(row: 0, section: 0), 30 | ] 31 | self.tableViewDataSource = TableViewDriver( 32 | tableView: self.mockTableView, 33 | shouldDeselectUponSelection: false, 34 | automaticDiffingEnabled: true 35 | ) 36 | } 37 | 38 | /// Tests that changes to individual rows result in the correct calls to update the 39 | /// table view. 40 | /// 41 | /// - Note: We're only testing one type of row update since this is sufficient to test the 42 | /// communication between the diffing lib and the table view. The diffing lib itself has 43 | /// extensive tests for the various diffing scenarios. 44 | func testChangingRows() { 45 | let userCell = LazyUserCell(user: "Name") 46 | let initialModel = TableViewModel( 47 | cellViewModels: [userCell] 48 | ) 49 | 50 | self.tableViewDataSource.tableViewModel = initialModel 51 | 52 | let testUser1 = LazyUserCell(user: "TestUser1") 53 | let testUser2 = LazyUserCell(user: "TestUser2") 54 | let updatedModel = TableViewModel( 55 | cellViewModels: [ 56 | userCell, 57 | testUser1, 58 | testUser2, 59 | ] 60 | ) 61 | 62 | self.tableViewDataSource.tableViewModel = updatedModel 63 | 64 | XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths.count, 1) 65 | XCTAssertEqual(self.mockTableView.callsToInsertRowAtIndexPaths[0].indexPaths, [IndexPath(row: 1, section: 0), IndexPath(row: 2, section: 0)]) 66 | } 67 | } 68 | 69 | final class LazyUserCell: TableCellViewModel, DiffableViewModel { 70 | var accessibilityFormat: CellAccessibilityFormat = "" 71 | let registrationInfo = ViewRegistrationInfo(classType: UITableViewCell.self) 72 | 73 | let user: String 74 | private(set) var diffingKeyAccessed: Bool = false 75 | 76 | init(user: String) { 77 | self.user = user 78 | } 79 | 80 | func applyViewModelToCell(_ cell: UITableViewCell) {} 81 | 82 | func willDisplay(cell: UITableViewCell) {} 83 | 84 | var diffingKey: String { 85 | self.diffingKeyAccessed = true 86 | return self.user 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/TableView/TableViewMocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | @testable import ReactiveLists 18 | 19 | class HeaderView: UITableViewHeaderFooterView {} 20 | class FooterView: UITableViewHeaderFooterView {} 21 | 22 | class TestTableView: UITableView { 23 | var callsToRegisterClass: [(viewClass: AnyClass?, identifier: String)] = [] 24 | var callsToDeselect = 0 25 | var callsToInsertRowAtIndexPaths: [(indexPaths: [IndexPath], animation: UITableView.RowAnimation)] = [] 26 | var callsToDeleteSections: [(sections: IndexSet, animation: UITableView.RowAnimation)] = [] 27 | var callsToReloadData = 0 28 | var indexPathsForVisibleRowsOverride: [IndexPath]? 29 | 30 | /// Setup after init to avoid crashes in iOS 10 31 | private var _window: UIWindow? 32 | 33 | override var window: UIWindow? { 34 | return self._window 35 | } 36 | 37 | override init(frame: CGRect, style: UITableView.Style) { 38 | super.init(frame: frame, style: style) 39 | self._window = UIWindow() 40 | } 41 | 42 | required init?(coder aDecoder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | override var indexPathsForVisibleRows: [IndexPath]? { 47 | if let indexPathsForVisibleRowsOverride = self.indexPathsForVisibleRowsOverride { 48 | return indexPathsForVisibleRowsOverride 49 | } 50 | return (0.. [IndexPath] in 51 | (0.. UITableViewCell? { 56 | return self.dataSource?.tableView(self, cellForRowAt: indexPath) 57 | } 58 | 59 | override func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell { 60 | return TestTableViewCell(identifier: identifier) 61 | } 62 | 63 | override func dequeueReusableHeaderFooterView(withIdentifier identifier: String) -> UITableViewHeaderFooterView? { 64 | return TestTableViewSectionHeaderFooter(identifier: identifier) 65 | } 66 | 67 | override func register(_ aClass: AnyClass?, forHeaderFooterViewReuseIdentifier identifier: String) { 68 | self.callsToRegisterClass.append((aClass, identifier)) 69 | } 70 | 71 | override func deselectRow(at indexPath: IndexPath, animated: Bool) { 72 | self.callsToDeselect += 1 73 | } 74 | 75 | override func insertRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) { 76 | self.callsToInsertRowAtIndexPaths.append((indexPaths: indexPaths, animation: animation)) 77 | } 78 | 79 | override func deleteSections(_ sections: IndexSet, with animation: UITableView.RowAnimation) { 80 | self.callsToDeleteSections.append((sections: sections, animation: animation)) 81 | } 82 | 83 | override func reloadData() { 84 | self.callsToReloadData += 1 85 | } 86 | 87 | override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { 88 | updates?() 89 | completion?(true) 90 | } 91 | } 92 | 93 | extension TableViewDriver { 94 | func _getCell(_ path: IndexPath) -> TestTableViewCell? { 95 | guard let cell = self.tableView(self.tableView, cellForRowAt: path) as? TestTableViewCell else { return nil } 96 | return cell 97 | } 98 | 99 | func _getHeader(_ section: Int) -> TestTableViewSectionHeaderFooter? { 100 | guard let cell = self.tableView(self.tableView, viewForHeaderInSection: section) as? TestTableViewSectionHeaderFooter else { return nil } 101 | return cell 102 | } 103 | 104 | func _getFooter(_ section: Int) -> TestTableViewSectionHeaderFooter? { 105 | guard let cell = self.tableView(self.tableView, viewForFooterInSection: section) as? TestTableViewSectionHeaderFooter else { return nil } 106 | return cell 107 | } 108 | } 109 | 110 | class MockCellViewModel: TableCellViewModel { 111 | var shouldSelect: Bool = true 112 | var accessibilityFormat: CellAccessibilityFormat = "_" 113 | let registrationInfo = ViewRegistrationInfo(classType: UITableViewCell.self) 114 | func applyViewModelToCell(_ cell: UITableViewCell) { } 115 | func willDisplay(cell: UITableViewCell) { } 116 | func shouldSelect(at: IndexPath) -> Bool { return shouldSelect } 117 | 118 | var didSelect: DidSelectClosure? 119 | var didSelectCalled = false 120 | var willBeginEditing: WillBeginEditingClosure? 121 | var willBeginEditingCalled = false 122 | var didEndEditing: DidEndEditingClosure? 123 | var didEndEditingCalled = false 124 | var commitEditingStyle: CommitEditingStyleClosure? 125 | var commitEditingStyleCalled: UITableViewCell.EditingStyle? 126 | 127 | init() { 128 | self.didSelect = { [unowned self] in self.didSelectCalled = true } 129 | self.willBeginEditing = { [unowned self] in self.willBeginEditingCalled = true } 130 | self.didEndEditing = { [unowned self] in self.didEndEditingCalled = true } 131 | self.commitEditingStyle = { [unowned self] in self.commitEditingStyleCalled = $0 } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/TableView/TestTableViewModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | import Foundation 18 | @testable import ReactiveLists 19 | 20 | struct TestCellViewModel: TableCellViewModel { 21 | let rowHeight: CGFloat? = 42 22 | let editingStyle = UITableViewCell.EditingStyle.delete 23 | let shouldHighlight = false 24 | let shouldIndentWhileEditing = false 25 | let accessibilityFormat: CellAccessibilityFormat = "access-%{section}.%{row}" 26 | let registrationInfo = ViewRegistrationInfo(classType: TestTableViewCell.self) 27 | 28 | let label: String 29 | var willBeginEditing: WillBeginEditingClosure? 30 | var didEndEditing: DidEndEditingClosure? 31 | var commitEditingStyle: CommitEditingStyleClosure? 32 | var didSelectClosure: DidSelectClosure? 33 | var didDeselectClosure: DidDeselectClosure? 34 | 35 | var diffingKey: DiffingKey { 36 | return self.label 37 | } 38 | 39 | init(label: String, 40 | willBeginEditing: WillBeginEditingClosure? = nil, 41 | didEndEditing: DidEndEditingClosure? = nil, 42 | commitEditingStyle: CommitEditingStyleClosure? = nil, 43 | didSelectClosure: DidSelectClosure? = nil, 44 | didDeselectClosure: DidDeselectClosure? = nil 45 | ) { 46 | self.label = label 47 | self.willBeginEditing = willBeginEditing 48 | self.didEndEditing = didEndEditing 49 | self.commitEditingStyle = commitEditingStyle 50 | self.didSelectClosure = didSelectClosure 51 | self.didDeselectClosure = didDeselectClosure 52 | } 53 | 54 | func applyViewModelToCell(_ cell: UITableViewCell) { 55 | guard let testCell = cell as? TestTableViewCell else { return } 56 | testCell.label = self.label 57 | } 58 | 59 | func shouldSelect(at: IndexPath) -> Bool { 60 | false 61 | } 62 | 63 | func willDisplay(cell: UITableViewCell) { } 64 | } 65 | 66 | class TestTableViewCell: UITableViewCell { 67 | var identifier: String? 68 | var label: String? 69 | 70 | init(identifier: String) { 71 | self.identifier = identifier 72 | super.init(style: .default, reuseIdentifier: identifier) 73 | } 74 | 75 | required init?(coder aDecoder: NSCoder) { 76 | super.init(coder: aDecoder) 77 | } 78 | } 79 | 80 | func path(_ section: Int, _ row: Int = 0) -> IndexPath { 81 | return IndexPath(row: row, section: section) 82 | } 83 | 84 | func generateTableCellViewModels(count: Int = 4) -> [TableCellViewModel] { 85 | var models = [TestCellViewModel]() 86 | for _ in 0.. TestCellViewModel { 93 | return TestCellViewModel(label: label ?? UUID().uuidString) 94 | } 95 | 96 | struct TestHeaderFooterViewModel: TableSectionHeaderFooterViewModel { 97 | let title: String? 98 | let height: CGFloat? 99 | let viewInfo: SupplementaryViewInfo? 100 | 101 | init(title: String?, height: CGFloat?, viewInfo: SupplementaryViewInfo? = nil) { 102 | self.title = title 103 | self.height = height 104 | self.viewInfo = viewInfo 105 | } 106 | 107 | init(height: CGFloat?, viewKind: SupplementaryViewKind = .header, label: String = "A") { 108 | let kindString = viewKind == .header ? "header" : "footer" 109 | 110 | self.title = "title_\(kindString)+\(label)" // e.g. title_header+3 111 | self.height = height 112 | 113 | self.viewInfo = SupplementaryViewInfo( 114 | registrationInfo: ViewRegistrationInfo(classType: viewKind == .header ? HeaderView.self : FooterView.self), 115 | kind: viewKind, 116 | accessibilityFormat: SupplementaryAccessibilityFormat("access_\(kindString)+%{section}")) // e.g. access_header+%{section} 117 | } 118 | 119 | func applyViewModelToView(_ view: UIView) { 120 | guard let view = view as? TestTableViewSectionHeaderFooter else { return } 121 | view.label = self.title 122 | } 123 | } 124 | 125 | final class PositionCapturingTestHeaderFooterViewModel: TableSectionHeaderFooterViewModel { 126 | let title: String? 127 | let height: CGFloat? 128 | let viewInfo: SupplementaryViewInfo? 129 | var lastPositionSent: TableSectionPosition? 130 | 131 | init() { 132 | self.title = nil 133 | self.height = nil 134 | self.viewInfo = nil 135 | self.lastPositionSent = nil 136 | } 137 | 138 | func height(forPosition position: TableSectionPosition) -> CGFloat? { 139 | self.lastPositionSent = position 140 | return nil 141 | } 142 | 143 | func applyViewModelToView(_ view: UIView) {} 144 | } 145 | 146 | class TestTableViewSectionHeaderFooter: UITableViewHeaderFooterView { 147 | var identifier: String? 148 | var label: String? 149 | 150 | init(identifier: String) { 151 | self.identifier = identifier 152 | super.init(reuseIdentifier: identifier) 153 | } 154 | 155 | required init?(coder aDecoder: NSCoder) { 156 | super.init(coder: aDecoder) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Tests/Utils/XCTest+Parameterized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanGrid 3 | // https://www.plangrid.com 4 | // https://medium.com/plangrid-technology 5 | // 6 | // Documentation 7 | // https://plangrid.github.io/ReactiveLists 8 | // 9 | // GitHub 10 | // https://github.com/plangrid/ReactiveLists 11 | // 12 | // License 13 | // Copyright © 2018-present PlanGrid, Inc. 14 | // Released under an MIT license: https://opensource.org/licenses/MIT 15 | // 16 | 17 | /** 18 | Allows to perform a parameterized tests, in which each test case can have different inputs and 19 | expecations but in which the test preparation & execution code are shared. 20 | */ 21 | public struct ParameterizedTest { 22 | 23 | public typealias Expectation = (TestParameter) -> Void 24 | public typealias TestCase = (TestParameter, expectation: Expectation) 25 | public typealias TestClosure = (TestParameter, Expectation) -> Void 26 | 27 | public static func test(testCases: [TestCase], testClosure: TestClosure) { 28 | testCases.forEach { testClosure($0.0, $0.expectation) } 29 | } 30 | } 31 | 32 | /// Run the same expectation code for any number of single parameter cases 33 | /// 34 | /// Note: For type inferencing to work on nil cases, you may have to do two things: 35 | /// 36 | /// (1) order the nil case first in the list of cases 37 | /// 38 | /// (2) explicitly type the nil, e.g. `Optional()` 39 | public func parameterize(cases: A..., expectation: ((A) -> Void)) { 40 | cases.forEach { expectation($0) } 41 | } 42 | -------------------------------------------------------------------------------- /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 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.reactivelists 7 | CFBundleName 8 | ReactiveLists 9 | DocSetPlatformFamily 10 | reactivelists 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | // On doc load, toggle the URL hash discussion if present 12 | $(document).ready(function() { 13 | if (!window.jazzy.docset) { 14 | var linkToHash = $('a[href="' + window.location.hash +'"]'); 15 | linkToHash.trigger("click"); 16 | } 17 | }); 18 | 19 | // On token click, toggle its discussion and animate token.marginLeft 20 | $(".token").click(function(event) { 21 | if (window.jazzy.docset) { 22 | return; 23 | } 24 | var link = $(this); 25 | var animationDuration = 300; 26 | var tokenOffset = "15px"; 27 | var original = link.css('marginLeft') == tokenOffset; 28 | link.animate({'margin-left':original ? "0px" : tokenOffset}, animationDuration); 29 | $content = link.parent().parent().next(); 30 | $content.slideToggle(animationDuration); 31 | 32 | // Keeps the document from jumping to the hash. 33 | var href = $(this).attr('href'); 34 | if (history.pushState) { 35 | history.pushState({}, '', href); 36 | } else { 37 | location.hash = href; 38 | } 39 | event.preventDefault(); 40 | }); 41 | 42 | // Dumb down quotes within code blocks that delimit strings instead of quotations 43 | // https://github.com/realm/jazzy/issues/714 44 | $("code q").replaceWith(function () { 45 | return ["\"", $(this).contents(), "\""]; 46 | }); 47 | -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/docsets/ReactiveLists.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/ReactiveLists.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/docsets/ReactiveLists.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plangrid/ReactiveLists/88ee64b2b8e50d5bcedf86585390bee98b551134/docs/img/gh.png -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | // On doc load, toggle the URL hash discussion if present 12 | $(document).ready(function() { 13 | if (!window.jazzy.docset) { 14 | var linkToHash = $('a[href="' + window.location.hash +'"]'); 15 | linkToHash.trigger("click"); 16 | } 17 | }); 18 | 19 | // On token click, toggle its discussion and animate token.marginLeft 20 | $(".token").click(function(event) { 21 | if (window.jazzy.docset) { 22 | return; 23 | } 24 | var link = $(this); 25 | var animationDuration = 300; 26 | var tokenOffset = "15px"; 27 | var original = link.css('marginLeft') == tokenOffset; 28 | link.animate({'margin-left':original ? "0px" : tokenOffset}, animationDuration); 29 | $content = link.parent().parent().next(); 30 | $content.slideToggle(animationDuration); 31 | 32 | // Keeps the document from jumping to the hash. 33 | var href = $(this).attr('href'); 34 | if (history.pushState) { 35 | history.pushState({}, '', href); 36 | } else { 37 | location.hash = href; 38 | } 39 | event.preventDefault(); 40 | }); 41 | 42 | // Dumb down quotes within code blocks that delimit strings instead of quotations 43 | // https://github.com/realm/jazzy/issues/714 44 | $("code q").replaceWith(function () { 45 | return ["\"", $(this).contents(), "\""]; 46 | }); 47 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/jesse/GitHub/ReactiveLists" 6 | } -------------------------------------------------------------------------------- /scripts/gen_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Docs by jazzy 4 | # https://github.com/realm/jazzy 5 | # ------------------------------ 6 | 7 | if which jazzy >/dev/null; then 8 | jazzy \ 9 | --clean \ 10 | --author 'PlanGrid' \ 11 | --author_url 'https://twitter.com/PlanGrid' \ 12 | --github_url 'https://github.com/plangrid/ReactiveLists' \ 13 | --module 'ReactiveLists' \ 14 | --source-directory . \ 15 | --readme 'README.md' \ 16 | --documentation 'Guides/*.md' \ 17 | --output docs/ \; 18 | else 19 | echo " 20 | Error: jazzy not installed! 21 | Install: gem install jazzy 22 | " 23 | exit 1 24 | fi 25 | --------------------------------------------------------------------------------