Collection View Changes

Enhancements to UICollectionView for iOS 9

PDF for offline use
Related Samples:
Related SDKs:

Let us know how you feel about this

Translation Quality


0/250

This article covers the enhancement to Collection Views (UICollectionView) that have been made for iOS 9 and how to implement them in Xamarin.iOS.

In iOS 9, the collection view (UICollectionView) now supports drag reordering of items out of the box by adding a new default gesture recognizer and several new supporting methods.

Using these new methods, you can easily implement drag to reorder in your collection view and have the option of customizing the items appearance during any stage of the reordering process.

In this article, we'll take a look at implementing drag-to-reorder in a Xamarin.iOS application as well as some of the other changes iOS 9 has made to the collection view control:

Easy Reordering of Items

As stated above, one of the most significant changes to the collection view in iOS 9 was the addition of easy drag-to-reorder functionality out of the box.

In iOS 9, the quickest way to add reordering to a collection view is to use a UICollectionViewController. The collection view controller now has a InstallsStandardGestureForInteractiveMovement property, which adds a standard gesture recognizer that supports dragging to reorder items in the collection. Since the default value is true, you only have to implement the MoveItem method of the UICollectionViewDataSource class to support drag-to-reorder. For example:

public override void MoveItem (UICollectionView collectionView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
{
    // Reorder our list of items
    ...
}

Simple Reordering Example

As a quick example, start a new Xamarin.iOS project and edit the Main.storyboard file. Drag a UICollectionViewController into the UI:

Now, select the Collection View, switch to the Size Inspector and set the following sizes for the Collection:

Next, edit the default Cell, change its background color to blue and add a label to act as the title for the cell:

Add two constraints to keep the Label centered inside the cell as it changes size:

Switch to the Identity Inspector and set the Class to TextCollectionViewCell:

In the Attribute Inspector, set the Collection Reusable View to Cell:

Finally, wire-up the Label to an Outlet called TextLabel:

Return to Xamarin Studio and allow the UI changes you made to sync. Now, edit the TextCollectionViewCell class and make it look like the following:

using System;
using Foundation;
using UIKit;

namespace CollectionView
{
    public partial class TextCollectionViewCell : UICollectionViewCell
    {
        #region Computed Properties
        public string Title {
            get { return TextLabel.Text; }
            set { TextLabel.Text = value; }
        }
        #endregion

        #region Constructors
        public TextCollectionViewCell (IntPtr handle) : base (handle)
        {
        }
        #endregion
    }
}

Here the Text property of the label is exposed as the title of the cell, so it can be set from code.

Add a new C# class to the project and call it WaterfallCollectionSource. Edit the file and make it look like the following:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;

namespace CollectionView
{
    public class WaterfallCollectionSource : UICollectionViewDataSource
    {
        #region Computed Properties
        public WaterfallCollectionView CollectionView { get; set;}
        public List<int> Numbers { get; set; } = new List<int> ();
        #endregion

        #region Constructors
        public WaterfallCollectionSource (WaterfallCollectionView collectionView)
        {
            // Initialize
            CollectionView = collectionView;

            // Init numbers collection
            for (int n = 0; n < 100; ++n) {
                Numbers.Add (n);
            }
        }
        #endregion

        #region Override Methods
        public override nint NumberOfSections (UICollectionView collectionView) {
            // We only have one section
            return 1;
        }

        public override nint GetItemsCount (UICollectionView collectionView, nint section) {
            // Return the number of items
            return Numbers.Count;
        }

        public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
        {
            // Get a reusable cell and set {~~it's~>its~~} title from the item
            var cell = collectionView.DequeueReusableCell ("Cell", indexPath) as TextCollectionViewCell;
            cell.Title = Numbers [(int)indexPath.Item].ToString();

            return cell;
        }

        public override bool CanMoveItem (UICollectionView collectionView, NSIndexPath indexPath) {
            // We can always move items
            return true;
        }

        public override void MoveItem (UICollectionView collectionView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
        {
            // Reorder our list of items
            var item = Numbers [(int)sourceIndexPath.Item];
            Numbers.RemoveAt ((int)sourceIndexPath.Item);
            Numbers.Insert ((int)destinationIndexPath.Item, item);
        }
        #endregion
    }
}

This class will be the data source for our collection view and provide the information for each cell in the collection. Notice that the MoveItem method is implemented to allow items in the collection to be drag reordered.

Add another new C# class to the project and call it WaterfallCollectionDelegate. Edit this file and make it look like the following:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;

namespace CollectionView
{
    public class WaterfallCollectionDelegate : UICollectionViewDelegate
    {
        #region Computed Properties
        public WaterfallCollectionView CollectionView { get; set;}
        #endregion

        #region Constructors
        public WaterfallCollectionDelegate (WaterfallCollectionView collectionView)
        {

            // Initialize
            CollectionView = collectionView;

        }
        #endregion

        #region Overrides Methods
        public override bool ShouldHighlightItem (UICollectionView collectionView, NSIndexPath indexPath) {
            // Always allow for highlighting
            return true;
        }

        public override void ItemHighlighted (UICollectionView collectionView, NSIndexPath indexPath)
        {
            // Get cell and change to green background
            var cell = collectionView.CellForItem(indexPath);
            cell.ContentView.BackgroundColor = UIColor.FromRGB(183,208,57);
        }

        public override void ItemUnhighlighted (UICollectionView collectionView, NSIndexPath indexPath)
        {
            // Get cell and return to blue background
            var cell = collectionView.CellForItem(indexPath);
            cell.ContentView.BackgroundColor = UIColor.FromRGB(164,205,255);
        }
        #endregion
    }
}

This will act as the delegate for our collection view. Methods have been overridden to highlight a cell as the user interacts with it in the collection view.

Add one last C# class to the project and call it WaterfallCollectionView. Edit this file and make it look like the following:

using System;
using UIKit;
using System.Collections.Generic;
using Foundation;

namespace CollectionView
{
    [Register("WaterfallCollectionView")]
    public class WaterfallCollectionView : UICollectionView
    {

        #region Constructors
        public WaterfallCollectionView (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();

            // Initialize
            DataSource = new WaterfallCollectionSource(this);
            Delegate = new WaterfallCollectionDelegate(this);

        }
        #endregion
    }
}

Notice that DataSource and Delegate that we created above are set when the collection view is constructed from its storyboard (or .xib file).

Edit the Main.storyboard file again and select the collection view and switch to the Identity Inspector. Set the Class to the custom WaterfallCollectionView class that we defined above:

Save the changes you made to the UI, return to Xamarin Studio and run the app. If the user selects an item from the list and drags it to a new location, the other items will animate automatically as they move out of the way of the item. When the user drops the item in a new location, it will stick to that location. For example:

Using a Custom Gesture Recognizer

In cases where you cannot use a UICollectionViewController and must use a regular UIViewController, or if you wish to take more control over the drag-and-drop gesture, you can create your own custom Gesture Recognizer and add it to the Collection View when the View loads. For example:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    // Create a custom gesture recognizer
    var longPressGesture = new UILongPressGestureRecognizer ((gesture) => {

        // Take action based on state
        switch(gesture.State) {
        case UIGestureRecognizerState.Began:
            var selectedIndexPath = CollectionView.IndexPathForItemAtPoint(gesture.LocationInView(View));
            if (selectedIndexPath !=null) {
                CollectionView.BeginInteractiveMovementForItem(selectedIndexPath);
            }
            break;
        case UIGestureRecognizerState.Changed:
            CollectionView.UpdateInteractiveMovementTargetPosition(gesture.LocationInView(View));
            break;
        case UIGestureRecognizerState.Ended:
            CollectionView.EndInteractiveMovement();
            break;
        default:
            CollectionView.CancelInteractiveMovement();
            break;
        }

    });

    // Add the custom recognizer to the collection view
    CollectionView.AddGestureRecognizer(longPressGesture);
}

Here we are using several new methods added to the collection view to implement and control the drag operation:

  • BeginInteractiveMovementForItem - Marks the start of a move operation.
  • UpdateInteractiveMovementTargetPosition - Is sent as the item's location is updated.
  • EndInteractiveMovement - Marks the end of an item move.
  • CancelInteractiveMovement - Marks the user canceling the move operation.

When the application is run, the drag operation will work exactly like the default drag gesture recognizer that comes with the collection view.

Custom Layouts and Reordering

In iOS 9, several new methods have been added to work with drag-to-reorder and custom layouts in a collection view. To explore this feature, let's add a custom layout to the collection.

First, add a new C# class called WaterfallCollectionLayout to the project. Edit it and make it look like the following:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;
using CoreGraphics;

namespace CollectionView
{
    [Register("WaterfallCollectionLayout")]
    public class WaterfallCollectionLayout : UICollectionViewLayout
    {
        #region Private Variables
        private int columnCount = 2;
        private nfloat minimumColumnSpacing = 10;
        private nfloat minimumInterItemSpacing = 10;
        private nfloat headerHeight = 0.0f;
        private nfloat footerHeight = 0.0f;
        private UIEdgeInsets sectionInset = new UIEdgeInsets(0, 0, 0, 0);
        private WaterfallCollectionRenderDirection itemRenderDirection = WaterfallCollectionRenderDirection.ShortestFirst;
        private Dictionary<nint,UICollectionViewLayoutAttributes> headersAttributes = new Dictionary<nint, UICollectionViewLayoutAttributes>();
        private Dictionary<nint,UICollectionViewLayoutAttributes> footersAttributes = new Dictionary<nint, UICollectionViewLayoutAttributes>();
        private List<CGRect> unionRects = new List<CGRect>();
        private List<nfloat> columnHeights = new List<nfloat>();
        private List<UICollectionViewLayoutAttributes> allItemAttributes = new List<UICollectionViewLayoutAttributes>();
        private List<List<UICollectionViewLayoutAttributes>> sectionItemAttributes = new List<List<UICollectionViewLayoutAttributes>>();
        private nfloat unionSize = 20;
        #endregion

        #region Computed Properties
        [Export("ColumnCount")]
        public int ColumnCount {
            get { return columnCount; }
            set {
                WillChangeValue ("ColumnCount");
                columnCount = value;
                DidChangeValue ("ColumnCount");

                InvalidateLayout ();
            }
        }

        [Export("MinimumColumnSpacing")]
        public nfloat MinimumColumnSpacing {
            get { return minimumColumnSpacing; }
            set {
                WillChangeValue ("MinimumColumnSpacing");
                minimumColumnSpacing = value;
                DidChangeValue ("MinimumColumnSpacing");

                InvalidateLayout ();
            }
        }

        [Export("MinimumInterItemSpacing")]
        public nfloat MinimumInterItemSpacing {
            get { return minimumInterItemSpacing; }
            set {
                WillChangeValue ("MinimumInterItemSpacing");
                minimumInterItemSpacing = value;
                DidChangeValue ("MinimumInterItemSpacing");

                InvalidateLayout ();
            }
        }

        [Export("HeaderHeight")]
        public nfloat HeaderHeight {
            get { return headerHeight; }
            set {
                WillChangeValue ("HeaderHeight");
                headerHeight = value;
                DidChangeValue ("HeaderHeight");

                InvalidateLayout ();
            }
        }

        [Export("FooterHeight")]
        public nfloat FooterHeight {
            get { return footerHeight; }
            set {
                WillChangeValue ("FooterHeight");
                footerHeight = value;
                DidChangeValue ("FooterHeight");

                InvalidateLayout ();
            }
        }

        [Export("SectionInset")]
        public UIEdgeInsets SectionInset {
            get { return sectionInset; }
            set {
                WillChangeValue ("SectionInset");
                sectionInset = value;
                DidChangeValue ("SectionInset");

                InvalidateLayout ();
            }
        }

        [Export("ItemRenderDirection")]
        public WaterfallCollectionRenderDirection ItemRenderDirection {
            get { return itemRenderDirection; }
            set {
                WillChangeValue ("ItemRenderDirection");
                itemRenderDirection = value;
                DidChangeValue ("ItemRenderDirection");

                InvalidateLayout ();
            }
        }
        #endregion

        #region Constructors
        public WaterfallCollectionLayout ()
        {
        }

        public WaterfallCollectionLayout(NSCoder coder) : base(coder) {

        }
        #endregion

        #region Public Methods
        public nfloat ItemWidthInSectionAtIndex(int section) {

            var width = CollectionView.Bounds.Width - SectionInset.Left - SectionInset.Right;
            return (nfloat)Math.Floor ((width - ((ColumnCount - 1) * MinimumColumnSpacing)) / ColumnCount);
        }
        #endregion

        #region Override Methods
        public override void PrepareLayout ()
        {
            base.PrepareLayout ();

            // Get the number of sections
            var numberofSections = CollectionView.NumberOfSections();
            if (numberofSections == 0)
                return;

            // Reset collections
            headersAttributes.Clear ();
            footersAttributes.Clear ();
            unionRects.Clear ();
            columnHeights.Clear ();
            allItemAttributes.Clear ();
            sectionItemAttributes.Clear ();

            // Initialize column heights
            for (int n = 0; n < ColumnCount; n++) {
                columnHeights.Add ((nfloat)0);
            }

            // Process all sections
            nfloat top = 0.0f;
            var attributes = new UICollectionViewLayoutAttributes ();
            var columnIndex = 0;
            for (nint section = 0; section < numberofSections; ++section) {
                // Calculate section specific metrics
                var minimumInterItemSpacing = (MinimumInterItemSpacingForSection == null) ? MinimumColumnSpacing :
                    MinimumInterItemSpacingForSection (CollectionView, this, section);

                // Calculate widths
                var width = CollectionView.Bounds.Width - SectionInset.Left - SectionInset.Right;
                var itemWidth = (nfloat)Math.Floor ((width - ((ColumnCount - 1) * MinimumColumnSpacing)) / ColumnCount);

                // Calculate section header
                var heightHeader = (HeightForHeader == null) ? HeaderHeight :
                    HeightForHeader (CollectionView, this, section);

                if (heightHeader > 0) {
                    attributes = UICollectionViewLayoutAttributes.CreateForSupplementaryView (UICollectionElementKindSection.Header, NSIndexPath.FromRowSection (0, section));
                    attributes.Frame = new CGRect (0, top, CollectionView.Bounds.Width, heightHeader);
                    headersAttributes.Add (section, attributes);
                    allItemAttributes.Add (attributes);

                    top = attributes.Frame.GetMaxY ();
                }

                top += SectionInset.Top;
                for (int n = 0; n < ColumnCount; n++) {
                    columnHeights [n] = top;
                }

                // Calculate Section Items
                var itemCount = CollectionView.NumberOfItemsInSection(section);
                List<UICollectionViewLayoutAttributes> itemAttributes = new List<UICollectionViewLayoutAttributes> ();

                for (nint n = 0; n < itemCount; n++) {
                    var indexPath = NSIndexPath.FromRowSection (n, section);
                    columnIndex = NextColumnIndexForItem (n);
                    var xOffset = SectionInset.Left + (itemWidth + MinimumColumnSpacing) * (nfloat)columnIndex;
                    var yOffset = columnHeights [columnIndex];
                    var itemSize = (SizeForItem == null) ? new CGSize (0, 0) : SizeForItem (CollectionView, this, indexPath);
                    nfloat itemHeight = 0.0f;

                    if (itemSize.Height > 0.0f && itemSize.Width > 0.0f) {
                        itemHeight = (nfloat)Math.Floor (itemSize.Height * itemWidth / itemSize.Width);
                    }

                    attributes = UICollectionViewLayoutAttributes.CreateForCell (indexPath);
                    attributes.Frame = new CGRect (xOffset, yOffset, itemWidth, itemHeight);
                    itemAttributes.Add (attributes);
                    allItemAttributes.Add (attributes);
                    columnHeights [columnIndex] = attributes.Frame.GetMaxY () + MinimumInterItemSpacing;
                }
                sectionItemAttributes.Add (itemAttributes);

                // Calculate Section Footer
                nfloat footerHeight = 0.0f;
                columnIndex = LongestColumnIndex();
                top = columnHeights [columnIndex] - MinimumInterItemSpacing + SectionInset.Bottom;
                footerHeight = (HeightForFooter == null) ? FooterHeight : HeightForFooter(CollectionView, this, section);

                if (footerHeight > 0) {
                    attributes = UICollectionViewLayoutAttributes.CreateForSupplementaryView (UICollectionElementKindSection.Footer, NSIndexPath.FromRowSection (0, section));
                    attributes.Frame = new CGRect (0, top, CollectionView.Bounds.Width, footerHeight);
                    footersAttributes.Add (section, attributes);
                    allItemAttributes.Add (attributes);
                    top = attributes.Frame.GetMaxY ();
                }

                for (int n = 0; n < ColumnCount; n++) {
                    columnHeights [n] = top;
                }
            }

            var i =0;
            var attrs = allItemAttributes.Count;
            while(i < attrs) {
                var rect1 = allItemAttributes [i].Frame;
                i = (int)Math.Min (i + unionSize, attrs) - 1;
                var rect2 = allItemAttributes [i].Frame;
                unionRects.Add (CGRect.Union (rect1, rect2));
                i++;
            }

        }

        public override CGSize CollectionViewContentSize {
            get {
                if (CollectionView.NumberOfSections () == 0) {
                    return new CGSize (0, 0);
                }

                var contentSize = CollectionView.Bounds.Size;
                contentSize.Height = columnHeights [0];
                return contentSize;
            }
        }

        public override UICollectionViewLayoutAttributes LayoutAttributesForItem (NSIndexPath indexPath)
        {
            if (indexPath.Section >= sectionItemAttributes.Count) {
                return null;
            }

            if (indexPath.Item >= sectionItemAttributes [indexPath.Section].Count) {
                return null;
            }

            var list = sectionItemAttributes [indexPath.Section];
            return list [(int)indexPath.Item];
        }

        public override UICollectionViewLayoutAttributes LayoutAttributesForSupplementaryView (NSString kind, NSIndexPath indexPath)
        {
            var attributes = new UICollectionViewLayoutAttributes ();

            switch (kind) {
            case "header":
                attributes = headersAttributes [indexPath.Section];
                break;
            case "footer":
                attributes = footersAttributes [indexPath.Section];
                break;
            }

            return attributes;
        }

        public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (CGRect rect)
        {
            var begin = 0;
            var end = unionRects.Count;
            List<UICollectionViewLayoutAttributes> attrs = new List<UICollectionViewLayoutAttributes> ();


            for (int i = 0; i < end; i++) {
                if (rect.IntersectsWith(unionRects[i])) {
                    begin = i * (int)unionSize;
                }
            }

            for (int i = end - 1; i >= 0; i--) {
                if (rect.IntersectsWith (unionRects [i])) {
                    end = (int)Math.Min ((i + 1) * (int)unionSize, allItemAttributes.Count);
                    break;
                }
            }

            for (int i = begin; i < end; i++) {
                var attr = allItemAttributes [i];
                if (rect.IntersectsWith (attr.Frame)) {
                    attrs.Add (attr);
                }
            }

            return attrs.ToArray();
        }

        public override bool ShouldInvalidateLayoutForBoundsChange (CGRect newBounds)
        {
            var oldBounds = CollectionView.Bounds;
            return (newBounds.Width != oldBounds.Width);
        }
        #endregion

        #region Private Methods
        private int ShortestColumnIndex() {
            var index = 0;
            var shortestHeight = nfloat.MaxValue;
            var n = 0;

            // Scan each column for the shortest height
            foreach (nfloat height in columnHeights) {
                if (height < shortestHeight) {
                    shortestHeight = height;
                    index = n;
                }
                ++n;
            }

            return index;
        }

        private int LongestColumnIndex() {
            var index = 0;
            var longestHeight = nfloat.MinValue;
            var n = 0;

            // Scan each column for the shortest height
            foreach (nfloat height in columnHeights) {
                if (height > longestHeight) {
                    longestHeight = height;
                    index = n;
                }
                ++n;
            }

            return index;
        }

        private int NextColumnIndexForItem(nint item) {
            var index = 0;

            switch (ItemRenderDirection) {
            case WaterfallCollectionRenderDirection.ShortestFirst:
                index = ShortestColumnIndex ();
                break;
            case WaterfallCollectionRenderDirection.LeftToRight:
                index = ColumnCount;
                break;
            case WaterfallCollectionRenderDirection.RightToLeft:
                index = (ColumnCount - 1) - ((int)item / ColumnCount);
                break;
            }

            return index;
        }
        #endregion

        #region Events
        public delegate CGSize WaterfallCollectionSizeDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, NSIndexPath indexPath);
        public delegate nfloat WaterfallCollectionFloatDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, nint section);
        public delegate UIEdgeInsets WaterfallCollectionEdgeInsetsDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, nint section);

        public event WaterfallCollectionSizeDelegate SizeForItem;
        public event WaterfallCollectionFloatDelegate HeightForHeader;
        public event WaterfallCollectionFloatDelegate HeightForFooter;
        public event WaterfallCollectionEdgeInsetsDelegate InsetForSection;
        public event WaterfallCollectionFloatDelegate MinimumInterItemSpacingForSection;
        #endregion
    }
}

