From bb983cf9180925a539277e5aee7cd28d2fc4ef13 Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Fri, 1 Sep 2017 13:49:12 -0700 Subject: [PATCH] Refactors/macos example Summary: Issue fixed: #381 - [X] All tests pass. Demo project builds and runs. - [ ] I added tests, an experiment, or detailed why my change isn't tested. - [X] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [X] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) Closes https://github.com/Instagram/IGListKit/pull/915 Differential Revision: D5758006 Pulled By: rnystrom fbshipit-source-id: cbc3f19b6bb9604d8a6c8c15a16414e558e3b70c --- .../project.pbxproj | 18 +++ .../Base.lproj/Main.storyboard | 105 ++++++---------- .../View/UserCollectionViewCell.swift | 29 +++++ .../View/UserCollectionViewCell.xib | 71 +++++++++++ .../ViewControllers/UsersViewController.swift | 113 ++++++++++-------- 5 files changed, 217 insertions(+), 119 deletions(-) create mode 100644 Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.swift create mode 100644 Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.xib diff --git a/Examples/Examples-macOS/IGListKitExamples.xcodeproj/project.pbxproj b/Examples/Examples-macOS/IGListKitExamples.xcodeproj/project.pbxproj index 867405bf..200fd2a3 100644 --- a/Examples/Examples-macOS/IGListKitExamples.xcodeproj/project.pbxproj +++ b/Examples/Examples-macOS/IGListKitExamples.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 7BF95C4D1F52732200F14EFE /* UserCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */; }; + 7BF95C4F1F5273A100F14EFE /* UserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */; }; 888609091DEF38A00019A4A5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888609081DEF38A00019A4A5 /* AppDelegate.swift */; }; 8886090B1DEF38A00019A4A5 /* UsersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8886090A1DEF38A00019A4A5 /* UsersViewController.swift */; }; 8886090D1DEF38A00019A4A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8886090C1DEF38A00019A4A5 /* Assets.xcassets */; }; @@ -22,6 +24,8 @@ /* Begin PBXFileReference section */ 1CAC2903BAE9D41694D58A7B /* Pods-IGListKitExamples.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IGListKitExamples.release.xcconfig"; path = "Pods/Target Support Files/Pods-IGListKitExamples/Pods-IGListKitExamples.release.xcconfig"; sourceTree = ""; }; 63F1F74ED983018C5D607DDC /* Pods_IGListKitExamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitExamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UserCollectionViewCell.xib; sourceTree = ""; }; + 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserCollectionViewCell.swift; sourceTree = ""; }; 888609051DEF38A00019A4A5 /* IGListKitExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IGListKitExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 888609081DEF38A00019A4A5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 8886090A1DEF38A00019A4A5 /* UsersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersViewController.swift; sourceTree = ""; }; @@ -48,6 +52,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7BF95C4B1F5272FA00F14EFE /* View */ = { + isa = PBXGroup; + children = ( + 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */, + 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; 888608FC1DEF38A00019A4A5 = { isa = PBXGroup; children = ( @@ -73,6 +86,7 @@ DDE3D8551E0311AF00F96BE4 /* Helpers */, DDE3D84D1E030A8000F96BE4 /* Models */, DDE3D84F1E030A9200F96BE4 /* Resources */, + 7BF95C4B1F5272FA00F14EFE /* View */, DDE3D84E1E030A8400F96BE4 /* ViewControllers */, ); path = IGListKitExamples; @@ -204,6 +218,7 @@ buildActionMask = 2147483647; files = ( DDE3D8541E03117600F96BE4 /* users.json in Resources */, + 7BF95C4D1F52732200F14EFE /* UserCollectionViewCell.xib in Resources */, 8886090D1DEF38A00019A4A5 /* Assets.xcassets in Resources */, 888609101DEF38A00019A4A5 /* Main.storyboard in Resources */, ); @@ -289,6 +304,7 @@ DD9018681E0319E40003789D /* IndexSet+Extensions.swift in Sources */, 888609091DEF38A00019A4A5 /* AppDelegate.swift in Sources */, DDE3D8511E030AFA00F96BE4 /* User.swift in Sources */, + 7BF95C4F1F5273A100F14EFE /* UserCollectionViewCell.swift in Sources */, DDE3D8571E0311D000F96BE4 /* UsersProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -407,6 +423,7 @@ COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; @@ -421,6 +438,7 @@ COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; diff --git a/Examples/Examples-macOS/IGListKitExamples/Base.lproj/Main.storyboard b/Examples/Examples-macOS/IGListKitExamples/Base.lproj/Main.storyboard index d1739aaa..e03fa184 100644 --- a/Examples/Examples-macOS/IGListKitExamples/Base.lproj/Main.storyboard +++ b/Examples/Examples-macOS/IGListKitExamples/Base.lproj/Main.storyboard @@ -1,7 +1,8 @@ - + - + + @@ -717,7 +718,7 @@ CA - + @@ -725,96 +726,58 @@ CA - + - - - + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - + + - + - - - - - + + + + - + - + - + diff --git a/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.swift b/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.swift new file mode 100644 index 00000000..cbe743b6 --- /dev/null +++ b/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.swift @@ -0,0 +1,29 @@ +// +// UserCollectionViewCell.swift +// IGListKitExamples +// +// Created by Weyert de Boer on 27/08/2017. +// Copyright © 2017 Instagram. All rights reserved. +// + +import Cocoa + +protocol UserCollectionViewCellDelegate: class { + + func itemDeleted(_ user: User) +} + +final class UserCollectionViewCell: NSCollectionViewItem { + + weak var delegate: UserCollectionViewCellDelegate? + + @IBAction func deleteButtonClicked(_ sender: AnyObject) { + guard let user = representedObject as? User else { return } + delegate?.itemDeleted(user) + } + + func bindViewModel(_ user: User) { + representedObject = user + textField?.stringValue = user.name + } +} diff --git a/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.xib b/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.xib new file mode 100644 index 00000000..562f7487 --- /dev/null +++ b/Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples-macOS/IGListKitExamples/ViewControllers/UsersViewController.swift b/Examples/Examples-macOS/IGListKitExamples/ViewControllers/UsersViewController.swift index 9d65ca96..a0ca7f65 100644 --- a/Examples/Examples-macOS/IGListKitExamples/ViewControllers/UsersViewController.swift +++ b/Examples/Examples-macOS/IGListKitExamples/ViewControllers/UsersViewController.swift @@ -17,7 +17,7 @@ import IGListKit final class UsersViewController: NSViewController { - @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var collectionView: NSCollectionView! // MARK: Data @@ -51,22 +51,40 @@ final class UsersViewController: NSViewController { // MARK: - // MARK: Diffing + var isFirstRun = true var filteredUsers = [User]() { didSet { - // get the difference between the old array of Users and the new array of Users - let diff = ListDiff(oldArray: oldValue, newArray: filteredUsers, option: .equality) - - // this difference is used here to update the table view, but it can be used - // to update collection views and other similar interface elements - // this code can also be added to an extension of NSTableView ;) - tableView.beginUpdates() - tableView.insertRows(at: diff.inserts, withAnimation: .slideDown) - tableView.removeRows(at: diff.deletes, withAnimation: .slideUp) - tableView.reloadData(forRowIndexes: diff.updates, columnIndexes: .zero) - diff.moves.forEach { move in - self.tableView.moveRow(at: move.from, to: move.to) + // A crash occurs when you try to use performBatchUpdates the first time + guard !isFirstRun else { + collectionView.reloadData() + isFirstRun = false + return } - tableView.endUpdates() + + // get the difference between the old array of Users and the new array of Users + let diff = ListDiffPaths(fromSection: 0, toSection: 0, oldArray: oldValue, newArray: filteredUsers, option: .equality) + let batchUpdates = diff.forBatchUpdates() + let inserts = Set(batchUpdates.inserts) + let deletes = Set(batchUpdates.deletes) + let updates = Set(batchUpdates.updates) + let moves = Set(batchUpdates.moves) + + // this difference is used here to update the collection view, but it can be used + // to update collection views and other similar interface elements + // this code can also be added to an extension of NSCollectionView ;) + + // Set the animation duration when updating the collection view + NSAnimationContext.current().duration = 0.25 + + // Perform the updates to the collection view + collectionView.animator().performBatchUpdates({ + collectionView.deleteItems(at: deletes) + collectionView.insertItems(at: inserts) + collectionView.reloadItems(at: updates) + moves.forEach { move in + collectionView.moveItem(at: move.from, to: move.to) + } + }, completionHandler: nil) } } @@ -87,6 +105,9 @@ final class UsersViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() + // The view needs to be backed by a CALayer to be able to enable the collections view animations you can + // enable this by selecting the view controller's view in the Interface Builder in the Core Animation section + // of the View Effects inspector tab, through code you can do by view.wantsLayer = true loadSampleUsers() } @@ -103,48 +124,44 @@ final class UsersViewController: NSViewController { @IBAction func search(_ sender: NSSearchField) { searchTerm = sender.stringValue } - - @IBAction func delete(_ sender: Any?) { - guard !tableView.selectedRowIndexes.isEmpty else { return } - - tableView.selectedRowIndexes.forEach({ self.delete(user: self.filteredUsers[$0]) }) - } - } -extension UsersViewController: NSTableViewDataSource { - - func numberOfRows(in tableView: NSTableView) -> Int { - return filteredUsers.count +extension UsersViewController: UserCollectionViewCellDelegate { + + func itemDeleted(_ user: User) { + self.delete(user: user) } - } -extension UsersViewController: NSTableViewDelegate { +extension UsersViewController: NSCollectionViewDelegate { +} +extension UsersViewController: NSCollectionViewDataSource { + private struct Storyboard { - static let cellIdentifier = "cell" + static let cellIdentifier = "UserCollectionViewCell" } - - func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard let cell = tableView.make(withIdentifier: Storyboard.cellIdentifier, owner: tableView) as? NSTableCellView else { - return nil - } - - cell.textField?.stringValue = filteredUsers[row].name - + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return self.filteredUsers.count + } + + @available(OSX 10.11, *) + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let item = collectionView.makeItem(withIdentifier: Storyboard.cellIdentifier, for: indexPath) + guard let cell = item as? UserCollectionViewCell else { return item } + + cell.delegate = self + cell.bindViewModel(filteredUsers[indexPath.item]) return cell } - - @available(OSX 10.11, *) - func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableRowActionEdge) -> [NSTableViewRowAction] { - let delete = NSTableViewRowAction(style: .destructive, title: "Delete") { _, row in - guard row < self.filteredUsers.count else { return } - - self.delete(user: self.filteredUsers[row]) - } - - return [delete] - } - +} + +extension UsersViewController: NSCollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { + + let availableWidth = collectionView.bounds.width + return CGSize(width: availableWidth, height: 44) + } }