├── .version ├── .swift-version ├── Cartfile ├── Cartfile.resolved ├── docs ├── img │ ├── gh.png │ ├── carat.png │ ├── dash.png │ └── spinner.gif ├── badge.svg ├── js │ ├── jazzy.js │ └── jazzy.search.js ├── css │ ├── highlight.css │ └── jazzy.css ├── Snapshot.html └── DataSources.html ├── Gemfile ├── assets ├── mountains.gif └── insertion_sort.gif ├── Examples ├── Example-iOS │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Extension.swift │ ├── LabelCell.swift │ ├── AppDelegate.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── TopViewController.swift │ ├── TopViewController.xib │ ├── LabelCell.xib │ ├── MountainsViewController.swift │ ├── InsertionSortViewController.xib │ ├── MountainsViewController.xib │ └── InsertionSortViewController.swift ├── Example-macOS │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Example_macOS.entitlements │ ├── LabelItem.swift │ ├── Info.plist │ └── MountainsViewController.swift ├── DiffableDataSourcesExamples.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Example-iOS.xcscheme ├── README.md ├── DiffableDataSourcesExamples.xcworkspace │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata └── MountainsRawData.swift ├── .gitmodules ├── Sources ├── Internal │ ├── HashableExtension.swift │ ├── UniversalError.swift │ ├── MainThreadSerialDispatcher.swift │ ├── DiffableDataSourceCore.swift │ └── SnapshotStructure.swift ├── Info.plist ├── AppKit │ └── CocoaCollectionViewDiffableDataSource.swift ├── UIKit │ ├── CollectionViewDiffableDataSource.swift │ └── TableViewDiffableDataSource.swift └── DiffableDataSourceSnapshot.swift ├── DiffableDataSources.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Tests.xcscheme │ └── DiffableDataSources.xcscheme ├── DiffableDataSources.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Makefile ├── Package.resolved ├── .swiftlint.yml ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.md │ ├── QUESTION.md │ └── BUG_REPORT.md └── PULL_REQUEST_TEMPLATE.md ├── .jazzy.yml ├── Tests ├── Info.plist ├── MainThreadSerialDispatcherTests.swift ├── CocoaCollectionViewDiffableDataSourceTests.swift ├── CollectionViewDiffableDataSourceTests.swift └── TableViewDiffableDataSourceTests.swift ├── Package.swift ├── XCConfigs └── DiffableDataSources.xcconfig ├── DiffableDataSources.podspec ├── azure-pipelines.yml ├── Gemfile.lock ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md └── LICENSE /.version: -------------------------------------------------------------------------------- 1 | 0.5.0 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ra1028/DifferenceKit" ~> 1.2 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ra1028/DifferenceKit" "1.2.0" 2 | -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/docs/img/dash.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '1.8.4' 4 | gem 'jazzy', '0.11.2' 5 | -------------------------------------------------------------------------------- /assets/mountains.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/assets/mountains.gif -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/docs/img/spinner.gif -------------------------------------------------------------------------------- /assets/insertion_sort.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ra1028/DiffableDataSources/HEAD/assets/insertion_sort.gif -------------------------------------------------------------------------------- /Examples/Example-iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Example-macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Example-macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | final class AppDelegate: NSObject, NSApplicationDelegate {} 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/DifferenceKit"] 2 | path = Carthage/Checkouts/DifferenceKit 3 | url = https://github.com/ra1028/DifferenceKit.git 4 | -------------------------------------------------------------------------------- /Sources/Internal/HashableExtension.swift: -------------------------------------------------------------------------------- 1 | extension Hashable { 2 | func isEqualHash(to other: Self) -> Bool { 3 | return hashValue == other.hashValue && self == other 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Internal/UniversalError.swift: -------------------------------------------------------------------------------- 1 | func universalError(_ message: String, file: StaticString = #file, line: UInt = #line) -> Never { 2 | fatalError("[DiffableDataSources] \(message)", file: file, line: line) 3 | } 4 | -------------------------------------------------------------------------------- /DiffableDataSources.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/DiffableDataSourcesExamples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 |

2 | DiffableDataSources Examples 3 |

