SwiftUI to UIKit 之 UICollectionView (UI 篇)
对于SwiftUI 的进行目前 不完善的情况 ,但是公司的项目 的需求不等人,所以无论是从临时解决问题的角度,还是对于swiftUI 项目中功能无法完成的角度,都有这样的应用场景,UIKit 还需要发挥替补的作用。
关于UI
1 从Struct 开始

从开头开始,如果你想弄一个View 那么
objectivec
struct PhotosUIKitView: UIViewRepresentable { ..... }
如果你想弄一个ViewController 那么
objectivec
struct PhotosUIKitViewController: UIViewControllerRepresentable { ..... }
从细节可以看出来,这个是个Struct 而不是个Class,这个是一切 写法不同的开始,因为是Struct 类型,所以对于内部的变量的更改,要不就是一个Mut 的方法 才可更改,要不就是对于@State @Binding 的数据进行更改,这个结构直接影响了整体的写法
2 MakeUIView
这个部分是直接问开发者,到底用什么控件的UIKit 来去填补SwiftUI 的部分
- 直接用UIKit
- CustomUI ,内部嵌套 UIKit
到底用哪个逻辑套路,这个看项目的需求,但是 对于开发者 两者 可能都要深刻了解,因为需求在变化,今天可能基础功能就能满足产品或者老板的需求,但是明天 需要自定义的时候,发现没有预留空间,就大大的降低效率,需要重头再来,或者在单一功能的局部小改动,造成 大范围的 重构,是不具有性价比的行为
swift
func makeUIView(context: Context) -> UICollectionView {
...... // set up uikit
// color. layout ....
}
3 UpDateUIView()
swift
func updateUIView(_ uiView: UICollectionView, context: Context) {
}
不仅仅是up Date UI 的事情。Swift UI 中的@State @Publish @Binding 的数据变化都会导致这个方法的频繁调用 特别是对于封装控件的时候,对于这个方法的 内部处理,直接控制 UI 频繁 刷新,内部是否有条件进行隔离。
"当 UIViewRepresentable 视图中的注入依赖发生变化时,SwiftUI 会调用 updateUIView 。其调用时机同标准 SwiftUI 视图的 body 一致,最大的不同为,调用 body 为计算值,而调用 updateview 仅为通知 UIViewRepresentable 视图依赖有变化,至于是否需要根据这些变化来做反应,则由开发者来自行处理。 " ---东坡肘子
这里延续出来一个概念关于SwiftUI 的就是视图树 ,直到视图 切换到另一个不包含该视图的视图树分支 。要不然 这个 方法会被频繁的 调用。
4 关于协调器
swift
func makeCoordinator() -> Coordinator {
.init(self)
}
Coordinator 这里Custom 自定义的类包含了,处理 UIKit 视图中的复杂逻辑,同 SwiftUI 框架保持沟通 等功能,特别是对于代理,数据处理,复杂计算等。 但是这里有个技巧,在看github的代码里 看到的 就是在makeUIView这个方法里面
swift
func makeUIView(context: Context) -> UICollectionView {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
context.coordinator.setUpView(collectionView)
}
这个 进一步的 把冗长的代码踢到了 Coordinator 里的自定义方法的 setUpView 进一步的加强了代码 可维护性 。降低维护成本
5 init 方法
这个本来应该写在第一个 标题的,这个还是放在最后比较合适。
- 控件需要多少个数据项,才能绘制
- 控件的回调是不是可以binding 的数据直接给到swiftUI
- Delegate 还是Block的回调是不是更具 性价比
- 别人在用这个控件是不是可快速上手
- 维护成本 特别当数据项需要更多的时候,的添加问题 这些往往都是控件写的差不多了,当需要迁入到项目里的时候,才会考虑的问题,so ,放在的最后一步

