iOS Tutorial: Collection View and Diffable Data Source [FREE]

栏目: IT技术 · 发布时间: 4年前

内容简介:In iOS 13, Apple introduced a major update to theIn this tutorial, you’ll learn how to:Further, you’ll see how easy it is to animate changes with this new type of data source. I hope you’re excited to get started!

In iOS 13, Apple introduced a major update to the UICollectionView API: UICollectionViewDiffableDataSource . This new API is more flexible and declarative than the complicated, brittle and error-prone UICollectionViewDataSource API.

In this tutorial, you’ll learn how to:

  • Replace the old UICollectionViewDataSource with the new UICollectionViewDiffableDataSource .
  • Use NSDiffableDataSourceSnapshot .
  • Add sections to the data source.
  • Add supplementary views to the data source.

Further, you’ll see how easy it is to animate changes with this new type of data source. I hope you’re excited to get started!

Getting Started

Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial.

The project, fittingly called RayTube, allows you to browse a collection of RayWenderlich video courses, search for a specific one, and tap it to view more details about it.

First up, open the starter project . Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

You’ll see a list of RayWenderlich videos. Tap a video to see its details. Also, try searching for a specific video by title using the search bar.

At the moment, the filtered videos aren’t animated when you perform a search query.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

Obviously, this isn’t ideal. This UI looks like it stutters, which may not be the animation you want. You need to that smooth animation! While adding a diffable data source, you will automagically solve this issue as well.

What is UICollectionViewDiffableDataSource?

Before iOS 13, you’d configure a UICollectionView ‘s data source by adopting UICollectionViewDataSource . This protocol tells the collection view what cell to display, how many cells to display, which section to display the cells in, and so on.

Note : If you haven’t used UICollectionView before, check out UICollectionView Tutorial: Getting Started to understand how it works.

The new UICollectionViewDiffableDataSource abstracts a significant amount of UICollectionViewDataSource ‘s logic. This leaves less room for client code errors when handling collection view’s data source.

Rather than telling the data source how many items to display, you tell it what sections and items to display.

The diffable part of UICollectionViewDiffableDataSource means that whenever you update the items you’re displaying, the collection view will automatically calculate the difference between the updated collection and the one previously shown. This will in turn cause the collection view to animate the changes, such as updates, insertions and deletions.

Benefits of UICollectionViewDiffableDataSource

Here are three benefits of implementing UICollectionViewDiffableDataSource :

  1. Automatic data change animations : Whenever you add, update or delete data, you can get the data change animation automatically.
  2. Automatic data synchronization : To utilize collection view’s standard animation without UICollectionViewDiffableDataSource , you’d have to manually manage and synchronize data changes between the collection view and the data source. If you have a misalignment in one of the synchronization operations, you’d see an error like this:

    iOS Tutorial: Collection View and Diffable Data Source [FREE]

  3. Reduced code : Overall, you can write less code and benefit from the collection view’s data change animations and data synchronization.

Smart, right!? So, how do you make use of the new UICollectionViewDiffableDataSource ? More on this next.

Creating a Diffable Data Source

UICollectionViewDiffableDataSource has two generic types: Section type and item type. If you’ve used collection views before, you should be familiar with the concept of sections and items.

To create your section type, add the following code below videoList in VideosViewController.swift :

enum Section {
  case main
}

Now that you’ve created the Section enum, it’s time to create the diffable data source.

To keep things concise, create a type alias for the data source. This mitigates the need to write UICollectionViewDiffableDataSource value types when you need to configure the data source as well as every time you need to reference the same data type.

Below the section type, write the following code to declare a DataSource type alias:

typealias DataSource = UICollectionViewDiffableDataSource<Section, Video>

Awesome! Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

iOS Tutorial: Collection View and Diffable Data Source [FREE]

As it turns out, your video data type needs to conform to Hashable .

Implementing Hashable

Hashable allows the diffable data source to perform updates when videos are added, removed or updated. Conformance to the protocol is needed in order to know whether or not two elements are equal to each other.

Open Video.swift . Make Video adopt Hashable :

class Video: Hashable {

Next, you need to implement the protocol methods. Add the following code below init(title:thumbnail:lessonCount:link:) :

// 1
func hash(into hasher: inout Hasher) {
  // 2
  hasher.combine(id)
}

// 3
static func == (lhs: Video, rhs: Video) -> Bool {
  lhs.id == rhs.id
}

Here’s what you did:

  1. Implemented hash(into:) , which hashes the given components.
  2. Added the id of Video to the hash. For videos, you only need the ID to know whether two videos are equal.
  3. Implemented the Equatable protocol’s == function, because all Hashable objects must also be Equatable .

Your project should now be able to build again without any errors.

Configuring The Diffable Data Source

Open VideosViewController.swift . Now that Video conforms to Hashable , you can finish creating the diffable data source.

Below viewDidLoad() , add the following code:

func makeDataSource() -> DataSource {
  // 1
  let dataSource = DataSource(
    collectionView: collectionView,
    cellProvider: { (collectionView, indexPath, video) ->
      UICollectionViewCell? in
      // 2
      let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "VideoCollectionViewCell",
        for: indexPath) as? VideoCollectionViewCell
      cell?.video = video
      return cell
  })
  return dataSource
}

Here:

  1. You create a dataSource , passing in collectionView and a cellProvider callback.
  2. Inside the cellProvider callback, you return a VideoCollectionViewCell . The code you write in this function is the same as you’re used to seeing in UICollectionViewDataSource ‘s collectionView(_:cellForItemAt:) .

Now that you’ve implemented makeDataSource() you can delete the data source methods under // MARK: - UICollectionViewDataSource` . Specifically, delete the following two methods:

  • collectionView(_: numberOfItemsInSection:)
  • collectionView(_:cellForItemAt:)

You can delete these methods because the diffable data source automatically handles these functionalities for you.

Next, it’s time to actually use the makeDataSource() you worked so hard on!

In VideosViewController , add the following property to the top:

private lazy var dataSource = makeDataSource()

This creates the data source for the collection view. You must mark it lazy because Swift requires VidoesViewController to complete initialization before you can call makeDataSource() .

Finally, inside collectionView(_:didSelectItemAt:) replace:

let video = videoList[indexPath.row]

…with:

guard let video = dataSource.itemIdentifier(for: indexPath) else {
  return
}

This ensures the app retrieves videos directly from the dataSource . This is important because UICollectionViewDiffableDataSource might do work in the background that makes videoList inconsistent with the currently displayed data.

Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

And…nothingness.

Before the collection view can display any cells, you need to tell it what data you’d like to display. This is where snapshots come in!

Using NSDiffableDataSourceSnapshot

NSDiffableDataSourceSnapshot stores your sections and items, which the diffable data source references to understand how many sections and cells to display.

Just like you created a type alias for UICollectionViewDiffableDataSource , you can create a Snapshot type alias as well.

Add the following type alias to VideosViewController :

typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Video>

NSDiffableDataSourceSnapshot , like UICollectionViewDiffableDataSource , takes a section type and an item type: Section and Video .

Now, it’s time to create a snapshot!

Below viewDidLoad() , add the following method:

// 1
func applySnapshot(animatingDifferences: Bool = true) {
  // 2
  var snapshot = Snapshot()
  // 3
  snapshot.appendSections([.main])
  // 4
  snapshot.appendItems(videoList)
  // 5
  dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

With applySnapshot(animatingDifferences:) you:

Snapshot
.main
dataSource

Great! Now call the method at the end of viewDidLoad() :

applySnapshot(animatingDifferences: false)

Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

It works! But there is a small problem. Search for something, and you’ll notice the user interface doesn’t update at all.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

Fortunately, fixing the search feature is super easy!

Fixing Search

Inside updateSearchResults(for:) , replace:

collectionView.reloadData()

With:

applySnapshot()

Instead of reloading the entire collection view, you new apply a new snapshot to the database, which will cause the changes to animate.

Build and run. Type No in the search bar and watch the UI animate:

iOS Tutorial: Collection View and Diffable Data Source [FREE]

If you run the app on an iPad, the animation is even more complex, all for free!

iOS Tutorial: Collection View and Diffable Data Source [FREE]

Success! Next, you’re going to learn how to implement multiple sections.

Multiple Sections

There are two ways you can implement multiple sections using the diffable data source API.

Option One

Remember the Section enum?

[spoiler title=”The Section enum”]

enum Section {
  case main
}

[/spoiler]

You can add another case to the enum to implement multiple sections. This option is great when you have a predefined set of sections you’d like to display. For example, a messaging app with a friends section and an others section.

However, if you have no easy way of knowing what sections you’d like your app to display, option two is for you!

Option Two

The second option is to change Section from a value type to a class. Afterward, you can freely create any number of these objects without having to predefine each section.

This option is great if you have a server that provides categories that can change at any time, or if you allow users to create sections dynamically.

Because the RayTube app has more than a few sections, and because these sections may change later if your data changes, you’ll go with this option.

Creating the Section Class

Inside VideosViewController.swift , remove the following code:

enum Section {
  case main
}

Next, create a new file named Section.swift . Add the following code to the file:

import UIKit
// 1
class Section: Hashable {
  var id = UUID()
  // 2
  var title: String
  var videos: [Video]
  
  init(title: String, videos: [Video]) {
    self.title = title
    self.videos = videos
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
  
  static func == (lhs: Section, rhs: Section) -> Bool {
    lhs.id == rhs.id
  }
}

Here’s what you did:

  1. Like the Video class, you conform Section to Hashable .
  2. Section has two important properties that you’ll use in a moment to categorize the videos: title and videos .

Next, you need to create some sections. Open Video.swift and scroll down. You’ll notice an extension on the class with a static property named allVideos . This array stores all the videos the app displays.

Now that you’re introducing multiple sections, this property is insufficient. Remove all the code below // MARK: - Video Sample Data .

This will cause an issue in the places where you use Video.allVideos , but don’t worry about that for now. You’ll fix that in just a moment.

Next, open Section.swift . Then paste the following code at the bottom of the file:

extension Section {
  static var allSections: [Section] = [
    Section(title: "SwiftUI", videos: [
      Video(
        title: "SwiftUI",
        thumbnail: UIImage(named: "swiftui"),
        lessonCount: 37,
        link: URL(string: "https://www.raywenderlich.com/4001741-swiftui")
      )
    ]),
    Section(title: "UIKit", videos: [
      Video(
        title: "Demystifying Views in iOS",
        thumbnail: UIImage(named: "views"),
        lessonCount: 26,
        link:
        URL(string:
          "https://www.raywenderlich.com/4518-demystifying-views-in-ios")
      ),
      Video(
        title: "Reproducing Popular iOS Controls",
        thumbnail: UIImage(named: "controls"),
        lessonCount: 31,
        link: URL(string: """
          https://www.raywenderlich.com/5298-reproducing
          -popular-ios-controls
          """)
      )
    ]),
    Section(title: "Frameworks", videos: [
      Video(
        title: "Fastlane for iOS",
        thumbnail: UIImage(named: "fastlane"),
        lessonCount: 44,
        link: URL(string:
          "https://www.raywenderlich.com/1259223-fastlane-for-ios")
      ),
      Video(
        title: "Beginning RxSwift",
        thumbnail: UIImage(named: "rxswift"),
        lessonCount: 39,
        link: URL(string:
          "https://www.raywenderlich.com/4743-beginning-rxswift")
      )
    ]),
    Section(title: "Miscellaneous", videos: [
      Video(
        title: "Data Structures & Algorithms in Swift",
        thumbnail: UIImage(named: "datastructures"),
        lessonCount: 29,
        link: URL(string: """
          https://www.raywenderlich.com/977854-data-structures
          -algorithms-in-swift
        """)
      ),
      Video(
        title: "Beginning ARKit",
        thumbnail: UIImage(named: "arkit"),
        lessonCount: 46,
        link: URL(string:
          "https://www.raywenderlich.com/737368-beginning-arkit")
      ),
      Video(
        title: "Machine Learning in iOS",
        thumbnail: UIImage(named: "machinelearning"),
        lessonCount: 15,
        link: URL(string: """
          https://www.raywenderlich.com/1320561-machine-learning-in-ios
        """)
      ),
      Video(
        title: "Push Notifications",
        thumbnail: UIImage(named: "notifications"),
        lessonCount: 33,
        link: URL(string:
          "https://www.raywenderlich.com/1258151-push-notifications")
      ),
    ])
  ]
}

Phew, lots of code. Here you created a static property allSections which has four sections with one or more videos each. This is basically just dummy data — in a fully fledged application you would fetch this information from a server.

With this, you can now access the app’s sections using the Section.allSections property.

Adopting the New Section Class

Head back to VideosViewController.swift .

Replace:

private var videoList = Video.allVideos

…with:

private var sections = Section.allSections

Next, you need to update applySnapshot(animatingDifferences:) to work with Section .

Replace the following code in applySnapshot(animatingDifferences:) :

snapshot.appendSections([.main])
snapshot.appendItems(videos)

…with:

snapshot.appendSections(sections)
sections.forEach { section in
  snapshot.appendItems(section.videos, toSection: section)
}

There are two changes here. First, you append the sections array to the snapshot. Second, you loop over each section and add its items (videos) to the snapshot.

You also specify each video’s section explicitly, because the data source won’t infer the item’s section correctly now that there are multiple sections.

Fixing Search, Again

Now that you have sections in the app, you need to fix the search feature again. This is the last time, I promise. The previous method that processed a search query returned an array of videos, so you need to write a new method that returns an array of sections.

Replace filteredVideos(for:) with the following method:

func filteredSections(for queryOrNil: String?) -> [Section] {
  let sections = Section.allSections

  guard 
    let query = queryOrNil, 
    !query.isEmpty 
    else {
      return sections
  }
    
  return sections.filter { section in
    var matches = section.title.lowercased().contains(query.lowercased())
    for video in section.videos {
      if video.title.lowercased().contains(query.lowercased()) {
        matches = true
        break
      }
    }
    return matches
  }
}

This new filter returns all sections whose name matches the search criteria plus those that contain a video whose title matches the search.

Inside updateSearchResults(for:) , replace:

videoList = filteredVideos(for: searchController.searchBar.text)

…with:

sections = filteredSections(for: searchController.searchBar.text)

This switches out the search filter for the new section-based version you just implemented.

Phew! That was quite an adventure.

You can see the sections better on an iPad, so use an iPad simulator. Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

Awesome! The videos are categorized! But, there isn’t an easy way to see what category a video is a part of.

Supplementary Views

To add a header to the sections, you need to implement a supplementary header view. Don’t worry because this isn’t as complicated as it sounds.

First, create a new file named SectionHeaderReusableView.swift . Add the following code to the file:

import UIKit

// 1
class SectionHeaderReusableView: UICollectionReusableView {
  static var reuseIdentifier: String {
    return String(describing: SectionHeaderReusableView.self)
  }

  // 2
  lazy var titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = UIFont.systemFont(
      ofSize: UIFont.preferredFont(forTextStyle: .title1).pointSize,
      weight: .bold)
    label.adjustsFontForContentSizeCategory = true
    label.textColor = .label
    label.textAlignment = .left
    label.numberOfLines = 1
    label.setContentCompressionResistancePriority(
      .defaultHigh, 
      for: .horizontal)
    return label
  }()
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    // 3
    backgroundColor = .systemBackground
    addSubview(titleLabel)

    if UIDevice.current.userInterfaceIdiom == .pad {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: leadingAnchor, 
          constant: 5),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: trailingAnchor, 
          constant: -5)])
    } else {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: readableContentGuide.leadingAnchor),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: readableContentGuide.trailingAnchor)
      ])
    }
    NSLayoutConstraint.activate([
      titleLabel.topAnchor.constraint(
        equalTo: topAnchor, 
        constant: 10),
      titleLabel.bottomAnchor.constraint(
        equalTo: bottomAnchor, 
        constant: -10)
    ])
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