4 | 5 | ## How to Run 6 | 7 | 1. Clone the DiffableDataSources repository. 8 | 1. Checkout the dependencies using terminal command `$ make setup` at the project root directory. 9 | 1. Open example project workspace. 10 | 1. Run. -------------------------------------------------------------------------------- /Examples/Example-iOS/Extension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewCell { 4 | static var name: String { 5 | return String(describing: self) 6 | } 7 | } 8 | 9 | extension UICollectionViewCell { 10 | static var name: String { 11 | return String(describing: self) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DiffableDataSources.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Example-iOS/LabelCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class LabelCell: UICollectionViewCell { 4 | @IBOutlet var label: UILabel! 5 | 6 | override func awakeFromNib() { 7 | super.awakeFromNib() 8 | 9 | layer.borderWidth = 1 10 | layer.borderColor = UIColor.gray.cgColor 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DiffableDataSources.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/DiffableDataSourcesExamples.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/DiffableDataSourcesExamples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DiffableDataSources.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | setup: 2 | git submodule update --init --recursive 3 | 4 | carthage-submodule: 5 | carthage update --no-build --use-submodules 6 | 7 | gems-install: 8 | bundle install --path vendor/bundle 9 | 10 | docs-gen: 11 | bundle exec jazzy --config .jazzy.yml 12 | 13 | lib-lint: 14 | bundle exec pod lib lint 15 | 16 | pod-release: 17 | bundle exec pod trunk push --allow-warnings 18 | -------------------------------------------------------------------------------- /Examples/Example-macOS/Example_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DifferenceKit", 6 | "repositoryURL": "https://github.com/ra1028/DifferenceKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "62745d7780deef4a023a792a1f8f763ec7bf9705", 10 | "version": "1.2.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Examples/DiffableDataSourcesExamples.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/realm/SwiftLint 2 | 3 | excluded: 4 | - .build 5 | - Carthage 6 | - Examples 7 | 8 | disabled_rules: 9 | - type_name 10 | - identifier_name 11 | - generic_type_name 12 | - force_cast 13 | 14 | nesting: 15 | type_level: 16 | warning: 2 17 | 18 | line_length: 19 | warning: 200 20 | 21 | file_length: 22 | warning: 600 23 | 24 | type_body_length: 25 | warning: 400 26 | 27 | function_body_length: 28 | warning: 50 29 | 30 | cyclomatic_complexity: 31 | warning: 12 32 | 33 | statement_position: 34 | statement_mode: uncuddled_else 35 | -------------------------------------------------------------------------------- /.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 | ## Carthage 34 | Carthage/* 35 | !Carthage/Checkouts 36 | 37 | ## Swift Package Manager 38 | .build 39 | -------------------------------------------------------------------------------- /.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 and documents. 8 | - [ ] Searched existing 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 and documents. 8 | - [ ] Searched existing 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 | - version: 21 | 22 | - Swift version: 23 | 24 | - iOS version: 25 | 26 | - Xcode version: 27 | 28 | - Devices/Simulators: 29 | 30 | - CocoaPods/Carthage version: 31 | -------------------------------------------------------------------------------- /Examples/Example-iOS/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]? = nil) -> Bool { 8 | let window = UIWindow() 9 | let navigationController = UINavigationController(rootViewController: TopViewController()) 10 | navigationController.navigationBar.tintColor = .black 11 | window.rootViewController = navigationController 12 | window.makeKeyAndVisible() 13 | self.window = window 14 | 15 | return true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/realm/jazzy 2 | 3 | author: Ryo Aoyama 4 | author_url: https://github.com/ra1028 5 | github_url: https://github.com/ra1028/DiffableDataSources 6 | module: DiffableDataSources 7 | readme: README.md 8 | output: docs 9 | theme: fullwidth 10 | clean: true 11 | skip_undocumented: true 12 | xcodebuild_arguments: 13 | - -workspace 14 | - DiffableDataSources.xcworkspace 15 | - -scheme 16 | - DiffableDataSources 17 | - -sdk 18 | - iphonesimulator 19 | 20 | exclude: 21 | - Sources/AppKit/CocoaCollectionViewDiffableDataSource.swift 22 | 23 | custom_categories: 24 | - name: DataSources 25 | children: 26 | - TableViewDiffableDataSource 27 | - CollectionViewDiffableDataSource 28 | - name: Snapshot 29 | children: 30 | - DiffableDataSourceSnapshot 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DiffableDataSources", 7 | platforms: [ 8 | .iOS(.v9), .macOS(.v10_11), .tvOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "DiffableDataSources", targets: ["DiffableDataSources"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ra1028/DifferenceKit.git", .upToNextMinor(from: "1.2.0")) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "DiffableDataSources", 19 | dependencies: ["DifferenceKit"], 20 | path: "Sources" 21 | ), 22 | .testTarget( 23 | name: "DiffableDataSourcesTests", 24 | dependencies: ["DiffableDataSources"], 25 | path: "Tests" 26 | ) 27 | ], 28 | swiftLanguageVersions: [.v5] 29 | ) 30 | -------------------------------------------------------------------------------- /XCConfigs/DiffableDataSources.xcconfig: -------------------------------------------------------------------------------- 1 | MACOSX_DEPLOYMENT_TARGET = 10.11 2 | IPHONEOS_DEPLOYMENT_TARGET = 9.0 3 | TVOS_DEPLOYMENT_TARGET = 9.0 4 | 5 | SDKROOT = 6 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator 7 | TARGETED_DEVICE_FAMILY = 1,2,3 8 | VALID_ARCHS[sdk=macosx*] = arm64 i386 x86_64 9 | VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s 10 | VALID_ARCHS[sdk=iphonesimulator*] = arm64 i386 x86_64 11 | VALID_ARCHS[sdk=appletv*] = arm64 12 | VALID_ARCHS[sdk=appletvsimulator*] = arm64 x86_64 13 | 14 | CODE_SIGN_IDENTITY = 15 | CODE_SIGN_STYLE = Manual 16 | INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks 17 | SKIP_INSTALL = YES 18 | DYLIB_COMPATIBILITY_VERSION = 1 19 | DYLIB_CURRENT_VERSION = 1 20 | DYLIB_INSTALL_NAME_BASE = @rpath 21 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks 22 | DEFINES_MODULE = NO 23 | -------------------------------------------------------------------------------- /Sources/Internal/MainThreadSerialDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class MainThreadSerialDispatcher { 4 | private let executingCount = UnsafeMutablePointer.allocate(capacity: 1) 5 | 6 | init() { 7 | executingCount.initialize(to: 0) 8 | } 9 | 10 | deinit { 11 | executingCount.deinitialize(count: 1) 12 | executingCount.deallocate() 13 | } 14 | 15 | func dispatch(_ action: @escaping () -> Void) { 16 | let count = OSAtomicIncrement32(executingCount) 17 | 18 | if Thread.isMainThread && count == 1 { 19 | action() 20 | OSAtomicDecrement32(executingCount) 21 | } 22 | else { 23 | DispatchQueue.main.async { [weak self] in 24 | guard let self = self else { 25 | return 26 | } 27 | 28 | action() 29 | OSAtomicDecrement32(self.executingCount) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.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 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/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 and documents. 9 | - [ ] Searched existing 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 | - version: 33 | 34 | - Swift version: 35 | 36 | - iOS version: 37 | 38 | - Xcode version: 39 | 40 | - Devices/Simulators: 41 | 42 | - CocoaPods/Carthage version: 43 | -------------------------------------------------------------------------------- /Examples/Example-macOS/LabelItem.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class LabelItem: NSCollectionViewItem { 4 | static var itemIdentifier: NSUserInterfaceItemIdentifier { 5 | return NSUserInterfaceItemIdentifier(String(describing: self)) 6 | } 7 | 8 | let label = NSTextField() 9 | 10 | override func loadView() { 11 | view = NSView() 12 | } 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | label.textColor = .gray 18 | label.font = .systemFont(ofSize: 16) 19 | label.isEditable = false 20 | label.translatesAutoresizingMaskIntoConstraints = false 21 | view.addSubview(label) 22 | 23 | let constraints = [ 24 | label.topAnchor.constraint(equalTo: view.topAnchor), 25 | label.bottomAnchor.constraint(equalTo: view.bottomAnchor), 26 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor), 27 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor) 28 | ] 29 | NSLayoutConstraint.activate(constraints) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 98% 23 | 24 | 25 | 98% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Examples/Example-macOS/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 | NSHumanReadableCopyright 26 | Copyright © 2019 Ryo Aoyama. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Examples/Example-macOS/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 | } -------------------------------------------------------------------------------- /DiffableDataSources.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'DiffableDataSources' 3 | spec.version = `cat .version` 4 | spec.author = { 'ra1028' => 'r.fe51028.r@gmail.com' } 5 | spec.homepage = 'https://github.com/ra1028/DiffableDataSources' 6 | spec.documentation_url = 'https://ra1028.github.io/DiffableDataSources' 7 | spec.summary = 'A library for backporting UITableView/UICollectionViewDiffableDataSource.' 8 | spec.source = { :git => 'https://github.com/ra1028/DiffableDataSources.git', :tag => spec.version.to_s } 9 | spec.license = { :type => 'Apache 2.0', :file => 'LICENSE' } 10 | spec.requires_arc = true 11 | spec.swift_versions = ["5.0", "5.1"] 12 | 13 | differenekit_version = '~> 1.1' 14 | 15 | spec.ios.dependency 'DifferenceKit/UIKitExtension', differenekit_version 16 | spec.tvos.dependency 'DifferenceKit/UIKitExtension', differenekit_version 17 | spec.osx.dependency 'DifferenceKit/AppKitExtension', differenekit_version 18 | 19 | spec.source_files = 'Sources/Internal/*.swift', 'Sources/*.swift' 20 | spec.ios.source_files = 'Sources/UIKit/*.swift' 21 | spec.tvos.source_files = 'Sources/UIKit/*.swift' 22 | spec.osx.source_files = 'Sources/AppKit/*.swift' 23 | 24 | spec.ios.frameworks = 'UIKit' 25 | spec.tvos.frameworks = 'UIKit' 26 | spec.osx.frameworks = 'Appkit' 27 | 28 | spec.ios.deployment_target = '9.0' 29 | spec.tvos.deployment_target = '9.0' 30 | spec.osx.deployment_target = '10.11' 31 | end 32 | -------------------------------------------------------------------------------- /Examples/Example-iOS/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Tests/MainThreadSerialDispatcherTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DiffableDataSources 3 | 4 | final class MainThreadSerialDispatcherTests: XCTestCase { 5 | func testMainThread() { 6 | let dispatcher = MainThreadSerialDispatcher() 7 | let queue = DispatchQueue.global() 8 | let expectation = self.expectation(description: "testMainThread") 9 | 10 | queue.async { 11 | dispatcher.dispatch { 12 | XCTAssertTrue(Thread.isMainThread) 13 | expectation.fulfill() 14 | } 15 | } 16 | 17 | waitForExpectations(timeout: 1) 18 | } 19 | 20 | func testMainThreadSerial() { 21 | let dispatcher = MainThreadSerialDispatcher() 22 | let queue = DispatchQueue.global() 23 | let expectation = self.expectation(description: "testMainThreadSerial") 24 | 25 | var array = [Int]() 26 | 27 | let group = DispatchGroup() 28 | 29 | queue.async(group: group) { 30 | dispatcher.dispatch { 31 | array.append(0) 32 | } 33 | } 34 | 35 | group.wait() 36 | 37 | queue.async(group: group) { 38 | dispatcher.dispatch { 39 | array.append(1) 40 | } 41 | 42 | dispatcher.dispatch { 43 | array.append(2) 44 | } 45 | } 46 | 47 | group.wait() 48 | 49 | dispatcher.dispatch { 50 | array.append(3) 51 | expectation.fulfill() 52 | } 53 | 54 | waitForExpectations(timeout: 1) { _ in 55 | XCTAssertEqual(array, [0, 1, 2, 3]) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Examples/Example-iOS/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 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | jobs: 5 | - job: macOS 6 | pool: 7 | vmImage: 'macOS 10.14' 8 | strategy: 9 | maxParallel: 10 10 | matrix: 11 | xcode10_2: 12 | DEVELOPER_DIR: /Applications/Xcode_10.2.app 13 | xcode11_2_1: 14 | DEVELOPER_DIR: /Applications/Xcode_11.2.1.app 15 | steps: 16 | - checkout: self 17 | submodules: true 18 | - script: xcodebuild -version 19 | displayName: xcodebuild -version 20 | condition: succeededOrFailed() 21 | - script: | 22 | set -o pipefail && 23 | xcodebuild build-for-testing test-without-building -workspace DiffableDataSources.xcworkspace -scheme DiffableDataSources -configuration Release ENABLE_TESTABILITY=YES | 24 | xcpretty -c -r junit -o build/reports/xcodebuild-macOS.xml 25 | displayName: xcodebuild test macOS 26 | condition: succeededOrFailed() 27 | - script: | 28 | set -o pipefail && 29 | xcodebuild build-for-testing test-without-building -workspace DiffableDataSources.xcworkspace -scheme DiffableDataSources -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' ENABLE_TESTABILITY=YES | 30 | xcpretty -c -r junit -o build/reports/xcodebuild-iOS.xml 31 | displayName: xcodebuild test iOS 32 | condition: succeededOrFailed() 33 | - script: | 34 | set -o pipefail && 35 | xcodebuild build-for-testing test-without-building -workspace DiffableDataSources.xcworkspace -scheme DiffableDataSources -configuration Release -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' ENABLE_TESTABILITY=YES | 36 | xcpretty -c -r junit -o build/reports/xcodebuild-tvOS.xml 37 | displayName: xcodebuild test tvOS 38 | condition: succeededOrFailed() 39 | - task: PublishTestResults@2 40 | inputs: 41 | testRunner: JUnit 42 | testResultsFiles: build/reports/** 43 | condition: succeededOrFailed() 44 | -------------------------------------------------------------------------------- /Examples/Example-iOS/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 | } -------------------------------------------------------------------------------- /DiffableDataSources.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Examples/Example-iOS/TopViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DiffableDataSources 3 | 4 | final class TopViewController: UIViewController { 5 | enum Section { 6 | case main 7 | } 8 | 9 | enum Item { 10 | case mountains 11 | case insertionSort 12 | } 13 | 14 | @IBOutlet private var tableView: UITableView! 15 | 16 | private lazy var dataSource = TableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in 17 | let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.name, for: indexPath) 18 | cell.textLabel?.text = item.title 19 | cell.accessoryType = .disclosureIndicator 20 | return cell 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "DiffableDataSources" 27 | 28 | tableView.delegate = self 29 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.name) 30 | tableView.rowHeight = 60 31 | tableView.contentInset.top = 30 32 | 33 | reset() 34 | } 35 | 36 | override func viewWillAppear(_ animated: Bool) { 37 | super.viewWillAppear(animated) 38 | 39 | if let indexPath = tableView.indexPathForSelectedRow { 40 | tableView.deselectRow(at: indexPath, animated: true) 41 | } 42 | } 43 | 44 | func reset() { 45 | var snapshot = DiffableDataSourceSnapshot() 46 | snapshot.appendSections([.main]) 47 | snapshot.appendItems([.mountains, .insertionSort]) 48 | dataSource.apply(snapshot) 49 | } 50 | } 51 | 52 | extension TopViewController: UITableViewDelegate { 53 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 54 | guard let item = dataSource.itemIdentifier(for: indexPath) else { 55 | return 56 | } 57 | 58 | let viewController: UIViewController 59 | 60 | switch item { 61 | case .mountains: 62 | viewController = MountainsViewController() 63 | 64 | case .insertionSort: 65 | viewController = InsertionSortViewController() 66 | } 67 | 68 | navigationController?.pushViewController(viewController, animated: true) 69 | } 70 | } 71 | 72 | private extension TopViewController.Item { 73 | var title: String { 74 | switch self { 75 | case .mountains: 76 | return "🗻 Mountains" 77 | 78 | case .insertionSort: 79 | return "📱 Insertion Sort" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Examples/Example-iOS/TopViewController.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 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 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.3.0) 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.3) 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.13.0) 72 | molinillo (0.6.6) 73 | mustache (1.1.1) 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.13.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.2 105 | -------------------------------------------------------------------------------- /Examples/Example-macOS/MountainsViewController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import DiffableDataSources 3 | 4 | final class MountainsViewController: NSViewController { 5 | enum Section { 6 | case main 7 | } 8 | 9 | struct Mountain: Hashable { 10 | var name: String 11 | 12 | func contains(_ filter: String) -> Bool { 13 | guard !filter.isEmpty else { 14 | return true 15 | } 16 | 17 | let lowercasedFilter = filter.lowercased() 18 | return name.lowercased().contains(lowercasedFilter) 19 | } 20 | } 21 | 22 | @IBOutlet private var searchField: NSSearchField! 23 | @IBOutlet private var collectionView: NSCollectionView! 24 | 25 | private lazy var dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, mountain in 26 | let item = collectionView.makeItem(withIdentifier: LabelItem.itemIdentifier, for: indexPath) as! LabelItem 27 | item.label.stringValue = mountain.name 28 | return item 29 | } 30 | 31 | private let allMountains: [Mountain] = mountainsRawData.components(separatedBy: .newlines).map { line in 32 | let name = line.components(separatedBy: ",")[0] 33 | return Mountain(name: name) 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | title = "Mountains Search" 40 | 41 | collectionView.delegate = self 42 | collectionView.register(LabelItem.self, forItemWithIdentifier: LabelItem.itemIdentifier) 43 | 44 | search(filter: "") 45 | } 46 | 47 | func search(filter: String) { 48 | let mountains = allMountains.lazy 49 | .filter { $0.contains(filter) } 50 | .sorted { $0.name < $1.name } 51 | 52 | var snapshot = DiffableDataSourceSnapshot() 53 | snapshot.appendSections([.main]) 54 | snapshot.appendItems(mountains) 55 | dataSource.apply(snapshot) 56 | } 57 | 58 | @IBAction func searchFieldDidChangeText(_ sender: NSSearchField) { 59 | search(filter: sender.stringValue) 60 | } 61 | } 62 | 63 | extension MountainsViewController: NSCollectionViewDelegateFlowLayout { 64 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 65 | return 10 66 | } 67 | 68 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 69 | return 10 70 | } 71 | 72 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, insetForSectionAt section: Int) -> NSEdgeInsets { 73 | return NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 74 | } 75 | 76 | func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { 77 | let column = 3 78 | let width = (collectionView.bounds.width - 10 * CGFloat(column + 1)) / CGFloat(column) 79 | return CGSize(width: width, height: 44) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Examples/Example-iOS/LabelCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Examples/Example-iOS/MountainsViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DiffableDataSources 3 | 4 | final class MountainsViewController: UIViewController { 5 | enum Section { 6 | case main 7 | } 8 | 9 | struct Mountain: Hashable { 10 | var name: String 11 | 12 | func contains(_ filter: String) -> Bool { 13 | guard !filter.isEmpty else { 14 | return true 15 | } 16 | 17 | let lowercasedFilter = filter.lowercased() 18 | return name.lowercased().contains(lowercasedFilter) 19 | } 20 | } 21 | 22 | @IBOutlet private var searchBar: UISearchBar! 23 | @IBOutlet private var collectionView: UICollectionView! 24 | 25 | private lazy var dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, mountain in 26 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LabelCell.name, for: indexPath) as! LabelCell 27 | cell.label.text = mountain.name 28 | return cell 29 | } 30 | 31 | private let allMountains: [Mountain] = mountainsRawData.components(separatedBy: .newlines).map { line in 32 | let name = line.components(separatedBy: ",")[0] 33 | return Mountain(name: name) 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | title = "Mountains Search" 40 | 41 | searchBar.delegate = self 42 | collectionView.delegate = self 43 | collectionView.register(UINib(nibName: LabelCell.name, bundle: .main), forCellWithReuseIdentifier: LabelCell.name) 44 | 45 | search(filter: "") 46 | } 47 | 48 | func search(filter: String) { 49 | let mountains = allMountains.lazy 50 | .filter { $0.contains(filter) } 51 | .sorted { $0.name < $1.name } 52 | 53 | var snapshot = DiffableDataSourceSnapshot() 54 | snapshot.appendSections([.main]) 55 | snapshot.appendItems(mountains) 56 | dataSource.apply(snapshot) 57 | } 58 | } 59 | 60 | extension MountainsViewController: UISearchBarDelegate { 61 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 62 | search(filter: searchText) 63 | } 64 | } 65 | 66 | extension MountainsViewController: UICollectionViewDelegateFlowLayout { 67 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 68 | return 10 69 | } 70 | 71 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 72 | return 10 73 | } 74 | 75 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 76 | return UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 77 | } 78 | 79 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 80 | let column = 2 81 | let width = (collectionView.bounds.width - 10 * CGFloat(column + 1)) / CGFloat(column) 82 | return CGSize(width: width, height: 32) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Examples/Example-iOS/InsertionSortViewController.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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /Examples/DiffableDataSourcesExamples.xcodeproj/xcshareddata/xcschemes/Example-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🎈 Contributing 2 | 3 | First of all, thanks for your interest. 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 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 | ### Lint 23 | 24 | Please introduce [SwiftLint](https://github.com/realm/SwiftLint) into your environment before start writing the code. 25 | Xcode automatically runs lint in the build phase. 26 | 27 | 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. 28 | 29 | ### Test 30 | 31 | The test will tells us the validity of your code. 32 | All codes entering the master must pass the all tests. 33 | If you change the code or add new features, you should add tests. 34 | 35 | ### Documentation 36 | 37 | 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. 38 | Documentation template is inserted automatically by using Xcode shortcut **⌥⌘/**. 39 | Our document style is slightly different from the template. The example is below. 40 | 41 | ```swift 42 | /// The example class for documentation. 43 | final class Foo { 44 | /// A property value. 45 | let prop: Int 46 | 47 | /// Create a new foo with a param. 48 | /// 49 | /// - Parameters: 50 | /// - param: An Int value for prop. 51 | init(param: Int) { 52 | prop = param 53 | } 54 | 55 | /// Returns a string value concatenating `param1` and `param2`. 56 | /// 57 | /// - Parameters: 58 | /// - param1: An Int value for prefix. 59 | /// - param2: A String value for suffix. 60 | /// 61 | /// - Returns: A string concatenating given params. 62 | func bar(param1: Int, param2: String) -> String { 63 | return "\(param1)" + param2 64 | } 65 | } 66 | ``` 67 | 68 | ## [Developer's Certificate of Origin 1.1](https://elinux.org/Developer_Certificate_Of_Origin) 69 | By making a contribution to this project, I certify that: 70 | 71 | (a) The contribution was created in whole or in part by me and I 72 | have the right to submit it under the open source license 73 | indicated in the file; or 74 | 75 | (b) The contribution is based upon previous work that, to the best 76 | of my knowledge, is covered under an appropriate open source 77 | license and I have the right under that license to submit that 78 | work with modifications, whether created in whole or in part 79 | by me, under the same open source license (unless I am 80 | permitted to submit under a different license), as indicated 81 | in the file; or 82 | 83 | (c) The contribution was provided directly to me by some other 84 | person who certified (a), (b) or (c) and I have not modified 85 | it. 86 | 87 | (d) I understand and agree that this project and the contribution 88 | are public and that a record of the contribution (including all 89 | personal information I submit with it, including my sign-off) is 90 | maintained indefinitely and may be redistributed consistent with 91 | this project or the open source license(s) involved. 92 | -------------------------------------------------------------------------------- /Sources/Internal/DiffableDataSourceCore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import QuartzCore 3 | import DifferenceKit 4 | 5 | final class DiffableDataSourceCore { 6 | typealias Section = SnapshotStructure.Section 7 | 8 | private let dispatcher = MainThreadSerialDispatcher() 9 | private var currentSnapshot = DiffableDataSourceSnapshot() 10 | private var sections: [Section] = [] 11 | 12 | func apply( 13 | _ snapshot: DiffableDataSourceSnapshot, 14 | view: View?, 15 | animatingDifferences: Bool, 16 | performUpdates: @escaping (View, StagedChangeset<[Section]>, @escaping ([Section]) -> Void) -> Void, 17 | completion: (() -> Void)? 18 | ) { 19 | dispatcher.dispatch { [weak self] in 20 | guard let self = self else { 21 | return 22 | } 23 | 24 | self.currentSnapshot = snapshot 25 | 26 | let newSections = snapshot.structure.sections 27 | 28 | guard let view = view else { 29 | return self.sections = newSections 30 | } 31 | 32 | func performDiffingUpdates() { 33 | let changeset = StagedChangeset(source: self.sections, target: newSections) 34 | performUpdates(view, changeset) { sections in 35 | self.sections = sections 36 | } 37 | } 38 | 39 | CATransaction.begin() 40 | CATransaction.setCompletionBlock(completion) 41 | 42 | if animatingDifferences { 43 | performDiffingUpdates() 44 | } 45 | else { 46 | CATransaction.setDisableActions(true) 47 | performDiffingUpdates() 48 | } 49 | 50 | CATransaction.commit() 51 | } 52 | } 53 | 54 | func snapshot() -> DiffableDataSourceSnapshot { 55 | var snapshot = DiffableDataSourceSnapshot() 56 | snapshot.structure.sections = currentSnapshot.structure.sections 57 | return snapshot 58 | } 59 | 60 | func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { 61 | guard 0.. ItemIdentifierType { 75 | guard let itemIdentifier = itemIdentifier(for: indexPath) else { 76 | universalError("Item not found at the specified index path(\(indexPath)).") 77 | } 78 | 79 | return itemIdentifier 80 | } 81 | 82 | func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { 83 | let indexPathMap: [ItemIdentifierType: IndexPath] = sections.enumerated() 84 | .reduce(into: [:]) { result, section in 85 | for (itemIndex, item) in section.element.elements.enumerated() { 86 | result[item.differenceIdentifier] = IndexPath( 87 | item: itemIndex, 88 | section: section.offset 89 | ) 90 | } 91 | } 92 | return indexPathMap[itemIdentifier] 93 | } 94 | 95 | func numberOfSections() -> Int { 96 | return sections.count 97 | } 98 | 99 | func numberOfItems(inSection section: Int) -> Int { 100 | return sections[section].elements.count 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /DiffableDataSources.xcodeproj/xcshareddata/xcschemes/DiffableDataSources.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 | -------------------------------------------------------------------------------- /Examples/Example-iOS/MountainsViewController.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 | -------------------------------------------------------------------------------- /Sources/AppKit/CocoaCollectionViewDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | 3 | import AppKit 4 | import DifferenceKit 5 | 6 | /// A class for backporting `NSCollectionViewDiffableDataSource` introduced in macOS 10.15+. 7 | /// Represents the data model object for `NSCollectionView` that can be applies the 8 | /// changes with automatic diffing. 9 | open class CocoaCollectionViewDiffableDataSource: NSObject, NSCollectionViewDataSource { 10 | /// The type of closure providing the item. 11 | public typealias ItemProvider = (NSCollectionView, IndexPath, ItemIdentifierType) -> NSCollectionViewItem? 12 | 13 | private weak var collectionView: NSCollectionView? 14 | private let itemProvider: ItemProvider 15 | private let core = DiffableDataSourceCore() 16 | 17 | /// Creates a new data source. 18 | /// 19 | /// - Parameters: 20 | /// - collectionView: A collection view instance to be managed. 21 | /// - itemProvider: A closure to make the item. 22 | public init(collectionView: NSCollectionView, itemProvider: @escaping ItemProvider) { 23 | self.collectionView = collectionView 24 | self.itemProvider = itemProvider 25 | super.init() 26 | 27 | collectionView.dataSource = self 28 | } 29 | 30 | /// Applies given snapshot to perform automatic diffing update. 31 | /// 32 | /// - Parameters: 33 | /// - snapshot: A snapshot object to be applied to data model. 34 | /// - animatingDifferences: A Boolean value indicating whether to update with 35 | /// diffing animation. 36 | /// - completion: An optional completion block which is called when the complete 37 | /// performing updates. 38 | public func apply(_ snapshot: DiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { 39 | core.apply( 40 | snapshot, 41 | view: collectionView, 42 | animatingDifferences: animatingDifferences, 43 | performUpdates: { collectionView, changeset, setSections in 44 | collectionView.reload(using: changeset, setData: setSections) 45 | }, 46 | completion: completion 47 | ) 48 | } 49 | 50 | /// Returns a new snapshot object of current state. 51 | /// 52 | /// - Returns: A new snapshot object of current state. 53 | public func snapshot() -> DiffableDataSourceSnapshot { 54 | return core.snapshot() 55 | } 56 | 57 | /// Returns an item identifier for given index path. 58 | /// 59 | /// - Parameters: 60 | /// - indexPath: An index path for the item identifier. 61 | /// 62 | /// - Returns: An item identifier for given index path. 63 | public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { 64 | return core.itemIdentifier(for: indexPath) 65 | } 66 | 67 | /// Returns an index path for given item identifier. 68 | /// 69 | /// - Parameters: 70 | /// - itemIdentifier: An identifier of item. 71 | /// 72 | /// - Returns: An index path for given item identifier. 73 | public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { 74 | return core.indexPath(for: itemIdentifier) 75 | } 76 | 77 | /// Returns the number of sections in the data source. 78 | /// 79 | /// - Parameters: 80 | /// - collectionView: A collection view instance managed by `self`. 81 | /// 82 | /// - Returns: The number of sections in the data source. 83 | public func numberOfSections(in collectionView: NSCollectionView) -> Int { 84 | return core.numberOfSections() 85 | } 86 | 87 | /// Returns the number of items in the specified section. 88 | /// 89 | /// - Parameters: 90 | /// - collectionView: A collection view instance managed by `self`. 91 | /// - section: An index of section. 92 | /// 93 | /// - Returns: The number of items in the specified section. 94 | public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 95 | return core.numberOfItems(inSection: section) 96 | } 97 | 98 | /// Returns an item at specified index path. 99 | /// 100 | /// - Parameters: 101 | /// - collectionView: A collection view instance managed by `self`. 102 | /// - indexPath: An index path for item. 103 | /// 104 | /// - Returns: An item at specified index path. 105 | open func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 106 | let itemIdentifier = core.unsafeItemIdentifier(for: indexPath) 107 | guard let item = itemProvider(collectionView, indexPath, itemIdentifier) else { 108 | universalError("NSCollectionView dataSource returned a nil item at index path: \(indexPath), collectionView: \(collectionView), itemIdentifier: \(itemIdentifier)") 109 | } 110 | 111 | return item 112 | } 113 | } 114 | 115 | #endif 116 | -------------------------------------------------------------------------------- /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/Snapshot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Snapshot Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | DiffableDataSources Docs 25 | 26 | (98% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 74 |
75 | 76 |
77 |
78 |

