preface

This article introduces some of the newer features of UICollectionView, including:

  • IOS 11 introduced drag & Drop
  • IOS 13 comes with composite layout CompositionalLayout
  • IOS 13 comes with DiffableDataSource

The main reference is WWDC sessions on UICollectionView over the years, with a link at the end.

Drag & Drop

UIView drag & drop

Ios after September 11, UICollectionView internal support drag and drop operation, only need to follow the agent and the collectionView. DragInteractionEnabled can be set to true, However, Drag & Drop is not the exclusive property of CollectionView or TableView. A normal View can follow the drag-and-drop protocol and have the drag-and-drop functionality that Apple has wrapped for you.

So, here’s drag & drop can be understood as an abstract concept, it is a drag and drop functionality, like click function, as long as you follow the corresponding agreement, and will be allowed to drag property is set to true, you can get this one function, it is divided into two parts, one is drag, you begin to drag operation, One is drop, which is what you do when you drop something, and these two parts don’t interfere with each other, so you can just implement one part and get the functionality of that part.

Take the drag-and-drop function for a View:

1. Add drag & Drop proxy to view (.dragInteractionEnabled is not required)

// Add drag interaction
view.addInteraction(UIDragInteraction(delegate: self))
       
// Add drop interaction
view.addInteraction(UIDropInteraction(delegate: self))
Copy the code

2. Request a drag element

// UIDragInteractionDelegate
func dragInteraction(_ interactionUIDragInteraction.itemsForBeginning session:UIDragSession)- > [UIDragItem] {
  // Object needs to follow the NSItemProviderWriting protocol
WWDC17 Data Delivery with Drag and Drop session
   let itemProvider = NSItemProvider(object: "Hello World" as NSString)
   let dragItem = UIDragItem(itemProvider: itemProvider)
   return [dragItem]
}
Copy the code

3. You can customize your drag elements or use the default

// UIDragInteractionDelegate
func dragInteraction(_ interactionUIDragInteraction.previewForLifting item:UIDragItem.sessionUIDragSession) -> UITargetedDragPreview? {
   let index = item.localObject as! Int
   return UITargetedDragPreview(view: views[index])
}
Copy the code

4. When you drag the element to move, you can set some effects, such as backflow, move or copy, or other actions

// UIDropInteractionDelegate/ / to request sessionAllowsMoveOperation, if returns false, a move action will not be allowed
func dropInteraction(_ interactionUIDropInteraction.sessionDidUpdate session:UIDropSession) -> UIDropProposal {
// cancel forbidden copy move
   return UIDropProposal(operation: UIDropOperation)}Copy the code

5. After the drag, handle the operation after you let go

// UIDropInteractionDelegatefunc dropInteraction(_ interactionUIDropInteraction.performDrop session:UIDropSession) {
 
   // func 1
