├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── BUG_REPORT.md
│ ├── FEATURE_REQUEST.md
│ └── QUESTION.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .hound.yml
├── .jazzy.yaml
├── .swift-mod.yml
├── .swift-version
├── .swiftlint.yml
├── Benchmark
├── Benchmark.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Benchmark.xcscheme
├── Benchmark.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Gemfile
├── Gemfile.lock
├── Makefile
├── Podfile
├── Podfile.lock
├── README.md
└── Sources
│ ├── BenchmarkTools.swift
│ ├── Info.plist
│ └── main.swift
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── DifferenceKit.playground
├── Contents.swift
├── Sources
│ └── TableViewController.swift
└── contents.xcplayground
├── DifferenceKit.podspec
├── DifferenceKit.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── DifferenceKit.xcscheme
├── DifferenceKit.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Examples
├── Example-iOS
│ ├── Example-iOS.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── Sources
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ │ ├── Common
│ │ ├── NibLoadable.swift
│ │ ├── Reusable.swift
│ │ ├── ReusableViewExtensions.swift
│ │ └── StringExtensions.swift
│ │ ├── HeaderFooter
│ │ ├── HeaderFooterCell.swift
│ │ ├── HeaderFooterMoreView.swift
│ │ ├── HeaderFooterMoreView.xib
│ │ ├── HeaderFooterSectionModel.swift
│ │ └── HeaderFooterViewController.swift
│ │ ├── Home
│ │ ├── HomeCell.swift
│ │ ├── HomeCell.xib
│ │ └── HomeViewController.swift
│ │ ├── Info.plist
│ │ ├── Random
│ │ ├── RandomLabelView.swift
│ │ ├── RandomLabelView.xib
│ │ ├── RandomModel.swift
│ │ ├── RandomPlainCell.swift
│ │ ├── RandomViewController.swift
│ │ └── RandomViewController.xib
│ │ └── ShuffleEmoticon
│ │ ├── EmojiCell.swift
│ │ ├── EmojiCell.xib
│ │ ├── EmojiViewController.swift
│ │ └── EmojiViewController.xib
├── Example-macOS
│ ├── Example-macOS.xcodeproj
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── Sources
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── Base.lproj
│ │ └── MainMenu.xib
│ │ ├── Info.plist
│ │ ├── ShuffleEmoticonCollectionViewItem.swift
│ │ ├── ShuffleEmoticonViewController.swift
│ │ └── StringExtensions.swift
└── Example-tvOS
│ ├── Example-tvOS.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── Sources
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── App Icon & Top Shelf Image.brandassets
│ │ ├── App Icon - App Store.imagestack
│ │ │ ├── Back.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Front.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Middle.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ ├── App Icon.imagestack
│ │ │ ├── Back.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Front.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Middle.imagestacklayer
│ │ │ │ ├── Content.imageset
│ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Top Shelf Image Wide.imageset
│ │ │ └── Contents.json
│ │ └── Top Shelf Image.imageset
│ │ │ └── Contents.json
│ └── Contents.json
│ ├── EmojiCell.swift
│ ├── EmojiCell.xib
│ ├── EmojiViewController.swift
│ ├── EmojiViewController.xib
│ ├── Info.plist
│ ├── NibLoadable.swift
│ ├── Reusable.swift
│ ├── ReusableViewExtensions.swift
│ └── StringExtensions.swift
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── LinuxMain.swift
├── Makefile
├── Package.swift
├── Package@swift-4.2.swift
├── Packages
├── Package.resolved
└── Package.swift
├── README.md
├── Sources
├── Algorithm.swift
├── AnyDifferentiable.swift
├── ArraySection.swift
├── Changeset.swift
├── ContentEquatable.swift
├── ContentIdentifiable.swift
├── Differentiable.swift
├── DifferentiableSection.swift
├── ElementPath.swift
├── Extensions
│ ├── AppKitExtension.swift
│ └── UIKitExtension.swift
├── Info.plist
└── StagedChangeset.swift
├── Tests
├── AlgorithmTest.swift
├── AnyDifferentiableTest.swift
├── ArraySectionTest.swift
├── ChangesetTest.swift
├── ContentEquatableTest.swift
├── ElementPathTest.swift
├── Info.plist
├── MeasurementTest.swift
├── StagedChangesetTest.swift
├── TestTools.swift
└── XCTestManifests.swift
├── XCConfigs
└── DifferenceKit.xcconfig
├── assets
├── logo.png
└── sample.gif
├── docs
├── Changeset.html
├── Diffing.html
├── Extensions
│ ├── Optional.html
│ ├── UICollectionView.html
│ └── UITableView.html
├── Protocols
│ ├── ContentEquatable.html
│ ├── Differentiable.html
│ └── DifferentiableSection.html
├── Structs
│ ├── AnyDifferentiable.html
│ ├── ArraySection.html
│ ├── Changeset.html
│ ├── ElementPath.html
│ └── StagedChangeset.html
├── UI Extensions.html
├── badge.svg
├── css
│ ├── highlight.css
│ └── jazzy.css
├── img
│ ├── carat.png
│ ├── dash.png
│ ├── gh.png
│ └── spinner.gif
├── index.html
├── js
│ ├── jazzy.js
│ ├── jazzy.search.js
│ ├── jquery.min.js
│ ├── lunr.min.js
│ └── typeahead.jquery.js
└── search.json
└── test-linux.sh
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ra1028
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Create a bug report.
4 | ---
5 |
6 | ## Checklist
7 | - [ ] This is not a Apple's bug.
8 | - [ ] Reviewed the [README](https://github.com/ra1028/DifferenceKit/blob/master/README.md) and [documents](https://ra1028.github.io/DifferenceKit).
9 | - [ ] Searched [existing issues](https://github.com/ra1028/DifferenceKit/issues) for ensure not duplicated.
10 |
11 | ## Expected Behavior
12 |
13 |
14 | ## Current Behavior
15 |
16 |
17 | ## Steps to Reproduce
18 |
19 |
20 | 1.
21 | 2.
22 | 3.
23 | 4.
24 |
25 | ## Detailed Description (Include Screenshots)
26 |
27 |
28 | ## Reproducible Demo Project
29 |
30 |
31 | ## Environments
32 | - Library version:
33 |
34 | - Swift version:
35 |
36 | - iOS version:
37 |
38 | - Xcode version:
39 |
40 | - Devices/Simulators:
41 |
42 | - CocoaPods/Carthage version:
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Create a feature request.
4 | ---
5 |
6 | ## Checklist
7 | - [ ] Reviewed the [README](https://github.com/ra1028/DifferenceKit/blob/master/README.md) and [documents](https://ra1028.github.io/DifferenceKit).
8 | - [ ] Searched [existing issues](https://github.com/ra1028/DifferenceKit/issues) for ensure not duplicated.
9 |
10 | ## Description
11 |
12 |
13 | ## Motivation and Context
14 |
15 |
16 |
17 | ## Proposed Solution
18 |
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/QUESTION.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Create a bug question.
4 | ---
5 |
6 | ## Checklist
7 | - [ ] Reviewed the [README](https://github.com/ra1028/DifferenceKit/blob/master/README.md) and [documents](https://ra1028.github.io/DifferenceKit).
8 | - [ ] Searched [existing issues](https://github.com/ra1028/DifferenceKit/issues) for ensure not duplicated.
9 |
10 | ## Expected Behavior
11 |
12 |
13 | ## Current Behavior
14 |
15 |
16 | ## Detailed Description (Include Screenshots)
17 |
18 |
19 | ## Environment
20 | - Library version:
21 |
22 | - Swift version:
23 |
24 | - iOS version:
25 |
26 | - Xcode version:
27 |
28 | - Devices/Simulators:
29 |
30 | - CocoaPods/Carthage version:
31 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Checklist
2 | - [ ] All tests are passed.
3 | - [ ] Added tests.
4 | - [ ] Documented the code using [Xcode markup](https://developer.apple.com/library/mac/documentation/Xcode/Reference/xcode_markup_formatting_ref).
5 | - [ ] Searched [existing pull requests](https://github.com/ra1028/DifferenceKit/pulls) for ensure not duplicated.
6 |
7 | ## Description
8 |
9 |
10 | ## Related Issue
11 |
12 |
13 |
14 |
15 |
16 | ## Motivation and Context
17 |
18 |
19 |
20 | ## Impact on Existing Code
21 |
22 |
23 | ## Screenshots (if appropriate)
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Actions
2 | on: [push, pull_request]
3 | jobs:
4 | linux:
5 | name: Test on linux
6 | runs-on: ubuntu-18.04
7 | container:
8 | image: swift:${{ matrix.swift_version }}
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | swift_version: ["4.2", "5.0", "5.1"]
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Show environments
16 | run: |
17 | swift --version
18 | - name: Validate mod
19 | if: matrix.swift_version == '5.1'
20 | run: make mod-check
21 | - name: Swift test
22 | run: |
23 | swift build
24 | swift test
25 |
26 | macOS:
27 | name: Test on macOS
28 | runs-on: macOS-10.15
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | xcode_version: ["11.7", "12.4"] # GitHub actions is now unsupported Xcode version 10.x.
33 | env:
34 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app
35 | steps:
36 | - uses: actions/checkout@v1
37 | - name: Show environments
38 | run: |
39 | swift --version
40 | xcodebuild -version
41 | - name: Swift test
42 | run: |
43 | swift build
44 | swift test
45 | - name: Xcode maxOS
46 | run: |
47 | set -o pipefail && xcodebuild build-for-testing test-without-building -scheme DifferenceKit -configuration Release ENABLE_TESTABILITY=YES | xcpretty -c
48 | - name: Xcode iOS
49 | run: |
50 | set -o pipefail && xcodebuild build-for-testing test-without-building -scheme DifferenceKit -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' ENABLE_TESTABILITY=YES | xcpretty -c
51 | - name: Xcode tvOS
52 | run: |
53 | set -o pipefail && xcodebuild build-for-testing test-without-building -scheme DifferenceKit -configuration Release -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' ENABLE_TESTABILITY=YES | xcpretty -c
54 | - name: Xcode watchOS
55 | run: |
56 | set -o pipefail && xcodebuild build -scheme DifferenceKit -configuration Release -sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 4 - 40mm' ENABLE_TESTABILITY=YES | xcpretty -c
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | */build/*
3 | *.pbxuser
4 | !default.pbxuser
5 | *.mode1v3
6 | !default.mode1v3
7 | *.mode2v3
8 | !default.mode2v3
9 | *.perspectivev3
10 | !default.perspectivev3
11 | xcuserdata
12 | xcbaselines
13 | profile
14 | *.moved-aside
15 | DerivedData
16 | .idea/
17 | *.hmap
18 | *.xccheckout
19 | *.xcuserstate
20 | build/
21 |
22 | ## Documentation
23 | docs/docsets/
24 | docs/undocumented.json
25 |
26 | ## Gems
27 | .bundle
28 | vendor
29 |
30 | ## CocoaPods
31 | Pods
32 |
33 | ## Swift Package build
34 | .build
35 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | swiftlint:
2 | config_file: .swiftlint.yml
3 |
4 | ruby:
5 | enabled: false
6 |
7 | javascript:
8 | enabled: false
9 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | author: Ryo Aoyama
2 | author_url: https://github.com/ra1028
3 | github_url: https://github.com/ra1028/DifferenceKit
4 | module: DifferenceKit
5 | readme: README.md
6 | exclude: Sources/Extensions/AppKitExtension.swift
7 | output: docs
8 | theme: fullwidth
9 | clean: true
10 | skip_undocumented: true
11 | xcodebuild_arguments:
12 | - -sdk
13 | - iphonesimulator
14 | - -scheme
15 | - DifferenceKit
16 | custom_categories:
17 | - name: Diffing
18 | children:
19 | - ContentEquatable
20 | - Differentiable
21 | - DifferentiableSection
22 | - AnyDifferentiable
23 | - ArraySection
24 | - ElementPath
25 | - Optional
26 | - name: Changeset
27 | children:
28 | - StagedChangeset
29 | - Changeset
30 | - name: UI Extensions
31 | children:
32 | - UITableView
33 | - UICollectionView
--------------------------------------------------------------------------------
/.swift-mod.yml:
--------------------------------------------------------------------------------
1 | format:
2 | indent: 4
3 | lineBreakBeforeEachArgument: true
4 | targets:
5 | main:
6 | paths:
7 | - Sources
8 | rules:
9 | defaultAccessLevel:
10 | accessLevel: openOrPublic
11 | implicitInternal: true
12 |
13 | defaultMemberwiseInitializer:
14 | implicitInitializer: true
15 | implicitInternal: true
16 | ignoreClassesWithInheritance: false
17 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 4.2
2 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/realm/SwiftLint
2 |
3 | excluded:
4 | - Tests/XCTestManifests.swift
5 | - Benchmark
6 |
7 | disabled_rules:
8 | - type_name
9 | - identifier_name
10 | - force_cast
11 | - xctfail_message
12 | - function_body_length
13 | - file_length
14 |
15 | nesting:
16 | type_level:
17 | warning: 2
18 |
19 | line_length:
20 | warning: 200
21 |
22 | type_body_length:
23 | warning: 400
24 |
25 | function_parameter_count:
26 | warning: 8
27 |
28 | cyclomatic_complexity:
29 | warning: 12
30 |
31 | statement_position:
32 | statement_mode: uncuddled_else
33 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcodeproj/xcshareddata/xcschemes/Benchmark.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Benchmark/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem 'cocoapods', '1.8.4'
4 |
--------------------------------------------------------------------------------
/Benchmark/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.1)
5 | activesupport (4.2.11.1)
6 | i18n (~> 0.7)
7 | minitest (~> 5.1)
8 | thread_safe (~> 0.3, >= 0.3.4)
9 | tzinfo (~> 1.1)
10 | algoliasearch (1.27.1)
11 | httpclient (~> 2.8, >= 2.8.3)
12 | json (>= 1.5.1)
13 | atomos (0.1.3)
14 | claide (1.0.3)
15 | cocoapods (1.8.4)
16 | activesupport (>= 4.0.2, < 5)
17 | claide (>= 1.0.2, < 2.0)
18 | cocoapods-core (= 1.8.4)
19 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
20 | cocoapods-downloader (>= 1.2.2, < 2.0)
21 | cocoapods-plugins (>= 1.0.0, < 2.0)
22 | cocoapods-search (>= 1.0.0, < 2.0)
23 | cocoapods-stats (>= 1.0.0, < 2.0)
24 | cocoapods-trunk (>= 1.4.0, < 2.0)
25 | cocoapods-try (>= 1.1.0, < 2.0)
26 | colored2 (~> 3.1)
27 | escape (~> 0.0.4)
28 | fourflusher (>= 2.3.0, < 3.0)
29 | gh_inspector (~> 1.0)
30 | molinillo (~> 0.6.6)
31 | nap (~> 1.0)
32 | ruby-macho (~> 1.4)
33 | xcodeproj (>= 1.11.1, < 2.0)
34 | cocoapods-core (1.8.4)
35 | activesupport (>= 4.0.2, < 6)
36 | algoliasearch (~> 1.0)
37 | concurrent-ruby (~> 1.1)
38 | fuzzy_match (~> 2.0.4)
39 | nap (~> 1.0)
40 | cocoapods-deintegrate (1.0.4)
41 | cocoapods-downloader (1.2.2)
42 | cocoapods-plugins (1.0.0)
43 | nap
44 | cocoapods-search (1.0.0)
45 | cocoapods-stats (1.1.0)
46 | cocoapods-trunk (1.4.1)
47 | nap (>= 0.8, < 2.0)
48 | netrc (~> 0.11)
49 | cocoapods-try (1.1.0)
50 | colored2 (3.1.2)
51 | concurrent-ruby (1.1.5)
52 | escape (0.0.4)
53 | fourflusher (2.3.1)
54 | fuzzy_match (2.0.4)
55 | gh_inspector (1.1.3)
56 | httpclient (2.8.3)
57 | i18n (0.9.5)
58 | concurrent-ruby (~> 1.0)
59 | json (2.3.1)
60 | minitest (5.12.2)
61 | molinillo (0.6.6)
62 | nanaimo (0.2.6)
63 | nap (1.1.0)
64 | netrc (0.11.0)
65 | ruby-macho (1.4.0)
66 | thread_safe (0.3.6)
67 | tzinfo (1.2.5)
68 | thread_safe (~> 0.1)
69 | xcodeproj (1.13.0)
70 | CFPropertyList (>= 2.3.3, < 4.0)
71 | atomos (~> 0.1.3)
72 | claide (>= 1.0.2, < 2.0)
73 | colored2 (~> 3.1)
74 | nanaimo (~> 0.2.6)
75 |
76 | PLATFORMS
77 | ruby
78 |
79 | DEPENDENCIES
80 | cocoapods (= 1.8.4)
81 |
82 | BUNDLED WITH
83 | 2.0.1
84 |
--------------------------------------------------------------------------------
/Benchmark/Makefile:
--------------------------------------------------------------------------------
1 | pods-install:
2 | bundle exec pod install || bundle exec pod install --repo-update
3 |
4 | gems-install:
5 | bundle check || bundle install --path vendor/bundle --clean --jobs=4
6 |
--------------------------------------------------------------------------------
/Benchmark/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '13.0'
2 |
3 | use_frameworks!
4 | inhibit_all_warnings!
5 |
6 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
7 |
8 | target 'Benchmark' do
9 | pod 'DifferenceKit', path: '../'
10 | pod 'Differentiator', '4.0.1'
11 | pod 'FlexibleDiff', '0.0.8'
12 | pod 'IGListKit', '3.4.0'
13 | pod 'DeepDiff', '2.2.0'
14 | pod 'Differ', '1.4.3'
15 | pod 'Dwifft', '0.9'
16 | end
17 |
--------------------------------------------------------------------------------
/Benchmark/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - DeepDiff (2.2.0)
3 | - Differ (1.4.3)
4 | - DifferenceKit (1.1.3):
5 | - DifferenceKit/Core (= 1.1.3)
6 | - DifferenceKit/UIKitExtension (= 1.1.3)
7 | - DifferenceKit/Core (1.1.3)
8 | - DifferenceKit/UIKitExtension (1.1.3):
9 | - DifferenceKit/Core
10 | - Differentiator (4.0.1)
11 | - Dwifft (0.9)
12 | - FlexibleDiff (0.0.8)
13 | - IGListKit (3.4.0):
14 | - IGListKit/Default (= 3.4.0)
15 | - IGListKit/Default (3.4.0):
16 | - IGListKit/Diffing
17 | - IGListKit/Diffing (3.4.0)
18 |
19 | DEPENDENCIES:
20 | - DeepDiff (= 2.2.0)
21 | - Differ (= 1.4.3)
22 | - DifferenceKit (from `../`)
23 | - Differentiator (= 4.0.1)
24 | - Dwifft (= 0.9)
25 | - FlexibleDiff (= 0.0.8)
26 | - IGListKit (= 3.4.0)
27 |
28 | SPEC REPOS:
29 | https://github.com/CocoaPods/Specs.git:
30 | - DeepDiff
31 | - Differ
32 | - Differentiator
33 | - Dwifft
34 | - FlexibleDiff
35 | - IGListKit
36 |
37 | EXTERNAL SOURCES:
38 | DifferenceKit:
39 | :path: "../"
40 |
41 | SPEC CHECKSUMS:
42 | DeepDiff: e329bc46dd14ca788d8ec08d34420799ba1d77f2
43 | Differ: 6c7477d6187e8c36d02ec342a3c321061b85a0ea
44 | DifferenceKit: e2c432b59833a7bae2a5cc3175950edbbbab40da
45 | Differentiator: 886080237d9f87f322641dedbc5be257061b0602
46 | Dwifft: 42912068ed2a8146077d1a1404df18625bd086e1
47 | FlexibleDiff: 4f487778bd152088a9528a0a9be06eba7952bb00
48 | IGListKit: 7a5d788e9fb746bcd402baa8e8b24bc3bd2a5a07
49 |
50 | PODFILE CHECKSUM: 2a95de47f8e05bd0b4653734f981d57cbc4395fb
51 |
52 | COCOAPODS: 1.8.4
53 |
--------------------------------------------------------------------------------
/Benchmark/README.md:
--------------------------------------------------------------------------------
1 | # How to Run
2 |
3 | 1. Change directory from the root of repository `cd ./Benchmark`
4 | 1. Install gems by Bundler `make gems-install`
5 | 1. Install dependencies by CocoaPods `make pods-install`
6 | 1. Open `Benchmark.xcworkspace` on Xcode
7 | 1. Run `Benchmark` scheme
8 | 1. See the benchmark result on the Xcode console
9 |
--------------------------------------------------------------------------------
/Benchmark/Sources/BenchmarkTools.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 | import Differentiator
3 | import IGListKit
4 | import DeepDiff
5 |
6 | extension UUID: Differentiable {}
7 |
8 | extension UUID: IdentifiableType {
9 | public var identity: UUID {
10 | return self
11 | }
12 | }
13 |
14 | extension UUID: DiffAware {
15 | public var diffId: Int {
16 | return hashValue
17 | }
18 |
19 | public static func compareContent(_ a: UUID, _ b: UUID) -> Bool {
20 | return a == b
21 | }
22 | }
23 |
24 | extension NSUUID: ListDiffable {
25 | public func diffIdentifier() -> NSObjectProtocol {
26 | return self
27 | }
28 |
29 | public func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
30 | guard let other = object as? NSUUID else {
31 | return false
32 | }
33 | return self == other
34 | }
35 | }
36 |
37 | struct BenchmarkData {
38 | var source: [UUID]
39 | var target: [UUID]
40 | var deleteRange: CountableRange
41 | var insertRange: CountableRange
42 | var shuffleRange: CountableRange
43 |
44 | init(count: Int, deleteRange: CountableRange, insertRange: CountableRange, shuffleRange: CountableRange) {
45 | source = (0.. () -> Void
60 |
61 | func measure(with data: BenchmarkData) -> CFAbsoluteTime {
62 | let action = prepare(data)
63 | let start = CFAbsoluteTimeGetCurrent()
64 | action()
65 | let end = CFAbsoluteTimeGetCurrent()
66 | return end - start
67 | }
68 | }
69 |
70 | struct BenchmarkRunner {
71 | var benchmarks: [Benchmark]
72 |
73 | init(_ benchmarks: Benchmark...) {
74 | self.benchmarks = benchmarks
75 | }
76 |
77 | func run(with data: BenchmarkData) {
78 | let benchmarks = self.benchmarks
79 | let sourceCount = String.localizedStringWithFormat("%d", data.source.count)
80 | let deleteCount = String.localizedStringWithFormat("%d", data.deleteRange.count)
81 | let insertCount = String.localizedStringWithFormat("%d", data.insertRange.count)
82 | let shuffleCount = String.localizedStringWithFormat("%d", data.shuffleRange.count)
83 |
84 | let maxLength = benchmarks.lazy
85 | .map { $0.name.count }
86 | .max() ?? 0
87 |
88 | let empty = String(repeating: " ", count: maxLength)
89 | let timeTitle = "Time(sec)".padding(toLength: maxLength, withPad: " ", startingAt: 0)
90 | let leftAlignSpacer = ":" + String(repeating: "-", count: maxLength - 1)
91 | let rightAlignSpacer = String(repeating: "-", count: maxLength - 1) + ":"
92 |
93 | print("#### - From \(sourceCount) elements to \(deleteCount) deleted, \(insertCount) inserted and \(shuffleCount) shuffled")
94 | print()
95 | print("""
96 | |\(empty)|\(timeTitle)|
97 | |\(leftAlignSpacer)|\(rightAlignSpacer)|
98 | """)
99 |
100 | var results = ContiguousArray(repeating: nil, count: benchmarks.count)
101 | let group = DispatchGroup()
102 | let queue = DispatchQueue(label: "Measure benchmark queue", attributes: .concurrent)
103 |
104 | for (offset, benchmark) in benchmarks.enumerated() {
105 | group.enter()
106 |
107 | queue.async(group: group) {
108 | let first = benchmark.measure(with: data)
109 | let second = benchmark.measure(with: data)
110 | let third = benchmark.measure(with: data)
111 | results[offset] = min(first, second, third)
112 | group.leave()
113 | }
114 | }
115 |
116 | group.wait()
117 |
118 | for (offset, benchmark) in benchmarks.enumerated() {
119 | guard let result = results[offset] else {
120 | fatalError("Measuring was not works correctly.")
121 | }
122 |
123 | let paddingName = benchmark.name.padding(toLength: maxLength, withPad: " ", startingAt: 0)
124 | let paddingTime = String(format: "`%.4f`", result).padding(toLength: maxLength, withPad: " ", startingAt: 0)
125 |
126 | print("|\(paddingName)|", terminator: "")
127 | print("\(paddingTime)|")
128 | }
129 |
130 | print()
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Benchmark/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIcons
10 |
11 | CFBundleIcons~ipad
12 |
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 1.0
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UISupportedInterfaceOrientations~ipad
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationPortraitUpsideDown
37 | UIInterfaceOrientationLandscapeLeft
38 | UIInterfaceOrientationLandscapeRight
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Benchmark/Sources/main.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import DifferenceKit
3 | import Differentiator
4 | import FlexibleDiff
5 | import IGListKit
6 | import DeepDiff
7 | import Differ
8 | import Dwifft
9 |
10 | let runner = BenchmarkRunner(
11 | Benchmark(name: "DifferenceKit") { data in
12 | return {
13 | _ = StagedChangeset(source: data.source, target: data.target)
14 | }
15 | },
16 | Benchmark(name: "RxDataSources") { data in
17 | let model = UUID()
18 | let initialSections = [AnimatableSectionModel(model: model, items: data.source)]
19 | let finalSections = [AnimatableSectionModel(model: model, items: data.target)]
20 |
21 | return {
22 | _ = try! Diff.differencesForSectionedView(initialSections: initialSections, finalSections: finalSections)
23 | }
24 | },
25 | Benchmark(name: "IGListKit") { data in
26 | let oldArray = data.source.map { $0 as NSUUID }
27 | let newArray = data.target.map { $0 as NSUUID }
28 |
29 | return {
30 | _ = ListDiff(oldArray: oldArray, newArray: newArray, option: .equality)
31 | }
32 | },
33 | Benchmark(name: "FlexibleDiff") { data in
34 | return {
35 | _ = FlexibleDiff.Changeset(previous: data.source, current: data.target, identifier: { $0 }, areEqual: ==)
36 | }
37 | },
38 | Benchmark(name: "DeepDiff") { data in
39 | return {
40 | _ = DeepDiff.diff(old: data.source, new: data.target)
41 | }
42 | },
43 | Benchmark(name: "Differ") { data in
44 | return {
45 | _ = data.source.diff(data.target) as Differ.Diff
46 | }
47 | },
48 | Benchmark(name: "Dwifft") { data in
49 | return {
50 | _ = Dwifft.diff(data.source, data.target)
51 | }
52 | },
53 | Benchmark(name: "Swift.CollectionDifference") { data in
54 | return {
55 | _ = data.target.difference(from: data.source).inferringMoves()
56 | }
57 | }
58 | )
59 |
60 | runner.run(with: BenchmarkData(
61 | count: 5000,
62 | deleteRange: 2000..<3000,
63 | insertRange: 3000..<4000,
64 | shuffleRange: 0..<200
65 | ))
66 |
67 | runner.run(with: BenchmarkData(
68 | count: 100000,
69 | deleteRange: 20000..<30000,
70 | insertRange: 30000..<40000,
71 | shuffleRange: 0..<2000
72 | ))
73 |
--------------------------------------------------------------------------------
/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
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 💻 Contributing to DifferenceKit
2 |
3 | First of all, thanks for your interest in DifferenceKit.
4 |
5 | There are several ways to contribute to this project. We welcome contributions in all ways.
6 | We have made some contribution guidelines to smoothly incorporate your opinions and code into this project.
7 |
8 | ## 📝 Open Issue
9 |
10 | When you found a bug or having a feature request, search for the issue from the [existing](https://github.com/ra1028/DifferenceKit/issues) and feel free to open the issue after making sure it isn't already reported.
11 |
12 | In order to we understand your issue accurately, please include as much information as possible in the issue template.
13 | The screenshot are also big clue to understand the issue.
14 |
15 | If you know exactly how to fix the bug you report or implement the feature you propose, please pull request instead of an issue.
16 |
17 | ## 🚀 Pull Request
18 |
19 | We are waiting for a pull request to make this project more better with us.
20 | If you want to add a new feature, let's discuss about it first on issue.
21 |
22 | ```bash
23 | $ git clone https://github.com/ra1028/DifferenceKit.git
24 | $ cd DifferenceKit/
25 | $ open DifferenceKit.xcworkspace
26 | ```
27 |
28 | ### Lint
29 |
30 | Please introduce [SwiftLint](https://github.com/realm/SwiftLint) into your environment before start writing the code.
31 | Xcode automatically runs lint in the build phase.
32 |
33 | The code written according to lint should match our coding style, but for particular cases where style is unknown, refer to the existing code base.
34 |
35 | ### Test
36 |
37 | The test will tells us the validity of your code.
38 | All codes entering the master must pass the all tests.
39 | If you change the code or add new features, you should add tests.
40 |
41 | ### Documentation
42 |
43 | Please write the document using [Xcode markup](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/) to the code you added.
44 | Documentation template is inserted automatically by using Xcode shortcut **⌥⌘/**.
45 | Our document style is slightly different from the template. The example is below.
46 | ```swift
47 | /// The example class for documentation.
48 | final class Foo {
49 | /// A property value.
50 | let prop: Int
51 |
52 | /// Create a new foo with a param.
53 | ///
54 | /// - Parameters:
55 | /// - param: An Int value for prop.
56 | init(param: Int) {
57 | prop = param
58 | }
59 |
60 | /// Returns a string value concatenating `param1` and `param2`.
61 | ///
62 | /// - Parameters:
63 | /// - param1: An Int value for prefix.
64 | /// - param2: A String value for suffix.
65 | ///
66 | /// - Returns: A string concatenating given params.
67 | func bar(param1: Int, param2: String) -> String {
68 | return "\(param1)" + param2
69 | }
70 | }
71 | ```
72 |
73 | ## [Developer's Certificate of Origin 1.1](https://elinux.org/Developer_Certificate_Of_Origin)
74 | By making a contribution to this project, I certify that:
75 |
76 | (a) The contribution was created in whole or in part by me and I
77 | have the right to submit it under the open source license
78 | indicated in the file; or
79 |
80 | (b) The contribution is based upon previous work that, to the best
81 | of my knowledge, is covered under an appropriate open source
82 | license and I have the right under that license to submit that
83 | work with modifications, whether created in whole or in part
84 | by me, under the same open source license (unless I am
85 | permitted to submit under a different license), as indicated
86 | in the file; or
87 |
88 | (c) The contribution was provided directly to me by some other
89 | person who certified (a), (b) or (c) and I have not modified
90 | it.
91 |
92 | (d) I understand and agree that this project and the contribution
93 | are public and that a record of the contribution (including all
94 | personal information I submit with it, including my sign-off) is
95 | maintained indefinitely and may be redistributed consistent with
96 | this project or the open source license(s) involved.
97 |
--------------------------------------------------------------------------------
/DifferenceKit.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | /*:
2 | ## Welcome to `DifferenceKit` Playground
3 | ----
4 | > 1. Open DifferenceKit.xcworkspace.
5 | > 2. Build the DifferenceKit.
6 | > 3. Open DifferenceKit playground in project navigator.
7 | > 4. Show the live view in assistant editor.
8 | */
9 | import DifferenceKit
10 | import PlaygroundSupport
11 | import UIKit
12 |
13 | let viewController = TableViewController()
14 | let navigationController = UINavigationController(rootViewController: viewController)
15 | navigationController.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
16 |
17 | PlaygroundPage.current.needsIndefiniteExecution = true
18 | PlaygroundPage.current.liveView = navigationController.view
19 |
20 | let source = [
21 | ArraySection(model: "Section 1", elements: ["A", "B", "C"]),
22 | ArraySection(model: "Section 2", elements: ["D", "E", "F"]),
23 | ArraySection(model: "Section 3", elements: ["G", "H", "I"]),
24 | ArraySection(model: "Section 4", elements: ["J", "K", "L"])
25 | ]
26 |
27 | let target = [
28 | ArraySection(model: "Section 5", elements: ["M", "N", "O"]),
29 | ArraySection(model: "Section 1", elements: ["A", "C"]),
30 | ArraySection(model: "Section 4", elements: ["J", "I", "K", "L"]),
31 | ArraySection(model: "Section 3", elements: ["G", "H", "Z"]),
32 | ArraySection(model: "Section 6", elements: ["P", "Q", "R"])
33 | ]
34 |
35 | viewController.dataInput = source
36 |
37 | var isSourceShown = true
38 | viewController.refreshAction = {
39 | viewController.dataInput = isSourceShown ? target : source
40 | isSourceShown = !isSourceShown
41 | }
42 |
--------------------------------------------------------------------------------
/DifferenceKit.playground/Sources/TableViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import DifferenceKit
3 |
4 | extension String: Differentiable {}
5 |
6 | public final class TableViewController: UITableViewController {
7 | public var refreshAction: (() -> Void)?
8 |
9 | public var dataInput: [ArraySection] {
10 | get { return data }
11 | set {
12 | let changeset = StagedChangeset(source: data, target: newValue)
13 | tableView.reload(using: changeset, with: .fade) { data in
14 | self.data = data
15 | }
16 | }
17 | }
18 |
19 | private var data = [ArraySection]()
20 |
21 | public init() {
22 | super.init(style: .grouped)
23 | tableView.sectionHeaderHeight = 30
24 | tableView.sectionFooterHeight = 0
25 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: String(describing: UITableViewCell.self))
26 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh))
27 | }
28 |
29 | public required init?(coder aDecoder: NSCoder) {
30 | fatalError("init(coder:) has not been implemented")
31 | }
32 |
33 | @objc private func refresh() {
34 | refreshAction?()
35 | }
36 |
37 | public override func numberOfSections(in tableView: UITableView) -> Int {
38 | return data.count
39 | }
40 |
41 | public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
42 | return data[section].elements.count
43 | }
44 |
45 | public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
46 | let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UITableViewCell.self), for: indexPath)
47 | cell.textLabel?.text = data[indexPath.section].elements[indexPath.row]
48 | return cell
49 | }
50 |
51 | public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
52 | return data[section].model
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/DifferenceKit.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/DifferenceKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'DifferenceKit'
3 | spec.version = '1.3.0'
4 | spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' }
5 | spec.homepage = 'https://github.com/ra1028/DifferenceKit'
6 | spec.documentation_url = 'https://ra1028.github.io/DifferenceKit'
7 | spec.summary = 'A fast and flexible O(n) difference algorithm framework for Swift collection.'
8 | spec.description = <<-DESC
9 | A fast and flexible O(n) difference algorithm framework for Swift collection.
10 | The algorithm is optimized based on the Paul Heckel's algorithm.
11 | DESC
12 | spec.source = { :git => 'https://github.com/ra1028/DifferenceKit.git', :tag => spec.version.to_s }
13 | spec.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
14 | spec.requires_arc = true
15 | spec.default_subspecs = 'Core', 'UIKitExtension'
16 | spec.swift_versions = ['4.2', '5.0']
17 |
18 | spec.ios.deployment_target = '9.0'
19 | spec.tvos.deployment_target = '9.0'
20 | spec.osx.deployment_target = '10.9'
21 | spec.watchos.deployment_target = '2.0'
22 |
23 | spec.subspec 'Core' do |subspec|
24 | subspec.source_files = 'Sources/*.swift'
25 | end
26 |
27 | spec.subspec 'UIKitExtension' do |subspec|
28 | subspec.dependency 'DifferenceKit/Core'
29 |
30 | source_files = 'Sources/Extensions/UIKitExtension.swift'
31 | frameworks = 'UIKit'
32 |
33 | subspec.ios.source_files = source_files
34 | subspec.tvos.source_files = source_files
35 |
36 | subspec.ios.frameworks = frameworks
37 | subspec.tvos.frameworks = frameworks
38 | end
39 |
40 | spec.subspec 'AppKitExtension' do |subspec|
41 | subspec.dependency 'DifferenceKit/Core'
42 |
43 | subspec.osx.source_files = 'Sources/Extensions/AppKitExtension.swift'
44 | subspec.osx.frameworks = 'AppKit'
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/DifferenceKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DifferenceKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DifferenceKit.xcodeproj/xcshareddata/xcschemes/DifferenceKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/DifferenceKit.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/DifferenceKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Example-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Example-iOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | final class AppDelegate: UIResponder, UIApplicationDelegate {
5 | var window: UIWindow?
6 |
7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
8 | configureUIAppearance()
9 |
10 | let window = UIWindow()
11 | let navigationController = UINavigationController(rootViewController: HomeViewController())
12 | window.rootViewController = navigationController
13 | window.makeKeyAndVisible()
14 | self.window = window
15 | return true
16 | }
17 |
18 | func configureUIAppearance() {
19 | let appearance = UINavigationBar.appearance()
20 | let titleTextAttributes: [NSAttributedString.Key: Any] = [
21 | .foregroundColor: UIColor.black
22 | ]
23 |
24 | appearance.tintColor = .darkText
25 | appearance.prefersLargeTitles = true
26 | appearance.titleTextAttributes = titleTextAttributes
27 | appearance.largeTitleTextAttributes = titleTextAttributes
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/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 | }
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/NibLoadable.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | protocol NibLoadable: class {
4 | static var nibName: String { get }
5 | static var nibBundle: Bundle? { get }
6 | }
7 |
8 | extension NibLoadable {
9 | static var nib: UINib {
10 | return UINib(nibName: nibName, bundle: nibBundle)
11 | }
12 |
13 | static var nibName: String {
14 | return String(describing: self)
15 | }
16 |
17 | static var nibBundle: Bundle? {
18 | return Bundle(for: self)
19 | }
20 | }
21 |
22 | extension NibLoadable where Self: UIView {
23 | static func loadFromNib() -> Self {
24 | return nib.instantiate(withOwner: nil, options: nil).first as! Self
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/Reusable.swift:
--------------------------------------------------------------------------------
1 | protocol Reusable: class {
2 | static var reuseIdentifier: String { get }
3 | }
4 |
5 | extension Reusable {
6 | static var reuseIdentifier: String {
7 | return String(reflecting: self)
8 | }
9 | }
10 |
11 | typealias NibReusable = NibLoadable & Reusable
12 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/ReusableViewExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UITableView {
4 | func register(cellType: T.Type) where T: NibReusable {
5 | register(T.nib, forCellReuseIdentifier: T.reuseIdentifier)
6 | }
7 |
8 | func register(cellType: T.Type) where T: Reusable {
9 | register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
10 | }
11 |
12 | func register(viewType: T.Type) where T: NibReusable {
13 | register(T.nib, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
14 | }
15 |
16 | func register(viewType: T.Type) where T: Reusable {
17 | register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
18 | }
19 |
20 | func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: Reusable {
21 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
22 | fatalError()
23 | }
24 | return cell
25 | }
26 |
27 | func dequeueReusableHeaderFooterView(viewType: T.Type = T.self) -> T where T: Reusable {
28 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else {
29 | fatalError()
30 | }
31 | return view
32 | }
33 | }
34 |
35 | extension UICollectionView {
36 | func register(cellType: T.Type) where T: NibReusable {
37 | register(T.nib, forCellWithReuseIdentifier: T.reuseIdentifier)
38 | }
39 |
40 | func register(cellType: T.Type) where T: Reusable {
41 | register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier)
42 | }
43 |
44 | func register(viewType: T.Type, forSupplementaryViewOfKind kind: String) where T: NibReusable {
45 | register(T.nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier)
46 | }
47 |
48 | func register(viewType: T.Type, forSupplementaryViewOfKind kind: String) where T: Reusable {
49 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier)
50 | }
51 |
52 | func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: Reusable {
53 | guard let cell = dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
54 | fatalError()
55 | }
56 | return cell
57 | }
58 |
59 | func dequeueReusableSupplementaryView(ofKind kind: String, for indexPath: IndexPath, viewType: T.Type = T.self) -> T where T: Reusable {
60 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
61 | fatalError()
62 | }
63 | return view
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Common/StringExtensions.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 |
3 | extension String: Differentiable {}
4 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/HeaderFooter/HeaderFooterCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class HeaderFooterPlainCell: UITableViewCell, Reusable {}
4 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/HeaderFooter/HeaderFooterMoreView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class HeaderFooterMoreView: UITableViewHeaderFooterView, NibReusable {
4 | var onMorePressed: (() -> Void)?
5 |
6 | @IBAction func handleMorePressed() {
7 | onMorePressed?()
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/HeaderFooter/HeaderFooterMoreView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/HeaderFooter/HeaderFooterSectionModel.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 |
3 | struct HeaderFooterSectionModel: Differentiable, Equatable {
4 | var id: Int
5 | var hasFooter: Bool
6 |
7 | var differenceIdentifier: Int {
8 | return id
9 | }
10 |
11 | var headerTitle: String {
12 | return "Section \(id)"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/HeaderFooter/HeaderFooterViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import DifferenceKit
3 |
4 | final class HeaderFooterViewController: UITableViewController {
5 | typealias Section = ArraySection
6 |
7 | private var data = [Section]()
8 |
9 | private var dataInput: [Section] {
10 | get { return data }
11 | set {
12 | let changeset = StagedChangeset(source: data, target: newValue)
13 | tableView.reload(using: changeset, with: .fade) { data in
14 | self.data = data
15 | }
16 | }
17 | }
18 |
19 | private let allTexts = (0x0041...0x005A).compactMap { UnicodeScalar($0).map(String.init) }
20 |
21 | @objc private func refresh() {
22 | let model = HeaderFooterSectionModel(id: 0, hasFooter: true)
23 | let section = Section(model: model, elements: allTexts.prefix(7))
24 | dataInput = [section]
25 | }
26 |
27 | private func showMore(in sectionIndex: Int) {
28 | var section = dataInput[sectionIndex]
29 | let texts = allTexts.dropFirst(section.elements.count).prefix(7)
30 | section.elements.append(contentsOf: texts)
31 | section.model.hasFooter = section.elements.count < allTexts.count
32 | dataInput[sectionIndex] = section
33 |
34 | let lastIndex = section.elements.index(before: section.elements.endIndex)
35 | let lastIndexPath = IndexPath(row: lastIndex, section: sectionIndex)
36 | tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: true)
37 | }
38 |
39 | override func viewWillAppear(_ animated: Bool) {
40 | super.viewWillAppear(animated)
41 | refresh()
42 | }
43 |
44 | override func viewDidLoad() {
45 | super.viewDidLoad()
46 |
47 | title = "Header Footer"
48 | tableView.allowsSelection = false
49 |
50 | tableView.register(cellType: HeaderFooterPlainCell.self)
51 | tableView.register(viewType: HeaderFooterMoreView.self)
52 |
53 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh))
54 | }
55 | }
56 |
57 | extension HeaderFooterViewController {
58 | override func numberOfSections(in tableView: UITableView) -> Int {
59 | return data.count
60 | }
61 |
62 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
63 | return data[section].elements.count
64 | }
65 |
66 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
67 | let cell: HeaderFooterPlainCell = tableView.dequeueReusableCell(for: indexPath)
68 | cell.textLabel?.text = data[indexPath.section].elements[indexPath.row]
69 | return cell
70 | }
71 |
72 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
73 | return data[section].model.headerTitle
74 | }
75 |
76 | override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
77 | guard data[section].model.hasFooter else { return nil }
78 |
79 | let view: HeaderFooterMoreView = tableView.dequeueReusableHeaderFooterView()
80 | view.onMorePressed = { [weak self] in
81 | self?.showMore(in: section)
82 | }
83 |
84 | return view
85 | }
86 |
87 | override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
88 | return data[section].model.hasFooter ? 44 : 0
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Home/HomeCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class HomeCell: UITableViewCell, NibReusable {
4 | @IBOutlet weak var titleLabel: UILabel!
5 | @IBOutlet weak var subtitleLabel: UILabel!
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Home/HomeCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Home/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class HomeViewController: UITableViewController {
4 | struct Component {
5 | var title: String
6 | var subtitle: String
7 | var initViewController: () -> UIViewController
8 | }
9 |
10 | private let components = [
11 | Component(title: "Shuffle Emojis", subtitle: "Shuffle sectioned Emojis in UICollectionView", initViewController: EmojiViewController.init),
12 | Component(title: "Header Footer Section", subtitle: "Update header/footer by reload section in UITableView", initViewController: HeaderFooterViewController.init),
13 | Component(title: "Random", subtitle: "Random diff in UICollectionView", initViewController: RandomViewController.init)
14 | ]
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | title = "Home"
20 | tableView.tableFooterView = UIView()
21 | tableView.register(cellType: HomeCell.self)
22 | }
23 | }
24 |
25 | extension HomeViewController {
26 | override func numberOfSections(in tableView: UITableView) -> Int {
27 | return 1
28 | }
29 |
30 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
31 | return components.count
32 | }
33 |
34 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
35 | let cell: HomeCell = tableView.dequeueReusableCell(for: indexPath)
36 | let component = components[indexPath.row]
37 | cell.titleLabel.text = component.title
38 | cell.subtitleLabel.text = component.subtitle
39 | return cell
40 | }
41 |
42 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
43 | let viewController = components[indexPath.row].initViewController()
44 | navigationController?.pushViewController(viewController, animated: true)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | UIUserInterfaceStyle
8 | Light
9 | CFBundleDisplayName
10 | DifferenceKit-iOS
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 1.0
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 |
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomLabelView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class RandomLabelView: UICollectionReusableView, NibReusable {
4 | @IBOutlet weak var label: UILabel!
5 | }
6 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomLabelView.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 |
42 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import DifferenceKit
3 |
4 | struct RandomModel: Differentiable {
5 | var id: UUID
6 | var isUpdated: Bool
7 |
8 | var differenceIdentifier: UUID {
9 | return id
10 | }
11 |
12 | init(_ id: UUID = UUID(), _ isUpdated: Bool = false) {
13 | self.id = id
14 | self.isUpdated = isUpdated
15 | }
16 |
17 | func isContentEqual(to source: RandomModel) -> Bool {
18 | return isUpdated == source.isUpdated
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomPlainCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class RandomPlainCell: UICollectionViewCell, Reusable {
4 | override func layoutSubviews() {
5 | super.layoutSubviews()
6 |
7 | layer.masksToBounds = true
8 | layer.cornerRadius = bounds.height / 2
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import DifferenceKit
3 |
4 | final class RandomViewController: UIViewController {
5 | private typealias Section = ArraySection
6 |
7 | @IBOutlet private weak var collectionView: UICollectionView!
8 |
9 | private var data = [Section]()
10 | private var dataInput: [Section] {
11 | get { return data }
12 | set {
13 | let changeset = StagedChangeset(source: data, target: newValue)
14 | collectionView.reload(using: changeset) { data in
15 | self.data = data
16 | }
17 | }
18 | }
19 |
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 |
23 | title = "Random"
24 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Random", style: .plain, target: self, action: #selector(refresh))
25 |
26 | collectionView.dataSource = self
27 | collectionView.delegate = self
28 | collectionView.register(cellType: RandomPlainCell.self)
29 | collectionView.register(viewType: RandomLabelView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader)
30 | }
31 |
32 | @objc private func refresh() {
33 | let defaultSourceSectionCount = 20
34 | let defaultSourceElementCount = 20
35 |
36 | func randomSection() -> ArraySection {
37 | let elements = (0.. sourceSectionCount ? deleteSectionCount : 0
55 | let insertSectionCount = Int.random(in: minInsertCount.. IndexPath {
112 | let sectionIndex = Int.random(in: 0.. Int {
133 | return data.count
134 | }
135 |
136 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
137 | return data[section].elements.count
138 | }
139 |
140 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
141 | let cell: RandomPlainCell = collectionView.dequeueReusableCell(for: indexPath)
142 | let model = data[indexPath.section].elements[indexPath.item]
143 | cell.contentView.backgroundColor = model.isUpdated ? .cyan : .orange
144 | return cell
145 | }
146 |
147 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
148 | guard kind == UICollectionView.elementKindSectionHeader else {
149 | return UICollectionReusableView()
150 | }
151 |
152 | let model = data[indexPath.section].model
153 | let view: RandomLabelView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, for: indexPath)
154 | view.label.text = "Section ID: \(model.id)"
155 | view.label.textColor = model.isUpdated ? .red : .darkText
156 | return view
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/Random/RandomViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/ShuffleEmoticon/EmojiCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class EmojiCell: UICollectionViewCell, NibReusable {
4 | @IBOutlet weak var label: UILabel!
5 |
6 | override func awakeFromNib() {
7 | super.awakeFromNib()
8 |
9 | label.layer.cornerRadius = 8
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/ShuffleEmoticon/EmojiCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/ShuffleEmoticon/EmojiViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import DifferenceKit
3 |
4 | final class EmojiViewController: UIViewController {
5 | enum SectionID: Differentiable, CaseIterable {
6 | case first, second, third
7 | }
8 |
9 | typealias Section = ArraySection
10 |
11 | @IBOutlet private weak var collectionView: UICollectionView!
12 |
13 | private var data = [Section]()
14 | private var dataInput: [Section] {
15 | get { return data }
16 | set {
17 | let changeset = StagedChangeset(source: data, target: newValue)
18 | collectionView.reload(using: changeset) { data in
19 | self.data = data
20 | }
21 | }
22 | }
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | title = "Emoji"
28 | collectionView.delegate = self
29 | collectionView.dataSource = self
30 | collectionView.register(cellType: EmojiCell.self)
31 |
32 | refresh()
33 | }
34 |
35 | @IBAction func refresh() {
36 | let ids = SectionID.allCases
37 | let Emojis = (0x1F600...0x1F647).compactMap { UnicodeScalar($0).map(String.init) }
38 | let splitedCount = Int((Double(Emojis.count) / Double(ids.count)).rounded(.up))
39 |
40 | dataInput = ids.enumerated().map { offset, model in
41 | let start = offset * splitedCount
42 | let end = min(start + splitedCount, Emojis.endIndex)
43 | let Emojis = Emojis[start.. Int {
71 | return data.count
72 | }
73 |
74 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
75 | return data[section].elements.count
76 | }
77 |
78 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
79 | let cell: EmojiCell = collectionView.dequeueReusableCell(for: indexPath)
80 | cell.label.text = data[indexPath.section].elements[indexPath.item]
81 | return cell
82 | }
83 |
84 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
85 | remove(at: indexPath)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Examples/Example-iOS/Sources/ShuffleEmoticon/EmojiViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Example-macOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Example-macOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | @NSApplicationMain
4 | final class AppDelegate: NSObject, NSApplicationDelegate {
5 | @IBOutlet weak var window: NSWindow!
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSMainNibFile
26 | MainMenu
27 | NSPrincipalClass
28 | NSApplication
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/ShuffleEmoticonCollectionViewItem.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | final class ShuffleEmoticonCollectionViewItem: NSCollectionViewItem {
4 | static var itemIdentifier: NSUserInterfaceItemIdentifier {
5 | return NSUserInterfaceItemIdentifier(String(describing: self))
6 | }
7 |
8 | var emoticon: String {
9 | get { return _textField.stringValue }
10 | set { _textField.stringValue = newValue }
11 | }
12 |
13 | private let _textField = NSTextField()
14 |
15 | override func loadView() {
16 | view = NSView(frame: NSRect(x: 0, y: 0, width: 60, height: 54))
17 | }
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | _textField.font = .systemFont(ofSize: 40)
22 | _textField.alignment = .center
23 | _textField.isEditable = false
24 |
25 | _textField.translatesAutoresizingMaskIntoConstraints = false
26 | view.addSubview(_textField)
27 |
28 | let constraints = [
29 | _textField.topAnchor.constraint(equalTo: view.topAnchor),
30 | _textField.bottomAnchor.constraint(equalTo: view.bottomAnchor),
31 | _textField.leadingAnchor.constraint(equalTo: view.leadingAnchor),
32 | _textField.trailingAnchor.constraint(equalTo: view.trailingAnchor)
33 | ]
34 | NSLayoutConstraint.activate(constraints)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/ShuffleEmoticonViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import DifferenceKit
3 |
4 | final class ShuffleEmoticonViewController: NSViewController {
5 | @IBOutlet private weak var collectionView: NSCollectionView!
6 | @IBOutlet private weak var tableView: NSTableView!
7 |
8 | private var data = (0x1F600...0x1F647).compactMap { UnicodeScalar($0).map(String.init) }
9 | private var dataInput: [String] {
10 | get { return data }
11 | set {
12 | let changeset = StagedChangeset(source: data, target: newValue)
13 | collectionView.reload(using: changeset) { data in
14 | self.data = data
15 | }
16 | tableView.reload(using: changeset, with: .effectFade) { data in
17 | self.data = data
18 | }
19 | }
20 | }
21 |
22 | @IBAction func shufflePress(_ button: NSButton) {
23 | dataInput.shuffle()
24 | }
25 |
26 | override func awakeFromNib() {
27 | super.awakeFromNib()
28 |
29 | tableView.selectionHighlightStyle = .none
30 | collectionView.register(ShuffleEmoticonCollectionViewItem.self, forItemWithIdentifier: ShuffleEmoticonCollectionViewItem.itemIdentifier)
31 | }
32 | }
33 |
34 | extension ShuffleEmoticonViewController: NSCollectionViewDataSource {
35 | func numberOfSections(in collectionView: NSCollectionView) -> Int {
36 | return 1
37 | }
38 |
39 | func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
40 | return data.count
41 | }
42 |
43 | func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
44 | let item = collectionView.makeItem(withIdentifier: ShuffleEmoticonCollectionViewItem.itemIdentifier, for: indexPath) as! ShuffleEmoticonCollectionViewItem
45 | item.emoticon = data[indexPath.item]
46 | return item
47 | }
48 | }
49 |
50 | extension ShuffleEmoticonViewController: NSTableViewDataSource, NSTableViewDelegate {
51 | func numberOfRows(in tableView: NSTableView) -> Int {
52 | return data.count
53 | }
54 |
55 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
56 | let view = tableView.makeView(withIdentifier: NSTableCellView.itemIdentifier, owner: tableView) as! NSTableCellView
57 | view.textField?.stringValue = data[row]
58 | return view
59 | }
60 | }
61 |
62 | private extension NSTableCellView {
63 | static var itemIdentifier: NSUserInterfaceItemIdentifier {
64 | return NSUserInterfaceItemIdentifier(String(describing: self))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Examples/Example-macOS/Sources/StringExtensions.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 |
3 | extension String: Differentiable { }
4 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Example-tvOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Example-tvOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | final class AppDelegate: UIResponder, UIApplicationDelegate {
5 | var window: UIWindow?
6 |
7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
8 | let window = UIWindow()
9 | let navigationController = UINavigationController(rootViewController: EmojiViewController())
10 | window.rootViewController = navigationController
11 | window.makeKeyAndVisible()
12 | self.window = window
13 | return true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "layers" : [
3 | {
4 | "filename" : "Front.imagestacklayer"
5 | },
6 | {
7 | "filename" : "Middle.imagestacklayer"
8 | },
9 | {
10 | "filename" : "Back.imagestacklayer"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "layers" : [
3 | {
4 | "filename" : "Front.imagestacklayer"
5 | },
6 | {
7 | "filename" : "Middle.imagestacklayer"
8 | },
9 | {
10 | "filename" : "Back.imagestacklayer"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "size" : "1280x768",
5 | "idiom" : "tv",
6 | "filename" : "App Icon - App Store.imagestack",
7 | "role" : "primary-app-icon"
8 | },
9 | {
10 | "size" : "400x240",
11 | "idiom" : "tv",
12 | "filename" : "App Icon.imagestack",
13 | "role" : "primary-app-icon"
14 | },
15 | {
16 | "size" : "2320x720",
17 | "idiom" : "tv",
18 | "filename" : "Top Shelf Image Wide.imageset",
19 | "role" : "top-shelf-image-wide"
20 | },
21 | {
22 | "size" : "1920x720",
23 | "idiom" : "tv",
24 | "filename" : "Top Shelf Image.imageset",
25 | "role" : "top-shelf-image"
26 | }
27 | ],
28 | "info" : {
29 | "version" : 1,
30 | "author" : "xcode"
31 | }
32 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/EmojiCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class EmojiCell: UICollectionViewCell, NibReusable {
4 | @IBOutlet weak var label: UILabel!
5 |
6 | override func awakeFromNib() {
7 | super.awakeFromNib()
8 |
9 | contentView.backgroundColor = UIColor(white: 0.95, alpha: 1)
10 | contentView.layer.cornerRadius = 8
11 | contentView.layer.shadowOffset = CGSize(width: 0, height: 7)
12 | }
13 |
14 | override var isHighlighted: Bool {
15 | didSet { alpha = isHidden ? 0.2 : 1 }
16 | }
17 |
18 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
19 | coordinator.addCoordinatedAnimations({ [weak self] in
20 | guard let self = self else { return }
21 | self.contentView.layer.shadowOpacity = self.isFocused ? 0.3 : 0
22 | self.contentView.layer.transform = self.isFocused ? CATransform3DMakeScale(1.1, 1.1, 1) : CATransform3DIdentity
23 | self.layer.zPosition = self.isFocused ? 1 : 0
24 | })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/EmojiCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/EmojiViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import DifferenceKit
3 |
4 | final class EmojiViewController: UIViewController {
5 | enum SectionID: Differentiable, CaseIterable {
6 | case first, second, third
7 | }
8 |
9 | typealias Section = ArraySection
10 |
11 | @IBOutlet private weak var collectionView: UICollectionView!
12 |
13 | private var data = [Section]()
14 | private var dataInput: [Section] {
15 | get { return data }
16 | set {
17 | let changeset = StagedChangeset(source: data, target: newValue)
18 | collectionView.reload(using: changeset) { data in
19 | self.data = data
20 | }
21 | }
22 | }
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | title = "Emoji"
28 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh))
29 |
30 | collectionView.delegate = self
31 | collectionView.dataSource = self
32 | collectionView.register(cellType: EmojiCell.self)
33 |
34 | refresh()
35 | }
36 |
37 | @IBAction func refresh() {
38 | let ids = SectionID.allCases
39 | let Emojis = (0x1F600...0x1F647).compactMap { UnicodeScalar($0).map(String.init) }
40 | let splitedCount = Int((Double(Emojis.count) / Double(ids.count)).rounded(.up))
41 |
42 | dataInput = ids.enumerated().map { offset, model in
43 | let start = offset * splitedCount
44 | let end = min(start + splitedCount, Emojis.endIndex)
45 | let Emojis = Emojis[start.. Int {
73 | return data.count
74 | }
75 |
76 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
77 | return data[section].elements.count
78 | }
79 |
80 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
81 | let cell: EmojiCell = collectionView.dequeueReusableCell(for: indexPath)
82 | cell.label.text = data[indexPath.section].elements[indexPath.item]
83 | return cell
84 | }
85 |
86 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
87 | remove(at: indexPath)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/EmojiViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
35 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | DifferenceKit-tvOS
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UIRequiredDeviceCapabilities
26 |
27 | arm64
28 |
29 | UIUserInterfaceStyle
30 | Automatic
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/NibLoadable.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | protocol NibLoadable: class {
4 | static var nibName: String { get }
5 | static var nibBundle: Bundle? { get }
6 | }
7 |
8 | extension NibLoadable {
9 | static var nib: UINib {
10 | return UINib(nibName: nibName, bundle: nibBundle)
11 | }
12 |
13 | static var nibName: String {
14 | return String(describing: self)
15 | }
16 |
17 | static var nibBundle: Bundle? {
18 | return Bundle(for: self)
19 | }
20 | }
21 |
22 | extension NibLoadable where Self: UIView {
23 | static func loadFromNib() -> Self {
24 | return nib.instantiate(withOwner: nil, options: nil).first as! Self
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/Reusable.swift:
--------------------------------------------------------------------------------
1 | protocol Reusable: class {
2 | static var reuseIdentifier: String { get }
3 | }
4 |
5 | extension Reusable {
6 | static var reuseIdentifier: String {
7 | return String(reflecting: self)
8 | }
9 | }
10 |
11 | typealias NibReusable = NibLoadable & Reusable
12 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/ReusableViewExtensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UITableView {
4 | func register(cellType: T.Type) where T: NibReusable {
5 | register(T.nib, forCellReuseIdentifier: T.reuseIdentifier)
6 | }
7 |
8 | func register(cellType: T.Type) where T: Reusable {
9 | register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
10 | }
11 |
12 | func register(viewType: T.Type) where T: NibReusable {
13 | register(T.nib, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
14 | }
15 |
16 | func register(viewType: T.Type) where T: Reusable {
17 | register(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
18 | }
19 |
20 | func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: Reusable {
21 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
22 | fatalError()
23 | }
24 | return cell
25 | }
26 |
27 | func dequeueReusableHeaderFooterView(viewType: T.Type = T.self) -> T where T: Reusable {
28 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else {
29 | fatalError()
30 | }
31 | return view
32 | }
33 | }
34 |
35 | extension UICollectionView {
36 | func register(cellType: T.Type) where T: NibReusable {
37 | register(T.nib, forCellWithReuseIdentifier: T.reuseIdentifier)
38 | }
39 |
40 | func register(cellType: T.Type) where T: Reusable {
41 | register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier)
42 | }
43 |
44 | func register(viewType: T.Type, forSupplementaryViewOfKind kind: String) where T: NibReusable {
45 | register(T.nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier)
46 | }
47 |
48 | func register(viewType: T.Type, forSupplementaryViewOfKind kind: String) where T: Reusable {
49 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier)
50 | }
51 |
52 | func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: Reusable {
53 | guard let cell = dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
54 | fatalError()
55 | }
56 | return cell
57 | }
58 |
59 | func dequeueReusableSupplementaryView(ofKind kind: String, for indexPath: IndexPath, viewType: T.Type = T.self) -> T where T: Reusable {
60 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
61 | fatalError()
62 | }
63 | return view
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Examples/Example-tvOS/Sources/StringExtensions.swift:
--------------------------------------------------------------------------------
1 | import DifferenceKit
2 |
3 | extension String: Differentiable {}
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem 'cocoapods', '1.8.4'
4 | gem 'jazzy', '0.11.2'
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.1)
5 | activesupport (4.2.11.1)
6 | i18n (~> 0.7)
7 | minitest (~> 5.1)
8 | thread_safe (~> 0.3, >= 0.3.4)
9 | tzinfo (~> 1.1)
10 | algoliasearch (1.27.1)
11 | httpclient (~> 2.8, >= 2.8.3)
12 | json (>= 1.5.1)
13 | atomos (0.1.3)
14 | claide (1.0.3)
15 | cocoapods (1.8.4)
16 | activesupport (>= 4.0.2, < 5)
17 | claide (>= 1.0.2, < 2.0)
18 | cocoapods-core (= 1.8.4)
19 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
20 | cocoapods-downloader (>= 1.2.2, < 2.0)
21 | cocoapods-plugins (>= 1.0.0, < 2.0)
22 | cocoapods-search (>= 1.0.0, < 2.0)
23 | cocoapods-stats (>= 1.0.0, < 2.0)
24 | cocoapods-trunk (>= 1.4.0, < 2.0)
25 | cocoapods-try (>= 1.1.0, < 2.0)
26 | colored2 (~> 3.1)
27 | escape (~> 0.0.4)
28 | fourflusher (>= 2.3.0, < 3.0)
29 | gh_inspector (~> 1.0)
30 | molinillo (~> 0.6.6)
31 | nap (~> 1.0)
32 | ruby-macho (~> 1.4)
33 | xcodeproj (>= 1.11.1, < 2.0)
34 | cocoapods-core (1.8.4)
35 | activesupport (>= 4.0.2, < 6)
36 | algoliasearch (~> 1.0)
37 | concurrent-ruby (~> 1.1)
38 | fuzzy_match (~> 2.0.4)
39 | nap (~> 1.0)
40 | cocoapods-deintegrate (1.0.4)
41 | cocoapods-downloader (1.2.2)
42 | cocoapods-plugins (1.0.0)
43 | nap
44 | cocoapods-search (1.0.0)
45 | cocoapods-stats (1.1.0)
46 | cocoapods-trunk (1.4.1)
47 | nap (>= 0.8, < 2.0)
48 | netrc (~> 0.11)
49 | cocoapods-try (1.1.0)
50 | colored2 (3.1.2)
51 | concurrent-ruby (1.1.5)
52 | escape (0.0.4)
53 | ffi (1.11.1)
54 | fourflusher (2.3.1)
55 | fuzzy_match (2.0.4)
56 | gh_inspector (1.1.3)
57 | httpclient (2.8.3)
58 | i18n (0.9.5)
59 | concurrent-ruby (~> 1.0)
60 | jazzy (0.11.2)
61 | cocoapods (~> 1.5)
62 | mustache (~> 1.1)
63 | open4
64 | redcarpet (~> 3.4)
65 | rouge (>= 2.0.6, < 4.0)
66 | sassc (~> 2.1)
67 | sqlite3 (~> 1.3)
68 | xcinvoke (~> 0.3.0)
69 | json (2.3.1)
70 | liferaft (0.0.6)
71 | minitest (5.12.2)
72 | molinillo (0.6.6)
73 | mustache (1.1.0)
74 | nanaimo (0.2.6)
75 | nap (1.1.0)
76 | netrc (0.11.0)
77 | open4 (1.3.4)
78 | redcarpet (3.5.1)
79 | rouge (3.12.0)
80 | ruby-macho (1.4.0)
81 | sassc (2.2.1)
82 | ffi (~> 1.9)
83 | sqlite3 (1.4.1)
84 | thread_safe (0.3.6)
85 | tzinfo (1.2.5)
86 | thread_safe (~> 0.1)
87 | xcinvoke (0.3.0)
88 | liferaft (~> 0.0.6)
89 | xcodeproj (1.13.0)
90 | CFPropertyList (>= 2.3.3, < 4.0)
91 | atomos (~> 0.1.3)
92 | claide (>= 1.0.2, < 2.0)
93 | colored2 (~> 3.1)
94 | nanaimo (~> 0.2.6)
95 |
96 | PLATFORMS
97 | ruby
98 |
99 | DEPENDENCIES
100 | cocoapods (= 1.8.4)
101 | jazzy (= 0.11.2)
102 |
103 | BUNDLED WITH
104 | 2.0.1
105 |
--------------------------------------------------------------------------------
/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import DifferenceKitTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += DifferenceKitTests.__allTests()
7 |
8 | XCTMain(tests)
9 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | gems-install:
2 | bundle config path vendor/bundle
3 | bundle install --jobs 4 --retry 3
4 |
5 | docs-gen:
6 | bundle exec jazzy --config .jazzy.yaml
7 |
8 | lib-lint:
9 | bundle exec pod lib lint
10 |
11 | pod-release:
12 | bundle exec pod trunk push DifferenceKit.podspec
13 |
14 | test-linux:
15 | docker run -v `pwd`:`pwd` -w `pwd` --rm swift:latest swift test
16 |
17 | mod:
18 | swift run -c release --package-path ./Packages swift-mod
19 |
20 | mod-check:
21 | swift run -c release --package-path ./Packages swift-mod --check
22 |
23 | generate-linuxmain:
24 | swift test --generate-linuxmain
25 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "DifferenceKit",
7 | platforms: [
8 | .iOS(.v9), .macOS(.v10_10), .tvOS(.v9), .watchOS(.v2)
9 | ],
10 | products: [
11 | .library(name: "DifferenceKit", targets: ["DifferenceKit"])
12 | ],
13 | targets: [
14 | .target(
15 | name: "DifferenceKit",
16 | path: "Sources"
17 | ),
18 | .testTarget(
19 | name: "DifferenceKitTests",
20 | dependencies: ["DifferenceKit"],
21 | path: "Tests"
22 | )
23 | ],
24 | swiftLanguageVersions: [.v4_2, .v5]
25 | )
26 |
--------------------------------------------------------------------------------
/Package@swift-4.2.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "DifferenceKit",
7 | products: [
8 | .library(name: "DifferenceKit", targets: ["DifferenceKit"])
9 | ],
10 | targets: [
11 | .target(
12 | name: "DifferenceKit",
13 | path: "Sources"
14 | ),
15 | .testTarget(
16 | name: "DifferenceKitTests",
17 | dependencies: ["DifferenceKit"],
18 | path: "Tests"
19 | )
20 | ],
21 | swiftLanguageVersions: [.v4_2]
22 | )
23 |
--------------------------------------------------------------------------------
/Packages/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "swift-mod",
6 | "repositoryURL": "https://github.com/ra1028/swift-mod.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "85deacdd78b6078e9cb252c58be35658e002a120",
10 | "version": "0.0.1"
11 | }
12 | },
13 | {
14 | "package": "SwiftSyntax",
15 | "repositoryURL": "https://github.com/apple/swift-syntax",
16 | "state": {
17 | "branch": null,
18 | "revision": "3e3eb191fcdbecc6031522660c4ed6ce25282c25",
19 | "version": "0.50100.0"
20 | }
21 | },
22 | {
23 | "package": "swift-tools-support-core",
24 | "repositoryURL": "https://github.com/apple/swift-tools-support-core.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "693aba4c4c9dcc4767cc853a0dd38bf90ad8c258",
28 | "version": "0.0.1"
29 | }
30 | },
31 | {
32 | "package": "Yams",
33 | "repositoryURL": "https://github.com/jpsim/Yams.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
37 | "version": "2.0.0"
38 | }
39 | }
40 | ]
41 | },
42 | "version": 1
43 | }
44 |
--------------------------------------------------------------------------------
/Packages/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Modules",
7 | dependencies: [
8 | .package(url: "https://github.com/ra1028/swift-mod.git", .exact("0.0.1"))
9 | ]
10 | )
11 |
--------------------------------------------------------------------------------
/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 | public init(_ base: D) {
46 | if let anyDifferentiable = base as? AnyDifferentiable {
47 | self = anyDifferentiable
48 | }
49 | else {
50 | box = DifferentiableBox(base)
51 | }
52 | }
53 |
54 | /// Indicate whether the content of `base` is equals to the content of the given source value.
55 | ///
56 | /// - Parameters:
57 | /// - source: A source value to be compared.
58 | ///
59 | /// - Returns: A Boolean value indicating whether the content of `base` is equals
60 | /// to the content of `base` of the given source value.
61 | @inlinable
62 | public func isContentEqual(to source: AnyDifferentiable) -> Bool {
63 | return box.isContentEqual(to: source.box)
64 | }
65 | }
66 |
67 | extension AnyDifferentiable: CustomDebugStringConvertible {
68 | public var debugDescription: String {
69 | return "AnyDifferentiable(\(String(reflecting: base)))"
70 | }
71 | }
72 |
73 | @usableFromInline
74 | internal protocol AnyDifferentiableBox {
75 | var base: Any { get }
76 | var differenceIdentifier: AnyHashable { get }
77 |
78 | func isContentEqual(to source: AnyDifferentiableBox) -> Bool
79 | }
80 |
81 | @usableFromInline
82 | internal struct DifferentiableBox: AnyDifferentiableBox {
83 | @usableFromInline
84 | internal let baseComponent: Base
85 |
86 | @inlinable
87 | internal var base: Any {
88 | return baseComponent
89 | }
90 |
91 | @inlinable
92 | internal var differenceIdentifier: AnyHashable {
93 | return baseComponent.differenceIdentifier
94 | }
95 |
96 | @usableFromInline
97 | internal init(_ base: Base) {
98 | baseComponent = base
99 | }
100 |
101 | @inlinable
102 | internal func isContentEqual(to source: AnyDifferentiableBox) -> Bool {
103 | guard let sourceBase = source.base as? Base else {
104 | return false
105 | }
106 | return baseComponent.isContentEqual(to: sourceBase)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/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 | public init(model: Model, elements: C) where C.Element == Element {
23 | self.model = model
24 | self.elements = Array(elements)
25 | }
26 |
27 | /// Creates a new section reproducing the given source section with replacing the elements.
28 | ///
29 | /// - Parameters:
30 | /// - source: A source section to reproduce.
31 | /// - elements: The collection of elements for the new section.
32 | @inlinable
33 | public init(source: ArraySection, elements: C) where C.Element == Element {
34 | self.init(model: source.model, elements: elements)
35 | }
36 |
37 | /// Indicate whether the content of `self` is equals to the content of
38 | /// the given source section.
39 | ///
40 | /// - Note: It's compared by the model of `self` and the specified section.
41 | ///
42 | /// - Parameters:
43 | /// - source: A source section to compare.
44 | ///
45 | /// - Returns: A Boolean value indicating whether the content of `self` is equals
46 | /// to the content of the given source section.
47 | @inlinable
48 | public func isContentEqual(to source: ArraySection) -> Bool {
49 | return model.isContentEqual(to: source.model)
50 | }
51 | }
52 |
53 | extension ArraySection: Equatable where Model: Equatable, Element: Equatable {
54 | public static func == (lhs: ArraySection, rhs: ArraySection) -> Bool {
55 | return lhs.model == rhs.model && lhs.elements == rhs.elements
56 | }
57 | }
58 |
59 | extension ArraySection: CustomDebugStringConvertible {
60 | public var debugDescription: String {
61 | return """
62 | ArraySection(
63 | model: \(model),
64 | elements: \(elements)
65 | )
66 | """
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/Changeset.swift:
--------------------------------------------------------------------------------
1 | /// A set of changes in the sectioned collection.
2 | ///
3 | /// Changes to the section of the linear collection should be empty.
4 | ///
5 | /// Notice that the value of the changes represents offsets of collection not index.
6 | /// Since offsets are unordered, order is ignored when comparing two `Changeset`s.
7 | public struct Changeset {
8 | /// The collection after changed.
9 | public var data: Collection
10 |
11 | /// The offsets of deleted sections.
12 | public var sectionDeleted: [Int]
13 | /// The offsets of inserted sections.
14 | public var sectionInserted: [Int]
15 | /// The offsets of updated sections.
16 | public var sectionUpdated: [Int]
17 | /// The pairs of source and target offset of moved sections.
18 | public var sectionMoved: [(source: Int, target: Int)]
19 |
20 | /// The paths of deleted elements.
21 | public var elementDeleted: [ElementPath]
22 | /// The paths of inserted elements.
23 | public var elementInserted: [ElementPath]
24 | /// The paths of updated elements.
25 | public var elementUpdated: [ElementPath]
26 | /// The pairs of source and target path of moved elements.
27 | public var elementMoved: [(source: ElementPath, target: ElementPath)]
28 |
29 | /// Creates a new `Changeset`.
30 | ///
31 | /// - Parameters:
32 | /// - data: The collection after changed.
33 | /// - sectionDeleted: The offsets of deleted sections.
34 | /// - sectionInserted: The offsets of inserted sections.
35 | /// - sectionUpdated: The offsets of updated sections.
36 | /// - sectionMoved: The pairs of source and target offset of moved sections.
37 | /// - elementDeleted: The paths of deleted elements.
38 | /// - elementInserted: The paths of inserted elements.
39 | /// - elementUpdated: The paths of updated elements.
40 | /// - elementMoved: The pairs of source and target path of moved elements.
41 | public init(
42 | data: Collection,
43 | sectionDeleted: [Int] = [],
44 | sectionInserted: [Int] = [],
45 | sectionUpdated: [Int] = [],
46 | sectionMoved: [(source: Int, target: Int)] = [],
47 | elementDeleted: [ElementPath] = [],
48 | elementInserted: [ElementPath] = [],
49 | elementUpdated: [ElementPath] = [],
50 | elementMoved: [(source: ElementPath, target: ElementPath)] = []
51 | ) {
52 | self.data = data
53 | self.sectionDeleted = sectionDeleted
54 | self.sectionInserted = sectionInserted
55 | self.sectionUpdated = sectionUpdated
56 | self.sectionMoved = sectionMoved
57 | self.elementDeleted = elementDeleted
58 | self.elementInserted = elementInserted
59 | self.elementUpdated = elementUpdated
60 | self.elementMoved = elementMoved
61 | }
62 | }
63 |
64 | public extension Changeset {
65 | /// The number of section changes.
66 | @inlinable
67 | var sectionChangeCount: Int {
68 | return sectionDeleted.count
69 | + sectionInserted.count
70 | + sectionUpdated.count
71 | + sectionMoved.count
72 | }
73 |
74 | /// The number of element changes.
75 | @inlinable
76 | var elementChangeCount: Int {
77 | return elementDeleted.count
78 | + elementInserted.count
79 | + elementUpdated.count
80 | + elementMoved.count
81 | }
82 |
83 | /// The number of all changes.
84 | @inlinable
85 | var changeCount: Int {
86 | return sectionChangeCount + elementChangeCount
87 | }
88 |
89 | /// A Boolean value indicating whether has section changes.
90 | @inlinable
91 | var hasSectionChanges: Bool {
92 | return sectionChangeCount > 0
93 | }
94 |
95 | /// A Boolean value indicating whether has element changes.
96 | @inlinable
97 | var hasElementChanges: Bool {
98 | return elementChangeCount > 0
99 | }
100 |
101 | /// A Boolean value indicating whether has changes.
102 | @inlinable
103 | var hasChanges: Bool {
104 | return changeCount > 0
105 | }
106 | }
107 |
108 | extension Changeset: Equatable where Collection: Equatable {
109 | public static func == (lhs: Changeset, rhs: Changeset) -> Bool {
110 | return lhs.data == rhs.data
111 | && Set(lhs.sectionDeleted) == Set(rhs.sectionDeleted)
112 | && Set(lhs.sectionInserted) == Set(rhs.sectionInserted)
113 | && Set(lhs.sectionUpdated) == Set(rhs.sectionUpdated)
114 | && Set(lhs.sectionMoved.map(HashablePair.init)) == Set(rhs.sectionMoved.map(HashablePair.init))
115 | && Set(lhs.elementDeleted) == Set(rhs.elementDeleted)
116 | && Set(lhs.elementInserted) == Set(rhs.elementInserted)
117 | && Set(lhs.elementUpdated) == Set(rhs.elementUpdated)
118 | && Set(lhs.elementMoved.map(HashablePair.init)) == Set(rhs.elementMoved.map(HashablePair.init))
119 | }
120 | }
121 |
122 | extension Changeset: CustomDebugStringConvertible {
123 | public var debugDescription: String {
124 | guard !data.isEmpty || hasChanges else {
125 | return """
126 | Changeset(
127 | data: []
128 | )"
129 | """
130 | }
131 |
132 | var description = """
133 | Changeset(
134 | data: \(data.isEmpty ? "[]" : "[\n \(data.map { "\($0)" }.joined(separator: ",\n").split(separator: "\n").joined(separator: "\n "))\n ]")
135 | """
136 |
137 | func appendDescription(name: String, elements: [T]) {
138 | guard !elements.isEmpty else { return }
139 |
140 | description += ",\n \(name): [\n \(elements.map { "\($0)" }.joined(separator: ",\n").split(separator: "\n").joined(separator: "\n "))\n ]"
141 | }
142 |
143 | appendDescription(name: "sectionDeleted", elements: sectionDeleted)
144 | appendDescription(name: "sectionInserted", elements: sectionInserted)
145 | appendDescription(name: "sectionUpdated", elements: sectionUpdated)
146 | appendDescription(name: "sectionMoved", elements: sectionMoved)
147 | appendDescription(name: "elementDeleted", elements: elementDeleted)
148 | appendDescription(name: "elementInserted", elements: elementInserted)
149 | appendDescription(name: "elementUpdated", elements: elementUpdated)
150 | appendDescription(name: "elementMoved", elements: elementMoved)
151 |
152 | description += "\n)"
153 | return description
154 | }
155 | }
156 |
157 | private struct HashablePair: Hashable {
158 | let first: H
159 | let second: H
160 | }
161 |
--------------------------------------------------------------------------------
/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 |
54 | extension Array: ContentEquatable where Element: ContentEquatable {
55 | /// Indicate whether the content of `self` is equals to the content of
56 | /// the given source value.
57 | ///
58 | /// - Parameters:
59 | /// - source: A source value to be compared.
60 | ///
61 | /// - Returns: A Boolean value indicating whether the content of `self` is equals
62 | /// to the content of the given source value.
63 | @inlinable
64 | public func isContentEqual(to source: [Element]) -> Bool {
65 | return count == source.count
66 | && zip(self, source).allSatisfy { $0.isContentEqual(to: $1) }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/ContentIdentifiable.swift:
--------------------------------------------------------------------------------
1 | /// Represents the value that identified for differentiate.
2 | public protocol ContentIdentifiable {
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 ContentIdentifiable 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 |
--------------------------------------------------------------------------------
/Sources/Differentiable.swift:
--------------------------------------------------------------------------------
1 | /// Represents a type that can be used for identifying and comparing for equality.
2 | public typealias Differentiable = ContentIdentifiable & ContentEquatable
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | public init(element: Int, section: Int) {
16 | self.element = element
17 | self.section = section
18 | }
19 | }
20 |
21 | extension ElementPath: CustomDebugStringConvertible {
22 | public var debugDescription: String {
23 | return "[element: \(element), section: \(section)]"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Extensions/AppKitExtension.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS)
2 | import AppKit
3 |
4 | public extension NSTableView {
5 | /// Applies multiple animated updates in stages using `StagedChangeset`.
6 | ///
7 | /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`.
8 | /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it.
9 | /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages.
10 | ///
11 | /// - Parameters:
12 | /// - stagedChangeset: A staged set of changes.
13 | /// - animation: An option to animate the updates.
14 | /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated
15 | /// updates should be stopped and performed reloadData. Default is nil.
16 | /// - setData: A closure that takes the collection as a parameter.
17 | /// The collection should be set to data-source of NSTableView.
18 |
19 | func reload(
20 | using stagedChangeset: StagedChangeset,
21 | with animation: @autoclosure () -> NSTableView.AnimationOptions,
22 | interrupt: ((Changeset) -> Bool)? = nil,
23 | setData: (C) -> Void
24 | ) {
25 | reload(
26 | using: stagedChangeset,
27 | deleteRowsAnimation: animation(),
28 | insertRowsAnimation: animation(),
29 | reloadRowsAnimation: animation(),
30 | interrupt: interrupt,
31 | setData: setData
32 | )
33 | }
34 |
35 | /// Applies multiple animated updates in stages using `StagedChangeset`.
36 | ///
37 | /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`.
38 | /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it.
39 | /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages.
40 | ///
41 | /// - Parameters:
42 | /// - stagedChangeset: A staged set of changes.
43 | /// - deleteRowsAnimation: An option to animate the row deletion.
44 | /// - insertRowsAnimation: An option to animate the row insertion.
45 | /// - reloadRowsAnimation: An option to animate the row reload.
46 | /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated
47 | /// updates should be stopped and performed reloadData. Default is nil.
48 | /// - setData: A closure that takes the collection as a parameter.
49 | /// The collection should be set to data-source of NSTableView.
50 | func reload(
51 | using stagedChangeset: StagedChangeset,
52 | deleteRowsAnimation: @autoclosure () -> NSTableView.AnimationOptions,
53 | insertRowsAnimation: @autoclosure () -> NSTableView.AnimationOptions,
54 | reloadRowsAnimation: @autoclosure () -> NSTableView.AnimationOptions,
55 | interrupt: ((Changeset) -> Bool)? = nil,
56 | setData: (C) -> Void
57 | ) {
58 | if case .none = window, let data = stagedChangeset.last?.data {
59 | setData(data)
60 | return reloadData()
61 | }
62 |
63 | for changeset in stagedChangeset {
64 | if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
65 | setData(data)
66 | return reloadData()
67 | }
68 |
69 | beginUpdates()
70 | setData(changeset.data)
71 |
72 | if !changeset.elementDeleted.isEmpty {
73 | removeRows(at: IndexSet(changeset.elementDeleted.map { $0.element }), withAnimation: deleteRowsAnimation())
74 | }
75 |
76 | if !changeset.elementUpdated.isEmpty {
77 | reloadData(forRowIndexes: IndexSet(changeset.elementUpdated.map { $0.element }), columnIndexes: IndexSet(0..(
116 | using stagedChangeset: StagedChangeset,
117 | interrupt: ((Changeset) -> Bool)? = nil,
118 | setData: (C) -> Void
119 | ) {
120 | if case .none = window, let data = stagedChangeset.last?.data {
121 | setData(data)
122 | return reloadData()
123 | }
124 |
125 | for changeset in stagedChangeset {
126 | if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
127 | setData(data)
128 | return reloadData()
129 | }
130 |
131 | animator().performBatchUpdates({
132 | setData(changeset.data)
133 |
134 | if !changeset.elementDeleted.isEmpty {
135 | deleteItems(at: Set(changeset.elementDeleted.map { IndexPath(item: $0.element, section: $0.section) }))
136 | }
137 |
138 | if !changeset.elementInserted.isEmpty {
139 | insertItems(at: Set(changeset.elementInserted.map { IndexPath(item: $0.element, section: $0.section) }))
140 | }
141 |
142 | if !changeset.elementUpdated.isEmpty {
143 | reloadItems(at: Set(changeset.elementUpdated.map { IndexPath(item: $0.element, section: $0.section) }))
144 | }
145 |
146 | for (source, target) in changeset.elementMoved {
147 | moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section))
148 | }
149 | })
150 | }
151 | }
152 | }
153 | #endif
154 |
--------------------------------------------------------------------------------
/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/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 | public init(_ changesets: C) where C.Element == Changeset {
41 | self.changesets = ContiguousArray(changesets)
42 | }
43 | }
44 |
45 | extension StagedChangeset: RandomAccessCollection, RangeReplaceableCollection, MutableCollection {
46 | public typealias Element = Changeset
47 |
48 | @inlinable
49 | public init() {
50 | self.init([])
51 | }
52 |
53 | @inlinable
54 | public var startIndex: Int {
55 | return changesets.startIndex
56 | }
57 |
58 | @inlinable
59 | public var endIndex: Int {
60 | return changesets.endIndex
61 | }
62 |
63 | @inlinable
64 | public func index(after i: Int) -> Int {
65 | return changesets.index(after: i)
66 | }
67 |
68 | @inlinable
69 | public subscript(position: Int) -> Changeset {
70 | get { return changesets[position] }
71 | set { changesets[position] = newValue }
72 | }
73 |
74 | @inlinable
75 | public mutating func replaceSubrange(_ subrange: R, with newElements: C) where C.Element == Changeset, R.Bound == Int {
76 | changesets.replaceSubrange(subrange, with: newElements)
77 | }
78 | }
79 |
80 | extension StagedChangeset: Equatable where Collection: Equatable {
81 | @inlinable
82 | public static func == (lhs: StagedChangeset, rhs: StagedChangeset) -> Bool {
83 | return lhs.changesets == rhs.changesets
84 | }
85 | }
86 |
87 | extension StagedChangeset: ExpressibleByArrayLiteral {
88 | @inlinable
89 | public init(arrayLiteral elements: Changeset...) {
90 | self.init(elements)
91 | }
92 | }
93 |
94 | extension StagedChangeset: CustomDebugStringConvertible {
95 | public var debugDescription: String {
96 | guard !isEmpty else { return "[]" }
97 |
98 | return "[\n\(map { " \($0.debugDescription.split(separator: "\n").joined(separator: "\n "))" }.joined(separator: ",\n"))\n]"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Tests/AnyDifferentiableTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class AnyDifferentiableTestCase: XCTestCase {
5 | func testHashable() {
6 | let base1 = M(0, false)
7 |
8 | let d1 = AnyDifferentiable(base1)
9 | let d2 = AnyDifferentiable(base1)
10 |
11 | XCTAssertEqual(d1.differenceIdentifier.hashValue, d2.differenceIdentifier.hashValue)
12 | XCTAssertEqual(d1.differenceIdentifier, d2.differenceIdentifier)
13 | XCTAssertTrue(d1.isContentEqual(to: d2))
14 |
15 | let base2 = M(1, false)
16 |
17 | let d3 = AnyDifferentiable(base1)
18 | let d4 = AnyDifferentiable(base2)
19 |
20 | XCTAssertNotEqual(d3.differenceIdentifier.hashValue, d4.differenceIdentifier.hashValue)
21 | XCTAssertNotEqual(d3.differenceIdentifier, d4.differenceIdentifier)
22 | XCTAssertFalse(d3.isContentEqual(to: d4))
23 |
24 | let base3 = M(1, true)
25 |
26 | let d5 = AnyDifferentiable(base2)
27 | let d6 = AnyDifferentiable(base3)
28 |
29 | XCTAssertEqual(d5.differenceIdentifier.hashValue, d6.differenceIdentifier.hashValue)
30 | XCTAssertEqual(d5.differenceIdentifier, d6.differenceIdentifier)
31 | XCTAssertFalse(d5.isContentEqual(to: d6))
32 | }
33 |
34 | func testRedundantWrapping() {
35 | let differentiable = 0
36 | let anyDifferentiable1 = AnyDifferentiable(differentiable)
37 | let anyDifferentiable2 = AnyDifferentiable(anyDifferentiable1)
38 |
39 | XCTAssertEqual(anyDifferentiable1.base as? Int, differentiable)
40 | XCTAssertEqual(anyDifferentiable2.base as? Int, differentiable)
41 | XCTAssertEqual(anyDifferentiable1.base as? Int, anyDifferentiable2.base as? Int)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/ArraySectionTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class ArraySectionTestCase: XCTestCase {
5 | func testReinitialize() {
6 | let s1 = ArraySection(model: D.a, elements: [0])
7 | let s2 = ArraySection(model: s1.model, elements: s1.elements)
8 |
9 | XCTAssertEqual(s1.model.differenceIdentifier, s2.model.differenceIdentifier)
10 | XCTAssertEqual(s1.model.differenceIdentifier.hashValue, s2.model.differenceIdentifier.hashValue)
11 | XCTAssertEqual(s1.elements, s2.elements)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/ChangesetTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class ChangesetTestCase: XCTestCase {
5 | func testchangeCount() {
6 | let c1 = Changeset(data: [()], sectionDeleted: [0, 1])
7 | XCTAssertEqual(c1.sectionChangeCount, 2)
8 | XCTAssertEqual(c1.elementChangeCount, 0)
9 | XCTAssertEqual(c1.changeCount, 2)
10 |
11 | let c2 = Changeset(data: [()], sectionInserted: [0, 1, 2])
12 | XCTAssertEqual(c2.sectionChangeCount, 3)
13 | XCTAssertEqual(c2.elementChangeCount, 0)
14 | XCTAssertEqual(c2.changeCount, 3)
15 |
16 | let c3 = Changeset(data: [()], sectionUpdated: [0, 1, 2, 3])
17 | XCTAssertEqual(c3.sectionChangeCount, 4)
18 | XCTAssertEqual(c3.elementChangeCount, 0)
19 | XCTAssertEqual(c3.changeCount, 4)
20 |
21 | let c4 = Changeset(data: [()], sectionMoved: [(source: 0, target: 1)])
22 | XCTAssertEqual(c4.sectionChangeCount, 1)
23 | XCTAssertEqual(c4.elementChangeCount, 0)
24 | XCTAssertEqual(c4.changeCount, 1)
25 |
26 | let c5 = Changeset(
27 | data: [()],
28 | elementDeleted: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
29 | )
30 | XCTAssertEqual(c5.sectionChangeCount, 0)
31 | XCTAssertEqual(c5.elementChangeCount, 2)
32 | XCTAssertEqual(c5.changeCount, 2)
33 |
34 | let c6 = Changeset(
35 | data: [()],
36 | elementInserted: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
37 | )
38 | XCTAssertEqual(c6.sectionChangeCount, 0)
39 | XCTAssertEqual(c6.elementChangeCount, 2)
40 | XCTAssertEqual(c6.changeCount, 2)
41 |
42 | let c7 = Changeset(
43 | data: [()],
44 | elementUpdated: [ElementPath(element: 0, section: 0), ElementPath(element: 1, section: 1)]
45 | )
46 | XCTAssertEqual(c7.sectionChangeCount, 0)
47 | XCTAssertEqual(c7.elementChangeCount, 2)
48 | XCTAssertEqual(c7.changeCount, 2)
49 |
50 | let c8 = Changeset(
51 | data: [()],
52 | elementMoved: [(source: ElementPath(element: 0, section: 0), target: ElementPath(element: 1, section: 1))]
53 | )
54 | XCTAssertEqual(c8.sectionChangeCount, 0)
55 | XCTAssertEqual(c8.elementChangeCount, 1)
56 | XCTAssertEqual(c8.changeCount, 1)
57 |
58 | let c9 = Changeset(
59 | data: [()],
60 | sectionDeleted: [0],
61 | sectionInserted: [1],
62 | sectionUpdated: [2],
63 | sectionMoved: [(source: 3, target: 4)],
64 | elementDeleted: [ElementPath(element: 5, section: 6)],
65 | elementInserted: [ElementPath(element: 7, section: 8)],
66 | elementUpdated: [ElementPath(element: 9, section: 10)],
67 | elementMoved: [(source: ElementPath(element: 11, section: 12), target: ElementPath(element: 13, section: 14))]
68 | )
69 | XCTAssertEqual(c9.sectionChangeCount, 4)
70 | XCTAssertEqual(c9.elementChangeCount, 4)
71 | XCTAssertEqual(c9.changeCount, 8)
72 | }
73 |
74 | func testHasChanges() {
75 | let c1 = Changeset(data: [()])
76 | XCTAssertFalse(c1.hasSectionChanges)
77 | XCTAssertFalse(c1.hasElementChanges)
78 | XCTAssertFalse(c1.hasChanges)
79 |
80 | let c2 = Changeset(data: [()], sectionDeleted: [0])
81 | XCTAssertTrue(c2.hasSectionChanges)
82 | XCTAssertFalse(c2.hasElementChanges)
83 | XCTAssertTrue(c2.hasChanges)
84 | }
85 |
86 | func testEquatable() {
87 | let data = [0]
88 |
89 | let c1 = Changeset(
90 | data: data,
91 | sectionDeleted: [0, 1, 2],
92 | sectionInserted: [3, 4, 5],
93 | sectionUpdated: [6, 7, 8],
94 | sectionMoved: [
95 | (source: 9, target: 10),
96 | (source: 11, target: 12)
97 | ],
98 | elementDeleted: [ElementPath(element: 13, section: 14), ElementPath(element: 15, section: 16)],
99 | elementInserted: [ElementPath(element: 17, section: 18), ElementPath(element: 19, section: 20)],
100 | elementUpdated: [ElementPath(element: 21, section: 22), ElementPath(element: 23, section: 24)],
101 | elementMoved: [
102 | (source: ElementPath(element: 25, section: 26), target: ElementPath(element: 27, section: 28)),
103 | (source: ElementPath(element: 29, section: 30), target: ElementPath(element: 31, section: 32))
104 | ]
105 | )
106 |
107 | let c2 = Changeset(
108 | data: data,
109 | sectionDeleted: [2, 0, 1],
110 | sectionInserted: [3, 5, 4],
111 | sectionUpdated: [7, 6, 8],
112 | sectionMoved: [
113 | (source: 11, target: 12),
114 | (source: 9, target: 10)
115 | ],
116 | elementDeleted: [ElementPath(element: 15, section: 16), ElementPath(element: 13, section: 14)],
117 | elementInserted: [ElementPath(element: 19, section: 20), ElementPath(element: 17, section: 18)],
118 | elementUpdated: [ElementPath(element: 23, section: 24), ElementPath(element: 21, section: 22)],
119 | elementMoved: [
120 | (source: ElementPath(element: 29, section: 30), target: ElementPath(element: 31, section: 32)),
121 | (source: ElementPath(element: 25, section: 26), target: ElementPath(element: 27, section: 28))
122 | ]
123 | )
124 |
125 | // Should be equal ignoring the order of each changes
126 | XCTAssertEqual(c1, c2)
127 |
128 | let c3 = Changeset(data: data, sectionMoved: [(source: 0, target: 1)])
129 | let c4 = Changeset(data: data, sectionMoved: [(source: 1, target: 0)])
130 |
131 | // Should not be equal if the section move's source and target are exchanged
132 | XCTAssertNotEqual(c3, c4)
133 |
134 | let c5 = Changeset(
135 | data: data,
136 | elementMoved: [
137 | (source: ElementPath(element: 0, section: 1), target: ElementPath(element: 2, section: 3))
138 | ]
139 | )
140 |
141 | let c6 = Changeset(
142 | data: data,
143 | elementMoved: [
144 | (source: ElementPath(element: 1, section: 0), target: ElementPath(element: 2, section: 3))
145 | ]
146 | )
147 |
148 | // Should not be equal if the element move's source and target are exchanged
149 | XCTAssertNotEqual(c5, c6)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/Tests/ContentEquatableTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class ContentEquatableTestCase: XCTestCase {
5 | func testEquatableValue() {
6 | let value1 = D.a
7 | let value2 = D.a
8 | let value3 = D.b
9 |
10 | XCTAssertEqual(value1, value2)
11 | XCTAssertTrue(value1.isContentEqual(to: value2))
12 |
13 | XCTAssertNotEqual(value1, value3)
14 | XCTAssertFalse(value1.isContentEqual(to: value3))
15 | }
16 |
17 | func testOptionalValue() {
18 | let value1: D? = .a
19 | let value2: D? = .a
20 | let value3: D? = .b
21 |
22 | XCTAssertTrue(value1.isContentEqual(to: value2))
23 | XCTAssertFalse(value1.isContentEqual(to: value3))
24 | XCTAssertFalse(value1.isContentEqual(to: nil))
25 | XCTAssertFalse(D?.none.isContentEqual(to: value1))
26 | XCTAssertTrue(D?.none.isContentEqual(to: nil))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/ElementPathTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class ElementPathTestCase: XCTestCase {
5 | func testHashable() {
6 | let e1 = ElementPath(element: 0, section: 0)
7 | let e2 = ElementPath(element: 0, section: 0)
8 |
9 | XCTAssertEqual(e1, e2)
10 | XCTAssertEqual(e1.hashValue, e2.hashValue)
11 |
12 | let e3 = ElementPath(element: 0, section: 0)
13 | let e4 = ElementPath(element: 0, section: 1)
14 |
15 | XCTAssertNotEqual(e3, e4)
16 | XCTAssertNotEqual(e3.hashValue, e4.hashValue)
17 |
18 | let e5 = ElementPath(element: 0, section: 0)
19 | let e6 = ElementPath(element: 1, section: 1)
20 |
21 | XCTAssertNotEqual(e5, e6)
22 | XCTAssertNotEqual(e5.hashValue, e6.hashValue)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/MeasurementTest.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import DifferenceKit
3 |
4 | final class MeasurementTestCase: XCTestCase {
5 | func testMeasureAlgorithmForLinearCollection() {
6 | let source = Array(1...100000)
7 | let target = source.mutated(removeAt: IndexSet(10000..<20000), insertAt: IndexSet(30000..<40000))
8 |
9 | measure {
10 | _ = StagedChangeset(source: source, target: target)
11 | }
12 | }
13 |
14 | func testMeasureAlgorithmForSectionedCollection() {
15 | let source: [ArraySection] = D.allCases.enumerated().map { o, d in
16 | let lowerBound = o * 30000 + 1
17 | let upperBound = lowerBound + 20000
18 | return ArraySection(model: d, elements: Array(lowerBound...upperBound))
19 | }
20 | let target = source.map { section in
21 | ArraySection(
22 | model: section.model,
23 | elements: section.elements.mutated(removeAt: IndexSet(2000..<5000), insertAt: IndexSet(5000..<8000))
24 | )
25 | }
26 |
27 | measure {
28 | _ = StagedChangeset(source: source, target: target)
29 | }
30 | }
31 | }
32 |
33 | private extension RangeReplaceableCollection where Element == Int {
34 | func mutated(removeAt: IndexSet, insertAt: IndexSet) -> Self {
35 | var subject = ContiguousArray(self)
36 | var max = subject.max(by: <) ?? 0
37 |
38 | for range in removeAt.rangeView.reversed() {
39 | subject.removeSubrange(range)
40 | }
41 |
42 | for range in insertAt.rangeView {
43 | let lowerBound = max + 1
44 | let upperBound = lowerBound + range.count
45 | max += range.count
46 | subject.insert(contentsOf: lowerBound..()
9 | let c2 = StagedChangeset<[Int]>()
10 |
11 | // Should be equal if both are empty
12 | XCTAssertEqual(c1, c2)
13 |
14 | let c3 = StagedChangeset([
15 | Changeset(data: data, sectionDeleted: [2, 0, 1])
16 | ])
17 |
18 | let c4 = StagedChangeset([
19 | Changeset(data: data, sectionDeleted: [0, 1, 2])
20 | ])
21 |
22 | // Should be equal ignoring the order of each inner changes
23 | XCTAssertEqual(c3, c4)
24 |
25 | let c5 = StagedChangeset([
26 | Changeset(data: data, sectionDeleted: [0, 1, 2]),
27 | Changeset(data: data, sectionInserted: [3, 4, 5])
28 | ])
29 |
30 | let c6 = StagedChangeset([
31 | Changeset(data: data, sectionInserted: [3, 4, 5]),
32 | Changeset(data: data, sectionDeleted: [0, 1, 2])
33 | ])
34 |
35 | // Should not equal if the order of Changeset is different
36 | XCTAssertNotEqual(c5, c6)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | #if !canImport(ObjectiveC)
2 | import XCTest
3 |
4 | extension AlgorithmTestCase {
5 | // DO NOT MODIFY: This is autogenerated, use:
6 | // `swift test --generate-linuxmain`
7 | // to regenerate.
8 | static let __allTests__AlgorithmTestCase = [
9 | ("testComplicated1", testComplicated1),
10 | ("testComplicated10", testComplicated10),
11 | ("testComplicated11", testComplicated11),
12 | ("testComplicated2", testComplicated2),
13 | ("testComplicated3", testComplicated3),
14 | ("testComplicated4", testComplicated4),
15 | ("testComplicated5", testComplicated5),
16 | ("testComplicated6", testComplicated6),
17 | ("testComplicated7", testComplicated7),
18 | ("testComplicated8", testComplicated8),
19 | ("testComplicated9", testComplicated9),
20 | ("testDeleted", testDeleted),
21 | ("testDuplicated", testDuplicated),
22 | ("testDuplicatedElement", testDuplicatedElement),
23 | ("testDuplicatedSection", testDuplicatedSection),
24 | ("testDuplicatedSectionAndElement", testDuplicatedSectionAndElement),
25 | ("testEmptyChangesets", testEmptyChangesets),
26 | ("testInserted", testInserted),
27 | ("testMixedChanges", testMixedChanges),
28 | ("testMixedSectionChanges", testMixedSectionChanges),
29 | ("testMoved", testMoved),
30 | ("testSameHashValue", testSameHashValue),
31 | ("testSectionDeleted", testSectionDeleted),
32 | ("testSectionedEmptyChangesets", testSectionedEmptyChangesets),
33 | ("testSectionInserted", testSectionInserted),
34 | ("testSectionMoved", testSectionMoved),
35 | ("testSectionUpdated", testSectionUpdated),
36 | ("testUpdated", testUpdated),
37 | ]
38 | }
39 |
40 | extension AnyDifferentiableTestCase {
41 | // DO NOT MODIFY: This is autogenerated, use:
42 | // `swift test --generate-linuxmain`
43 | // to regenerate.
44 | static let __allTests__AnyDifferentiableTestCase = [
45 | ("testHashable", testHashable),
46 | ("testRedundantWrapping", testRedundantWrapping),
47 | ]
48 | }
49 |
50 | extension ArraySectionTestCase {
51 | // DO NOT MODIFY: This is autogenerated, use:
52 | // `swift test --generate-linuxmain`
53 | // to regenerate.
54 | static let __allTests__ArraySectionTestCase = [
55 | ("testReinitialize", testReinitialize),
56 | ]
57 | }
58 |
59 | extension ChangesetTestCase {
60 | // DO NOT MODIFY: This is autogenerated, use:
61 | // `swift test --generate-linuxmain`
62 | // to regenerate.
63 | static let __allTests__ChangesetTestCase = [
64 | ("testchangeCount", testchangeCount),
65 | ("testEquatable", testEquatable),
66 | ("testHasChanges", testHasChanges),
67 | ]
68 | }
69 |
70 | extension ContentEquatableTestCase {
71 | // DO NOT MODIFY: This is autogenerated, use:
72 | // `swift test --generate-linuxmain`
73 | // to regenerate.
74 | static let __allTests__ContentEquatableTestCase = [
75 | ("testEquatableValue", testEquatableValue),
76 | ("testOptionalValue", testOptionalValue),
77 | ]
78 | }
79 |
80 | extension ElementPathTestCase {
81 | // DO NOT MODIFY: This is autogenerated, use:
82 | // `swift test --generate-linuxmain`
83 | // to regenerate.
84 | static let __allTests__ElementPathTestCase = [
85 | ("testHashable", testHashable),
86 | ]
87 | }
88 |
89 | extension MeasurementTestCase {
90 | // DO NOT MODIFY: This is autogenerated, use:
91 | // `swift test --generate-linuxmain`
92 | // to regenerate.
93 | static let __allTests__MeasurementTestCase = [
94 | ("testMeasureAlgorithmForLinearCollection", testMeasureAlgorithmForLinearCollection),
95 | ("testMeasureAlgorithmForSectionedCollection", testMeasureAlgorithmForSectionedCollection),
96 | ]
97 | }
98 |
99 | extension StagedChangesetTestCase {
100 | // DO NOT MODIFY: This is autogenerated, use:
101 | // `swift test --generate-linuxmain`
102 | // to regenerate.
103 | static let __allTests__StagedChangesetTestCase = [
104 | ("testEquatable", testEquatable),
105 | ]
106 | }
107 |
108 | public func __allTests() -> [XCTestCaseEntry] {
109 | return [
110 | testCase(AlgorithmTestCase.__allTests__AlgorithmTestCase),
111 | testCase(AnyDifferentiableTestCase.__allTests__AnyDifferentiableTestCase),
112 | testCase(ArraySectionTestCase.__allTests__ArraySectionTestCase),
113 | testCase(ChangesetTestCase.__allTests__ChangesetTestCase),
114 | testCase(ContentEquatableTestCase.__allTests__ContentEquatableTestCase),
115 | testCase(ElementPathTestCase.__allTests__ElementPathTestCase),
116 | testCase(MeasurementTestCase.__allTests__MeasurementTestCase),
117 | testCase(StagedChangesetTestCase.__allTests__StagedChangesetTestCase),
118 | ]
119 | }
120 | #endif
121 |
--------------------------------------------------------------------------------
/XCConfigs/DifferenceKit.xcconfig:
--------------------------------------------------------------------------------
1 | MACOSX_DEPLOYMENT_TARGET = 10.9
2 | IPHONEOS_DEPLOYMENT_TARGET = 9.0
3 | TVOS_DEPLOYMENT_TARGET = 9.0
4 | WATCHOS_DEPLOYMENT_TARGET = 2.0
5 |
6 | SDKROOT =
7 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator
8 | TARGETED_DEVICE_FAMILY = 1,2,3,4
9 | VALID_ARCHS[sdk=macosx*] = arm64 i386 x86_64
10 | VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s
11 | VALID_ARCHS[sdk=iphonesimulator*] = arm64 i386 x86_64
12 | VALID_ARCHS[sdk=appletv*] = arm64
13 | VALID_ARCHS[sdk=appletvsimulator*] = arm64 x86_64
14 | VALID_ARCHS[sdk=watchos*] = armv7k
15 | VALID_ARCHS[sdk=watchsimulator*] = arm64 i386
16 |
17 | CODE_SIGN_IDENTITY =
18 | CODE_SIGN_STYLE = Manual
19 | INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks
20 | SKIP_INSTALL = YES
21 | DYLIB_COMPATIBILITY_VERSION = 1
22 | DYLIB_CURRENT_VERSION = 1
23 | DYLIB_INSTALL_NAME_BASE = @rpath
24 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks
25 | DEFINES_MODULE = NO
26 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/assets/logo.png
--------------------------------------------------------------------------------
/assets/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/assets/sample.gif
--------------------------------------------------------------------------------
/docs/UI Extensions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | UI Extensions Reference
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
34 |
35 |
41 |
42 |
43 |
44 |
45 | DifferenceKit Reference
46 |
47 | UI Extensions Reference
48 |
49 |
50 |
51 |
103 |
104 |
105 |
106 |
107 |
UI Extensions
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | -
117 |
124 |
125 |
126 |
127 |
128 |
132 |
133 |
Declaration
134 |
135 |
Swift
136 |
public extension UITableView
137 |
138 |
139 |
140 |
141 |
142 |
143 | -
144 |
151 |
152 |
153 |
154 |
155 |
159 |
160 |
Declaration
161 |
162 |
Swift
163 |
public extension UICollectionView
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/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/img/carat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/docs/img/carat.png
--------------------------------------------------------------------------------
/docs/img/dash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/docs/img/dash.png
--------------------------------------------------------------------------------
/docs/img/gh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/docs/img/gh.png
--------------------------------------------------------------------------------
/docs/img/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ra1028/DifferenceKit/02ca1968b10305d4d6771f4005a7ce2c507a8539/docs/img/spinner.gif
--------------------------------------------------------------------------------
/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 | function toggleItem($link, $content) {
12 | var animationDuration = 300;
13 | $link.toggleClass('token-open');
14 | $content.slideToggle(animationDuration);
15 | }
16 |
17 | function itemLinkToContent($link) {
18 | return $link.parent().parent().next();
19 | }
20 |
21 | // On doc load + hash-change, open any targetted item
22 | function openCurrentItemIfClosed() {
23 | if (window.jazzy.docset) {
24 | return;
25 | }
26 | var $link = $(`.token[href="${location.hash}"]`);
27 | $content = itemLinkToContent($link);
28 | if ($content.is(':hidden')) {
29 | toggleItem($link, $content);
30 | }
31 | }
32 |
33 | $(openCurrentItemIfClosed);
34 | $(window).on('hashchange', openCurrentItemIfClosed);
35 |
36 | // On item link ('token') click, toggle its discussion
37 | $('.token').on('click', function(event) {
38 | if (window.jazzy.docset) {
39 | return;
40 | }
41 | var $link = $(this);
42 | toggleItem($link, itemLinkToContent($link));
43 |
44 | // Keeps the document from jumping to the hash.
45 | var href = $link.attr('href');
46 | if (history.pushState) {
47 | history.pushState({}, '', href);
48 | } else {
49 | location.hash = href;
50 | }
51 | event.preventDefault();
52 | });
53 |
54 | // Clicks on links to the current, closed, item need to open the item
55 | $("a:not('.token')").on('click', function() {
56 | if (location == this.href) {
57 | openCurrentItemIfClosed();
58 | }
59 | });
60 |
--------------------------------------------------------------------------------
/docs/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var $typeahead = $('[data-typeahead]');
3 | var $form = $typeahead.parents('form');
4 | var searchURL = $form.attr('action');
5 |
6 | function displayTemplate(result) {
7 | return result.name;
8 | }
9 |
10 | function suggestionTemplate(result) {
11 | var t = '';
12 | t += '' + result.name + '';
13 | if (result.parent_name) {
14 | t += '' + result.parent_name + '';
15 | }
16 | t += '
';
17 | return t;
18 | }
19 |
20 | $typeahead.one('focus', function() {
21 | $form.addClass('loading');
22 |
23 | $.getJSON(searchURL).then(function(searchData) {
24 | const searchIndex = lunr(function() {
25 | this.ref('url');
26 | this.field('name');
27 | this.field('abstract');
28 | for (const [url, doc] of Object.entries(searchData)) {
29 | this.add({url: url, name: doc.name, abstract: doc.abstract});
30 | }
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3,
37 | autoselect: true
38 | },
39 | {
40 | limit: 10,
41 | display: displayTemplate,
42 | templates: { suggestion: suggestionTemplate },
43 | source: function(query, sync) {
44 | const lcSearch = query.toLowerCase();
45 | const results = searchIndex.query(function(q) {
46 | q.term(lcSearch, { boost: 100 });
47 | q.term(lcSearch, {
48 | boost: 10,
49 | wildcard: lunr.Query.wildcard.TRAILING
50 | });
51 | }).map(function(result) {
52 | var doc = searchData[result.ref];
53 | doc.url = result.ref;
54 | return doc;
55 | });
56 | sync(results);
57 | }
58 | }
59 | );
60 | $form.removeClass('loading');
61 | $typeahead.trigger('focus');
62 | });
63 | });
64 |
65 | var baseURL = searchURL.slice(0, -"search.json".length);
66 |
67 | $typeahead.on('typeahead:select', function(e, result) {
68 | window.location = baseURL + result.url;
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/test-linux.sh:
--------------------------------------------------------------------------------
1 | # !/bin/bash
2 |
3 | set -e
4 |
5 | if [[ `uname` == "Darwin" ]]; then
6 | if [[ -z `which docker-machine 2>/dev/null` || -z `which virtualbox 2>/dev/null` ]] ; then
7 | echo "Install docker-machine and virtualbox ahead."
8 | exit -1
9 | fi
10 |
11 | if [[ ! $(docker info 2>/dev/null) ]]; then
12 | echo "Launch docker-machine ahead."
13 | exit -1
14 | fi
15 |
16 | DOCKER_HOST_NAME=com.ryo.DifferenceKit.test
17 | WORKING_DIR=$(pwd)
18 |
19 | echo "Starting to running tests on Linux by Docker..."
20 | docker-machine create --driver virtualbox $DOCKER_HOST_NAME || true
21 | docker run -v $WORKING_DIR:$WORKING_DIR -w $WORKING_DIR -it --privileged swift:latest bash -c "bash $0" || true
22 | docker-machine stop $DOCKER_HOST_NAME || true
23 | docker-machine rm -f $DOCKER_HOST_NAME || true
24 | echo "Finish"
25 |
26 | elif [[ `uname` == "Linux" ]]; then
27 | swift build
28 | swift test
29 |
30 | else
31 | echo "Unsupported OS (`uname`)"
32 | exit -1
33 |
34 | fi
35 |
--------------------------------------------------------------------------------