版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.04.28 星期二 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
开始
首先看下主要内容:
在本iOS教程中,您将学习如何使用
UICollectionViewDiffableDataSource
和NSDiffableDataSourceSnapshot
实现collection view
。来自翻译。
下面看下写作环境:
Swift 5, iOS 13, Xcode 11
在iOS 13中,苹果对UICollectionView API
进行了重大更新:UICollectionViewDiffableDataSource
。这个新的API比复杂、脆弱和容易出错的UICollectionViewDataSource API
更加灵活和具有声明性。
在本教程中,您将学习如何:
- 用新的
UICollectionViewDiffableDataSource
替换旧的UICollectionViewDataSource
。 - 使用
NSDiffableDataSourceSnapshot
。 - 向数据源添加
sections
。 - 向数据源添加
supplementary view
。
此外,您将看到使用这种新类型的数据源使更改具有动画效果是多么容易。我希望你对开始感到兴奋!
打开起始项目,这个项目被称为RayTube
,它允许你浏览一系列的RayWenderlich
视频课程,搜索一个特定的课程,点击它来查看更多的细节。
首先,打开启动项目。构建和运行。
你会看到一系列的RayWenderlich
视频。点击视频查看详细信息。另外,尝试使用搜索栏通过标题搜索特定的视频。
同时,在执行搜索查询时,过滤后的视频不会被动画化。
显然,这并不理想。这个UI
看起来断断续续的,这可能不是你想要的动画。你需要那个平滑的动画!在添加可扩散的数据源时,您也可以自动地解决这个问题。
What is UICollectionViewDiffableDataSource?
在iOS 13
之前,你需要配置UICollectionView
的数据源通过采用UICollectionViewDataSource
。这个协议告诉集合collection view
什么单元格,显示多少单元格,在哪个section
显示单元格,等等。
新的UICollectionViewDiffableDataSource
抽象了大量的UICollectionViewDataSource
的逻辑。这在处理collection view
的数据源时为客户端代码错误留下更少的空间。
不是告诉数据源要显示多少项items
,而是告诉它要显示哪些sections and items
。
UICollectionViewDiffableDataSource
的diffable
部分意味着,每当您更新正在显示的项items
时,collection view
将自动计算更新后的collection
与先前显示的集合之间的差异。这又将导致collection view
将更改(如更新、插入和删除)设置为动画。
Benefits of UICollectionViewDiffableDataSource
下面是实现UICollectionViewDiffableDataSource
的三个好处:
- 1) Automatic data change animations:自动数据更改动画,无论何时添加、更新或删除数据,都可以自动获得数据更改动画。
- 2) Automatic data synchronization:自动数据同步,要利用没有
UICollectionViewDiffableDataSource
的collection view
的标准动画,您必须手动管理和同步collection view
和数据源之间的数据更改。如果你有一个失调的同步操作,你会看到这样的错误:
- 3) Reduced code:减少代码,总的来说,您可以编写更少的代码,并从
collection view
的数据更改动画和数据同步中获益。
很智能,对吧?那么,如何利用新的UICollectionViewDiffableDataSource
呢? 下节课再详细介绍。
Creating a Diffable Data Source
UICollectionViewDiffableDataSource
有两个通用类型:节类型和项类型(Section type and item type)
。如果您以前使用过集合视图,那么应该熟悉sections and items
的概念。
要创建你的section
类型,在VideosViewController.swift
中videoList
下面添加以下代码:
enum Section {
case main
}
既然已经创建了Section enum
,现在就该创建diffable data source
。
为了保持简洁,为数据源创建一个类型别名。这减少了在需要配置数据源以及每次需要引用相同数据类型时编写UICollectionViewDiffableDataSource
值类型的需要。
在section
类型下面,编写以下代码来声明DataSource
类型别名:
typealias DataSource = UICollectionViewDiffableDataSource
太棒了!构建和运行。
事实证明,您的视频数据类型需要符合Hashable
。
Implementing Hashable
当视频被添加、删除或更新时,Hashable
允许diffable data source
执行更新。为了知道两个元素是否相等,需要遵循该协议。
打开Video.swift
。使Video
遵循Hashable
协议:
class Video: Hashable {
接下来,您需要实现协议方法。在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
}
你是这样做的:
- 1) 实现了
hash(into:)
,它散列给定的组件。 - 2) 将视频
Video
的id
添加到hash
中。对于视频,只需要ID
就可以知道两个视频是否相等。 - 3) 实现了
Equatable
协议的==
函数,因为所有可Hashable
的对象也必须是Equatable
。
您的项目现在应该能够再次构建,没有任何错误。
Configuring The Diffable Data Source
打开VideosViewController.swift
。现在,该Video
遵循Hashable
,您可以完成创建diffable data source
。
在viewDidLoad()
下面,添加以下代码:
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
}
在这里:
- 1) 创建一个数据源,传入
collectionView
和cellProvider
回调。 - 2) 在
cellProvider
回调函数中,您返回一个VideoCollectionViewCell
。您在这个函数中编写的代码与您在UICollectionViewDataSource
的collectionView(_:cellForItemAt:)
中看到的代码相同。
现在您已经实现了makeDataSource()
,您可以删除// MARK: - UICollectionViewDataSource
下的数据源方法。具体来说,删除以下两个方法:
collectionView (_: numberOfItemsInSection:)
collectionView (_: cellForItemAt:)
您可以删除这些方法,因为diffable data source
会自动为您处理这些函数。
接下来,是时候实际使用您辛苦使用的makeDataSource()
了!
在VideosViewController
中,在顶部添加以下属性:
private lazy var dataSource = makeDataSource()
这将为collection view
创建数据源。必须将其标记为lazy
,因为在调用makeDataSource()
之前,Swift
需要VidoesViewController
完成初始化。
最后,在collectionView(_:didSelectItemAt:)
中替换:
let video = videoList[indexPath.row]
使用
guard let video = dataSource.itemIdentifier(for: indexPath) else {
return
}
这确保应用程序直接从dataSource
检索视频。这很重要,因为UICollectionViewDiffableDataSource
可能在后台工作,使videoList
与当前显示的数据不一致。
构建和运行。
在collection view
可以显示任何单元格之前,您需要告诉它要显示什么数据。这就是snapshots
发挥作用的地方!
Using NSDiffableDataSourceSnapshot
NSDiffableDataSourceSnapshot
存储diffable data source
引用的sections and items
,以了解要显示多少节和单元格。
就像您为UICollectionViewDiffableDataSource
创建了类型别名一样,您也可以创建Snapshot
类型别名。
添加以下类型别名到VideosViewController
:
typealias Snapshot = NSDiffableDataSourceSnapshot
NSDiffableDataSourceSnapshot
,像UICollectionViewDiffableDataSource
,接受一个section
类型和一个item
类型:section
和Video
。
现在,该创建snapshot
了!
在viewDidLoad()
下面,添加以下方法:
// 1
func applySnapshot(animatingDifferences: Bool = true) {
// 2
var snapshot = Snapshot()
// 3
snapshot.appendSections([.main])
// 4
snapshot.appendItems(videoList)
// 5
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
使用applySnapshot (animatingDifferences:)
你:
- 1) 创建一个将
snapshot
应用于数据源的新方法。该方法接受一个布尔值,该布尔值决定对数据源的更改是否应该被动画。 - 2) 创建一个新的
Snapshot
对象。 - 3) 将
.main
部分添加到snapshot
。这是您当前为应用程序定义的惟一section
类型。 - 4) 将视频数组添加到
snapshot
。 - 5) 告诉
dataSource
最新的snapshot
,以便它可以相应地更新和动画。
太棒了!现在在viewDidLoad()
的末尾调用这个方法:
applySnapshot(animatingDifferences: false)
构建和运行
它起作用了!但有一个小问题。搜索一些东西,你会发现用户界面根本没有更新。
幸运的是,修复搜索功能超级简单!
Fixing Search
内部updateSearchResults(:)
,替换:
collectionView.reloadData()
使用
applySnapshot()
不要重新加载整个collection view
,而是将一个新的snapshot
应用到数据库,这将导致更改进行动画。
构建和运行。在搜索栏中输入No
,然后观看UI动画:
如果你在iPad上运行这个应用程序,动画会更复杂,而且是自然进行的,你不用多做什么。
成功!接下来,您将学习如何实现多个sections
。
Multiple Sections
有两种方法可以使用diffable data source API
实现多个sections
。
1. Option One
还记得Section
枚举吗?
enum Section { case main }
您可以向enum
添加另一种case
来实现多个sections
。当您想要显示一组预定义的sections
时,此选项非常有用。例如,一个消息应用程序有一个friends section
和一个others section
。
然而,如果你没有简单的方法知道你想要你的应用显示什么sections
,选择二是为你!
2. Option Two
第二个选项是将Section
从值类型更改为类。之后,您可以自由地创建任意数量的这些对象,而不必预先定义每个section
。
如果您的服务器提供可以随时更改的类别,或者允许用户动态创建sections
,那么这个选项非常好。
因为RayTube
应用程序有多个sections
,而且这些sections
可能会随着数据的改变而改变,所以您将使用这个选项。
Creating the Section Class
在VideosViewController.swift
,删除以下代码:
enum Section {
case main
}
接下来,创建一个名为Section.swift
的新文件。添加以下代码到文件:
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
}
}
你是这样做的:
- 1) 与
Video
类一样,您也可以将Section
遵循Hashable
。 - 2)
Section
有两个重要的属性,稍后您将使用它们对视频进行分类:title and videos
。
接下来,您需要创建一些sections
。打开Video.swift
,向下滚动。您将注意到该类上的一个扩展,它具有一个名为allVideos
的静态属性。这个数组存储应用程序显示的所有视频videos
。
现在您正在介绍多个sections
,这个属性还不够。删除// MARK: - Video Sample Data
下的所有代码。
这会在你使用Video.allVideos
的地方引起问题。不过现在先不用担心。你马上就能修好。
接下来,打开Section.swift
。然后粘贴以下代码在文件的底部:
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")
),
])
]
}
很多代码。在这里,您创建了一个静态属性allSections
,其中有四个sections
,每个部分有一个或多个视频。这基本上只是虚拟的数据——在一个完全成熟的应用程序中,你可以从服务器获取这些信息。
有了这个,你现在可以使用Section.allSections
属性访问应用程序的Section
。
Adopting the New Section Class
回到VideosViewController.swift
。
替换:
private var videoList = Video.allVideos
使用
private var sections = Section.allSections
接下来,您需要更新applySnapshot(animatingDifferences:)
来处理Section
。
在applySnapshot(animatingDifferences:)
中替换以下代码:
snapshot.appendSections([.main])
snapshot.appendItems(videos)
使用
snapshot.appendSections(sections)
sections.forEach { section in
snapshot.appendItems(section.videos, toSection: section)
}
这里有两个变化。首先,将sections
数组附加到快照。其次,对每个section
进行循环,并将其items (videos)
添加到snapshot
中。
您还可以显式地指定每个视频的section
,因为既然有多个sections
,数据源就不能正确地推断出item’s section
。
Fixing Search, Again
现在应用程序中已经有了sections
,您需要再次修复搜索功能。这是最后一次了,我保证。前面处理搜索查询的方法返回了一个视频数组,因此需要编写一个返回section
数组的新方法。
用以下方法替换filteredVideos(for:)
:
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
}
}
这个新的过滤器返回所有的sections
,其名称匹配搜索条件加上那些包含的视频,其标题匹配搜索。
在updateSearchResults(:)
内部,替换:
videoList = filteredVideos(for: searchController.searchBar.text)
使用
sections = filteredSections(for: searchController.searchBar.text)
这将关闭刚刚实现的section-based
的新版本的搜索筛选器。
那真是一次冒险。
你可以在iPad上看sections
更清楚,所以使用iPad
模拟器。构建和运行。
太棒了!视频被分类!但是,没有一个简单的方法可以看出一个视频属于哪一类。
Supplementary Views
要将header
添加到sections
中,您需要实现一个supplementary header view
。不要担心,因为这并不像听起来那么复杂。
首先,创建一个名为SectionHeaderReusableView.swift
的新文件。添加以下代码到文件:
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")
}
}
这是相当大的代码块,但没有太多。简而言之,视图有一个label
,用于显示section
的标题。检查代码:
- 1) 你添加一个类,让它成为
UICollectionReusableView
的子类。这意味着section header
视图可以像单元格一样重用。 - 2) 设置标题
label
的样式 - 3) 在初始化时,将
title label
添加到header view
并设置其Auto Layout
约束。取决于你是否在iPad
上使用不同的规则。
打开VideosViewController.swift
。在// MARK: - Layout Handling
下面,configureLayout()
的开头添加以下代码:
collectionView.register(
SectionHeaderReusableView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier
)
这将注册您刚刚使用collection view
编写的header view
,因此您可以使用section headers
。
接下来,在相同的方法中,在sectionProvider
闭包和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]
这段代码告诉布局系统,您希望为每个section
显示一个header
。
你几乎完成了!在makeDataSource()
中,在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
}
在这里你:
- 1) 获取
supplementary view
section
的一个实例。 - 2) 确保
supplementary view provider
请求一个header
。 - 3)
dequeue
一个新的header view
。 - 4) 从数据源检索该
section
,然后将titleLabel
的文本值设置为该section
的标题。
构建和运行。
下面是这款应用在iPad上的样子:
成功了!
如果需要挑战,请尝试基于数据源返回的item
引入不同的collection view cells
。
后记
本篇主要讲述了
UICollectionView UICollectionViewDiffableDataSource
的使用,感兴趣的给个赞或者关注~~~