session.loadObjects(ofClass: UIImage.self) { objects in
       for image in objects as! [UIImage] {
           self.imageView.image = image
       }
   }
 
   // or func 2
   for item in session.items {
       item.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
           if object ! = nil {
               DispatchQueue.main.async {
                   self.imageView?.image = (object as! UIImage)}}else {
               // Handle th Error}}}}Copy the code

This allows you to add drag-and-drop functionality to a View. Proxies for defining elements and animation effects are not mandatory. You only need to implement two protocols:

// UIDragInteractionDelegate// Provide the element to drag
func dragInteraction(_ interactionUIDragInteraction.itemsForBeginning session:UIDragSession)- > [UIDragItem] {
    return[]}// UIDropInteractionDelegate// Handle the operation after release
func dropInteraction(_ interactionUIDropInteraction.performDrop session:UIDropSession){}Copy the code

Refer to the following two pictures for more details about drag & Drop’s deadline:

The general process is as follows: User select -> request developer to return drag element -> Lift animation -> User move -> Start drag -> Allow custom preview & animation -> Drag End -> Callback Perform -> Perform Drop animation (customable) -> Transfer data (asynchronous)

Collection View DragDelegate and DropDelegate

The CollectionView needs to implement a slightly different delegate, but the principle is the same. Before we had this protocol, we needed to implement drag and drop ourselves. The code would look like this:

@objc func longPressAction(_ longPressUILongPressGestureRecognizer) {
   let point = longPress.location(in: collectionView)
   let indexPath = collectionView.indexPathForItem(at: point)
   switch longPress.state {
   case .began:
       if indexPath = = nil { break }
       // begin move
       collectionView.beginInteractiveMovementForItem(at: indexPath!)
   case .changed:
       collectionView.updateInteractiveMovementTargetPosition(point)
   case .ended:
       collectionView.endInteractiveMovement()
   default// cancel remove
       collectionView.cancelInteractiveMovement()
   }
}
Copy the code

BeginInteractiveMovementForItem (ats) series method is provided after ios9.0, if more front, that the need to achieve even more.

After iOS 11, all you need to do is set the dragDelegate and dropDelegate and implement them, and then set dragInteractionEnabled to true.

collectionView.dragDelegate = self
collectionView.dropDelegate = self
// The default is true for iPad, false for iPhone before ios15, and true after ios15
collectionView.dragInteractionEnabled = true
Copy the code

Take a look at some of the methods of the two proxies:

// UICollectionViewDragDelegate// @required
// Start dragging
// Return an empty array and do not respond to the drag event
func collectionView(_ collectionViewUICollectionView.itemsForBeginning session:UIDragSession.at indexPathIndexPath)- > [UIDragItem]) {}// @optional
// Add item to the session being dragged
func collectionView(_ collectionViewUICollectionView.itemsForAddingTo sessionUIDragSession.at indexPathIndexPath.pointCGPoint)- > [UIDragItem]) {}// UICollectionViewDropDelegate// @required
// Release the finger. If not, the default will be used
func collectionView(_ collectionViewUICollectionView.performDropWith coordinator:UICollectionViewDropCoordinator){}Copy the code

So you can do drag and drop. Of course, there are some additional code methods to help you get some state and custom views or animations for drag and drop:

// UICollectionViewDragDelegate// Custom drag preview, if not implemented or returns nil, the entire cell will be used as preview
func collectionView(_ collectionViewUICollectionView.dragPreviewParametersForItemAtindexPathIndexPath) -> UIDragPreviewParameters? {}
​
// This method is called after the animation of the selected item is complete and before the drag begins
func collectionView(_ collectionViewUICollectionView.dragSessionWillBegin session:UIDragSession) {}
​
// This method is called when the drag ends
func collectionView(_ collectionViewUICollectionView.dragSessionDidEnd session:UIDragSession) {}
​
// Controls whether the drag session allows move operations. Default is true, if false, then.move operations are disallowed
Four operations: / / UIDropOperation cancel | who | copy | move
func collectionView(_ collectionViewUICollectionView.dragSessionAllowsMoveOperationsessionUIDragSession) -> Bool {}
​
// Can not be dragged to another app, default is false
func collectionView(_ collectionViewUICollectionView.dragSessionIsRestrictedToDraggingApplication sessionUIDragSession) -> Bool {}
Copy the code
// UICollectionViewDropDelegate// If false is returned, the drop event is not responded to
- (BOOL)collectionView:(UICollectionView *)collectionView canHandleDropSession:(id<UIDropSession>)session;
​
// Drop session is called when it enters the coordinates of the collectionView
// Call order
// dragSessionWillBegin
// dropSessionDidEnter
// dragSessionDidEnd
// dropSessionDidEnd
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnter:(id<UIDropSession>)session;
​
// If you start dragging and the session being dragged is in the collectionView area, this method will be called over and over again
// It tracks the behavior of drop, and since it is called very frequently, you need to minimize the amount of work in this method
// Allow you to customize some drop schemes, but this may not be allowed by the system, the system will execute its own scheme
- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionViewdropSessionDidUpdate:(id<UIDropSession>)session withDestinationIndexPath:(nullableNSIndexPath *)destinationIndexPath;
​
// This is called when the session you drag is moved outside the collectionView area
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidExit:(id<UIDropSession>)session;
​
// Drop is called after the drop is complete
- (void)collectionView:(UICollectionView *)collectionView dropSessionDidEnd:(id<UIDropSession>)session;
​
After the drop is complete, a preview image will be placed first, and the preview image will be replaced after the collectionView data refresh
- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionViewdropPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath;
Copy the code