This can be used class to provide a custom two column, waterfall type layout to the collection view. The code uses Key-Value Coding (via the WillChangeValue and DidChangeValue methods) to provide data binding for our computed properties in this class.

Next, edit the WaterfallCollectionSource and make the following changes and additions:

private Random rnd = new Random();
...

public List<nfloat> Heights { get; set; } = new List<nfloat> ();
...

public WaterfallCollectionSource (WaterfallCollectionView collectionView)
{
    // Initialize
    CollectionView = collectionView;

    // Init numbers collection
    for (int n = 0; n < 100; ++n) {
        Numbers.Add (n);
        Heights.Add (rnd.Next (0, 100) + 40.0f);
    }
}

This will create a random height for each of the items that will be displayed in the list.

Next, edit the WaterfallCollectionView class and add the following helper property:

public WaterfallCollectionSource Source {
    get { return (WaterfallCollectionSource)DataSource; }
}

This will make it easier to get at our data source (and the item heights) from the custom layout.

Finally, edit the view controller and add the following code:

public override void AwakeFromNib ()
{
    base.AwakeFromNib ();

    var waterfallLayout = new WaterfallCollectionLayout ();

    // Wireup events
    waterfallLayout.SizeForItem += (collectionView, layout, indexPath) => {
        var collection = collectionView as WaterfallCollectionView;
        return new CGSize((View.Bounds.Width-40)/3,collection.Source.Heights[(int)indexPath.Item]);
    };

    // Attach the custom layout to the collection
    CollectionView.SetCollectionViewLayout(waterfallLayout, false);
}

