内容简介: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 newUICollectionViewDiffableDataSource
. -
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.
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.
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
:
- Automatic data change animations : Whenever you add, update or delete data, you can get the data change animation automatically.
-
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: - 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.
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:
-
Implemented
hash(into:)
, which hashes the given components. -
Added the
id
ofVideo
to the hash. For videos, you only need the ID to know whether two videos are equal. -
Implemented the
Equatable
protocol’s==
function, because allHashable
objects must also beEquatable
.
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:
-
You create a
dataSource
, passing incollectionView
and acellProvider
callback. -
Inside the
cellProvider
callback, you return aVideoCollectionViewCell
. The code you write in this function is the same as you’re used to seeing inUICollectionViewDataSource
‘scollectionView(_: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.
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.
It works! But there is a small problem. Search for something, and you’ll notice the user interface doesn’t update at all.
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:
If you run the app on an iPad, the animation is even more complex, all for 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:
-
Like the
Video
class, you conformSection
toHashable
. -
Section
has two important properties that you’ll use in a moment to categorize the videos:title
andvideos
.
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.
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:
- Get an instance of the section for the supplementary view.
- Ensure the supplementary view provider asks for a header.
- Dequeue a new header view.
-
Retrieve the section from the data source, then set the
titleLabel
‘s text value to thesection
‘s title.
Build and run.
And, here’s how the app looks on an iPad:
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!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
文本上的算法——深入浅出自然语言处理
路彦雄 / 人民邮电出版社 / 2018-3-1 / 69.00元
本书结合作者多年学习和从事自然语言处理相关工作的经验,力图用生动形象的方式深入浅出地介绍自然语言处理的理论、方法和技术。本书抛弃掉繁琐的证明,提取出算法的核心,帮助读者尽快地掌握自然语言处理所必备的知识和技能。本书主要分两大部分。第一部分是理论篇,包含前3章内容,主要介绍一些基础的数学知识、优化理论知识和一些机器学习的相关知识。第二部分是应用篇,包含第4章到第8章,分别针对计算性能、文本处理的术语......一起来看看 《文本上的算法——深入浅出自然语言处理》 这本书的介绍吧!