Reflux mode:

UICollectionViewDropProposal. Indent, set in dropSessionDidUpdate method:

/ / UICollectionViewDropProposal. Indent the properties to control gap opened the effect of the drop
// Unspecified: The gap is not opened and the target index is not highlighted
/ / insertAtDestinationIndexPath: open a gap, mimic the effects of final release
/ / insertIntoDestinationIndexPath: can't open the gap, but the target index will be highlighted
Copy the code

Default effect:

I’ll show you how to do it in the Demo, including you can drag from collectionView to tableView, from this app to another app.

CompositionalLayout

This part can directly see the Compositional Layout details for easy operation of CollectionView! This article, write very comprehensive, this part is basically I read this article a note.

The problem with Compositional Layout is that layouts can be stacked in a way that allows you to create a variety of variations from existing layouts without having to write a new Layout algorithm each time for a new UI setting. In other words, the biggest difference between Compostional Layout and the original UICollectionViewLayout is that it uses many components to describe the appearance of a Layout.

In UICollectionViewLayout, we start from zero and calculate the size and position of items of all points and tell the system. On the other hand, Compostional Layout is like, we have several components that represent Layout, for example, a section has three items, the size of the items should be the same as the container, and then we tell the system, The system itself combines these components to set layout, and the calculation of details is not what we need to care about.

Take a look at the Composition Layout:

For Compositional Layout, our process goes something like this: Create a Layout component for each item, set the size, boundary distance, and so on, and each item that looks different will have its own object to describe it. After setting the item, create a group Layou component and put all the generated item items into the group. Set the size and boundary distance of the group, and then put them into the Layout component belonging to section, and so on. At the end of the day, a Collection View is represented by a single largest Layout object that contains sections, which in turn contain groups, which contain items. So this diagram up here, which is also a hierarchy of these components, you can think of these blocks as components in your program.

Basic components

To implement a horizontally scrolling Collection View:

Code:

func createLayout(a) -> UICollectionViewLayout {
   / / 1
   let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),heightDimension: .fractionalHeight(1.0))
   let item = NSCollectionLayoutItem(layoutSize: itemSize)
   
   / / 2
   let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .absolute(120))
   let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems:[item])
   group.interItemSpacing = .fixed(8)
   
   / / 3
   let section = NSCollectionLayoutSection(group: group)
   section.orthogonalScrollingBehavior = .continuous
   section.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5,trailing: 5)
   
   / / 4
   let layout = UICollectionViewCompositionalLayout(section: section)
   return layout
}
​
func configureHieratchy(a) {
self.collectionView = UICollectionView(frame: view.bounds, collectionViewLayout:createLayout())
}
Copy the code

Starting with //1, this is the code that sets item:

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
Copy the code

NSCollectionLayoutSize is an important class that describes all groups and items. Set the width and size to 0.2 of group and the height to the same as group (1x). WidthDismension and heightDismension is NSCollectionLayoutDimension two parameters. NSCollectionLayoutDimension is used to describe the width and height of the component:

open class NSCollectionLayoutDimension : NSObject.NSCopying {
// Width is some proportion of the container, such as 1 (equal) or 0.5 (half)
   open class func fractionalWidth(_ fractionalWidthCGFloat) - >Self// The height is some proportion of the container, such as 1 (equal) or 0.5 (half)open class func fractionalHeight(_ fractionalHeightCGFloat) - >Self// Set an absolute valueopen class func absolute(_ absoluteDimensionCGFloat) - >Self// This is an estimate, after the actual layout is complete, the actual content size will be used to displayopen class func estimated(_ estimatedDimensionCGFloat) - >Self
}
Copy the code

Generate a component that describes item and specify that its width is 0.2 of the width of the group and its height is the same as that of the group.

Go ahead, look //2:

// same as itemSize, which means the width is the same as the section and the height is absolute value 120
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .absolute(120))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// The spacing of items within the group
group.interItemSpacing = .fixed(8)
Copy the code

NSCollectionLayoutGroup.horizontal(layoutSize: Subitems 🙂 : Generates a component that describes the group, sets its size to groupSize, and specifies the types of items in the group.

/ / 3:

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5,trailing: 5)
Copy the code