Snapshot

79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 |
    87 |
  • 88 |
    89 | 90 | 91 | 92 | DiffableDataSourceSnapshot 93 | 94 |
    95 |
    96 |
    97 |
    98 |
    99 |
    100 |

    A class for backporting NSDiffableDataSourceSnapshot introduced in iOS 13.0+, macOS 10.15+, tvOS 13.0+. 101 | Represents the mutable state of diffable data source of UI.

    102 | 103 | See more 104 |
    105 |
    106 |

    Declaration

    107 |
    108 |

    Swift

    109 |
    public struct DiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
    110 | 111 |
    112 |
    113 |
    114 |
    115 |
  • 116 |
117 |
118 |
119 |
120 | 121 |
122 |
123 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /Examples/Example-iOS/InsertionSortViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DiffableDataSources 3 | 4 | final class InsertionSortViewController: UIViewController { 5 | final class Section: Hashable { 6 | var id = UUID() 7 | var nodes: [Node] 8 | private(set) var isSorted = false 9 | private var currentIndex = 1 10 | 11 | init(count: Int) { 12 | nodes = (0.. 1 else { 21 | return isSorted = true 22 | } 23 | 24 | var index = currentIndex 25 | let currentNode = nodes[index] 26 | index -= 1 27 | 28 | while index >= 0 && currentNode.value < nodes[index].value { 29 | let node = nodes[index] 30 | nodes[index] = currentNode 31 | nodes[index + 1] = node 32 | index -= 1 33 | } 34 | 35 | currentIndex += 1 36 | 37 | if currentIndex >= nodes.count { 38 | isSorted = true 39 | } 40 | } 41 | 42 | static func == (lhs: Section, rhs: Section) -> Bool { 43 | return lhs.id == rhs.id 44 | } 45 | } 46 | 47 | struct Node: Hashable { 48 | var id = UUID() 49 | var value: Int 50 | var color: UIColor 51 | 52 | init(value: Int, maxValue: Int) { 53 | let hue = CGFloat(value) / CGFloat(maxValue) 54 | self.value = value 55 | color = UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1) 56 | id = UUID() 57 | } 58 | 59 | func hash(into hasher: inout Hasher) { 60 | hasher.combine(id) 61 | } 62 | 63 | static func == (lhs: Node, rhs: Node) -> Bool { 64 | return lhs.id == rhs.id 65 | } 66 | } 67 | 68 | @IBOutlet private var collectionView: UICollectionView! 69 | private var isSorting = false 70 | 71 | let nodeSize = CGSize(width: 16, height: 34) 72 | 73 | private lazy var dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, node in 74 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UICollectionViewCell.name, for: indexPath) 75 | cell.backgroundColor = node.color 76 | return cell 77 | } 78 | 79 | override func viewDidLoad() { 80 | super.viewDidLoad() 81 | 82 | title = "Insertion Sort" 83 | 84 | collectionView.delegate = self 85 | collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: UICollectionViewCell.name) 86 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: nil, style: .plain, target: self, action: #selector(toggleSort)) 87 | 88 | updateSortButtonTitle() 89 | } 90 | 91 | override func viewDidLayoutSubviews() { 92 | super.viewDidLayoutSubviews() 93 | randmize(animated: false) 94 | } 95 | 96 | @objc func toggleSort() { 97 | isSorting.toggle() 98 | updateSortButtonTitle() 99 | 100 | if isSorting { 101 | startInsertionSort() 102 | } 103 | } 104 | 105 | func updateSortButtonTitle() { 106 | navigationItem.rightBarButtonItem?.title = isSorting ? "Stop" : "Sort" 107 | } 108 | 109 | func randmize(animated: Bool) { 110 | var snapshot = DiffableDataSourceSnapshot() 111 | let rows = Int(collectionView.bounds.height / nodeSize.height) - 1 112 | let columns = Int(collectionView.bounds.width / nodeSize.width) 113 | 114 | for _ in 0.. CGFloat { 158 | return 0 159 | } 160 | 161 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 162 | return 0 163 | } 164 | 165 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 166 | return .zero 167 | } 168 | 169 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 170 | return nodeSize 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/UIKit/CollectionViewDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | 3 | import UIKit 4 | import DifferenceKit 5 | 6 | /// A class for backporting `UICollectionViewDiffableDataSource` introduced in iOS 13.0+, tvOS 13.0+. 7 | /// Represents the data model object for `UICollectionView` that can be applies the 8 | /// changes with automatic diffing. 9 | open class CollectionViewDiffableDataSource: NSObject, UICollectionViewDataSource { 10 | /// The type of closure providing the cell. 11 | public typealias CellProvider = (UICollectionView, IndexPath, ItemIdentifierType) -> UICollectionViewCell? 12 | 13 | /// The type of closure providing the supplementary view for element of kind. 14 | public typealias SupplementaryViewProvider = (UICollectionView, String, IndexPath) -> UICollectionReusableView? 15 | 16 | /// A closure to dequeue the views for element of kind. 17 | public var supplementaryViewProvider: SupplementaryViewProvider? 18 | 19 | private weak var collectionView: UICollectionView? 20 | private let cellProvider: CellProvider 21 | private let core = DiffableDataSourceCore() 22 | 23 | /// Creates a new data source. 24 | /// 25 | /// - Parameters: 26 | /// - collectionView: A collection view instance to be managed. 27 | /// - cellProvider: A closure to dequeue the cell for items. 28 | public init(collectionView: UICollectionView, cellProvider: @escaping CellProvider) { 29 | self.collectionView = collectionView 30 | self.cellProvider = cellProvider 31 | super.init() 32 | 33 | collectionView.dataSource = self 34 | } 35 | 36 | /// Applies given snapshot to perform automatic diffing update. 37 | /// 38 | /// - Parameters: 39 | /// - snapshot: A snapshot object to be applied to data model. 40 | /// - animatingDifferences: A Boolean value indicating whether to update with 41 | /// diffing animation. 42 | /// - completion: An optional completion block which is called when the complete 43 | /// performing updates. 44 | public func apply(_ snapshot: DiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { 45 | core.apply( 46 | snapshot, 47 | view: collectionView, 48 | animatingDifferences: animatingDifferences, 49 | performUpdates: { collectionView, changeset, setSections in 50 | collectionView.reload(using: changeset, setData: setSections) 51 | }, 52 | completion: completion 53 | ) 54 | } 55 | 56 | /// Returns a new snapshot object of current state. 57 | /// 58 | /// - Returns: A new snapshot object of current state. 59 | public func snapshot() -> DiffableDataSourceSnapshot { 60 | return core.snapshot() 61 | } 62 | 63 | /// Returns an item identifier for given index path. 64 | /// 65 | /// - Parameters: 66 | /// - indexPath: An index path for the item identifier. 67 | /// 68 | /// - Returns: An item identifier for given index path. 69 | public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { 70 | return core.itemIdentifier(for: indexPath) 71 | } 72 | 73 | /// Returns an index path for given item identifier. 74 | /// 75 | /// - Parameters: 76 | /// - itemIdentifier: An identifier of item. 77 | /// 78 | /// - Returns: An index path for given item identifier. 79 | public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { 80 | return core.indexPath(for: itemIdentifier) 81 | } 82 | 83 | /// Returns the number of sections in the data source. 84 | /// 85 | /// - Parameters: 86 | /// - collectionView: A collection view instance managed by `self`. 87 | /// 88 | /// - Returns: The number of sections in the data source. 89 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 90 | return core.numberOfSections() 91 | } 92 | 93 | /// Returns the number of items in the specified section. 94 | /// 95 | /// - Parameters: 96 | /// - collectionView: A collection view instance managed by `self`. 97 | /// - section: An index of section. 98 | /// 99 | /// - Returns: The number of items in the specified section. 100 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 101 | return core.numberOfItems(inSection: section) 102 | } 103 | 104 | /// Returns a cell for item at specified index path. 105 | /// 106 | /// - Parameters: 107 | /// - collectionView: A collection view instance managed by `self`. 108 | /// - indexPath: An index path for cell. 109 | /// 110 | /// - Returns: A cell for row at specified index path. 111 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 112 | let itemIdentifier = core.unsafeItemIdentifier(for: indexPath) 113 | guard let cell = cellProvider(collectionView, indexPath, itemIdentifier) else { 114 | universalError("UICollectionView dataSource returned a nil cell for item at index path: \(indexPath), collectionView: \(collectionView), itemIdentifier: \(itemIdentifier)") 115 | } 116 | 117 | return cell 118 | } 119 | 120 | /// Returns a supplementary view for element of kind at specified index path. 121 | /// 122 | /// - Parameters: 123 | /// - collectionView: A collection view instance managed by `self`. 124 | /// - kind: The kind of element to be display. 125 | /// - indexPath: An index path for supplementary view. 126 | /// 127 | /// - Returns: A supplementary view for element of kind at specified index path. 128 | open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 129 | guard let view = supplementaryViewProvider?(collectionView, kind, indexPath) else { 130 | return UICollectionReusableView() 131 | } 132 | 133 | return view 134 | } 135 | 136 | /// Returns whether it is possible to edit a row at given index path. 137 | /// 138 | /// - Parameters: 139 | /// - collectionView: A collection view instance managed by `self`. 140 | /// - section: An index of section. 141 | /// 142 | /// - Returns: A boolean for row at specified index path. 143 | open func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 144 | return false 145 | } 146 | 147 | /// Moves a row at given index path. 148 | /// 149 | /// - Parameters: 150 | /// - collectionView: A collection view instance managed by `self`. 151 | /// - sourceIndexPath: An index path for given cell position. 152 | /// - destinationIndexPath: An index path for target cell position. 153 | /// 154 | /// - Returns: Void. 155 | public func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 156 | // Empty implementation. 157 | } 158 | } 159 | 160 | #endif 161 | -------------------------------------------------------------------------------- /Tests/CocoaCollectionViewDiffableDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | 3 | import XCTest 4 | import AppKit 5 | @testable import DiffableDataSources 6 | 7 | final class CocoaCollectionViewDiffableDataSourceTests: XCTestCase { 8 | func testInit() { 9 | let collectionView = MockCollectionView() 10 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 11 | NSCollectionViewItem() 12 | } 13 | 14 | XCTAssertTrue(collectionView.dataSource === dataSource) 15 | } 16 | 17 | func testApply() { 18 | let collectionView = MockCollectionView() 19 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 20 | NSCollectionViewItem() 21 | } 22 | 23 | var snapshot = DiffableDataSourceSnapshot() 24 | 25 | let e1 = expectation(description: "testApply() e1") 26 | dataSource.apply(snapshot, completion: e1.fulfill) 27 | wait(for: [e1], timeout: 1) 28 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 0) 29 | 30 | snapshot.appendSections([0]) 31 | snapshot.appendItems([0]) 32 | 33 | let e2 = expectation(description: "testApply() e2") 34 | dataSource.apply(snapshot, completion: e2.fulfill) 35 | wait(for: [e2], timeout: 1) 36 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 1) 37 | 38 | let e3 = expectation(description: "testApply() e3") 39 | dataSource.apply(snapshot, completion: e3.fulfill) 40 | wait(for: [e3], timeout: 1) 41 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 1) 42 | 43 | snapshot.appendItems([1]) 44 | 45 | let e4 = expectation(description: "testApply() e4") 46 | dataSource.apply(snapshot, completion: e4.fulfill) 47 | wait(for: [e4], timeout: 1) 48 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 2) 49 | } 50 | 51 | func testSnapshot() { 52 | let collectionView = MockCollectionView() 53 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 54 | NSCollectionViewItem() 55 | } 56 | 57 | let snapshot1 = dataSource.snapshot() 58 | XCTAssertEqual(snapshot1.sectionIdentifiers, []) 59 | XCTAssertEqual(snapshot1.itemIdentifiers, []) 60 | 61 | var snapshot2 = dataSource.snapshot() 62 | snapshot2.appendSections([0, 1, 2]) 63 | 64 | let snapshot3 = dataSource.snapshot() 65 | XCTAssertEqual(snapshot3.sectionIdentifiers, []) 66 | XCTAssertEqual(snapshot3.itemIdentifiers, []) 67 | 68 | var snapshotToApply = DiffableDataSourceSnapshot() 69 | snapshotToApply.appendSections([0, 1, 2]) 70 | snapshotToApply.appendItems([0, 1, 2]) 71 | dataSource.apply(snapshotToApply) 72 | 73 | let snapshot4 = dataSource.snapshot() 74 | XCTAssertEqual(snapshot4.sectionIdentifiers, [0, 1, 2]) 75 | XCTAssertEqual(snapshot4.itemIdentifiers, [0, 1, 2]) 76 | 77 | var snapshot5 = dataSource.snapshot() 78 | snapshot5.appendSections([3, 4, 5]) 79 | 80 | var snapshot6 = dataSource.snapshot() 81 | XCTAssertEqual(snapshot6.sectionIdentifiers, [0, 1, 2]) 82 | XCTAssertEqual(snapshot6.itemIdentifiers, [0, 1, 2]) 83 | 84 | snapshot6.appendSections([3, 4, 5]) 85 | snapshot6.appendItems([3, 4, 5]) 86 | dataSource.apply(snapshot6) 87 | 88 | let snapshot7 = dataSource.snapshot() 89 | XCTAssertEqual(snapshot7.sectionIdentifiers, [0, 1, 2, 3, 4, 5]) 90 | XCTAssertEqual(snapshot7.itemIdentifiers, [0, 1, 2, 3, 4, 5]) 91 | } 92 | 93 | func testItemIdentifier() { 94 | let collectionView = MockCollectionView() 95 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 96 | NSCollectionViewItem() 97 | } 98 | 99 | var snapshot = DiffableDataSourceSnapshot() 100 | snapshot.appendSections([0, 1, 2]) 101 | snapshot.appendItems([0, 1, 2], toSection: 0) 102 | dataSource.apply(snapshot) 103 | 104 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 1, section: 0)), 1) 105 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 100, section: 100)), nil) 106 | } 107 | 108 | func testIndexPath() { 109 | let collectionView = MockCollectionView() 110 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 111 | NSCollectionViewItem() 112 | } 113 | 114 | var snapshot = DiffableDataSourceSnapshot() 115 | snapshot.appendSections([0, 1, 2]) 116 | snapshot.appendItems([0, 1, 2], toSection: 0) 117 | dataSource.apply(snapshot) 118 | 119 | XCTAssertEqual(dataSource.indexPath(for: 2), IndexPath(item: 2, section: 0)) 120 | XCTAssertEqual(dataSource.indexPath(for: 100), nil) 121 | } 122 | 123 | func testNumberOfSections() { 124 | let collectionView = MockCollectionView() 125 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 126 | NSCollectionViewItem() 127 | } 128 | 129 | XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 0) 130 | 131 | var snapshot = DiffableDataSourceSnapshot() 132 | snapshot.appendSections([0, 1, 2]) 133 | snapshot.appendItems([0, 1, 2], toSection: 0) 134 | dataSource.apply(snapshot) 135 | 136 | XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 3) 137 | } 138 | 139 | func testNumberOfRowsInSection() { 140 | let collectionView = MockCollectionView() 141 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 142 | NSCollectionViewItem() 143 | } 144 | 145 | var snapshot = DiffableDataSourceSnapshot() 146 | snapshot.appendSections([0, 1, 2]) 147 | snapshot.appendItems([0, 1, 2], toSection: 0) 148 | dataSource.apply(snapshot) 149 | 150 | XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 3) 151 | } 152 | 153 | func testCellForRowAt() { 154 | let collectionView = MockCollectionView() 155 | let item = NSCollectionViewItem() 156 | let dataSource = CocoaCollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 157 | item 158 | } 159 | 160 | var snapshot = DiffableDataSourceSnapshot() 161 | snapshot.appendSections([0, 1, 2]) 162 | snapshot.appendItems([0, 1, 2], toSection: 0) 163 | dataSource.apply(snapshot) 164 | 165 | XCTAssertEqual( 166 | dataSource.collectionView(collectionView, itemForRepresentedObjectAt: IndexPath(item: 1, section: 0)), 167 | item 168 | ) 169 | } 170 | } 171 | 172 | final class MockCollectionView: NSCollectionView { 173 | var isPerformBatchUpdatesCalledCount = 0 174 | 175 | init() { 176 | super.init(frame: .zero) 177 | 178 | let window = NSWindow() 179 | window.contentView = self 180 | } 181 | 182 | @available(*, unavailable) 183 | required init?(coder decoder: NSCoder) { 184 | fatalError("init(coder:) has not been implemented") 185 | } 186 | 187 | override func performBatchUpdates(_ updates: (() -> Void)?, completionHandler: ((Bool) -> Void)? = nil) { 188 | isPerformBatchUpdatesCalledCount += 1 189 | updates?() 190 | completionHandler?(true) 191 | } 192 | 193 | override func insertItems(at indexPaths: Set) {} 194 | } 195 | 196 | #endif 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | DiffableDataSources 3 |