This creates an instance of our custom layout, sets the event to provide the size of each item and attaches the new layout to our collection view.

If we run the Xamarin.iOS app again, the collection view will now look like the following:

We can still drag-to-reorder items as before, but the items will now change size to fit their new location when they are dropped.

Collection View Changes

In the following sections, we'll take a detailed look at the changes made to each class in the collection view by iOS 9.

UICollectionView

The following changes or additions have been made to the UICollectionView class for iOS 9:

  • BeginInteractiveMovementForItem – Marks the start of a drag operation.
  • CancelInteractiveMovement – Informs the collection view that the user has canceled a drag operation.
  • EndInteractiveMovement – Informs the collection view that the user has finished a drag operation.
  • GetIndexPathsForVisibleSupplementaryElements – Returns the indexPath of a header or footer in a collection view section.
  • GetSupplementaryView – Returns the given header or footer.
  • GetVisibleSupplementaryViews – Returns a list of all visible header and footers.
  • UpdateInteractiveMovementTargetPosition – Informs the collection view that the user has moved, or is moving, an item during a drag operation.

UICollectionViewController

The following changes or additions have been made to the UICollectionViewController class in iOS 9:

  • InstallsStandardGestureForInteractiveMovement – If true the new Gesture Recognizer that automatically supports drag-to-reorder will be used.
  • CanMoveItem – Informs the collection view if a given item can be drag reordered.
  • GetTargetContentOffset – Used to get the offset of a given collection view item.
  • GetTargetIndexPathForMove – Gets the indexPath of a given item for a drag operation.
  • MoveItem – Moves the order of a given item in the list.

