SwiftUI to UIKit 之 UICollectionView (UI 篇)

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 方法

这个本来应该写在第一个 标题的,这个还是放在最后比较合适。

  1. 控件需要多少个数据项,才能绘制
  2. 控件的回调是不是可以binding 的数据直接给到swiftUI
  3. Delegate 还是Block的回调是不是更具 性价比
  4. 别人在用这个控件是不是可快速上手
  5. 维护成本 特别当数据项需要更多的时候,的添加问题 这些往往都是控件写的差不多了,当需要迁入到项目里的时候,才会考虑的问题,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以上,部分包括 本地图片的加载,在滑动的时候,内存无法释放的情况,就会有因内存爆满而崩溃的情况。这个不可避免,因为确实 在手机内部有那么多的照片需要加载。 这个时候 就开始方案了

  1. 分页
  2. 多线程
  3. 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...

引用:

fatbobman.com/zh/posts/ui...

developer.apple.com/wwdc21/1025...

github.com/JiongXing/P...

stackoverflow.com/questions/5...

Building High-Performance Lists and Collection Views | Apple Developer Documentation

Smoothen your table view data loading using UITableViewDataSource Prefetching

UICollectionView current visible cell index

相关推荐
每天开心7 分钟前
别再怕 useRef 了!一篇文章讲清楚它和 DOM 的关系
前端·面试·前端框架
小喷友8 分钟前
第 2 章:页面与路由系统
前端·react.js·next.js
Java水解10 分钟前
前端绘图基础——SVG详解
前端·svg
在钱塘江10 分钟前
《你不知道的JavaScript-中卷》第一部分-类型和语法-笔记-3-原生函数
前端·javascript
YungFan11 分钟前
iOS26适配指南之动画
ios·swift
帅夫帅夫15 分钟前
Vue2 Diff算法详解 - 双端比较的奥秘
前端
阿琳a_19 分钟前
解决vue中使用vite-plugin-cesium插件打包后运行项目报错
前端·javascript·vue.js·vite·cesium
前端岳大宝21 分钟前
React条件渲染
前端·react.js·前端框架
用户68238060322527 分钟前
Ant Design v5 不兼容 React 19,导致 message.xxx() 内部渲染逻辑没有生效(也就是弹不出消息框)。
前端
BER_c30 分钟前
Vitest学习笔记
前端