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

Conversation

jverdi
Copy link
Contributor

@jverdi jverdi commented Oct 24, 2017

I had a desire for interactive reordering in a personal project, so here's a first attempt at adding support in IGListKit.

I figured I might as well get a WIP PR up for comments before I continue further as there are a few aspects to interactive reordering that don't interplay perfectly with IGListKit.

As discussed in #291, I went after two prime use cases:

  1. Moving items amongst a section
  2. Rearranging whole sections

I also "disabled" moving items between sections by having those moves revert, to mimic interactive reordering cancellation as closely as possible.

You can see both in the Mixed Data example. Grid items can be moved within a section, while users can be moved to reorder whole sections. But trying to move a grid item out of a grid or a user item into a grid will auto-revert. The revert animation isn't as tight as it should be. It may be more desirable to disable the animation - though you lose the visual cue.

There is a also a new example, ReorderableViewController, that demonstrates 2 in its pure form (likely the most desired use case), where all sections are reorderable single rows.

Happy to take feedback -- this is my first experience working on IGListKit, so I would expect there to be gaps. (Ex. I haven't used IGListStackedSectionController, and its tests failed as I hadn't implemented reordering delegates for it. Those are simply stubbed out for now.)

Changes in this pull request

Issue fixed: #291

Checklist

  • All tests pass. Demo project builds and runs.
  • I added tests, an experiment, or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have reviewed the contributing guide

Additional TODO

  • Proper support in IGListStackedSectionController

@iglistkit-bot
Copy link

iglistkit-bot commented Oct 24, 2017

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@weyert
Copy link
Contributor

weyert commented Oct 24, 2017

Thank you for this pull request @jverdi. Would this increase the minimal iOS version requirement from 8.0 to 9.0? As I think the interactive movement feature was introduced in iOS 9? The library is targeting 8.0 at the moment but maybe this can be changed @rnystrom if necessary? As Instagram itself seems to be iOS 9 or later.

@jverdi
Copy link
Contributor Author

jverdi commented Oct 24, 2017

Hi @weyert - the PR shouldn't require changing the deployment target to iOS 9. The 9+ specific code for interactive reordering only gets implemented in the client apps (and IGListKit example projects). But, to take advantage of this optional feature would require iOS 9.

Common solutions to support interactive reordering on iOS <9 typically impose a specific UICollectionViewLayout like LXReorderableCollectionViewFlowLayout or DraggableCollectionView. I think this strikes a better balance for a library - but definitely up to the maintainers.

@rnystrom
Copy link
Contributor

@jverdi incredible. I’m going to set aside some time to thoroughly review this. Been a feature I and others have wanted for a while. Really excited for this and want to make sure it gets merged!

Sent with GitHawk

@weyert
Copy link
Contributor

weyert commented Oct 24, 2017

Great thank you, @jverdi. Sounds like a great addition to the project! (and sorry for the dumb question ☺️)

@jessesquires
Copy link
Contributor

Thanks @jverdi ! 🙌

@jessesquires jessesquires added this to the 3.2.0 milestone Oct 24, 2017
@@ -60,7 +60,27 @@ NS_SWIFT_NAME(ListAdapterDataSource)
adding the background view and maintaining its visibility.
*/
- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter;


@optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noting:

we can keep this for 3.2 to avoid breaking clients and remove for 4.0.

after this merges, let's be sure to open a new issue to track this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jverdi could we move this to just be a new delegate on IGListAdapter called IGListAdapterMoveDelegate? Then we:

  • Don't have to use @optional at all
  • Can let it be nil by default
  • Don't add any boilerplate to clients that don't use moving (all of them at the beginning). This will saves hundreds of empty implementations within Instagram 😉

Copy link
Contributor

@rnystrom rnystrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome, I think this is absolutely on the right track. Would love to land this and include in 3.2 announcement.

Other thing to start thinking about is testing. We will definitely need to cover this w/ some tests. Doesn't have to be incredibly exhaustive, but at least get some groundwork so if a regression happens we can add tests on top of the initial ones.

@param fromIndex The source index of the section to move.
@param toIndex The destination index of the section to move.
*/
- (void)moveSectionInCollectionView:(UICollectionView *)collectionView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is a breaking change, but I'm not sure if ppl are making their own updaters. @jessesquires thoughts?

@param sectionIndex The index of the section proposed to be moved.
@param index The index of the object proposed to be moved.
*/
- (BOOL)listAdapter:(IGListAdapter *)listAdapter canMoveObjectInSection:(NSInteger)sectionIndex atIndex:(NSInteger)index;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we could make this a property (defaulted to NO) on the adapter? Adding another (eventually) required method like this will be a pretty big pain to upgrade.

Another option, (which I like a lot more), can we add a IGListAdapterMovingDelegate or something onto IGListAdapter? Then we can make all the methods required and not break any upgrades. Plus, if you don't use moving, it makes your data source implementation much cleaner.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this entirely based on later feedback.