Basically see section. OrthogonalScrollingBehavior =. Continuous, set this parameter, the Collection of the View section becomes a horizontal Scroll View, We can just use the Collection View as we did before, regardless of the extra Scroll View. It’s very convenient. What it really means is: I want this section to scroll 90 degrees from the Collection View. Generally, if the Collection View is scrolled vertically, this parameter means that the section can be moved horizontally. If the Collection View moves horizontally, the meaning of this parameter is to allow the section to scroll vertically. It has different values that you can set, and we’ll talk about that later.

/ / 4:

let layout = UICollectionViewCompositionalLayout(section: section)
return layout
Copy the code

The layout is initialized by section and returned to the Collection View.

But in the code above we get something like this:

There’s a gap, and it actually looks like this:

Although the width of the item is set to 0.2, there is no room for five items in the group due to interItemSpacing, so there is an extra space at the far right. So you can probably see the function of the group. The function of the group is to circle one or more items together. Using this feature, you can make a lot of changes. For example, to add three vertically arranged cells to a horizontally scrolling section, you only need to set a vertical group and then include three items in it.

Going back to the requirement, we want to have a section that scrolls horizontally, and items are equally spaced from each other. How do we do that?

func createLayout(a) -> UICollectionViewLayout {
   / / 1
   let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .fractionalHeight(1.0))
   let item = NSCollectionLayoutItem(layoutSize: itemSize)
   
   / / 2
   let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),heightDimension: .absolute(120))
   let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems:[item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil, trailing:.fixed(8), bottom: nil)
   
   / / 3
   let section = NSCollectionLayoutSection(group: group)
   section.orthogonalScrollingBehavior = .continuous
   section.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5,trailing: 5)
   
   / / 4
   let layout = UICollectionViewCompositionalLayout(section: section)
   return layout
}
Copy the code

// set item to be the same width as group. // set group to be 0.2 times the width of the container. In other words, a group is now grayed with an item that is the same size as the group. //3, group. EdgeSpacing is used to set spacing between groups.

// NSCollectionLayoutEdgeSpacingclass NSCollectionLayoutEdgeSpacing {
public convenience init(leadingNSCollectionLayoutSpacing? .top:NSCollectionLayoutSpacing? .trailingNSCollectionLayoutSpacing? .bottom:NSCollectionLayoutSpacing?). 
}
​
class NSCollectionLayoutSpacing {
// Specify a minimum distance that can be extended
open class func flexible(_ flexibleSpacingCGFloat) - >Self // i.e. >= // Specify a fixed distanceopen class func fixed(_ fixedSpacingCGFloat) - >Self // i.e. ==
}
Copy the code

So we have a Collection View that moves horizontally correctly:

Layout of the Demos,

Horizontal scrolling of items stacked vertically

Code:

// Provide three different shapes of items
let layoutSize = NSCollectionLayoutSize(widthDimension: .absolute(110),heightDimension: .absolute(45))
let item = NSCollectionLayoutItem(layoutSize: layoutSize)
let layoutSize2 = NSCollectionLayoutSize(widthDimension: .absolute(110),heightDimension: .absolute(65))
let item2 = NSCollectionLayoutItem(layoutSize: layoutSize2)
let layoutSize3 = NSCollectionLayoutSize(widthDimension: .absolute(110),heightDimension: .absolute(85))
let item3 = NSCollectionLayoutItem(layoutSize: layoutSize3)
// Give the group the right size
let groupLayoutSize = NSCollectionLayoutSize(widthDimension: .absolute(110),heightDimension: .absolute(205))
// use. Vertical to indicate that our groups are arranged vertically
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupLayoutSize, subitems:[item, item2, item3])
// This refers to the vertical spacing
group.interItemSpacing = .fixed(5)
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil, trailing:.fixed(10), bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
Copy the code

Why the item spacing can only use a interItemSpacing can represent, but need to use NSCollectionLayoutEdgeSpacing spacing between group to specify the parameters of the four directions can be?

As you can see the permutations in the group, there are only three cases:

  • .horizontal: Horizontal arrangement
  • .vertical: Vertical alignment
  • .custom: custom

In the first two cases, interItemSpacing refers to either horizontal or vertical spacing, so the system does not need to specify the orientation to know what this spacing represents. Custom is a bit special. This is actually a method we are familiar with, calculating the size and position of a cell, we will mention later.