4 |

5 | 💾 A library for backporting UITableView/UICollectionViewDiffableDataSource
6 | powered by DifferenceKit. 7 |

8 | 9 |

10 | Swift5 11 | Release 12 | CocoaPods 13 | Carthage 14 | Swift Package Manager 15 |
16 | Build Status 17 | Platform 18 | Lincense 19 |

20 | 21 |

22 | Made with ❤️ by Ryo Aoyama 23 |

24 | 25 | --- 26 | 27 | ## Introduction 28 | 29 | 30 | 31 | 32 | Apple has announced a diffable data source at WWDC 2019. 33 | It's a great API that easily updating our table view and collection view items using automatic diffing. 34 | However, it's a little while before we can use it in a production service. 35 | That because it requires the latest OS to use. 36 | DiffableDataSources make it possible to introduce almost the same functionality from now on. 37 | 38 | Uses a sophisticated open source [DifferenceKit](https://github.com/ra1028/DifferenceKit) for the algorithm engine. 39 | It's extremely fast and completely avoids synchronization bugs, exceptions, and crashes. 40 | 41 |
42 | 43 | --- 44 | 45 | ## Difference from the Official 46 | 47 | #### Spec 48 | 49 | - Supports iOS 9.0+ / macOS 10.11+ / tvOS 9.0+ 50 | - Open sourced algorithm. 51 | - Duplicate sections or items are allowed. 52 | - Using `performBatchUpdates` for diffing updates. 53 | 54 | #### Namings 55 | 56 | `DiffableDataSources` have different class names to avoid conflicts with the official API. 57 | Correspondence table is below. 58 | 59 | |Official |Backported | 60 | |:---------------------------------------------------------------------------|:------------------------------------| 61 | |[NSDiffableDataSourceSnapshot][NSDiffableDataSourceSnapshot_doc] |DiffableDataSourceSnapshot | 62 | |[UITableViewDiffableDataSource][UITableViewDiffableDataSource_doc] |TableViewDiffableDataSource | 63 | |[UICollectionViewDiffableDataSource][UICollectionViewDiffableDataSource_doc]|CollectionViewDiffableDataSource | 64 | |[NSCollectionViewDiffableDataSource][NSCollectionViewDiffableDataSource_doc]|CocoaCollectionViewDiffableDataSource| 65 | 66 | [NSDiffableDataSourceSnapshot_doc]: https://developer.apple.com/documentation/uikit/uitableviewdiffabledatasource 67 | [UITableViewDiffableDataSource_doc]: https://developer.apple.com/documentation/uikit/uitableviewdiffabledatasource 68 | [UICollectionViewDiffableDataSource_doc]: https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource 69 | [NSCollectionViewDiffableDataSource_doc]: https://developer.apple.com/documentation/appkit/nscollectionviewdiffabledatasource 70 | 71 | --- 72 | 73 | ## Getting Started 74 | 75 | - [API Documentation](https://ra1028.github.io/DiffableDataSources) 76 | - [Example Apps](https://github.com/ra1028/DiffableDataSources/tree/master/Examples) 77 | - [WWDC 2019 Session](https://developer.apple.com/videos/play/wwdc2019/220) 78 | 79 | #### Build Project 80 | 81 | ```sh 82 | $ git clone https://github.com/ra1028/DiffableDataSources.git 83 | $ cd DiffableDataSources/ 84 | $ make setup 85 | $ open DiffableDataSources.xcworkspace 86 | ``` 87 | 88 | --- 89 | 90 | ## Basic Usage 91 | 92 | First, define the type representing section. 93 | It should conforms to `Hashable` for identifies from the all sections. 94 | Type of enum can used conveniently because it conforms `Hashable` by default. 95 | 96 | ```swift 97 | enum Section { 98 | case main 99 | } 100 | ``` 101 | 102 | Then, define the item type conforms to `Hashable`. 103 | 104 | ```swift 105 | struct User: Hashable { 106 | var name: String 107 | } 108 | ``` 109 | 110 | Create a data source object, it will be set to table view automatically. 111 | You should dequeue the non nil cells via closure. 112 | 113 | ```swift 114 | final class UsersViewController: UIViewController { 115 | let tableView: UITableView = ... 116 | 117 | lazy var dataSource = TableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, user in 118 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 119 | cell.textLabel?.text = user.name 120 | return cell 121 | } 122 | 123 | override func viewDidLoad() { 124 | super.viewDidLoad() 125 | 126 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 127 | } 128 | } 129 | ``` 130 | 131 | Manages and updates the data sources intuitively by intermediating `DiffableDataSourceSnapshot`. 132 | The UI isn't updated until you apply the edited snapshot object. 133 | Update the UI with diffing animation automatically calculated by applying an edited snapshot. 134 | 135 | ```swift 136 | let users = [ 137 | User(name: "Steve Jobs"), 138 | User(name: "Stephen Wozniak"), 139 | User(name: "Tim Cook"), 140 | User(name: "Jonathan Ive") 141 | ] 142 | 143 | let snapshot = DiffableDataSourceSnapshot() 144 | snapshot.appendSections([.main]) 145 | snapshot.appendItems(users) 146 | 147 | dataSource.apply(snapshot) { 148 | // completion 149 | } 150 | ``` 151 | 152 | Check the documentation for more detailed API. 153 | 154 |

155 | [See More Usage] 156 |

157 | 158 | --- 159 | 160 | ## Requirements 161 | 162 | - Swift 5.0+ 163 | - iOS 9.0+ 164 | - macOS 10.11+ 165 | - tvOS 9.0+ 166 | 167 | --- 168 | 169 | ## Installation 170 | 171 | ### [CocoaPods](https://cocoapods.org) 172 | Add the following to your `Podfile`: 173 | ```ruby 174 | pod 'DiffableDataSources' 175 | ``` 176 | 177 | ### [Carthage](https://github.com/Carthage/Carthage) 178 | Add the following to your `Cartfile`: 179 | ``` 180 | github "ra1028/DiffableDataSources" 181 | ``` 182 | 183 | ### [Swift Package Manager](https://swift.org/package-manager/) 184 | Add the following to the dependencies of your `Package.swift`: 185 | ```swift 186 | .package(url: "https://github.com/ra1028/DiffableDataSources.git", from: "x.x.x") 187 | ``` 188 | 189 | --- 190 | 191 | ## Contributing 192 | 193 | Pull requests, bug reports and feature requests are welcome 🚀 194 | Please see the [CONTRIBUTING](https://github.com/ra1028/DiffableDataSources/blob/master/CONTRIBUTING.md) file for learn how to contribute to DiffableDataSources. 195 | 196 | --- 197 | 198 | ## Relations 199 | 200 | #### [DifferenceKit](https://github.com/ra1028/DifferenceKit) 201 | A fast and flexible O(n) difference algorithm framework for Swift collection. 202 | 203 | #### [Carbon](https://github.com/ra1028/Carbon) 204 | A declarative library for building component-based user interfaces in UITableView and UICollectionView. 205 | 206 | --- 207 | 208 | ## License 209 | 210 | DiffableDataSources is released under the [Apache 2.0 License](https://github.com/ra1028/DiffableDataSources/blob/master/LICENSE). 211 | -------------------------------------------------------------------------------- /docs/DataSources.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DataSources Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | DiffableDataSources Docs 25 | 26 | (98% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 74 |
75 | 76 |
77 |
78 |

DataSources

79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 |
    87 |
  • 88 |
    89 | 90 | 91 | 92 | TableViewDiffableDataSource 93 | 94 |
    95 |
    96 |
    97 |
    98 |
    99 |
    100 |

    A class for backporting UITableViewDiffableDataSource introduced in iOS 13.0+, tvOS 13.0+. 101 | Represents the data model object for UITableView that can be applies the 102 | changes with automatic diffing.

    103 | 104 | See more 105 |
    106 |
    107 |

    Declaration

    108 |
    109 |

    Swift

    110 |
    open class TableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject, UITableViewDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
    111 | 112 |
    113 |
    114 |
    115 |
    116 |
  • 117 |
118 |
119 |
120 |
    121 |
  • 122 |
    123 | 124 | 125 | 126 | CollectionViewDiffableDataSource 127 | 128 |
    129 |
    130 |
    131 |
    132 |
    133 |
    134 |

    A class for backporting UICollectionViewDiffableDataSource introduced in iOS 13.0+, tvOS 13.0+. 135 | Represents the data model object for UICollectionView that can be applies the 136 | changes with automatic diffing.

    137 | 138 | See more 139 |
    140 |
    141 |

    Declaration

    142 |
    143 |

    Swift

    144 |
    open class CollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject, UICollectionViewDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
    145 | 146 |
    147 |
    148 |
    149 |
    150 |
  • 151 |