// for ease of implementation, first we check with the listAdapter
if ([dataSource respondsToSelector:@selector(listAdapter:canMoveObjectInSection:atIndex:)]) {
return [dataSource listAdapter:self canMoveObjectInSection:sectionIndex atIndex:itemIndex];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a note above, but wondering if we can just skip this entirely? Maybe just rely on the section controller deciding if a move is allowed or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure -- I was thinking about whole section reordering here. My motivation was that for section controllers to be reusable, they might be re-orderable or not depending on the context of the collection/adapter they appear in.

But, if the delegates are all to be required eventually, yes, it's worth dropping this to reduce complexity.

IGAssert(dataSource != nil, @"Found a nil dataSource when requesting canMoveItemAtIndexPath for interactive reordering");

NSInteger sectionIndex = indexPath.section;
NSInteger itemIndex = indexPath.item;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: const on both

NSInteger sourceItemIndex = sourceIndexPath.item;
NSInteger destinationItemIndex = destinationIndexPath.item;
NSInteger sourceSectionIndex = sourceIndexPath.section;
NSInteger destinationSectionIndex = destinationIndexPath.section;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: const all of it

// the "item" representing our section was dropped
// into the beginning of a destination section rather than the end
// so it really belongs one section before the section where it landed
destinationSectionIndex -= 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great!


// otherwise this is a move of an _item_ from one section to another section
// this is not currently supported, so we need to revert the change as it's too late to cancel
[self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're comments are 💯


@param index The index of the unhighlighted cell.

@note Interactive reordering is supported both for items within a single section, as well as for reordering sections themselves when sections contain only one item. The default implementation returns false.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's newline. I try to wrap comments at 120 chars (we should add a lint)

Also "false" should be "NO" with markdown code ticks

screen shot 2017-10-24 at 3 57 04 pm

}

- (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex {
IGFailAssert(@"Section controller %@ must override %s if interactive reordering is enabled.", self, __PRETTY_FUNCTION__);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@param sourceIndex The starting index of the object.
@param destinationIndex The ending index of the object.
*/
- (void)listAdapter:(IGListAdapter *)listAdapter moveSectionAtIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B/c data sources could be out of sync w/ what is being displayed (ultimately held by IGListAdapter internally), I think we should do one of the following:

  • Add params for to/from objects
  • Add to/from object arrays

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@jverdi
Copy link
Contributor Author

jverdi commented Oct 25, 2017

Ok, feedback addressed and tests added. I still have to dive into IGListStackedSectionController, but the PR is close I think.

@bosbefok
Copy link

Super keen to see this PR merged as it's something I'm keen to use in a current project.

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@facebook-github-bot
Copy link
Contributor

@jverdi has updated the pull request. View: changes

@rnystrom
Copy link
Contributor

I'm going to hold on importing until Monday since everyone will be back from the holidays. SO excited!

@jverdi
Copy link
Contributor Author

jverdi commented Nov 23, 2017

Sounds great! Thanks for all the feedback.

@adenisonafifi
Copy link

@jverdi I was trying out your interactive reordering functionality and I experienced a reoccurring crash in the following scenario:

UICollectionView with layout: IGListCollectionViewLayout
Section controllers with one item per section
Three sections per collection view row like: ( [ ] - [ ] - [ ] )

While dragging to move a section controller it would crash on: collectionView.updateInteractiveMovementTargetPosition(position)

I was able to successfully use your PR by switching my data source to use one section controller with many items. Please ignore if it was intended to not function with many section controllers per row.

return NO;
}

- (BOOL)canMoveItemAtIndex:(NSInteger)sourceItemIndex toIndex:(NSInteger)destinationItemIndex {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be exposed for overriding in order to allow users to prevent moving to specific indices?
Currently even if canMoveItemAtIndex: returns false for a specific index that item can still be moved when you move another item into its place.

@rnystrom
Copy link
Contributor

Thanks for the reminder! Going to merge

Sent with GitHawk

Copy link
Contributor

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rnystrom is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@rnystrom rnystrom modified the milestones: 3.2.0, 3.3.0 Jan 29, 2018
IGListSectionController *sectionController = [self sectionControllerForObjectIndex:index];
// during interactive reordering, its possible for an item to be moved into a section beyond the last section
// in that case, just return the size of the current last item in the section
const NSInteger maxIndex = [self numberOfItems]-1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h/t to @amonshiz for catching this bug. numberOfItems can absolutely return 0. I'm updating locally before landing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😊

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol oops 😳

@rnystrom
Copy link
Contributor

Made a gif of it in action 😄

reorder

@jessesquires
Copy link
Contributor

nice! 😎

@RamblinWreck77
Copy link

This is super cool! I'm really looking forward to 3.3.0 now. We're working on a live data gauge cluster and the ability to move/reorder them like this would be really slick. Great work @jverdi !!!

@rnystrom will this work on an NxN grid as well?

@rnystrom
Copy link
Contributor

@RamblinWreck77 3.3.0 WIP right now 😇

I believe it should work just fine w/ an NxN grid but haven't done that myself. Would be an awesome addition to the demos app!

@JGVB
Copy link

JGVB commented May 22, 2020

Why didn't this get merged? :(

@jessesquires
Copy link
Contributor

@JGVB it was merged. f15b167

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for interactive cell movement/reordering