从UICollctionView 代理入手 解决快速滑动 加载问题
- 1 UICollectionViewDataSource
- 2 UICollectionViewDelegateFlowLayout
- 3 UICollectionViewDataSourcePrefetching
这里最迷的就是 UICollectionViewDataSourcePrefetching Pre-Fetching预加载机制用于提升它的性能, 一个预加载数据 ,一个取消提前加载数据。 坑就在这开始了 "Cancels a previously triggered data prefetch request." 这个是文档的说明。 但是 在实践过程中这个作用的比较好用的情况是在uitableView,在uicollection view中 不是很频繁的调用,甚至可是说仅有在滑动快停止的时候才会调用。包括苹果的官方文档Demo
developer.apple.com/wwdc21/1025...
仍然有这个问题 这里紧跟着一个问题就是内存的飙升
切换布局 和滑动 时候
1 取消预加载
数量级比较小的图片处理 不需要这步骤,但是如果图片的数量很大1w以上,部分包括 本地图片的加载,在滑动的时候,内存无法释放的情况,就会有因内存爆满而崩溃的情况。这个不可避免,因为确实 在手机内部有那么多的照片需要加载。 这个时候 就开始方案了
- 分页
- 多线程
- loading等待 这三个方案 都是正确的方案,但是正确的又有点不正确。因为 忘记了这个是UIKit,而不是后端的大批量数据,这是个本质的问题。大数据的后端数据处理,和ui端不同的思维方式
经过反复的demo测试 最后选了一个方案
swift
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 数据加载
// 数据队列出队显示结果
}
swift
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
// 数据队列入队
}
swift
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 取消数据正在处理的没来得及显示的操作
// 数据队列出队取消
}

利用cell 的recycle use的机制,避开了多线程数里数据,简称 用多少 处理多少
didEndDisplaying 在这里对快速滑动的数据处理性能优化。避开了多线程的操作逻辑
2 本地图片还有网络图片
内存的飙升和 图片质量的程度成正比。 关于不同的Layout 对应不同的,根据layout的不同,来控制图片的显示质量问题 // 后期最佳方案是draw 到cell collectionview 上,而不是 uiiimage 被 add sub view
关于Operation OperationQueue
- OperationQueue // 操作队列
- Operation // 操作
- DataStorage // 对象容器
- PhotoLibManager // 单一操作
用key value 的方式记录 对应的 Operation (继承) 详情请看 《SwiftUI to UIKit 之 UICollectionView (数据 篇)》
附加 内存 gif 图片
关于Layout

对于外部使用的时候 比较麻烦的就是swiftuI的 渲染树的部分,因为渲染时回出现多次重复调用,和外部重复绘制等问题,因为SwiftUI 在内部已经进行的二次确认的控制,但是对于uikit 却没有这一成的保护。目前简单的处理是 通过变量的方式进行控制。
关于layout 在makeupui 的时候进行一次, updateUI的时候再进行 更改。
ini
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
flowLayout.minimumInteritemSpacing = 1
flowLayout.sectionHeadersPinToVisibleBounds = true
flowLayout.sectionInset = .init(top: 5, left: 20, bottom: 5, right: 20)
flowLayout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.size.width, height: isEdit ? 50 : 50)
flowLayout.itemSize = CGSize(width: (UIScreen.main.bounds.size.width / CGFloat(self.changeUI.numColum)) - 1, height: (UIScreen.main.bounds.size.width / CGFloat(self.changeUI.numColum)) - 1)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
更改 collectionView 的Layout
less
uiView.collectionViewLayout.invalidateLayout()
uiView.setCollectionViewLayout(createLayout(), animated: true)
这里的坑 还是在切换 layout 的时候,header 和footer 的ui 并不会刷新,如果切换layout同时要切换headr 的内容。
swift
private func refreshVisHeaders(_ uiView: UICollectionView) {
let indexPaths = uiView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader)
print("indexpaths ... is \(indexPaths)")
indexPaths.forEach { indexPath in
let headers_view = uiView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: indexPath)
guard let h = headers_view as? PhotosMainHeaderView else { return }
if self.data.count > indexPath.section {
h.upDateHeaderView(!isEdit,self.data[indexPath.section].sectionTitle)
} else {
h.upDateHeaderView(!isEdit,self.data[0].sectionTitle)
}
if self.selectedSessionIndexs.contains(indexPath){
h.upDataHeaderSelected(true)
} else {
h.upDataHeaderSelected(false)
}
}
}
Demo :
延伸 Layout
对于 产品经理的 不同要求,特定的UI 交互需求。特定的场景需要 等等等,各种变化的布局Layout
swift
* override public func prepare(), 重写准备方法
* override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?, 重写单个格子 item 的布局方法
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?, 重写格子视图 UICollection 的布局方法
Demo : github.com/eleev/uicol...
引用:
developer.apple.com/wwdc21/1025...
stackoverflow.com/questions/5...
Building High-Performance Lists and Collection Views | Apple Developer Documentation
Smoothen your table view data loading using UITableViewDataSource Prefetching