This article is published on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching for HelloWorld Jieshao in wechat.
Written in the beginning
Good morning everyone, it’s time to share your development tips every week! Last week, I shared an article about UICollectionView custom layout implementation Cover Flow. This is the fourth article in the series I shared about UICollectionView. So today I’m going to continue with the fifth and final installment in the UICollectionView development series. Of course, if the Apple developer team comes out with new technology for UICollectionView or if I find new technology in development, I will continue to update the series, Finally, I hope that through this series of articles, I can summarize the core technology points of the UICollectionView control, after all, the scope of use of UICollectionView is too wide.
Supplementary View
Those of you who have used the UITableView control know that we can add a headerView and a footerView to each section of it, so can we do the same in UICollectionView? And the answer is yes, in the UICollectionView we call this Supplementary View, which translates to Supplementary View, How to arrange a headerView and footerView for each section in UICollectionView?
Before we implement the code logic, let’s familiarize ourselves with a few important API methods. They are:
open func register(_ viewClass: AnyClass? , forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String)Copy the code
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
Copy the code
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
Copy the code
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
Copy the code
The first method needs to register the headerView and footerView that need to be added to the UICollectionView section. The three parameters in the UICollectionView are:
- The class to which the view is to be added, such as “BaseHeaderView.self”
- Additional view type, is the head or the tail views, respectively with UICollectionView. ElementKindSectionHeader and UICollectionView elementKindSectionFooter
- The UICollectionView will be able to tell if the view is loaded with a header, a footer, or a normal cell
In the second method is UICollectionViewDataSource agreement, return a UICollectionReusableView objects, implement it as a return for Supplementary View instance, And then UICollectionView loads it; It takes three arguments, which are:
- UICollectionView object
- Append the view type to distinguish between header and footer
- The IndexPath object, which determines which section it is, initializes the different HeaderViews and FooterViews
The third and fourth method is simpler and returns the size of the appended view (headerView,footerView)
After understanding, then we begin to hand code!
Add a Supplementary View to the UICollectionView
First take a look at the renderings:
The code logic is as follows, and comments have been added to the code:
// // BaseAPIViewController.swift // SwiftScrollBanner // // Created by shenjie on 2021/2/26. // import UIKit class BaseAPIViewController: UIViewController { fileprivate var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.title = "SupplementaryView" let flowLayout = UICollectionViewFlowLayout() let margin: CGFloat = 20 let section: CGFloat = 15 flowLayout.minimumLineSpacing = margin flowLayout.minimumInteritemSpacing = margin flowLayout.sectionInset = UIEdgeInsets(top: section, left: margin, bottom: section, right: margin) flowLayout.scrollDirection = .vertical collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height), collectionViewLayout: FlowLayout) / / registered Cell collectionView. Register (UICollectionViewCell. Self, forCellWithReuseIdentifier: "CellID)/view/registered head collectionView. Register (BaseHeaderView. Self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView") / / registered rear view collectionView. Register (BaseFooterView. Self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footerView") collectionView.delegate = self collectionView.dataSource = self self.view.addSubview(collectionView) } } extension BaseAPIViewController: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {return CGSize(width: 80, height: 120)}} extension BaseAPIViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { } } extension BaseAPIViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 8 } func numberOfSections(in collectionView: UICollectionView) -> Int { return 4 } // The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellID", for: IndexPath) cell.backgroundColor = UIColor(red: CGFloat(arc4Random ()%256)/256.0, green: CGFloat (arc4random () % 256) / 256.0, blue: CGFloat (arc4random () % 256) / 256.0, alpha: Func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == UICollectionView.elementKindSectionHeader { let headerView: BaseHeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "headerView", for: indexPath) as! BaseHeaderView return headerView } else if kind == UICollectionView.elementKindSectionFooter { let footerView: BaseFooterView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footerView", for: indexPath) as! BaseFooterView return footerView} return UICollectionReusableView()} // Return the size of the append view UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: Func collectionView(_ collectionView: UICollectionView, Layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: 50) } }Copy the code
Now that I’ve added a headerView and footerView to the section of the UICollectionView, we can implement a rich append view as required, such as in the App Store:
Sticky Section Header
A Sticky Section Header is an effect that you implement with an append view, which is that when the UICollectionView is scrolling, as long as the headerView of the current Section is scrolling up to the top, it will stick to the top of the screen and it won’t hide, The specific effects are as follows:
It looks a little complicated at first glance, but it takes at least a hundred lines of code to make it work! However, I can tell you that Just two lines of code will solve the problem.
In iOS 9 UICollectionViewFlowLayout introduced two attributes, sectionHeadersPinToVisibleBounds and sectionFootersPinToVisibleBounds, You can easily fix the header and footer by adding the following two lines to the layout logic:
flowLayout.sectionHeadersPinToVisibleBounds = true
flowLayout.sectionFootersPinToVisibleBounds = true
Copy the code
The specific effects are as follows:
Alright, that’s enough for Supplementary View, let’s look at the other technical aspect of UICollectionView, Decoration View.
Decoration View
As the name suggests, it is used to beautify the UICollectionView and improve the user experience. If your product manager gives you a requirement that you set the background for the section of the UICollectionView, but when you look at the document, You’ll notice that UICollectionView doesn’t have properties to set a different background color for the section.
What Section background belongs to the UICollectionView? In fact, it is not the Cell View or Supplementary View, but the Decoration View of the UICollectionView. Unlike the previous two, the Decoration View cannot be set by the data source, but can only be defined and managed by the layout object.
In order to bring you a better understanding of Decoration View, I am here to teach you to develop and make an electronic bookshelf on site! In the next section, you will learn the following:
- How to create Decoration View in UICollectionView
- Custom layout properties that calculate the location and size of the section’s background image
- UICollectionView drag adjustment order
Before we get started, let’s look at the renderings:
This effect is not bad! So what are you waiting for, so roll up your sleeves and get started
Create Decoration View
The UICollectionViewLayout class provides a way to register the Decoration View, which can only be defined and managed by the layout object.
open func register(_ viewClass: AnyClass? , forDecorationViewOfKind elementKind: String)Copy the code
Normally, when need to decorate the view to rewrite UICollectionViewFlowLayout, then registered in its subclasses to decorate the view.
Create a new class that inherits from UICollectionReusableView with the following code:
// // DecorationView.swift // SwiftScrollBanner // // Created by shenjie on 2021/2/26. // import UIKit class DecorationView: UICollectionReusableView { fileprivate var bg_imageView = UIImageView() override init(frame: CGRect) { super.init(frame: frame) bg_imageView.frame = bounds self.bg_imageView.image = UIImage(named: "bookshelf") self.addSubview(bg_imageView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) } }Copy the code
And register it in a subclass that inherits from UICollectionViewLayout:
Override init() {super.init()} self.register(decorationView.self, forDecorationViewOfKind: "DecorationView")}Copy the code
That’s how we implement the decorator view in the UICollectionView.
Calculates the background layout properties
To implement this layered style, we need to set a background image for each section, such as:
However, since the coordinate position of each section is not fixed, we need to calculate the positions and sizes of all sections in the preparation stage, which can be calculated in the prepare() method of UICollectionViewLayout. The relevant logic has been noted in the code, which is as follows:
override func prepare() { super.prepare() // 1. Guard let numberOfSections = self. CollectionView? .numberOfSections, let layoutDelegate = self.collectionView? Delegate as? UICollectionViewDelegateFlowLayout else {return} / / to clear style sectionAttrs removeAll () / / 2. Calculate the layout properties of each section's decorative view for section in 0.. <numberOfSections {guard let numberOfItems = self.collectionView? .numberOfItems(inSection: section), numberOfItems > 0, let firstItem = self.layoutAttributesForItem(at: IndexPath(item: 0, section: section)), let lastItem = self.layoutAttributesForItem(at: IndexPath(item: numberOfItems - 1, section: Var sectionInset = self.sectionInset if let inset = self.sectionInset layoutDelegate.collectionView? (self.collectionView! , layout: self, insetForSectionAt: Var sectionFrame = firstitem.frame. union(lastitem.frame) Sectionframe.origin. X = 0 sectionFrame.origin. Y -= sectionInset.top // 2.4 Calculate the actual size of the section if self.scrollDirection == .horizontal { sectionFrame.size.width += sectionInset.left + sectionInset.right sectionFrame.size.height = self.collectionView! .frame.height } else { sectionFrame.size.width = self.collectionView! .frame. Width sectionFrame.size. Height += sectionInset.top + sectionInset.bottom UICollectionViewLayoutAttributes(forDecorationViewOfKind: "DecorationView", with: IndexPath(item: 0, section: section)) decorations.frame = sectionFrame decorations.zIndex = -1 self.sectionAttrs.append(decorations) } }Copy the code
Attribute evaluation is good, and then at the time of layout update, to return to we calculated in advance good properties, the func layoutAttributesForElements (in the rect: CGRect) method to add the following code:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {var attrs = super. LayoutAttributesForElements (in: the rect) / / in the current filter sectionAttrs the rect area in the array are familiar with, only returned to fellowship with the rect location attributes attrs! .append(contentsOf: self.sectionAttrs.filter { return rect.intersects($0.frame) }) return attrs }Copy the code
So we’ve got a background image for each section, and we’re almost done with the shelf layout, and then we add the data source and our shelf will be presented in the UIViewController, but to further demonstrate the power of the UICollectionView, I’ve also implemented a feature that makes it possible to sort the books on the shelf by dragging and dropping them. This is where another knowledge point comes in: the gesture UIGestureRecognizer
Gestures to drag and drop
After iOS9, the UICollectionView properties come with a reorder effect. Apple has introduced several important methods for UICollectionView:
@ the available (iOS 9.0. *) open func beginInteractiveMovementForItem (ats indexPath: IndexPath) -> Bool returns NO if reordering was successful from beginning - otherwise YES @available(iOS 9.0) *) open func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint) @Available (iOS 9.0, *) Open Func endInteractiveMovement() @available(iOS 9.0, *) *) open func cancelInteractiveMovement()Copy the code
They represent the following meanings:
- Begin to interact
- Update interactive location
- The end of the interaction
- Cancel the interaction
After adding a gesture to the UICollectionView, call each of the four methods above to implement drag-and-drop sorting, depending on the three states provided by the gesture. In addition, since the order of the Cell will be adjusted, we need to update the data source in time to ensure that the drag and drop result will be restored after the view is refreshed. The specific implementation code is as follows:
Add gestures
/ / add gestures to let longPressGesture = UILongPressGestureRecognizer (target: the self, the action: #selector(handleLongGesture(_:))) collectionView.addGestureRecognizer(longPressGesture)Copy the code
Gesture state judgment
@objc func handleLongGesture(_ gesture: UILongPressGestureRecognizer) { switch(gesture.state) { case .began: guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: Selectionview)) else {break} prevIndexPath = selectedIndexPath collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) case .changed: / / update position if the let moveIndexPath: IndexPath = self. CollectionView. IndexPathForItem (ats: gesture. The location (in: self.collectionView)) { if prevIndexPath == moveIndexPath { collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!) )} else {/ / to determine whether a bookshelf full if collectionView numberOfItems (inSection: moveIndexPath.section) < 4 { collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!) )} else {break}}} case. Ended: / / end interaction collectionView endInteractiveMovement (default) : / / the default to cancel the interaction collectionView. CancelInteractiveMovement ()}}Copy the code
Update data source
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let book = mockData[sourceIndexPath.section].remove(at: sourceIndexPath.row)
mockData[destinationIndexPath.section].insert(book, at: (destinationIndexPath as NSIndexPath).row)
}
Copy the code
Here, the drag sort function is done, with the system provided methods, and then achieve this effect is much simpler! Finally, let’s look at the final effect:
The last
Before writing the UICollectionView series, which ends briefly today, I had this idea that the UICollectionView was just a little bit more complex than the UITableView. But when I really go to sort out some of its technical point, I found that it is too flexible, feel additional view, before decorating view these things is very simple, a few lines of code, but in fact, when you want to achieve some highly custom interface, you will realize my own shortcomings, you don’t have a deeper cognition about these knowledge, It’s only when you start implementing it yourself that you start saying, “Oh, this is how these apis work, this is what this thing looks like.” Finally, in accordance with international practice, the address of Demo project in this paper is attached:
Github.com/ShenJieSuzh…
Related reading:
Swift custom layout achieves Cover Flow effect
UICollectionView Custom layout implementation waterfall flow view
Use UICollectionView to achieve the paging slide effect
Use UICollectionView to achieve the card rotation effect of home page
Follow my technical public account “HelloWorld Jieshao “to get more quality technical articles.