UICollectionViewDataSource

The following changes or additions have been made to the UICollectionViewDataSource class in iOS 9:

  • CanMoveItem – Informs the collection view if a given item can be drag reordered.
  • MoveItem – Moves the order of a given item in the list.

UICollectionViewDelegate

The following changes or additions have been made to the UICollectionViewDelegate class in iOS 9:

  • GetTargetContentOffset – Used to get the offset of a given collection view item.
  • GetTargetIndexPathForMove – Gets the indexPath of a given item for a drag operation.

UICollectionViewFlowLayout

The following changes or additions have been made to the UICollectionViewFlowLayout class in iOS 9:

  • SectionFootersPinToVisibleBounds – Sticks the section footers to the visible collection view bounds.
  • SectionHeadersPinToVisibleBounds – Sticks the section headers to the visible collection view bounds.

UICollectionViewLayout

The following changes or additions have been made to the UICollectionViewLayout class in iOS 9:

  • GetInvalidationContextForEndingInteractiveMovementOfItems – Returns the invalidation context at the end of a drag operation when the user either finishes the drag or cancels it.
  • GetInvalidationContextForInteractivelyMovingItems – Returns the invalidation context at the start of a drag operation.
  • GetLayoutAttributesForInteractivelyMovingItem – Gets the Layout Attributes for a given item while dragging an item.
  • GetTargetIndexPathForInteractivelyMovingItem – Returns the indexPath of the item that is at the given point when dragging an item.