Nested group

Code:

let layoutSize = NSCollectionLayoutSize(widthDimension: .absolute(65), heightDimension:.absolute(120))
let item = NSCollectionLayoutItem(layoutSize: layoutSize)
let layoutSize2 = NSCollectionLayoutSize(widthDimension: .absolute(65),heightDimension: .absolute(45))
let item2 = NSCollectionLayoutItem(layoutSize: layoutSize2)
let layoutSize3 = NSCollectionLayoutSize(widthDimension: .absolute(65),heightDimension: .absolute(65))
let item3 = NSCollectionLayoutItem(layoutSize: layoutSize3)
// Subgroup on the right
let subGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(65),heightDimension: .absolute(120))
let subGroup = NSCollectionLayoutGroup.vertical(layoutSize: subGroupSize, subitems:[item2, item3])
subGroup.interItemSpacing = .fixed(10)
// A large group containing a left item and a right subgroup
let groupLayoutSize = NSCollectionLayoutSize(widthDimension: .absolute(135),heightDimension: .absolute(120))
// Add both the Group and item layout components to the group
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupLayoutSize, subitems:[item, subGroup])
group.interItemSpacing = .fixed(5)
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil, trailing:.fixed(10), bottom: nil)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
Copy the code

NSCollectionLayoutGroup

open class NSCollectionLayoutGroup : NSCollectionLayoutItem.NSCopying {
.
}
Copy the code

Group is a subclass of Item. That is, anywhere you put NSCollectionLayoutItem, you can put NSCollectionLayoutGroup. Can be converted into groups and items.

The Nested group layout can be implemented by adding a subGroup and two items to the subGroup.

UICollectionLayoutSectionOrthogonalScrollingBehavior scroll way

Section can be set orthogonalScrollingBehavior, mentioned earlier, such as the Collection View is vertical scrolling, after that you set up a section of orthogonalScrollingBehavior, This section can be scrolled horizontally. This parameter can be set to different values:

  • .noneDefault, no scrolling
  • .continuousContinuous rolling
  • .continuousGroupLeadingBoundaryIt keeps rolling, but eventually it stopsgruopThe leading edge (gruop’s starting position offset by a certain value)
  • .pagingEach scroll is the same width (or height) as the Collection View
  • .groupPagingIt’s going to scroll one at a timegroup
  • .groupPagingCenteredScroll one at a timegroupAnd stop atgroupThe midpoint of

Take a look at the results:

Header – Boundary Supplementary Item

Add headers to section.

In UICollectionView, the header and footer are kind of supplementary views that are attached to the section. Compostional Layout also has a way to set these supplementary layouts, and the logic is the same as setting other layouts:

// ...
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
// ...
// Set the header size
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),heightDimension: .absolute(40))
// Be responsible for describing items that have supplementary items
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,elementKind: UICollectionView.elementKindSectionHeader, alignment: .top, absoluteOffset:CGPoint(x: 0, y: -5))
let section = NSCollectionLayoutSection(group: group)
// Assign supplementary item to section
section.boundarySupplementaryItems = [headerItem]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
Copy the code

The meaning of the above code can be illustrated by the following image:

If you want to implement footer, change alignment to.bottom. If the section moves horizontally, the header alignment may be.leading.

Of course, want to remember in UICollectionViewDataSource implement this method, according to the received viewForSupplementaryElementOfKind to provide the corresponding header view:

@objc open func collectionView(_ collectionViewUICollectionView.viewForSupplementaryElementOfKind kindString.at indexPathIndexPath) ->UICollectionReusableView
Copy the code

Remember to register this supplementary View as well:

CollectionView.register(HeaderView.self, forSupplementaryViewOfKind:UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
Copy the code

Red badge – Supplementary Item

The definition of NSCollectionLayoutBoundarySupplementaryItem:

open class NSCollectionLayoutBoundarySupplementaryItem :NSCollectionLayoutSupplementaryItem.NSCopying {
// ...
}
Copy the code

It is based on NSCollectionLayoutSupplementaryItem provide some convenient method, and the default outside section of supplementary item. That the NSCollectionLayoutSupplementaryItem have what different? Boundary supplementary item basically describes a view that is supposed to be outside the section, and supplementary item is a broader description of anything that is supplementary item, For example, the little red dot in the upper left corner of section that we’re going to introduce now.

Let’s look at the implementation:

// ...
let badgeSize = NSCollectionLayoutSize(widthDimension: .absolute(20), heightDimension:.absolute(20))
let badgeContainerAnchor = NSCollectionLayoutAnchor(edges: [.top, .leading],absoluteOffset: CGPoint(x: 10, y: 10))
           let badgeItemAnchor = NSCollectionLayoutAnchor(edges: [.bottom, .trailing],absoluteOffset: CGPoint(x: 0, y: 0))
let badgeItem = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind:BadgeView.supplementaryKind, containerAnchor: badgeContainerAnchor, itemAnchor:badgeItemAnchor)
let item = NSCollectionLayoutItem(layoutSize: layoutSize, supplementaryItems:[badgeItem])
// ...
Copy the code

Here, there is a new student: NSCollectionLayoutAnchor, which is used to specify the anchor point. For a NSCollectionLayoutSupplementaryItem, you in addition to specify its element size, kind, you also need to specify its two anchors, containerAnchor itemAnchor, You can see what containerAnchor and itemAnchor respectively represent in the figure:

As you can see from the figure above, itemAnchor represents the anchor that items are aligned with, while containerAnchor represents the anchor that the section or group containing this item is aligned with. NSCollectionLayoutSupplementaryItem will in the layout, will be the item with the container of anchor point alignment, so by the two parameters, We can put supplementary View anywhere we want. But itemAnchor this parameter commonly used commonly, if we do not initialize NSCollectionLayoutSupplementaryItem set, the default itemAnchor will be in the top left corner of the item (0, 0) position, Just using containerAnchor is enough to set the location of the little red dot, which is the example above which can be simplified as:

let badgeContainerAnchor = NSCollectionLayoutAnchor(edges: [.top, .leading],absoluteOffset: CGPoint(x: -10, y: -10))
let badgeItem = NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind:BadgeView.supplementaryKind, containerAnchor: badgeContainerAnchor)
Copy the code

The result is exactly the same on the UI, of course if you want to set containerAnchor light years away and then pull it back with itemAnchor no one will stop you.

Of course, if not with Diffable DataSource, remember to realize UICollectionViewDataSource inside:

optional func CollectionView(_ CollectionViewUICollectionView.viewForSupplementaryElementOfKind kindString.at indexPathIndexPath) -> UICollectionReusableView
Copy the code

And handle BadgeView. SupplementaryKind element of this kind of supplementary view.

Provide section background decoration items

This layout is a pain for all UI engineers. This is a section with 5 items, and it has a rounded gray background. In the past, you must provide an decoration view and implement UICollectionViewLayout to calculate its size. Other methods are only more complicated than calculating mathematics. I have also seen items with up, middle and lower styles, just like hamburgers. And put them on top of each other.

Now, compostional layout can control the layout of the decoratino view. First, we need to have a decoration view that just supplementary:

class SectionBackgroundDecorationViewUICollectionReusableView . }
Copy the code

The code for setting layout looks like this:

// Set decoration View
let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(
           elementKind: SectionBackgroundDecorationView.elementKind)
sectionBackgroundDecoration.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5,bottom: 5, trailing: 5)
section.decorationItems = [sectionBackgroundDecoration]
​
// Register decoration View
layout.register(SectionBackgroundDecorationView.self, forDecorationViewOfKind:SectionBackgroundDecorationView.elementKind)
​
return layout
Copy the code

You’ll see the same logic as supplementary Layout: We created a NSCollectionLayoutDecorationitem background, and its corresponding set elementKind contentInsets, in the layout, Just new SectionBackgroundDecorationView will be used as a background to stick in the back of the section, additional space set boundaries. The difference with Supplementary Views is that remember to register the Decoration View when you upload the layout, not the Collection View.

Fully customized Custom Layout

Use the. Custom build method of NSCollectionLayoutGroup:

let height: CGFloat = 120.0
let groupLayoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),heightDimension: .absolute(height))
let group = NSCollectionLayoutGroup.custom(layoutSize: groupLayoutSize) { (env) ->[NSCollectionLayoutGroupCustomItemin
   let size = env.container.contentSize
   let spacing: CGFloat = 8.0
   let itemWidth = (size.width-spacing*4)/3.0
   return [
       NSCollectionLayoutGroupCustomItem(frame: CGRect(x: 0, y: 0, width: itemWidth,height: height/3.0)),
       NSCollectionLayoutGroupCustomItem(frame: CGRect(x: (itemWidth+spacing), y:height/3.0, width: itemWidth, height: height/3.0)),
       NSCollectionLayoutGroupCustomItem(frame: CGRect(x: ((itemWidth+spacing)*2), y:height*2/3.0, width: itemWidth, height: height/3.0))]}Copy the code

Start with the.custom definition:

open class func custom(layoutSizeNSCollectionLayoutSize.itemProvider: @escapingNSCollectionLayoutGroupCustomItemProvider) - >Self
Copy the code

The.custom parameter specifies the size of the group. The layoutSize parameter specifies the size of the group. ItemProvider (NSCollectionLayoutEnvironment) – > [NSCollectionLayouGroupCustomItem] closure, before the system is ready to present the item, The closure is called to provide a frame for each item. The closure will be introduced to a representative of the container NSCollectionLayoutEnvironment come in, and then we can use the information provided by the environment, to calculate the size of the item with location, And in the closure of item size with good location, through NSCollectionLayoutGroupCustomItem this component, the returned value is an array, mean you want to specify the group can have several items in total, So you can see above, we through the env. Container. The contentSize get the size of the container. We can through this to calculate the group of three the absolute position of the item, and then through NSCollectionLayoutGroupCustomItem packaged returned back. Get the groups of the contents, and you’ll know how to present the items internally.

summary

There are so many different combinations possible beyond these basic routines, but you’ll find that once you understand the basic principles of group, item, spacing, dimention and so on, any variation becomes as easy as building castles by finding the right blocks.

Another recommended resource is the Official Apple profile: Implementing Modern Collection Views is a sample program that covers almost all common Compostional Layout routines, and each routine is divided into view controllers. Just execute the sample app and check the name of the View Controller identified on the UI to find the sample application for the corresponding function.

Compostional Layout is quite separate from the data source, you can use compositional Layout + the original data source, or use the new Diffable data Source, There is no need to change each other’s code when substituting.

Currently Compositional Layout only works on iOS 13 + machines, but Kishikawa Katsumi has implemented it, Let you can use in the SDK ios12 and below: IBPCollectionViewCompositionalLayout.

Diffable Data Source

ios 13

Before pushing the Diffable Data Source, developers need to build the Data Source using the numberOfItemsInSection and cellForItemAt methods. To update the Data Source, Use the performBatchUpdates and reloadData() methods.

In addition, previously, the controller maintained a data source, and the UI also maintained a data source, requiring developers to control the data consistency between the two. ReloadData can create and refresh data sources, but does not support animation; And performBatchUpdates use, easily lead to some common problems, such as NSInternalInconsistencyException, because the data source is not centralized management, it is easy to appear inconsistent data source, leading to collapse.

With the new Diffable Data Source, data can be provided uniformly through Snapshot.

Snapshot represents a state of a data source. It does not rely on the index path to update items. Instead, it relies on unique identifters to identify unique sections and items.

Also, if you use apply to update the data source, it allows you to use animations as long as you set the animatingDifferences parameter in Apply to true. Even better, the Apply method can be executed in asynchronous queues, reducing the amount of work you have to do in the main thread.

You can read the current state of the UI by reading datasource.snapshot () and add or remove items accordingly.

Building raw data

Let’s start with the basics, when building raw data:

dataSource = UICollectionViewDiffableDataSource<Int.UUID>(collectionView:collectionView) {
   (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: UUID) - >UICollectionViewCellin
   // configure and return cell
}
​
// Create a snapshot
/ / 2
var snapshot = NSDiffableDataSourceSnapshot<Int.UUID> ()// Populate the snapshot, add a section, add a set of items to the section, and pass nil and take the last section
/ / 3
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()], toSection: nil)
​
// Apply the snapshot.
/ / 4
dataSource.apply(snapshot, animatingDifferences: true)
Copy the code

