├── .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 | [](https://travis-ci.org/plangrid/ReactiveLists) [][podLink] [][mitLink] [](https://codecov.io/gh/plangrid/ReactiveLists) [][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 |
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 |
--------------------------------------------------------------------------------