UICollectionViewLayoutAttributes

The following changes or additions have been made to the UICollectionViewLayoutAttributes class in iOS 9:

  • CollisionBoundingPath – Returns the collision path of two items during a drag operation.
  • CollisionBoundsType – Returns the type of collision (as a UIDynamicItemCollisionBoundsType) that has occurred during a drag operation.

UICollectionViewLayoutInvalidationContext

The following changes or additions have been made to the UICollectionViewLayoutInvalidationContext class in iOS 9:

  • InteractiveMovementTarget – Returns the target item of a drag operation.
  • PreviousIndexPathsForInteractivelyMovingItems – Returns the indexPaths of other items involved in a drag to reorder operation.
  • TargetIndexPathsForInteractivelyMovingItems – Returns the indexPaths of items that will be reordered as a result of a drag-to-reorder operation.

UICollectionViewSource

The following changes or additions have been made to the UICollectionViewSource class in iOS 9:

  • CanMoveItem – Informs the collection view if a given item can be drag reordered.
  • GetTargetContentOffset – Returns the offsets of items that will be moved via a drag-to-reorder operation.
  • GetTargetIndexPathForMove – Returns the indexPath of an item that will be moved during a drag-to-reorder operation.
  • MoveItem – Moves the order of a given item in the list.

Summary

This article has covered the changes to collection views in iOS 9 and described how to implement them in Xamarin.iOS. It covered implementing a simple drag-to-reorder action in a collection view; using a custom Gesture Recognizer with drag-to-reorder; and how drag-to-reorder affects a custom collection view layout.

Xamarin Workbook

If it's not already installed, install the Xamarin Workbooks app first. The workbook file should download automatically, but if it doesn't, just click to start the workbook download manually.