Let’s take a look at //1.

dataSource = UICollectionViewDiffableDataSource<Int.UUID>(collectionView:collectionView) {
   (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: UUID) - >UICollectionViewCellin
   // configure and return cell
}
​
// UICollectionViewDiffableDataSource
open class UICollectionViewDiffableDataSource<SectionIdentifierType.ItemIdentifierType> : NSObject.UICollectionViewDataSource where SectionIdentifierTypeHashable.ItemIdentifierType : Hashable {
// Build method
public init(collectionViewUICollectionView.cellProvider@escapingUICollectionViewDiffableDataSource<SectionIdentifierType.ItemIdentifierType>.CellProvider) 
 
// Cell Provider
public typealias CellProvider = (_ collectionView: UICollectionView._ indexPath:IndexPath._ itemIdentifier: ItemIdentifierType) - >UICollectionViewCell?
}
Copy the code

First of all, the dataSource is UICollectionViewDiffableDataSource actually, Where SectionIdentifierType and ItemIdentifierType are the types of the unique identifiers of the section and item of the Collection View respectively. Its initialization method, passing in a CollectionView, and then in the trailing closure, implementing CellProvider to return a UICollectionViewCell, This CellProvider actually corresponds to the previous cellForItemAt: method.

//2:

var snapshot = NSDiffableDataSourceSnapshot<Int.UUID> ()Copy the code

Create a snapshot. Similarly, Int and UUID are the types of the section identifier and item identifier respectively, corresponding to the types in //1.

/ / 3:

snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()], toSection: nil)
Copy the code

Add sections and items, and the data type passed in must be Hashable, following Hashable. The toSection in appItems is optional, left blank or nil, and is appended to the section that was last assigned.

/ / 4:

dataSource.apply(snapshot, animatingDifferences: true)
​
// Apply method definition
nonisolated open func apply(_ snapshot:NSDiffableDataSourceSnapshot<SectionIdentifierType.ItemIdentifierType>,animatingDifferencesBool = true.completion: (() - >Void)? = nil)
Copy the code

This step is to call apply. AnimatingDifferences control whether animation is required when refreshing. Generally, the animatingDifferences are set to false when initializing and true when refreshing again. Also, this method provides a Completion callback.

The refresh data

When data needs to be refreshed, simply create a snapshot and call apply().

var snapshot = NSDiffableDataSourceSnapshot<Int.UUID>()
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID()])
dataSource.apply(snapshot, animatingDifferences: true)
Copy the code

The rest is taken care of, the Collection View compares differences internally, changes updates to the UI, and you can own its animation. No longer need to use performBatchUpdates: to refresh an indexPath, these are encapsulated within Apply.

iOS 14

Ios 14 has introduced SectionSnapshots, which make it easy to create expanded collection views, and the new Recordering API, Recording API can be inserted into diffable data source to help us read fast.

It also added UICollectionViewListCell, a more versatile cell. If you are interested, check out iOS 14’s Diffable Data Source, which allows you to easily create and update large amounts of Data

conclusion

Since iOS 11, a series of changes have taken place in the Collection View, including support for drag and drop, new layout methods, new data sources, new Cell registration methods, and more modern Collection cells. We believe that Collection View will become more and more powerful, and we can pay more attention to the creativity of our app instead of wasting too much time on the implementation of functions.

The resources

WWDC16 What’s New in UICollectionView in iOS 10

WWDC17 Introducing Drag and Drop

WWDC17 Drag and Drop with Collection and TableView

WWDC18 A Tour of UICollectionView

WWDC19 Advances in UI Data Sources

WWDC19 Advances in Collection View Layout

WWDC20 Advances in diffable data sources

WWDC20 Modern cell configuration

WWDC20 Lists in UICollectionView

Drag section sessions:

WWDC17 Data Delivery with Drag and Drop

WWDC17 Mastering Drag and Drop

WWDC17 File Provider Enhancements

Documentation reference:

Compositional Layout for easy operation of CollectionView!

IOS 14’s Diffable Data Source lets you easily create and update lots of Data

You can download the official Demo as a reference. The drag-and-drop Demo is iPad only, so it’s in a different location.

Collection View after iOS 11