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

相关推荐
慧一居士几秒前
vue.config.js 文件功能介绍,使用说明,对应完整示例演示
前端·vue.js
颜酱3 分钟前
用导游的例子来理解 Visitor 模式,实现AST 转换
前端·javascript·算法
蒙特卡洛的随机游走22 分钟前
Spark的宽依赖与窄依赖
大数据·前端·spark
共享家952729 分钟前
QT-常用控件(多元素控件)
开发语言·前端·qt
葱头的故事30 分钟前
将传给后端的数据转换为以formData的类型传递
开发语言·前端·javascript
QWQ___qwq38 分钟前
SwiftUI 布局之美:Padding 让界面呼吸感拉满
ios·swiftui·swift
_233342 分钟前
vue3二次封装element-plus表格,slot透传,动态slot。
前端·vue.js
jump6801 小时前
js中数组详解
前端·面试
崽崽长肉肉1 小时前
iOS 基于Vision.framework从图片中提取文字
前端
温宇飞1 小时前
Web Abort API - AbortSignal 与 AbortController
前端