This is quite the block of code, but there’s not much to it. In short, the view has one label which displays the title of the section. Going over the code:

UICollectionReusableView

Open VideosViewController.swift . Below // MARK: - Layout Handling , add the following code to the beginning of configureLayout() :

collectionView.register(
  SectionHeaderReusableView.self, 
  forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 
  withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier
)

This registers the header view you just wrote with the collection view, so you can use section headers.

Next, in the same method, add the following code inside the sectionProvider closure and right before return section :

// Supplementary header view setup
let headerFooterSize = NSCollectionLayoutSize(
  widthDimension: .fractionalWidth(1.0), 
  heightDimension: .estimated(20)
)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
  layoutSize: headerFooterSize, 
  elementKind: UICollectionView.elementKindSectionHeader, 
  alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]

This code tells the layout system that you’d like to display a header for every section.

You’re almost done! Inside makeDataSource() , add the following code right before return dataSource :

// 1
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
  // 2
  guard kind == UICollectionView.elementKindSectionHeader else {
    return nil
  }
  // 3
  let view = collectionView.dequeueReusableSupplementaryView(
    ofKind: kind,
    withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier,
    for: indexPath) as? SectionHeaderReusableView
  // 4
  let section = self.dataSource.snapshot()
    .sectionIdentifiers[indexPath.section]
  view?.titleLabel.text = section.title
  return view
}

Here you:

  1. Get an instance of the section for the supplementary view.
  2. Ensure the supplementary view provider asks for a header.
  3. Dequeue a new header view.
  4. Retrieve the section from the data source, then set the titleLabel ‘s text value to the section ‘s title.

Build and run.

iOS Tutorial: Collection View and Diffable Data Source [FREE]

And, here’s how the app looks on an iPad:

iOS Tutorial: Collection View and Diffable Data Source [FREE]

Success!

Where to Go From Here?

Great job getting this far! You can download the final project by using the Download Materials button at the top or bottom of this page.

In this tutorial, you’ve learned how to add UICollectionViewDiffableDataSource to your existing collection view-based project.

If you want a challenge, try to introduce different collection view cells based on the item returned by the data source.

The sample app also uses compositional layouts. If you’d like to learn more about them, check out Modern Collection Views with Compositional Layouts .

If you have any questions or comments, join the forum below!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance Python

High Performance Python

Andrew Lewis / O'Reilly Media, Inc. / 2010-09-15 / USD 34.99

Chapter 1. Introduction Section 1.1. The High Performance Buzz-word Chapter 2. The Theory of Computation Section 2.1. Introduction Section 2.2. Problems Section 2.3. Models of Computati......一起来看看 《High Performance Python》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具