152 |
153 |
154 |
155 | 156 |
157 |
158 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /Tests/CollectionViewDiffableDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | 3 | import XCTest 4 | import UIKit 5 | @testable import DiffableDataSources 6 | 7 | final class CollectionViewDiffableDataSourceTests: XCTestCase { 8 | func testInit() { 9 | let collectionView = MockCollectionView() 10 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 11 | UICollectionViewCell() 12 | } 13 | 14 | XCTAssertTrue(collectionView.dataSource === dataSource) 15 | } 16 | 17 | func testApply() { 18 | let collectionView = MockCollectionView() 19 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 20 | UICollectionViewCell() 21 | } 22 | 23 | var snapshot = DiffableDataSourceSnapshot() 24 | 25 | let e1 = expectation(description: "testApply() e1") 26 | dataSource.apply(snapshot, completion: e1.fulfill) 27 | wait(for: [e1], timeout: 1) 28 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 0) 29 | 30 | snapshot.appendSections([0]) 31 | snapshot.appendItems([0]) 32 | 33 | let e2 = expectation(description: "testApply() e2") 34 | dataSource.apply(snapshot, completion: e2.fulfill) 35 | wait(for: [e2], timeout: 1) 36 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 1) 37 | 38 | let e3 = expectation(description: "testApply() e3") 39 | dataSource.apply(snapshot, completion: e3.fulfill) 40 | wait(for: [e3], timeout: 1) 41 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 1) 42 | 43 | snapshot.appendItems([1]) 44 | 45 | let e4 = expectation(description: "testApply() e4") 46 | dataSource.apply(snapshot, completion: e4.fulfill) 47 | wait(for: [e4], timeout: 1) 48 | XCTAssertEqual(collectionView.isPerformBatchUpdatesCalledCount, 2) 49 | } 50 | 51 | func testSnapshot() { 52 | let collectionView = MockCollectionView() 53 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 54 | UICollectionViewCell() 55 | } 56 | 57 | let snapshot1 = dataSource.snapshot() 58 | XCTAssertEqual(snapshot1.sectionIdentifiers, []) 59 | XCTAssertEqual(snapshot1.itemIdentifiers, []) 60 | 61 | var snapshot2 = dataSource.snapshot() 62 | snapshot2.appendSections([0, 1, 2]) 63 | 64 | let snapshot3 = dataSource.snapshot() 65 | XCTAssertEqual(snapshot3.sectionIdentifiers, []) 66 | XCTAssertEqual(snapshot3.itemIdentifiers, []) 67 | 68 | var snapshotToApply = DiffableDataSourceSnapshot() 69 | snapshotToApply.appendSections([0, 1, 2]) 70 | snapshotToApply.appendItems([0, 1, 2]) 71 | dataSource.apply(snapshotToApply) 72 | 73 | let snapshot4 = dataSource.snapshot() 74 | XCTAssertEqual(snapshot4.sectionIdentifiers, [0, 1, 2]) 75 | XCTAssertEqual(snapshot4.itemIdentifiers, [0, 1, 2]) 76 | 77 | var snapshot5 = dataSource.snapshot() 78 | snapshot5.appendSections([3, 4, 5]) 79 | 80 | var snapshot6 = dataSource.snapshot() 81 | XCTAssertEqual(snapshot6.sectionIdentifiers, [0, 1, 2]) 82 | XCTAssertEqual(snapshot6.itemIdentifiers, [0, 1, 2]) 83 | 84 | snapshot6.appendSections([3, 4, 5]) 85 | snapshot6.appendItems([3, 4, 5]) 86 | dataSource.apply(snapshot6) 87 | 88 | let snapshot7 = dataSource.snapshot() 89 | XCTAssertEqual(snapshot7.sectionIdentifiers, [0, 1, 2, 3, 4, 5]) 90 | XCTAssertEqual(snapshot7.itemIdentifiers, [0, 1, 2, 3, 4, 5]) 91 | } 92 | 93 | func testItemIdentifier() { 94 | let collectionView = MockCollectionView() 95 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 96 | UICollectionViewCell() 97 | } 98 | 99 | var snapshot = DiffableDataSourceSnapshot() 100 | snapshot.appendSections([0, 1, 2]) 101 | snapshot.appendItems([0, 1, 2], toSection: 0) 102 | dataSource.apply(snapshot) 103 | 104 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 1, section: 0)), 1) 105 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 100, section: 100)), nil) 106 | } 107 | 108 | func testIndexPath() { 109 | let collectionView = MockCollectionView() 110 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 111 | UICollectionViewCell() 112 | } 113 | 114 | var snapshot = DiffableDataSourceSnapshot() 115 | snapshot.appendSections([0, 1, 2]) 116 | snapshot.appendItems([0, 1, 2], toSection: 0) 117 | dataSource.apply(snapshot) 118 | 119 | XCTAssertEqual(dataSource.indexPath(for: 2), IndexPath(item: 2, section: 0)) 120 | XCTAssertEqual(dataSource.indexPath(for: 100), nil) 121 | } 122 | 123 | func testNumberOfSections() { 124 | let collectionView = MockCollectionView() 125 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 126 | UICollectionViewCell() 127 | } 128 | 129 | XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 0) 130 | 131 | var snapshot = DiffableDataSourceSnapshot() 132 | snapshot.appendSections([0, 1, 2]) 133 | snapshot.appendItems([0, 1, 2], toSection: 0) 134 | dataSource.apply(snapshot) 135 | 136 | XCTAssertEqual(dataSource.numberOfSections(in: collectionView), 3) 137 | } 138 | 139 | func testNumberOfRowsInSection() { 140 | let collectionView = MockCollectionView() 141 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 142 | UICollectionViewCell() 143 | } 144 | 145 | var snapshot = DiffableDataSourceSnapshot() 146 | snapshot.appendSections([0, 1, 2]) 147 | snapshot.appendItems([0, 1, 2], toSection: 0) 148 | dataSource.apply(snapshot) 149 | 150 | XCTAssertEqual(dataSource.collectionView(collectionView, numberOfItemsInSection: 0), 3) 151 | } 152 | 153 | func testCellForRowAt() { 154 | let collectionView = MockCollectionView() 155 | let cell = UICollectionViewCell() 156 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 157 | cell 158 | } 159 | 160 | var snapshot = DiffableDataSourceSnapshot() 161 | snapshot.appendSections([0, 1, 2]) 162 | snapshot.appendItems([0, 1, 2], toSection: 0) 163 | dataSource.apply(snapshot) 164 | 165 | XCTAssertEqual( 166 | dataSource.collectionView(collectionView, cellForItemAt: IndexPath(item: 1, section: 0)), 167 | cell 168 | ) 169 | } 170 | 171 | func testCanMoveRowAt() { 172 | let collectionView = MockCollectionView() 173 | let cell = UICollectionViewCell() 174 | let dataSource = CollectionViewDiffableDataSource(collectionView: collectionView) { _, _, _ in 175 | cell 176 | } 177 | 178 | var snapshot = DiffableDataSourceSnapshot() 179 | snapshot.appendSections([0, 1, 2]) 180 | snapshot.appendItems([0, 1, 2], toSection: 0) 181 | dataSource.apply(snapshot) 182 | 183 | XCTAssertEqual( 184 | dataSource.collectionView(collectionView, canMoveItemAt: IndexPath(item: 1, section: 0)), 185 | false 186 | ) 187 | } 188 | } 189 | 190 | final class MockCollectionView: UICollectionView { 191 | var isPerformBatchUpdatesCalledCount = 0 192 | 193 | init() { 194 | super.init(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 195 | 196 | let window = UIWindow() 197 | window.addSubview(self) 198 | } 199 | 200 | @available(*, unavailable) 201 | required init?(coder aDecoder: NSCoder) { 202 | fatalError("init(coder:) has not been implemented") 203 | } 204 | 205 | override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { 206 | isPerformBatchUpdatesCalledCount += 1 207 | updates?() 208 | completion?(true) 209 | } 210 | 211 | override func insertSections(_ sections: IndexSet) {} 212 | override func insertItems(at indexPaths: [IndexPath]) {} 213 | } 214 | 215 | #endif 216 | -------------------------------------------------------------------------------- /Examples/MountainsRawData.swift: -------------------------------------------------------------------------------- 1 | // https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/using_collection_view_compositional_layouts_and_diffable_data_sources 2 | 3 | let mountainsRawData = """ 4 | Mount Everest,8848 5 | K2,8611 6 | Kangchenjunga,8586 7 | Lhotse,8516 8 | Makalu,8485 9 | Cho Oyu,8201 10 | Dhaulagiri,8167 11 | Manaslu,8163 12 | Nanga Parbat,8126 13 | Annapurna,8091 14 | Gasherbrum I,8080 15 | Broad Peak,8051 16 | Gasherbrum II,8035 17 | Shishapangma,8027 18 | Gasherbrum III,7952 19 | Gyachung Kang,7952 20 | Annapurna II,7937 21 | Gasherbrum IV,7932 22 | Himalchuli,7893 23 | Distaghil Sar,7885 24 | Ngadi Chuli,7871 25 | Nuptse,7861 26 | Khunyang Chhish,7852 27 | Masherbrum,7821 28 | Nanda Devi,7816 29 | Chomo Lonzo,7804 30 | Batura Sar,7795 31 | Kanjut Sar,7790 32 | Rakaposhi,7788 33 | Namcha Barwa,7782 34 | Kamet,7756 35 | Saltoro Kangri,7742 36 | Jannu,7710 37 | Tirich Mir,7708 38 | Molamenqing,7703 39 | Gurla Mandhata,7694 40 | Saser Kangri,7672 41 | Chogolisa,7665 42 | Kongur Tagh,7649 43 | Shispare,7611 44 | Silberzacken,7597 45 | Changtse,7583 46 | Trivor,7577 47 | Gangkhar Puensum,7570 48 | Gongga Shan,7556 49 | Annapurna III,7555 50 | Kula Kangri,7554 51 | Muztagh Ata,7546 52 | Skyang Kangri,7545 53 | Liankang Kangri,7535 54 | Yukshin Gardan Sar,7530 55 | Annapurna IV,7525 56 | Mamostong Kangri,7516 57 | Ismoil Somoni Peak,7495 58 | Noshaq,7492 59 | Pumari Chhish,7492 60 | Passu Sar,7476 61 | Malubiting,7458 62 | Gangapurna,7455 63 | Muchu Chhish (Batura V),7453 64 | Jengish Chokusu,7439 65 | K12,7428 66 | Sia Kangri,7422 67 | Istor-o-Nal,7403 68 | Ghent Kangri,7401 69 | Haramosh Peak,7397 70 | Kabru,7394 71 | Ultar,7388 72 | Rimo I,7385 73 | Sherpi Kangri,7380 74 | Labuche Kang,7367 75 | Kirat Chuli,7365 76 | Skil Brum,7360 77 | Gimmigela Chuli,7350 78 | Bojohagur Duanasir,7329 79 | Chamlang,7319 80 | Jomolhari / Chomolhari,7314 81 | Baltoro Kangri,7312 82 | Siguang Ri,7308 83 | The Crown,7295 84 | Gyala Peri,7294 85 | Porong Ri,7292 86 | Baintha Brakk,7285 87 | Yutmaru Sar,7283 88 | K6,7282 89 | Kangpenqing,7281 90 | Mana Peak,7272 91 | Muztagh Tower,7273 92 | Diran,7257 93 | Apsarasas Kangri,7245 94 | Langtang Lirung,7227 95 | Khartaphu,7213 96 | Tongshanjiabu,7207 97 | Langtang Ri,7205 98 | Kangphu Kang,7204 99 | Annapurna South,7219 100 | Melungtse,7181 101 | Liushi Shan,7167 102 | Baruntse,7162 103 | Pumori,7161 104 | Hardeol,7151 105 | Gasherbrum V,7147 106 | Latok I,7145 107 | Nemjung,7140 108 | Udren Zom,7140 109 | Chaukhamba,7138 110 | Nun Kun,7135 111 | Tilicho Peak,7134 112 | Gauri Sankar,7134 113 | Lenin Peak,7134 114 | Api,7132 115 | Pauhunri,7128 116 | Trisul,7120 117 | Korzhenevskaya,7105 118 | Lunpo Gangri,7095 119 | Satopanth,7075 120 | Tirsuli,7074 121 | Dunagiri,7066 122 | Kangto,7060 123 | Nyegyi Kansang,7047 124 | Link Sar,7041 125 | Kezhen Peak,7038 126 | Shah Dhar,7038 127 | Spantik,7027 128 | Khan Tengri,7010 129 | Machapuchare,6993 130 | Laila Peak (Haramosh Valley),6985 131 | Kang Guru,6981 132 | Gasherbrum VI,6979 133 | Karun Kuh,6977 134 | Avicenna Peak,6974 135 | Ulugh Muztagh,6973 136 | Aconcagua,6962 137 | Napko Kangri,6956 138 | Kedarnath,6940 139 | K7,6934 140 | Panchchuli,6904 141 | Thalay Sagar,6904 142 | Lunkho e Dosare,6901 143 | Lunag Ri,6895 144 | Ojos del Salado,6891 145 | Siniolchu,6888 146 | Kanjiroba,6883 147 | Bairiga,6882 148 | Koyo Zom,6872 149 | Nanda Kot,6861 150 | Kubi Gangri,6859 151 | Angel Sar,6858 152 | Bhagirathi I,6856 153 | Jethi Bahurani,6850 154 | Chongra Peak,6830 155 | Reo Purgyil,6816 156 | Ama Dablam,6812 157 | Monte Pissis,6795 158 | Biarchedi,6781 159 | Huascarán Sur,6768 160 | Cerro Bonete,6759 161 | Nevado Tres Cruces,6748 162 | Kawagarbo,6740 163 | Llullaillaco,6739 164 | Cho Polu,6735 165 | Kangju Kangri,6725 166 | Changla,6721 167 | Mercedario,6720 168 | Mount Pandim,6691 169 | Num Ri,6677 170 | Lungser Kangri,6666 171 | Meru Peak,6660 172 | Gul Lasht Zom,6657 173 | Huascarán Norte,6655 174 | Khumbutse,6640 175 | Mount Kailash,6638 176 | Yerupajá,6635 177 | Nevado Tres Cruces Central,6629 178 | Thamserku,6623 179 | Geladaindong Peak,6621 180 | Incahuasi,6621 181 | Pangpoche,6620 182 | Manirang,6597 183 | Nilkantha,6596 184 | Phuparash Peak,6574 185 | Sickle Moon Peak,6574 186 | Buni Zom,6542 187 | Nevado Sajama,6542 188 | Ghamubar Zom,6518 189 | Singu Chuli,6501 190 | Taboche,6501 191 | Cerro El Muerto,6488 192 | Mera Peak,6476 193 | Hiunchuli,6441 194 | Cholatse,6440 195 | Illimani,6438 196 | Ancohuma,6427 197 | Coropuna,6425 198 | Antofalla,6409 199 | Kang Yatze,6400 200 | Huandoy,6395 201 | Ausangate,6384 202 | Illampu,6368 203 | Kusum Kangguru,6367 204 | Kinnaur Kailash,6349 205 | Parinaquta,6348 206 | Siula Grande,6344 207 | Bamba Dhura,6334 208 | Ampato,6288 209 | Amne Machin,6282 210 | Pomerape,6282 211 | Salcantay,6271 212 | Chimborazo,6267 213 | Mount Siguniang,6250 214 | Grid Nie Mountain,6224 215 | Yuzhu Peak,6224 216 | Genyen Massif,6204 217 | Kongde Ri,6187 218 | Aucanquilcha,6176 219 | Imja Tse,6189 220 | Denali (Mt. McKinley),6168 221 | Stok Kangri,6137 222 | Lobuche,6119 223 | Marmolejo,6108 224 | Laila Peak (Hushe Valley),6096 225 | Pisang Peak,6091 226 | Huayna Potosí,6088 227 | Aracar,6082 228 | Chachakumani,6074 229 | Chachani,6057 230 | Mianzimu,6054 231 | Acotango,6052 232 | Socompa,6051 233 | Acamarachi,6046 234 | Shayaz,6026 235 | Hualca Hualca,6025 236 | Uturunku,6020 237 | Mitre Peak,6010 238 | Laila Peak,5971 239 | Mount Logan,5959 240 | Alpamayo,5947 241 | Cerro Lípez,5929 242 | Licancabur,5920 243 | Falak Sar,5918 244 | Cotopaxi,5897 245 | Mount Kilimanjaro,5895 246 | Hkakabo Razi,5881 247 | San José,5856 248 | El Misti,5822 249 | Altun Shan,5798 250 | Cayambe,5790 251 | Pico Cristóbal Colón,5776 252 | Antisana,5753 253 | Nevado Pisco,5752 254 | Nevado Anallajsi,5750 255 | Pokalde,5745 256 | Ubinas,5672 257 | Pichu Pichu,5664 258 | Mount Elbrus,5642 259 | Mehrbani Peak,5639 260 | Pico de Orizaba,5636 261 | Mount Damavand,5610 262 | Nevado Mismi,5597 263 | Jade Dragon Snow Mountain,5596 264 | Lascar Volcano,5592 265 | Mount Xuebaoding,5588 266 | Kala Patthar,5545 267 | Mount Saint Elias,5489 268 | Concord Peak,5469 269 | Machoi Peak,5458 270 | El Plomo,5450 271 | Bogda Feng,5445 272 | Mount Little Xuebaoding,5443 273 | Cerro El Plomo,5434 274 | Popocatépetl,5426 275 | Kolahoi Peak,5425 276 | Chacaltaya,5421 277 | Mount Pomiu,5413 278 | Ritacuba Blanco,5410 279 | Haba Xueshan,5396 280 | Nevado del Ruiz,5389 281 | Nevado del Huila,5364 282 | El Altar,5320 283 | Mount Foraker,5304 284 | Mount Haramukh,5300 285 | Nevado del Tolima,5276 286 | Maipo,5264 287 | Illiniza,5248 288 | Sirbal Peak,5236 289 | Sangay,5230 290 | Iztaccíhuatl,5230 291 | Mount Lucania,5226 292 | Dykh-Tau,5205 293 | Shkhara,5201 294 | Mount Kenya,5199 295 | Malika Parbat,5190 296 | Amarnath Peak,5186 297 | King Peak,5173 298 | Boris Yeltsin Peak,5168 299 | Koshtan-Tau,5150 300 | Mount Ararat,5137 301 | Mount Stanley,5109 302 | Mount Steele,5073 303 | Janga,5051 304 | Mount Kazbek,5047 305 | Tungurahua,5023 306 | Carihuairazo,5018 307 | Mount Bona,5005 308 | Mount Blackburn,4996 309 | Pico Bolívar,4981 310 | Pik Talgar,4979 311 | Shota Rustaveli Peak,4960 312 | Gunshar,4950 313 | Mount Sanford,4949 314 | Pico Humboldt,4940 315 | Vinson Massif,4892 316 | Pico Bonpland,4890 317 | Puncak Jaya,4884 318 | Pico La Concha,4870 319 | Gistola,4860 320 | Tetnuldi,4858 321 | Mount Tyree,4852 322 | Huaynaputina,4850 323 | Alam Kuh,4850 324 | Mount Wood,4842 325 | Mount Vancouver,4812 326 | Sabalan,4811 327 | Mont Blanc,4810 328 | Corazón,4790 329 | Pichincha,4784 330 | Jimara,4780 331 | Mount Churchill,4766 332 | Puncak Mandala,4760 333 | Klyuchevskaya Sopka,4750 334 | Puncak Trikora,4750 335 | Mont Blanc de Courmayeur,4748 336 | Sunset Peak,4745 337 | Mount Slaggard,4742 338 | Pico Piedras Blancas,4740 339 | Pico El Toro,4730 340 | Tatakooti Peak,4725 341 | Rumiñahui,4721 342 | Pico El Leon,4720 343 | Ushba,4710 344 | Volcán Domuyo,4709 345 | Pico Los Nevados,4700 346 | Pico Pan de Azucar,4680 347 | Naltar Peak,4678 348 | Mount Fairweather,4663 349 | Pico Mucuñuque,4660 350 | Pico El Buitre,4650 351 | Khazret Sultan,4643 352 | Sierra Negra,4640 353 | Dufourspitze (Monte Rosa),4634 354 | Dunantspitze (Monte Rosa),4632 355 | Nordend (Monte Rosa),4609 356 | Mount Hubbard,4577 357 | Nevado de Toluca,4577 358 | Mount Meru,4566 359 | Zumsteinspitze (Monte Rosa),4563 360 | Signalkuppe (Monte Rosa),4554 361 | Dom,4545 362 | Ras Dashen,4533 363 | Eastern Liskamm (Lyskamm),4527 364 | Mount Bear,4521 365 | Mount Wilhelm,4509 366 | Mount Karisimbi,4507 367 | Mount Walsh,4507 368 | Belukha Mountain,4506 369 | Weisshorn,4506 370 | Tebulosmta,4493 371 | Täschhorn,4491 372 | Bazarduzu Dagi,4485 373 | Matterhorn,4478 374 | Mount Rutford,4477 375 | Mont Maudit,4465 376 | Babis Mta,4454 377 | Mount Shani,4451 378 | Dena,4448 379 | Vladimir Putin Peak,4446 380 | Mount Hunter,4442 381 | Parrotspitze (Monte Rosa),4432 382 | Mount Whitney,4421 383 | Mount Alverstone,4420 384 | University Peak,4411 385 | Aello Peak,4403 386 | Mount Elbert,4402 387 | Mount Massive,4395 388 | Mount Harvard,4395 389 | Mount Rainier,4392 390 | Kholeno,4387 391 | """ 392 | -------------------------------------------------------------------------------- /Tests/TableViewDiffableDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | 3 | import XCTest 4 | import UIKit 5 | @testable import DiffableDataSources 6 | 7 | final class TableViewDiffableDataSourceTests: XCTestCase { 8 | func testInit() { 9 | let tableView = MockTableView() 10 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 11 | UITableViewCell() 12 | } 13 | 14 | XCTAssertTrue(tableView.dataSource === dataSource) 15 | } 16 | 17 | func testApply() { 18 | let tableView = MockTableView() 19 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 20 | UITableViewCell() 21 | } 22 | 23 | var snapshot = DiffableDataSourceSnapshot() 24 | 25 | let e1 = expectation(description: "testApply() e1") 26 | dataSource.apply(snapshot, completion: e1.fulfill) 27 | wait(for: [e1], timeout: 1) 28 | XCTAssertEqual(tableView.isPerformBatchUpdatesCalledCount, 0) 29 | 30 | snapshot.appendSections([0]) 31 | snapshot.appendItems([0]) 32 | 33 | let e2 = expectation(description: "testApply() e2") 34 | dataSource.apply(snapshot, completion: e2.fulfill) 35 | wait(for: [e2], timeout: 1) 36 | XCTAssertEqual(tableView.isPerformBatchUpdatesCalledCount, 1) 37 | 38 | let e3 = expectation(description: "testApply() e3") 39 | dataSource.apply(snapshot, completion: e3.fulfill) 40 | wait(for: [e3], timeout: 1) 41 | XCTAssertEqual(tableView.isPerformBatchUpdatesCalledCount, 1) 42 | 43 | snapshot.appendItems([1]) 44 | 45 | let e4 = expectation(description: "testApply() e4") 46 | dataSource.apply(snapshot, completion: e4.fulfill) 47 | wait(for: [e4], timeout: 1) 48 | XCTAssertEqual(tableView.isPerformBatchUpdatesCalledCount, 2) 49 | } 50 | 51 | func testSnapshot() { 52 | let tableView = MockTableView() 53 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 54 | UITableViewCell() 55 | } 56 | 57 | let snapshot1 = dataSource.snapshot() 58 | XCTAssertEqual(snapshot1.sectionIdentifiers, []) 59 | XCTAssertEqual(snapshot1.itemIdentifiers, []) 60 | 61 | var snapshot2 = dataSource.snapshot() 62 | snapshot2.appendSections([0, 1, 2]) 63 | 64 | let snapshot3 = dataSource.snapshot() 65 | XCTAssertEqual(snapshot3.sectionIdentifiers, []) 66 | XCTAssertEqual(snapshot3.itemIdentifiers, []) 67 | 68 | var snapshotToApply = DiffableDataSourceSnapshot() 69 | snapshotToApply.appendSections([0, 1, 2]) 70 | snapshotToApply.appendItems([0, 1, 2]) 71 | dataSource.apply(snapshotToApply) 72 | 73 | let snapshot4 = dataSource.snapshot() 74 | XCTAssertEqual(snapshot4.sectionIdentifiers, [0, 1, 2]) 75 | XCTAssertEqual(snapshot4.itemIdentifiers, [0, 1, 2]) 76 | 77 | var snapshot5 = dataSource.snapshot() 78 | snapshot5.appendSections([3, 4, 5]) 79 | 80 | var snapshot6 = dataSource.snapshot() 81 | XCTAssertEqual(snapshot6.sectionIdentifiers, [0, 1, 2]) 82 | XCTAssertEqual(snapshot6.itemIdentifiers, [0, 1, 2]) 83 | 84 | snapshot6.appendSections([3, 4, 5]) 85 | snapshot6.appendItems([3, 4, 5]) 86 | dataSource.apply(snapshot6) 87 | 88 | let snapshot7 = dataSource.snapshot() 89 | XCTAssertEqual(snapshot7.sectionIdentifiers, [0, 1, 2, 3, 4, 5]) 90 | XCTAssertEqual(snapshot7.itemIdentifiers, [0, 1, 2, 3, 4, 5]) 91 | } 92 | 93 | func testItemIdentifier() { 94 | let tableView = MockTableView() 95 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 96 | UITableViewCell() 97 | } 98 | 99 | var snapshot = DiffableDataSourceSnapshot() 100 | snapshot.appendSections([0, 1, 2]) 101 | snapshot.appendItems([0, 1, 2], toSection: 0) 102 | dataSource.apply(snapshot) 103 | 104 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 1, section: 0)), 1) 105 | XCTAssertEqual(dataSource.itemIdentifier(for: IndexPath(item: 100, section: 100)), nil) 106 | } 107 | 108 | func testIndexPath() { 109 | let tableView = MockTableView() 110 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 111 | UITableViewCell() 112 | } 113 | 114 | var snapshot = DiffableDataSourceSnapshot() 115 | snapshot.appendSections([0, 1, 2]) 116 | snapshot.appendItems([0, 1, 2], toSection: 0) 117 | dataSource.apply(snapshot) 118 | 119 | XCTAssertEqual(dataSource.indexPath(for: 2), IndexPath(item: 2, section: 0)) 120 | XCTAssertEqual(dataSource.indexPath(for: 100), nil) 121 | } 122 | 123 | func testNumberOfSections() { 124 | let tableView = MockTableView() 125 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 126 | UITableViewCell() 127 | } 128 | 129 | XCTAssertEqual(dataSource.numberOfSections(in: tableView), 0) 130 | 131 | var snapshot = DiffableDataSourceSnapshot() 132 | snapshot.appendSections([0, 1, 2]) 133 | snapshot.appendItems([0, 1, 2], toSection: 0) 134 | dataSource.apply(snapshot) 135 | 136 | XCTAssertEqual(dataSource.numberOfSections(in: tableView), 3) 137 | } 138 | 139 | func testNumberOfRowsInSection() { 140 | let tableView = MockTableView() 141 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 142 | UITableViewCell() 143 | } 144 | 145 | var snapshot = DiffableDataSourceSnapshot() 146 | snapshot.appendSections([0, 1, 2]) 147 | snapshot.appendItems([0, 1, 2], toSection: 0) 148 | dataSource.apply(snapshot) 149 | 150 | XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 3) 151 | } 152 | 153 | func testCellForRowAt() { 154 | let tableView = MockTableView() 155 | let cell = UITableViewCell() 156 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 157 | cell 158 | } 159 | 160 | var snapshot = DiffableDataSourceSnapshot() 161 | snapshot.appendSections([0, 1, 2]) 162 | snapshot.appendItems([0, 1, 2], toSection: 0) 163 | dataSource.apply(snapshot) 164 | 165 | XCTAssertEqual( 166 | dataSource.tableView(tableView, cellForRowAt: IndexPath(item: 1, section: 0)), 167 | cell 168 | ) 169 | } 170 | 171 | func testCanEditRowAt() { 172 | let tableView = MockTableView() 173 | let cell = UITableViewCell() 174 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 175 | cell 176 | } 177 | 178 | var snapshot = DiffableDataSourceSnapshot() 179 | snapshot.appendSections([0, 1, 2]) 180 | snapshot.appendItems([0, 1, 2], toSection: 0) 181 | dataSource.apply(snapshot) 182 | 183 | XCTAssertEqual( 184 | dataSource.tableView(tableView, canEditRowAt: IndexPath(item: 1, section: 0)), 185 | false 186 | ) 187 | } 188 | 189 | func testCanMoveRowAt() { 190 | let tableView = MockTableView() 191 | let cell = UITableViewCell() 192 | let dataSource = TableViewDiffableDataSource(tableView: tableView) { _, _, _ in 193 | cell 194 | } 195 | 196 | var snapshot = DiffableDataSourceSnapshot() 197 | snapshot.appendSections([0, 1, 2]) 198 | snapshot.appendItems([0, 1, 2], toSection: 0) 199 | dataSource.apply(snapshot) 200 | 201 | XCTAssertEqual( 202 | dataSource.tableView(tableView, canMoveRowAt: IndexPath(item: 1, section: 0)), 203 | false 204 | ) 205 | } 206 | } 207 | 208 | final class MockTableView: UITableView { 209 | var isPerformBatchUpdatesCalledCount = 0 210 | 211 | init() { 212 | super.init(frame: .zero, style: .plain) 213 | 214 | let window = UIWindow() 215 | window.addSubview(self) 216 | } 217 | 218 | @available(*, unavailable) 219 | required init?(coder aDecoder: NSCoder) { 220 | fatalError("init(coder:) has not been implemented") 221 | } 222 | 223 | override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) { 224 | isPerformBatchUpdatesCalledCount += 1 225 | updates?() 226 | completion?(true) 227 | } 228 | 229 | override func insertSections(_ sections: IndexSet, with animation: RowAnimation) {} 230 | override func insertRows(at indexPaths: [IndexPath], with animation: RowAnimation) {} 231 | } 232 | 233 | #endif 234 | -------------------------------------------------------------------------------- /Sources/DiffableDataSourceSnapshot.swift: -------------------------------------------------------------------------------- 1 | /// A class for backporting `NSDiffableDataSourceSnapshot` introduced in iOS 13.0+, macOS 10.15+, tvOS 13.0+. 2 | /// Represents the mutable state of diffable data source of UI. 3 | public struct DiffableDataSourceSnapshot { 4 | internal var structure = SnapshotStructure() 5 | 6 | /// Creates a new empty snapshot object. 7 | public init() {} 8 | 9 | /// The number of item identifiers in the snapshot. 10 | public var numberOfItems: Int { 11 | return itemIdentifiers.count 12 | } 13 | 14 | /// The number of section identifiers in the snapshot. 15 | public var numberOfSections: Int { 16 | return sectionIdentifiers.count 17 | } 18 | 19 | /// All section identifiers in the snapshot. 20 | public var sectionIdentifiers: [SectionIdentifierType] { 21 | return structure.allSectionIDs 22 | } 23 | 24 | /// All item identifiers in the snapshot. 25 | public var itemIdentifiers: [ItemIdentifierType] { 26 | return structure.allItemIDs 27 | } 28 | 29 | /// Returns the number of item identifiers in the specified section. 30 | /// 31 | /// - Parameters: 32 | /// - identifier: An identifier of section. 33 | /// 34 | /// - Returns: The number of item identifiers in the specified section. 35 | public func numberOfItems(inSection identifier: SectionIdentifierType) -> Int { 36 | return itemIdentifiers(inSection: identifier).count 37 | } 38 | 39 | /// Returns the item identifiers in the specified section. 40 | /// 41 | /// - Parameters: 42 | /// - identifier: An identifier of section. 43 | /// 44 | /// - Returns: The item identifiers in the specified section. 45 | public func itemIdentifiers(inSection identifier: SectionIdentifierType) -> [ItemIdentifierType] { 46 | return structure.items(in: identifier) 47 | } 48 | 49 | /// Returns a section identifier containing the specified item. 50 | /// 51 | /// - Parameters: 52 | /// - identifier: An identifier of item. 53 | /// 54 | /// - Returns: A section identifier containing the specified item. 55 | public func sectionIdentifier(containingItem identifier: ItemIdentifierType) -> SectionIdentifierType? { 56 | return structure.section(containing: identifier) 57 | } 58 | 59 | /// Returns an index of the specified item. 60 | /// 61 | /// - Parameters: 62 | /// - identifier: An identifier of item. 63 | /// 64 | /// - Returns: An index of the specified item. 65 | public func indexOfItem(_ identifier: ItemIdentifierType) -> Int? { 66 | return itemIdentifiers.firstIndex { $0.isEqualHash(to: identifier) } 67 | } 68 | 69 | /// Returns an index of the specified section. 70 | /// 71 | /// - Parameters: 72 | /// - identifier: An identifier of section. 73 | /// 74 | /// - Returns: An index of the specified section. 75 | public func indexOfSection(_ identifier: SectionIdentifierType) -> Int? { 76 | return sectionIdentifiers.firstIndex { $0.isEqualHash(to: identifier) } 77 | } 78 | 79 | /// Appends the given item identifiers to the specified section or last section. 80 | /// 81 | /// - Parameters: 82 | /// - identifiers: The item identifiers to be appended. 83 | /// - sectionIdentifier: An identifier of section to append the given identiciers. 84 | public mutating func appendItems(_ identifiers: [ItemIdentifierType], toSection sectionIdentifier: SectionIdentifierType? = nil) { 85 | structure.append(itemIDs: identifiers, to: sectionIdentifier) 86 | } 87 | 88 | /// Inserts the given item identifiers before the specified item. 89 | /// 90 | /// - Parameters: 91 | /// - identifiers: The item identifiers to be inserted. 92 | /// - beforeIdentifier: An identifier of item. 93 | public mutating func insertItems(_ identifiers: [ItemIdentifierType], beforeItem beforeIdentifier: ItemIdentifierType) { 94 | structure.insert(itemIDs: identifiers, before: beforeIdentifier) 95 | } 96 | 97 | /// Inserts the given item identifiers after the specified item. 98 | /// 99 | /// - Parameters: 100 | /// - identifiers: The item identifiers to be inserted. 101 | /// - afterIdentifier: An identifier of item. 102 | public mutating func insertItems(_ identifiers: [ItemIdentifierType], afterItem afterIdentifier: ItemIdentifierType) { 103 | structure.insert(itemIDs: identifiers, after: afterIdentifier) 104 | } 105 | 106 | /// Deletes the specified items. 107 | /// 108 | /// - Parameters: 109 | /// - identifiers: The item identifiers to be deleted. 110 | public mutating func deleteItems(_ identifiers: [ItemIdentifierType]) { 111 | structure.remove(itemIDs: identifiers) 112 | } 113 | 114 | /// Deletes the all items in the snapshot. 115 | public mutating func deleteAllItems() { 116 | structure.removeAllItems() 117 | } 118 | 119 | /// Moves the given item identifier before the specified item. 120 | /// 121 | /// - Parameters: 122 | /// - identifier: An item identifier to be moved. 123 | /// - toIdentifier: An identifier of item. 124 | public mutating func moveItem(_ identifier: ItemIdentifierType, beforeItem toIdentifier: ItemIdentifierType) { 125 | structure.move(itemID: identifier, before: toIdentifier) 126 | } 127 | 128 | /// Moves the given item identifier after the specified item. 129 | /// 130 | /// - Parameters: 131 | /// - identifier: An item identifier to be moved. 132 | /// - toIdentifier: An identifier of item. 133 | public mutating func moveItem(_ identifier: ItemIdentifierType, afterItem toIdentifier: ItemIdentifierType) { 134 | structure.move(itemID: identifier, after: toIdentifier) 135 | } 136 | 137 | /// Reloads the specified items. 138 | /// 139 | /// - Parameters: 140 | /// - identifiers: The item identifiers to be reloaded. 141 | public mutating func reloadItems(_ identifiers: [ItemIdentifierType]) { 142 | structure.update(itemIDs: identifiers) 143 | } 144 | 145 | /// Appends the given section identifiers to the end of sections. 146 | /// 147 | /// - Parameters: 148 | /// - identifiers: The section identifiers to be appended. 149 | public mutating func appendSections(_ identifiers: [SectionIdentifierType]) { 150 | structure.append(sectionIDs: identifiers) 151 | } 152 | 153 | /// Inserts the given section identifiers before the specified section. 154 | /// 155 | /// - Parameters: 156 | /// - identifiers: The section identifiers to be inserted. 157 | /// - toIdentifier: An identifier of setion. 158 | public mutating func insertSections(_ identifiers: [SectionIdentifierType], beforeSection toIdentifier: SectionIdentifierType) { 159 | structure.insert(sectionIDs: identifiers, before: toIdentifier) 160 | } 161 | 162 | /// Inserts the given section identifiers after the specified section. 163 | /// 164 | /// - Parameters: 165 | /// - identifiers: The section identifiers to be inserted. 166 | /// - toIdentifier: An identifier of setion. 167 | public mutating func insertSections(_ identifiers: [SectionIdentifierType], afterSection toIdentifier: SectionIdentifierType) { 168 | structure.insert(sectionIDs: identifiers, after: toIdentifier) 169 | } 170 | 171 | /// Deletes the specified sections. 172 | /// 173 | /// - Parameters: 174 | /// - identifiers: The section identifiers to be deleted. 175 | public mutating func deleteSections(_ identifiers: [SectionIdentifierType]) { 176 | structure.remove(sectionIDs: identifiers) 177 | } 178 | 179 | /// Moves the given section identifier before the specified section. 180 | /// 181 | /// - Parameters: 182 | /// - identifier: A section identifier to be moved. 183 | /// - toIdentifier: An identifier of section. 184 | public mutating func moveSection(_ identifier: SectionIdentifierType, beforeSection toIdentifier: SectionIdentifierType) { 185 | structure.move(sectionID: identifier, before: toIdentifier) 186 | } 187 | 188 | /// Moves the given section identifier after the specified section. 189 | /// 190 | /// - Parameters: 191 | /// - identifier: A section identifier to be moved. 192 | /// - toIdentifier: An identifier of section. 193 | public mutating func moveSection(_ identifier: SectionIdentifierType, afterSection toIdentifier: SectionIdentifierType) { 194 | structure.move(sectionID: identifier, after: toIdentifier) 195 | } 196 | 197 | /// Reloads the specified sections. 198 | /// 199 | /// - Parameters: 200 | /// - identifiers: The section identifiers to be reloaded. 201 | public mutating func reloadSections(_ identifiers: [SectionIdentifierType]) { 202 | structure.update(sectionIDs: identifiers) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Sources/UIKit/TableViewDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | 3 | import UIKit 4 | import DifferenceKit 5 | 6 | /// A class for backporting `UITableViewDiffableDataSource` introduced in iOS 13.0+, tvOS 13.0+. 7 | /// Represents the data model object for `UITableView` that can be applies the 8 | /// changes with automatic diffing. 9 | open class TableViewDiffableDataSource: NSObject, UITableViewDataSource { 10 | /// The type of closure providing the cell. 11 | public typealias CellProvider = (UITableView, IndexPath, ItemIdentifierType) -> UITableViewCell? 12 | 13 | /// The default animation to updating the views. 14 | public var defaultRowAnimation: UITableView.RowAnimation = .automatic 15 | 16 | private weak var tableView: UITableView? 17 | private let cellProvider: CellProvider 18 | private let core = DiffableDataSourceCore() 19 | 20 | /// Creates a new data source. 21 | /// 22 | /// - Parameters: 23 | /// - tableView: A table view instance to be managed. 24 | /// - cellProvider: A closure to dequeue the cell for rows. 25 | public init(tableView: UITableView, cellProvider: @escaping CellProvider) { 26 | self.tableView = tableView 27 | self.cellProvider = cellProvider 28 | super.init() 29 | 30 | tableView.dataSource = self 31 | } 32 | 33 | /// Applies given snapshot to perform automatic diffing update. 34 | /// 35 | /// - Parameters: 36 | /// - snapshot: A snapshot object to be applied to data model. 37 | /// - animatingDifferences: A Boolean value indicating whether to update with 38 | /// diffing animation. 39 | /// - completion: An optional completion block which is called when the complete 40 | /// performing updates. 41 | public func apply(_ snapshot: DiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { 42 | core.apply( 43 | snapshot, 44 | view: tableView, 45 | animatingDifferences: animatingDifferences, 46 | performUpdates: { tableView, changeset, setSections in 47 | tableView.reload(using: changeset, with: self.defaultRowAnimation, setData: setSections) 48 | }, 49 | completion: completion 50 | ) 51 | } 52 | 53 | /// Returns a new snapshot object of current state. 54 | /// 55 | /// - Returns: A new snapshot object of current state. 56 | public func snapshot() -> DiffableDataSourceSnapshot { 57 | return core.snapshot() 58 | } 59 | 60 | /// Returns an item identifier for given index path. 61 | /// 62 | /// - Parameters: 63 | /// - indexPath: An index path for the item identifier. 64 | /// 65 | /// - Returns: An item identifier for given index path. 66 | public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { 67 | return core.itemIdentifier(for: indexPath) 68 | } 69 | 70 | /// Returns an index path for given item identifier. 71 | /// 72 | /// - Parameters: 73 | /// - itemIdentifier: An identifier of item. 74 | /// 75 | /// - Returns: An index path for given item identifier. 76 | public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { 77 | return core.indexPath(for: itemIdentifier) 78 | } 79 | 80 | /// Returns the number of sections in the data source. 81 | /// 82 | /// - Parameters: 83 | /// - tableView: A table view instance managed by `self`. 84 | /// 85 | /// - Returns: The number of sections in the data source. 86 | public func numberOfSections(in tableView: UITableView) -> Int { 87 | return core.numberOfSections() 88 | } 89 | 90 | /// Returns the number of items in the specified section. 91 | /// 92 | /// - Parameters: 93 | /// - tableView: A table view instance managed by `self`. 94 | /// - section: An index of section. 95 | /// 96 | /// - Returns: The number of items in the specified section. 97 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 98 | return core.numberOfItems(inSection: section) 99 | } 100 | 101 | /// Returns the title for the specified section's header. 102 | /// 103 | /// - Parameters: 104 | /// - tableView: A table view instance managed by `self`. 105 | /// - section: An index of section. 106 | /// 107 | /// - Returns: The title for the specified section's header, or `nil` for no title. 108 | open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 109 | return nil 110 | } 111 | 112 | /// Returns the title for the specified section's footer. 113 | /// 114 | /// - Parameters: 115 | /// - tableView: A table view instance managed by `self`. 116 | /// - section: An index of section. 117 | /// 118 | /// - Returns: The title for the specified section's footer, or `nil` for no title. 119 | open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 120 | return nil 121 | } 122 | 123 | /// Returns a cell for row at specified index path. 124 | /// 125 | /// - Parameters: 126 | /// - tableView: A table view instance managed by `self`. 127 | /// - indexPath: An index path for cell. 128 | /// 129 | /// - Returns: A cell for row at specified index path. 130 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 131 | let itemIdentifier = core.unsafeItemIdentifier(for: indexPath) 132 | guard let cell = cellProvider(tableView, indexPath, itemIdentifier) else { 133 | universalError("UITableView dataSource returned a nil cell for row at index path: \(indexPath), tableView: \(tableView), itemIdentifier: \(itemIdentifier)") 134 | } 135 | 136 | return cell 137 | } 138 | 139 | /// Returns whether it is possible to edit a row at given index path. 140 | /// 141 | /// - Parameters: 142 | /// - tableView: A table view instance managed by `self`. 143 | /// - section: An index of section. 144 | /// 145 | /// - Returns: A boolean for row at specified index path. 146 | open func tableView(_ tableView: UITableView, canEditRowAt: IndexPath) -> Bool { 147 | return false 148 | } 149 | 150 | /// Returns whether it is possible to move a row at given index path. 151 | /// 152 | /// - Parameters: 153 | /// - tableView: A table view instance managed by `self`. 154 | /// - section: An index of section. 155 | /// 156 | /// - Returns: A boolean for row at specified index path. 157 | open func tableView(_ tableView: UITableView, canMoveRowAt _: IndexPath) -> Bool { 158 | return false 159 | } 160 | 161 | /// Performs the edit action for a row at given index path. 162 | /// 163 | /// - Parameters: 164 | /// - tableView: A table view instance managed by `self`. 165 | /// - editingStyle: An action for given edit action. 166 | /// - indexPath: An index path for cell. 167 | /// 168 | /// - Returns: Void. 169 | open func tableView(_ tableView: UITableView, commit _: UITableViewCell.EditingStyle, forRowAt _: IndexPath) { 170 | // Empty implementation. 171 | } 172 | 173 | /// Moves a row at given index path. 174 | /// 175 | /// - Parameters: 176 | /// - tableView: A table view instance managed by `self`. 177 | /// - source: An index path for given cell position. 178 | /// - target: An index path for target cell position. 179 | /// 180 | /// - Returns: Void. 181 | open func tableView(_ tableView: UITableView, moveRowAt _: IndexPath, to _: IndexPath) { 182 | // Empty implementation. 183 | } 184 | 185 | /// Return list of section titles to display in section index view (e.g. "ABCD...Z#"). 186 | /// 187 | /// - Parameters: 188 | /// - tableView: A table view instance managed by `self`. 189 | /// 190 | /// - Returns: The list of section titles to display. 191 | open func sectionIndexTitles(for tableView: UITableView) -> [String]? { 192 | return nil 193 | } 194 | 195 | /// Tell table which section corresponds to section title/index (e.g. "B",1)). 196 | /// 197 | /// - Parameters: 198 | /// - tableView: A table view instance managed by `self`. 199 | /// - title: The title as displayed in the section index of tableView. 200 | /// - section: An index number identifying a section title in the array returned by sectionIndexTitles(for tableView:). 201 | /// 202 | /// - Returns: The list of section titles to display. 203 | open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle _: String, at section: Int) -> Int { 204 | return section 205 | } 206 | } 207 | 208 | #endif 209 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #4183c4; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | a.discouraged { 67 | text-decoration: line-through; } 68 | a.discouraged:hover, a.discouraged:focus { 69 | text-decoration: underline line-through; } 70 | 71 | table { 72 | background: #fff; 73 | width: 100%; 74 | border-collapse: collapse; 75 | border-spacing: 0; 76 | overflow: auto; 77 | margin: 0 0 0.85em; } 78 | 79 | tr:nth-child(2n) { 80 | background-color: #fbfbfb; } 81 | 82 | th, td { 83 | padding: 6px 13px; 84 | border: 1px solid #ddd; } 85 | 86 | pre { 87 | margin: 0 0 1.275em; 88 | padding: .85em 1em; 89 | overflow: auto; 90 | background: #f7f7f7; 91 | font-size: .85em; 92 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 93 | 94 | code { 95 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 96 | 97 | p > code, li > code { 98 | background: #f7f7f7; 99 | padding: .2em; } 100 | p > code:before, p > code:after, li > code:before, li > code:after { 101 | letter-spacing: -.2em; 102 | content: "\00a0"; } 103 | 104 | pre code { 105 | padding: 0; 106 | white-space: pre; } 107 | 108 | .content-wrapper { 109 | display: flex; 110 | flex-direction: column; } 111 | @media (min-width: 768px) { 112 | .content-wrapper { 113 | flex-direction: row; } } 114 | 115 | .header { 116 | display: flex; 117 | padding: 8px; 118 | font-size: 0.875em; 119 | background: #444; 120 | color: #999; } 121 | 122 | .header-col { 123 | margin: 0; 124 | padding: 0 8px; } 125 | 126 | .header-col--primary { 127 | flex: 1; } 128 | 129 | .header-link { 130 | color: #fff; } 131 | 132 | .header-icon { 133 | padding-right: 6px; 134 | vertical-align: -4px; 135 | height: 16px; } 136 | 137 | .breadcrumbs { 138 | font-size: 0.875em; 139 | padding: 8px 16px; 140 | margin: 0; 141 | background: #fbfbfb; 142 | border-bottom: 1px solid #ddd; } 143 | 144 | .carat { 145 | height: 10px; 146 | margin: 0 5px; } 147 | 148 | .navigation { 149 | order: 2; } 150 | @media (min-width: 768px) { 151 | .navigation { 152 | order: 1; 153 | width: 25%; 154 | max-width: 300px; 155 | padding-bottom: 64px; 156 | overflow: hidden; 157 | word-wrap: normal; 158 | background: #fbfbfb; 159 | border-right: 1px solid #ddd; } } 160 | 161 | .nav-groups { 162 | list-style-type: none; 163 | padding-left: 0; } 164 | 165 | .nav-group-name { 166 | border-bottom: 1px solid #ddd; 167 | padding: 8px 0 8px 16px; } 168 | 169 | .nav-group-name-link { 170 | color: #333; } 171 | 172 | .nav-group-tasks { 173 | margin: 8px 0; 174 | padding: 0 0 0 8px; } 175 | 176 | .nav-group-task { 177 | font-size: 1em; 178 | list-style-type: none; 179 | white-space: nowrap; } 180 | 181 | .nav-group-task-link { 182 | color: #808080; } 183 | 184 | .main-content { 185 | order: 1; } 186 | @media (min-width: 768px) { 187 | .main-content { 188 | order: 2; 189 | flex: 1; 190 | padding-bottom: 60px; } } 191 | 192 | .section { 193 | padding: 0 32px; 194 | border-bottom: 1px solid #ddd; } 195 | 196 | .section-content { 197 | max-width: 834px; 198 | margin: 0 auto; 199 | padding: 16px 0; } 200 | 201 | .section-name { 202 | color: #666; 203 | display: block; } 204 | 205 | .declaration .highlight { 206 | overflow-x: initial; 207 | padding: 8px 0; 208 | margin: 0; 209 | background-color: transparent; 210 | border: none; } 211 | 212 | .task-group-section { 213 | border-top: 1px solid #ddd; } 214 | 215 | .task-group { 216 | padding-top: 0px; } 217 | 218 | .task-name-container a[name]:before { 219 | content: ""; 220 | display: block; } 221 | 222 | .item-container { 223 | padding: 0; } 224 | 225 | .item { 226 | padding-top: 8px; 227 | width: 100%; 228 | list-style-type: none; } 229 | .item a[name]:before { 230 | content: ""; 231 | display: block; } 232 | .item .token, .item .direct-link { 233 | padding-left: 3px; 234 | margin-left: 0px; 235 | font-size: 1rem; } 236 | .item .declaration-note { 237 | font-size: .85em; 238 | color: #808080; 239 | font-style: italic; } 240 | 241 | .pointer-container { 242 | border-bottom: 1px solid #ddd; 243 | left: -23px; 244 | padding-bottom: 13px; 245 | position: relative; 246 | width: 110%; } 247 | 248 | .pointer { 249 | left: 21px; 250 | top: 7px; 251 | display: block; 252 | position: absolute; 253 | width: 12px; 254 | height: 12px; 255 | border-left: 1px solid #ddd; 256 | border-top: 1px solid #ddd; 257 | background: #fff; 258 | transform: rotate(45deg); } 259 | 260 | .height-container { 261 | display: none; 262 | position: relative; 263 | width: 100%; 264 | overflow: hidden; } 265 | .height-container .section { 266 | background: #fff; 267 | border: 1px solid #ddd; 268 | border-top-width: 0; 269 | padding-top: 10px; 270 | padding-bottom: 5px; 271 | padding: 8px 16px; } 272 | 273 | .aside, .language { 274 | padding: 6px 12px; 275 | margin: 12px 0; 276 | border-left: 5px solid #dddddd; 277 | overflow-y: hidden; } 278 | .aside .aside-title, .language .aside-title { 279 | font-size: 9px; 280 | letter-spacing: 2px; 281 | text-transform: uppercase; 282 | padding-bottom: 0; 283 | margin: 0; 284 | color: #aaa; 285 | -webkit-user-select: none; } 286 | .aside p:last-child, .language p:last-child { 287 | margin-bottom: 0; } 288 | 289 | .language { 290 | border-left: 5px solid #cde9f4; } 291 | .language .aside-title { 292 | color: #4183c4; } 293 | 294 | .aside-warning, .aside-deprecated, .aside-unavailable { 295 | border-left: 5px solid #ff6666; } 296 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 297 | color: #ff0000; } 298 | 299 | .graybox { 300 | border-collapse: collapse; 301 | width: 100%; } 302 | .graybox p { 303 | margin: 0; 304 | word-break: break-word; 305 | min-width: 50px; } 306 | .graybox td { 307 | border: 1px solid #ddd; 308 | padding: 5px 25px 5px 10px; 309 | vertical-align: middle; } 310 | .graybox tr td:first-of-type { 311 | text-align: right; 312 | padding: 7px; 313 | vertical-align: top; 314 | word-break: normal; 315 | width: 40px; } 316 | 317 | .slightly-smaller { 318 | font-size: 0.9em; } 319 | 320 | .footer { 321 | padding: 8px 16px; 322 | background: #444; 323 | color: #ddd; 324 | font-size: 0.8em; } 325 | .footer p { 326 | margin: 8px 0; } 327 | .footer a { 328 | color: #fff; } 329 | 330 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 331 | display: none; } 332 | 333 | html.dash .height-container { 334 | display: block; } 335 | 336 | form[role=search] input { 337 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 338 | font-size: 14px; 339 | line-height: 24px; 340 | padding: 0 10px; 341 | margin: 0; 342 | border: none; 343 | border-radius: 1em; } 344 | .loading form[role=search] input { 345 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 346 | 347 | form[role=search] .tt-menu { 348 | margin: 0; 349 | min-width: 300px; 350 | background: #fbfbfb; 351 | color: #333; 352 | border: 1px solid #ddd; } 353 | 354 | form[role=search] .tt-highlight { 355 | font-weight: bold; } 356 | 357 | form[role=search] .tt-suggestion { 358 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 359 | padding: 0 8px; } 360 | form[role=search] .tt-suggestion span { 361 | display: table-cell; 362 | white-space: nowrap; } 363 | form[role=search] .tt-suggestion .doc-parent-name { 364 | width: 100%; 365 | text-align: right; 366 | font-weight: normal; 367 | font-size: 0.9em; 368 | padding-left: 16px; } 369 | 370 | form[role=search] .tt-suggestion:hover, 371 | form[role=search] .tt-suggestion.tt-cursor { 372 | cursor: pointer; 373 | background-color: #4183c4; 374 | color: #fff; } 375 | 376 | form[role=search] .tt-suggestion:hover .doc-parent-name, 377 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 378 | color: #fff; } 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Sources/Internal/SnapshotStructure.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import DifferenceKit 3 | 4 | struct SnapshotStructure { 5 | struct Item: Differentiable, Equatable { 6 | var differenceIdentifier: ItemID 7 | var isReloaded: Bool 8 | 9 | init(id: ItemID, isReloaded: Bool) { 10 | self.differenceIdentifier = id 11 | self.isReloaded = isReloaded 12 | } 13 | 14 | init(id: ItemID) { 15 | self.init(id: id, isReloaded: false) 16 | } 17 | 18 | func isContentEqual(to source: Item) -> Bool { 19 | return !isReloaded && differenceIdentifier == source.differenceIdentifier 20 | } 21 | } 22 | 23 | struct Section: DifferentiableSection, Equatable { 24 | var differenceIdentifier: SectionID 25 | var elements: [Item] = [] 26 | var isReloaded: Bool 27 | 28 | init(id: SectionID, items: [Item], isReloaded: Bool) { 29 | self.differenceIdentifier = id 30 | self.elements = items 31 | self.isReloaded = isReloaded 32 | } 33 | 34 | init(id: SectionID) { 35 | self.init(id: id, items: [], isReloaded: false) 36 | } 37 | 38 | init(source: Section, elements: C) where C.Element == Item { 39 | self.init(id: source.differenceIdentifier, items: Array(elements), isReloaded: source.isReloaded) 40 | } 41 | 42 | func isContentEqual(to source: Section) -> Bool { 43 | return !isReloaded && differenceIdentifier == source.differenceIdentifier 44 | } 45 | } 46 | 47 | var sections: [Section] = [] 48 | 49 | var allSectionIDs: [SectionID] { 50 | return sections.map { $0.differenceIdentifier } 51 | } 52 | 53 | var allItemIDs: [ItemID] { 54 | return sections.lazy 55 | .flatMap { $0.elements } 56 | .map { $0.differenceIdentifier } 57 | } 58 | 59 | func items(in sectionID: SectionID, file: StaticString = #file, line: UInt = #line) -> [ItemID] { 60 | guard let sectionIndex = sectionIndex(of: sectionID) else { 61 | specifiedSectionIsNotFound(sectionID, file: file, line: line) 62 | } 63 | 64 | return sections[sectionIndex].elements.map { $0.differenceIdentifier } 65 | } 66 | 67 | func section(containing itemID: ItemID) -> SectionID? { 68 | return itemPositionMap()[itemID]?.section.differenceIdentifier 69 | } 70 | 71 | mutating func append(itemIDs: [ItemID], to sectionID: SectionID? = nil, file: StaticString = #file, line: UInt = #line) { 72 | let index: Array
.Index 73 | 74 | if let sectionID = sectionID { 75 | guard let sectionIndex = sectionIndex(of: sectionID) else { 76 | specifiedSectionIsNotFound(sectionID, file: file, line: line) 77 | } 78 | 79 | index = sectionIndex 80 | } 81 | else { 82 | guard !sections.isEmpty else { 83 | thereAreCurrentlyNoSections(file: file, line: line) 84 | } 85 | 86 | index = sections.index(before: sections.endIndex) 87 | } 88 | 89 | let items = itemIDs.lazy.map(Item.init) 90 | sections[index].elements.append(contentsOf: items) 91 | } 92 | 93 | mutating func insert(itemIDs: [ItemID], before beforeItemID: ItemID, file: StaticString = #file, line: UInt = #line) { 94 | guard let itemPosition = itemPositionMap()[beforeItemID] else { 95 | specifiedItemIsNotFound(beforeItemID, file: file, line: line) 96 | } 97 | 98 | let items = itemIDs.lazy.map(Item.init) 99 | sections[itemPosition.sectionIndex].elements.insert(contentsOf: items, at: itemPosition.itemRelativeIndex) 100 | } 101 | 102 | mutating func insert(itemIDs: [ItemID], after afterItemID: ItemID, file: StaticString = #file, line: UInt = #line) { 103 | guard let itemPosition = itemPositionMap()[afterItemID] else { 104 | specifiedItemIsNotFound(afterItemID, file: file, line: line) 105 | } 106 | 107 | let itemIndex = sections[itemPosition.sectionIndex].elements.index(after: itemPosition.itemRelativeIndex) 108 | let items = itemIDs.lazy.map(Item.init) 109 | sections[itemPosition.sectionIndex].elements.insert(contentsOf: items, at: itemIndex) 110 | } 111 | 112 | mutating func remove(itemIDs: [ItemID]) { 113 | let itemPositionMap = self.itemPositionMap() 114 | var removeIndexSetMap = [Int: IndexSet]() 115 | 116 | for itemID in itemIDs { 117 | guard let itemPosition = itemPositionMap[itemID] else { 118 | continue 119 | } 120 | 121 | removeIndexSetMap[itemPosition.sectionIndex, default: []].insert(itemPosition.itemRelativeIndex) 122 | } 123 | 124 | for (sectionIndex, removeIndexSet) in removeIndexSetMap { 125 | for range in removeIndexSet.rangeView.reversed() { 126 | sections[sectionIndex].elements.removeSubrange(range) 127 | } 128 | } 129 | } 130 | 131 | mutating func removeAllItems() { 132 | for sectionIndex in sections.indices { 133 | sections[sectionIndex].elements.removeAll() 134 | } 135 | } 136 | 137 | mutating func move(itemID: ItemID, before beforeItemID: ItemID, file: StaticString = #file, line: UInt = #line) { 138 | guard let removed = remove(itemID: itemID) else { 139 | specifiedItemIsNotFound(itemID, file: file, line: line) 140 | } 141 | 142 | guard let itemPosition = itemPositionMap()[beforeItemID] else { 143 | specifiedItemIsNotFound(beforeItemID, file: file, line: line) 144 | } 145 | 146 | sections[itemPosition.sectionIndex].elements.insert(removed, at: itemPosition.itemRelativeIndex) 147 | } 148 | 149 | mutating func move(itemID: ItemID, after afterItemID: ItemID, file: StaticString = #file, line: UInt = #line) { 150 | guard let removed = remove(itemID: itemID) else { 151 | specifiedItemIsNotFound(itemID, file: file, line: line) 152 | } 153 | 154 | guard let itemPosition = itemPositionMap()[afterItemID] else { 155 | specifiedItemIsNotFound(afterItemID, file: file, line: line) 156 | } 157 | 158 | let itemIndex = sections[itemPosition.sectionIndex].elements.index(after: itemPosition.itemRelativeIndex) 159 | sections[itemPosition.sectionIndex].elements.insert(removed, at: itemIndex) 160 | } 161 | 162 | mutating func update(itemIDs: [ItemID], file: StaticString = #file, line: UInt = #line) { 163 | let itemPositionMap = self.itemPositionMap() 164 | 165 | for itemID in itemIDs { 166 | guard let itemPosition = itemPositionMap[itemID] else { 167 | specifiedItemIsNotFound(itemID, file: file, line: line) 168 | } 169 | 170 | sections[itemPosition.sectionIndex].elements[itemPosition.itemRelativeIndex].isReloaded = true 171 | } 172 | } 173 | 174 | mutating func append(sectionIDs: [SectionID]) { 175 | let newSections = sectionIDs.lazy.map(Section.init) 176 | sections.append(contentsOf: newSections) 177 | } 178 | 179 | mutating func insert(sectionIDs: [SectionID], before beforeSectionID: SectionID, file: StaticString = #file, line: UInt = #line) { 180 | guard let sectionIndex = sectionIndex(of: beforeSectionID) else { 181 | specifiedSectionIsNotFound(beforeSectionID, file: file, line: line) 182 | } 183 | 184 | let newSections = sectionIDs.lazy.map(Section.init) 185 | sections.insert(contentsOf: newSections, at: sectionIndex) 186 | } 187 | 188 | mutating func insert(sectionIDs: [SectionID], after afterSectionID: SectionID, file: StaticString = #file, line: UInt = #line) { 189 | guard let beforeIndex = sectionIndex(of: afterSectionID) else { 190 | specifiedSectionIsNotFound(afterSectionID, file: file, line: line) 191 | } 192 | 193 | let sectionIndex = sections.index(after: beforeIndex) 194 | let newSections = sectionIDs.lazy.map(Section.init) 195 | sections.insert(contentsOf: newSections, at: sectionIndex) 196 | } 197 | 198 | mutating func remove(sectionIDs: [SectionID]) { 199 | for sectionID in sectionIDs { 200 | remove(sectionID: sectionID) 201 | } 202 | } 203 | 204 | mutating func move(sectionID: SectionID, before beforeSectionID: SectionID, file: StaticString = #file, line: UInt = #line) { 205 | guard let removed = remove(sectionID: sectionID) else { 206 | specifiedSectionIsNotFound(sectionID, file: file, line: line) 207 | } 208 | 209 | guard let sectionIndex = sectionIndex(of: beforeSectionID) else { 210 | specifiedSectionIsNotFound(beforeSectionID, file: file, line: line) 211 | } 212 | 213 | sections.insert(removed, at: sectionIndex) 214 | } 215 | 216 | mutating func move(sectionID: SectionID, after afterSectionID: SectionID, file: StaticString = #file, line: UInt = #line) { 217 | guard let removed = remove(sectionID: sectionID) else { 218 | specifiedSectionIsNotFound(sectionID, file: file, line: line) 219 | } 220 | 221 | guard let beforeIndex = sectionIndex(of: afterSectionID) else { 222 | specifiedSectionIsNotFound(afterSectionID, file: file, line: line) 223 | } 224 | 225 | let sectionIndex = sections.index(after: beforeIndex) 226 | sections.insert(removed, at: sectionIndex) 227 | } 228 | 229 | mutating func update(sectionIDs: [SectionID]) { 230 | for sectionID in sectionIDs { 231 | guard let sectionIndex = sectionIndex(of: sectionID) else { 232 | continue 233 | } 234 | 235 | sections[sectionIndex].isReloaded = true 236 | } 237 | } 238 | } 239 | 240 | private extension SnapshotStructure { 241 | struct ItemPosition { 242 | var item: Item 243 | var itemRelativeIndex: Int 244 | var section: Section 245 | var sectionIndex: Int 246 | } 247 | 248 | func sectionIndex(of sectionID: SectionID) -> Array
.Index? { 249 | return sections.firstIndex { $0.differenceIdentifier.isEqualHash(to: sectionID) } 250 | } 251 | 252 | @discardableResult 253 | mutating func remove(itemID: ItemID) -> Item? { 254 | guard let itemPosition = itemPositionMap()[itemID] else { 255 | return nil 256 | } 257 | 258 | return sections[itemPosition.sectionIndex].elements.remove(at: itemPosition.itemRelativeIndex) 259 | } 260 | 261 | @discardableResult 262 | mutating func remove(sectionID: SectionID) -> Section? { 263 | guard let sectionIndex = sectionIndex(of: sectionID) else { 264 | return nil 265 | } 266 | 267 | return sections.remove(at: sectionIndex) 268 | } 269 | 270 | func itemPositionMap() -> [ItemID: ItemPosition] { 271 | return sections.enumerated().reduce(into: [:]) { result, section in 272 | for (itemRelativeIndex, item) in section.element.elements.enumerated() { 273 | result[item.differenceIdentifier] = ItemPosition( 274 | item: item, 275 | itemRelativeIndex: itemRelativeIndex, 276 | section: section.element, 277 | sectionIndex: section.offset 278 | ) 279 | } 280 | } 281 | } 282 | 283 | func specifiedItemIsNotFound(_ id: ItemID, file: StaticString, line: UInt) -> Never { 284 | universalError("Specified item\(id) is not found.", file: file, line: line) 285 | } 286 | 287 | func specifiedSectionIsNotFound(_ id: SectionID, file: StaticString, line: UInt) -> Never { 288 | universalError("Specified section\(id) is not found.", file: file, line: line) 289 | } 290 | 291 | func thereAreCurrentlyNoSections(file: StaticString, line: UInt) -> Never { 292 | universalError("There are currently no sections.", file: file, line: line) 293 | } 294 | } 295 | --------------------------------------------------------------------------------