Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive Reordering #976

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fc5659b
Interactive Reordering - initial support
jverdi Oct 24, 2017
345385d
Interactive Reordering - Linter cleanup
jverdi Oct 24, 2017
8f7fb5a
Interactive Reordering - addressing minor feedback
jverdi Oct 24, 2017
a3c61c0
Interactive Reordering - remove adapter delegate, rely on section con…
jverdi Oct 24, 2017
d9bf7fc
Interactive Reordering - moveItemAtIndexPath cleanup
jverdi Oct 24, 2017
54e0b07
Interactive Reordering - list adapter datasource feedback
jverdi Oct 25, 2017
d3bedfc
Interactive Reordering - Tests
jverdi Oct 25, 2017
2c8a42d
Interactive Reordering - Stacked Section Controller support
jverdi Oct 28, 2017
f6dce23
Merge branch 'master' into feature/interactive-reordering
jverdi Oct 28, 2017
893a8be
Merge branch 'master' into feature/interactive-reordering
jverdi Oct 28, 2017
9f355f1
Interactive Reordering - Stacked Section Controller Tests
jverdi Oct 28, 2017
3a70048
Interactive Reordering - Update Changelog
jverdi Oct 28, 2017
5b20441
Examples: pod umbrella file is missing a header reference
jverdi Oct 28, 2017
42188c0
Interactive Reordering - removing assertion that’s no longer necessary
jverdi Oct 28, 2017
16e35b3
Interactive Reordering - swizzle layout methods to reorder past last …
jverdi Nov 3, 2017
6e74899
Interactive Reordering - cleanup stray NSLog
jverdi Nov 3, 2017
fd7d721
Merge branch 'master' into feature/interactive-reordering
jverdi Nov 21, 2017
50b81a4
Interactive Reordering - create a separate moveDelegate for section m…
jverdi Nov 21, 2017
ee69e66
Interactive Reordering - swizzling associated list adapter should be …
jverdi Nov 21, 2017
4ce6401
Interactive Reordering - cleanup tests, run swizzling through tests
jverdi Nov 21, 2017
7aaf51e
Interactive Reordering - make moveDelegate available on iOS 9+ only
jverdi Nov 22, 2017
400a0bc
Interactive Reordering - style feedback
jverdi Nov 22, 2017
1ecb145
Interactive Reordering - respect layout’s w/custom invalidationContex…
jverdi Nov 22, 2017
85d8849
Interactive Reordering - more iOS 9+ markers/conditionals to suppress…
jverdi Nov 22, 2017
f63cb30
Interactive Reordering - per feedback, adapter method is heavy-handed…
jverdi Nov 22, 2017
35b1c4e
Interactive Reordering - style
jverdi Nov 22, 2017
e0b3773
Interactive Reordering - respect layout’s w/custom flowlayout invalid…
jverdi Nov 22, 2017
1d808f0
Merge branch 'master' into feature/interactive-reordering
jverdi Nov 22, 2017
fad162a
Merge branch 'master' into feature/interactive-reordering
jverdi Nov 22, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag

- Added experiment to make `-[IGListAdapter visibleSectionControllers:]` a bit faster. [Maxime Ollivier](https://github.com/maxoll) (tbd)

- Added support for UICollectionView's interactive reordering in iOS 9+. Updates include `-[IGListSectionController canMoveItemAtIndex:]` to enable the behavior, `-[IGListSectionController moveObjectFromIndex:toIndex:]` called when items within a section controller were moved through reordering, `-[IGListAdapterDataSource listAdapter:moveObject:from:to]` called when section controllers themselves were reordered (only possible when all section controllers contain exactly 1 object), and `-[IGListUpdatingDelegate moveSectionInCollectionView:fromIndex:toIndex]` to enable custom updaters to conform to the reordering behavior. The update also includes two new examples `ReorderableSectionController` and `ReorderableStackedViewController` to demonstrate how to enable interactive reordering in your client app. [Jared Verdi](https://github.com/jverdi) [(#976)](https://github.com/Instagram/IGListKit/pull/976)

### Fixes

- Duplicate objects for initial data source setup filtered out. [Mikhail Vashlyaev](https://github.com/yemodin) [(#993](https://github.com/Instagram/IGListKit/pull/993)
Expand Down
16 changes: 16 additions & 0 deletions Examples/Examples-iOS/IGListKitExamples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

/* Begin PBXBuildFile section */
07C17786BCC6ABD68C8BFA69 /* Pods_IGListKitTodayExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DEF56A3C9C414B461D113F /* Pods_IGListKitTodayExample.framework */; };
13BD88281FA3C630001BA5F5 /* PrefixedLabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BD88271FA3C630001BA5F5 /* PrefixedLabelSectionController.swift */; };
13DF01681F9D9CBD0092A320 /* ReorderableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */; };
13DF016A1F9D9F600092A320 /* ReorderableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01691F9D9F600092A320 /* ReorderableViewController.swift */; };
13DF01811FA391740092A320 /* ReorderableStackedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01801FA391740092A320 /* ReorderableStackedViewController.swift */; };
26271C8E1DAE9D3F0073E116 /* SingleSectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26271C8D1DAE9D3F0073E116 /* SingleSectionViewController.swift */; };
26271C921DAE9EFC0073E116 /* NibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26271C911DAE9EFC0073E116 /* NibCell.xib */; };
26271C941DAE9F050073E116 /* NibCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26271C931DAE9F050073E116 /* NibCell.swift */; };
Expand Down Expand Up @@ -140,6 +144,10 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
13BD88271FA3C630001BA5F5 /* PrefixedLabelSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedLabelSectionController.swift; sourceTree = "<group>"; };
13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableSectionController.swift; sourceTree = "<group>"; };
13DF01691F9D9F600092A320 /* ReorderableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableViewController.swift; sourceTree = "<group>"; };
13DF01801FA391740092A320 /* ReorderableStackedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableStackedViewController.swift; sourceTree = "<group>"; };
26271C8D1DAE9D3F0073E116 /* SingleSectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSectionViewController.swift; sourceTree = "<group>"; };
26271C911DAE9EFC0073E116 /* NibCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NibCell.xib; sourceTree = "<group>"; };
26271C931DAE9F050073E116 /* NibCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -335,7 +343,9 @@
292658631E74A2550041B56D /* MonthSectionController.swift */,
295D8A981E92EC96001F7C06 /* PostSectionController.h */,
295D8A991E92EC96001F7C06 /* PostSectionController.m */,
13BD88271FA3C630001BA5F5 /* PrefixedLabelSectionController.swift */,
2942FF891D9F39E00015D24B /* RemoveSectionController.swift */,
13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */,
2942FF8A1D9F39E00015D24B /* SearchSectionController.swift */,
296DD7541DD2150600206780 /* SelfSizingSectionController.swift */,
821BC4B91DB8B61200172ED0 /* StoryboardLabelSectionController.swift */,
Expand Down Expand Up @@ -401,6 +411,8 @@
2991F9231D7BB89F00B0C58F /* NestedAdapterViewController.swift */,
56C05B671E49B2120026DB39 /* ObjcDemoViewController.h */,
56C05B681E49B2120026DB39 /* ObjcDemoViewController.m */,
13DF01801FA391740092A320 /* ReorderableStackedViewController.swift */,
13DF01691F9D9F600092A320 /* ReorderableViewController.swift */,
299B53FF1D6BD6630074A202 /* SearchViewController.swift */,
296DD7521DD2147500206780 /* SelfSizingCellsViewController.swift */,
82D91B681DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift */,
Expand Down Expand Up @@ -861,6 +873,7 @@
82D91B691DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift in Sources */,
29D2E4AF1DD69C0E00CD255D /* DisplayViewController.swift in Sources */,
2942FF911D9F39E00015D24B /* LabelSectionController.swift in Sources */,
13BD88281FA3C630001BA5F5 /* PrefixedLabelSectionController.swift in Sources */,
295D8A9D1E92ECDE001F7C06 /* Post.m in Sources */,
29F7E2AD1E92843A00197586 /* IncrementAnnouncer.swift in Sources */,
2981BA391DB874BB00A987F9 /* WorkingRangeViewController.swift in Sources */,
Expand All @@ -870,6 +883,7 @@
2991F9191D7BADC900B0C58F /* CenterLabelCell.swift in Sources */,
295D8A9A1E92EC96001F7C06 /* PostSectionController.m in Sources */,
29628F141D91905A0026B15A /* DetailLabelCell.swift in Sources */,
13DF01681F9D9CBD0092A320 /* ReorderableSectionController.swift in Sources */,
56C05B751E49B33C0026DB39 /* InteractiveCell.m in Sources */,
2991F9301D7BC0E400B0C58F /* EmptyViewController.swift in Sources */,
29F7E2AF1E92858500197586 /* ListeningSectionController.swift in Sources */,
Expand All @@ -881,10 +895,12 @@
2942FF941D9F39E00015D24B /* UserSectionController.swift in Sources */,
29459C001DBE48E200F05375 /* DiffTableViewController.swift in Sources */,
2991F92C1D7BBE5400B0C58F /* RemoveCell.swift in Sources */,
13DF01811FA391740092A320 /* ReorderableStackedViewController.swift in Sources */,
2942FF8D1D9F39E00015D24B /* EmbeddedSectionController.swift in Sources */,
29C6297B1DCFD857004A5BB1 /* SupplementaryViewController.swift in Sources */,
296DD7551DD2150600206780 /* SelfSizingSectionController.swift in Sources */,
2991F9281D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift in Sources */,
13DF016A1F9D9F600092A320 /* ReorderableViewController.swift in Sources */,
2942FF8F1D9F39E00015D24B /* GridSectionController.swift in Sources */,
821BC4B81DB8B48300172ED0 /* StoryboardCell.swift in Sources */,
56C05B781E49B3A50026DB39 /* PhotoCell.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ final class GridItem: NSObject {
let color: UIColor
let itemCount: Int

var items: [String] = []

init(color: UIColor, itemCount: Int) {
self.color = color
self.itemCount = itemCount

super.init()

self.items = computeItems()
}

private func computeItems() -> [String] {
return [Int](1...itemCount).map {
String(describing: $0)
}
}
}

extension GridItem: ListDiffable {
Expand All @@ -42,8 +53,10 @@ extension GridItem: ListDiffable {
final class GridSectionController: ListSectionController {

private var object: GridItem?
private let isReorderable: Bool

override init() {
required init(isReorderable: Bool = false) {
self.isReorderable = isReorderable
super.init()
self.minimumInteritemSpacing = 1
self.minimumLineSpacing = 1
Expand All @@ -63,7 +76,7 @@ final class GridSectionController: ListSectionController {
guard let cell = collectionContext?.dequeueReusableCell(of: CenterLabelCell.self, for: self, at: index) as? CenterLabelCell else {
fatalError()
}
cell.text = "\(index + 1)"
cell.text = object?.items[index] ?? "undefined"
cell.backgroundColor = object?.color
return cell
}
Expand All @@ -72,4 +85,13 @@ final class GridSectionController: ListSectionController {
self.object = object as? GridItem
}

override func canMoveItem(at index: Int) -> Bool {
return isReorderable
}

override func moveObject(from sourceIndex: Int, to destinationIndex: Int) {
guard let object = object else { return }
let item = object.items.remove(at: sourceIndex)
object.items.insert(item, at: destinationIndex)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
Copyright (c) 2016-present, Facebook, Inc. All rights reserved.

The examples provided by Facebook are for non-commercial testing and evaluation
purposes only. Facebook reserves all rights not expressly granted.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import UIKit
import IGListKit

final class PrefixedLabelSectionController: ListSectionController, ListSupplementaryViewSource {

private var object: LabelsItem?

private let prefix: String
private let group: Int

required init(prefix: String, group: Int) {
self.prefix = prefix
self.group = group
super.init()
self.minimumInteritemSpacing = 1
self.minimumLineSpacing = 1
self.supplementaryViewSource = self
}

override func numberOfItems() -> Int {
guard let object = object else { return 0 }
return group == 1 ? object.labels1.count : object.labels2.count
}

override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}

override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCell(of: LabelCell.self, for: self, at: index) as? LabelCell else {
fatalError()
}
if let object = object {
if group == 1 {
cell.text = "\(prefix) \(object.labels1[index])"
} else {
cell.text = "\(prefix) \(object.labels2[index])"
}
} else {
cell.text = "\(prefix) [X]"
}
cell.backgroundColor = object?.color
return cell
}

override func didUpdate(to object: Any) {
self.object = object as? LabelsItem
}

override func canMoveItem(at index: Int) -> Bool {
return true
}

override func moveObject(from sourceIndex: Int, to destinationIndex: Int) {
guard let object = object else { return }
if group == 1 {
let item = object.labels1.remove(at: sourceIndex)
object.labels1.insert(item, at: destinationIndex)
} else {
let item = object.labels2.remove(at: sourceIndex)
object.labels2.insert(item, at: destinationIndex)
}
}

// MARK: ListSupplementaryViewSource

func supportedElementKinds() -> [String] {
return [UICollectionElementKindSectionHeader]
}

func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView {
guard let view = collectionContext?.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader,
for: self,
nibName: "UserHeaderView",
bundle: nil,
at: index) as? UserHeaderView else {
fatalError()
}
view.name = "Sections of Letters & Numbers"
view.handle = ""
return view
}

func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 40)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
Copyright (c) 2016-present, Facebook, Inc. All rights reserved.

The examples provided by Facebook are for non-commercial testing and evaluation
purposes only. Facebook reserves all rights not expressly granted.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import UIKit
import IGListKit

final class ReorderableSectionController: ListSectionController {

private var object: String?

override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}

override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCell(of: LabelCell.self, for: self, at: index) as? LabelCell else {
fatalError()
}
cell.text = object
return cell
}

override func didUpdate(to object: Any) {
self.object = String(describing: object)
}

override func canMoveItem(at index: Int) -> Bool {
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import IGListKit
final class UserSectionController: ListSectionController {

private var user: User?
private let isReorderable: Bool

required init(isReorderable: Bool = false) {
self.isReorderable = isReorderable
super.init()
}

override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
Expand All @@ -36,4 +42,7 @@ final class UserSectionController: ListSectionController {
self.user = object as? User
}

override func canMoveItem(at index: Int) -> Bool {
return isReorderable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ final class DemosViewController: UIViewController, ListAdapterDataSource {
DemoItem(name: "Calendar (auto diffing)",
controllerClass: CalendarViewController.self),
DemoItem(name: "Dependency Injection",
controllerClass: AnnouncingDepsViewController.self)
controllerClass: AnnouncingDepsViewController.self),
DemoItem(name: "Reorder Cells",
controllerClass: ReorderableViewController.self),
DemoItem(name: "Reorder Stacked Section Controllers",
controllerClass: ReorderableStackedViewController.self)
]

override func viewDidLoad() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
import UIKit
import IGListKit

final class MixedDataViewController: UIViewController, ListAdapterDataSource {
final class MixedDataViewController: UIViewController, ListAdapterDataSource, ListAdapterMoveDelegate {

lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self)
}()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())

let data: [Any] = [
var data: [Any] = [
"Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.",
GridItem(color: UIColor(red: 237/255.0, green: 73/255.0, blue: 86/255.0, alpha: 1), itemCount: 6),
User(pk: 2, name: "Ryan Olson", handle: "ryanolsonk"),
Expand Down Expand Up @@ -54,9 +54,38 @@ final class MixedDataViewController: UIViewController, ListAdapterDataSource {
control.addTarget(self, action: #selector(MixedDataViewController.onControl(_:)), for: .valueChanged)
navigationItem.titleView = control

if #available(iOS 9.0, *) {
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(MixedDataViewController.handleLongGesture(gesture:)))
collectionView.addGestureRecognizer(longPressGesture)
}

view.addSubview(collectionView)
adapter.collectionView = collectionView
adapter.dataSource = self
if #available(iOS 9.0, *) {
adapter.moveDelegate = self
}
}

@available(iOS 9.0, *)
@objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
let touchLocation = gesture.location(in: self.collectionView)
guard let selectedIndexPath = collectionView.indexPathForItem(at: touchLocation) else {
break
}
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
if let view = gesture.view {
let position = gesture.location(in: view)
collectionView.updateInteractiveMovementTargetPosition(position)
}
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}

override func viewDidLayoutSubviews() {
Expand All @@ -82,10 +111,16 @@ final class MixedDataViewController: UIViewController, ListAdapterDataSource {
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
switch object {
case is String: return ExpandableSectionController()
case is GridItem: return GridSectionController()
default: return UserSectionController()
case is GridItem: return GridSectionController(isReorderable: true)
default: return UserSectionController(isReorderable: true)
}
}

func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil }

// MARK: - ListAdapterMoveDelegate

func listAdapter(_ listAdapter: ListAdapter, move object: Any, from previousObjects: [Any], to objects: [Any]) {
data = objects
}
}
Loading