scrollview下拉刷新_SwiftUI之View Tree 实战3(下拉刷新)
不得不说,在SwiftUI中,Preference这项技术实在是太神奇了,这也是我为什么写这么多与其相关文章的原因,它的原理是如此的简单,但加上我们的想象力,它却又无所不能。
在本篇文章中,我们将再次基于此技术,来实现一个最常用的功能,如下图所示:
SwiftUI为ScrollView提供的功能非常有限,以至于在UIKit中很容易实现的功能,在SwiftUI中,却变得毫无头绪,本例的核心思想非常简单,看下图就可明白:
核心思想就是,计算MovingView和FixedView两者之间的y的差,从而得到offset。
其中,FixedPositionView的代码如下:
/// 位置固定不变的view
struct FixedPositionView: View {var body: some View {GeometryReader { proxy inColor.clear.preference(key: MCRefreshablePreferenceTypes.MCRefreshablePreferenceKey.self,value: [MCRefreshablePreferenceTypes.MCRefreshablePreferenceData(viewType: .fixedPositionView, bounds: proxy.frame(in: .global))])}}
}
可以看出,我们通过.preference
为其绑定了一个MCRefreshablePreferenceData类型的数据,最重要的目的是保存该view的bounds。那么同理,MovingPositionView的代码如下:
/// 位置随着滑动变化的view,高度为0
struct MovingPositionView: View {var body: some View {GeometryReader { proxy inColor.clear.preference(key: MCRefreshablePreferenceTypes.MCRefreshablePreferenceKey.self,value: [MCRefreshablePreferenceTypes.MCRefreshablePreferenceData(viewType: .movingPositionView, bounds: proxy.frame(in: .global))])}.frame(height: 0)}
}
当然,这两个view对用户来说都是不可见的,一个作为背景,另一个放到ScrollView内容的最上边:
struct MCRefreshableVerticalScrollView<Content: View>: View {...var body: some View {VStack {ScrollView {ZStack(alignment: .top) {MovingPositionView()...}}.background(FixedPositionView())...}...
}
至于PreferenceKey的用法,我们在之前的文章中已经反复讲过了,基本上算是固定写法了,这里就不贴代码了。
接下来最重要的内容就是如何计算offset了,代码如下:
func calculate(_ values: [MCRefreshablePreferenceTypes.MCRefreshablePreferenceData]) {DispatchQueue.main.async {/// 计算croll offsetlet movingBounds = values.first(where: { $0.viewType == .movingPositionView })?.bounds ?? .zerolet fixedBounds = values.first(where: { $0.viewType == .fixedPositionView })?.bounds ?? .zeroself.offset = movingBounds.minY - fixedBounds.minYself.rotation = self.headerRotation(self.offset)/// 触发刷新if !self.refreshing, self.offset > self.threshold, self.preOffset <= self.threshold {self.refreshing = true}if self.refreshing {if self.preOffset > self.threshold, self.offset <= self.threshold {self.frozen = true}} else {self.frozen = false}self.preOffset = self.offset}
}
主要步骤如下:
- 获取Fixed和Moving的bounds:
fixedBounds
,movingBounds
- 计算offset:
movingBounds.minY - fixedBounds.minY
- 计算箭头旋转的角度:
self.headerRotation(self.offset)
- 计算refreshing和frozen:当offset大于等于threshold时,就会触发refreshing,当松手弹回后,offset小于等于threshold时,触发frozen
上边的计算过程还算简单,我们在看看RefreshHeader的代码:
struct RefreshHeader: View {var height: CGFloatvar loading: Boolvar frozen: Boolvar rotation: Anglevar updateTime: Datelet dateFormatter: DateFormatter = {let df = DateFormatter()df.dateFormat = "MM月dd日 HH时mm分ss秒"return df}()var body: some View {HStack(spacing: 20) {Spacer()Group {if self.loading {VStack {Spacer()ActivityRep()Spacer()}} else {Image(systemName: "arrow.down").resizable().aspectRatio(contentMode: .fit).rotationEffect(rotation)}}.frame(width: height * 0.25, height: height * 0.8).fixedSize().offset(y: (loading && frozen) ? 0 : -height)VStack(spacing: 5) {Text("(self.loading ? "正在刷新数据" : "下拉刷新数据")").foregroundColor(.secondary).font(.subheadline)Text("(self.dateFormatter.string(from: updateTime))").foregroundColor(.secondary).font(.subheadline)}.offset(y: -height + (loading && frozen ? +height : 0.0))Spacer()}.frame(height: height)}
}
我们为该view传过来还几个参数:
var height: CGFloat
var loading: Bool
var frozen: Bool
var rotation: Angle
var updateTime: Date
然后就需要根据这些参数,生成不同状态下的UI,这个就不多做说明了,我这里只是用了一种常见的刷新样式,大家可以把这个RefreshHeader改成任何其他想要的样式,这里的rotation参数表示旋转角度,完全可以改成percent,表示当前下拉的进度,这样就能实现下拉放大或者其他动画效果。
还有一点值得注意:先看代码:
struct MCRefreshableVerticalScrollView<Content: View>: View {@State private var preOffset: CGFloat = 0@State private var offset: CGFloat = 0@State private var frozen = false@State private var rotation: Angle = .degrees(0)@State private var updateTime: Date = Date()var threshold: CGFloat = 70@Binding var refreshing: Boollet content: Content...
}
refreshing是一个@Binding,因此外部可以修改这个值,比如当网络请求回来之后,我们需要在外部把该值设为false,在本例中,代码类似于:
class MyTestModel: ObservableObject {@Published var loading: Bool = false {didSet {if oldValue == false, loading == true {load()}}}@Published var articleArray: [Article] = articlesfunc load() {DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) {self.loading = falseself.articleArray.shuffle()}}
}
struct ContentView: View {@StateObject private var model = MyTestModel()var body: some View {NavigationView {MCRefreshableVerticalScrollView(refreshing: self.$model.loading) {VStack(spacing: 0) {ForEach(model.articleArray) { article inRow(article: article)}}}.navigationBarTitle("我的文章", displayMode: .inline)}}
}
当MCRefreshableVerticalScrollView下拉出发刷新后,通过修改self.$model.loading
,触发了MyTestModel中的load方法,我们模拟了一个延时操作,4秒过后,self.loading = false
,我们修改了这个值,然后MCRefreshableVerticalScrollView做了两件事情:
- 更新view,刷新结束
- 保存刷新成功时的Date
对于更新view来说,它是自动的一个过程,这个我们无需关心,SwiftUI的核心观念就是根据数据显示UI,那么另一个问题,就是如何监听refreshing的改变,这个系统也给了我们一个modifier:
.onChange(of: refreshing) { refreshing inDispatchQueue.main.async {if !refreshing {self.updateTime = Date()}}}
总结
这篇是Preference的最后一篇文章了,通过这几篇文章,相信大家已经彻底掌握了Preference的用法,加下来,我们写一篇SwiftUI布局(layout)的文章,讲解一下其核心布局观念。
由于示例代码太多,我单独弄了一个仓库:MCSwiftUIRefresh
SwiftUI集合:FuckingSwiftUI
参考:ScrollView – Pull to Refresh
scrollview下拉刷新_SwiftUI之View Tree 实战3(下拉刷新)相关推荐
- android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件
下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的PullToRefresh,或是修改自该框架的其他库.而到现在已经有了更多的选择,github上还是有很多体验 ...
- Android 怎么实现支持所有View的通用的下拉刷新控件
转载请标明出处: http://blog.csdn.net/u010386612/article/details/51372696 本文出自:[AItsuki的博客] 下拉刷新对于一个app来说是必不 ...
- 中改变了值但是数据没有刷新_SwiftUI数据流
SwiftUI 中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作. 数据处理的基本原则 Data Access as a Dependency:在 ...
- 【Android 性能优化】布局渲染优化 ( GPU 过度绘制优化总结 | CPU 渲染过程 | Layout Inspector 工具 | View Tree 分析 | 布局组件层级分析 )
文章目录 一. GPU 过度绘制优化总结 二. CPU 渲染过程 三. CPU 渲染性能调试工具 Layout Inspector 四. Layout Inspector 组件树 DecorView ...
- element做树形下拉_一个基于 elementUi的vue树形下拉框组件
# wl-vue-select,wl-tree-selectcss # 简介vue 用于vue框架的树形下拉框及带全选的普通下拉框. node Tree drop-down box for vue ...
- html下拉框代码默认选中状态,@Html.DropDownListFor 下拉框绑定(选择默认值)
首先先构建绑定下拉框的数据源 private void GetSalesList() { var userList = _rmaExpressAppService.GetUserList(); Tem ...
- 两个下拉框相关联ajax,触发第二个下拉框以显示基于从第一个下拉框中选择的值的值ajax...
我有两个引导程序下拉框.当我们点击另一个下拉菜单时,其中一个会根据用户选择的国家显示来自数据库的所有国家名称,另一个下拉菜单应该选择状态. 当我点击一个下拉菜单时,我做了一个ajax请求来显示国家名称 ...
- win7 win10 win8系统文件夹重命名要刷新下文件名才会改变,桌面也不会自动刷新...
网友问:WIN7系统文件夹重命名后要点击刷新后才能显示新的文件名.还有就是我删除一个文件也要点击刷新再删掉文件图标.粘贴文件也是这样的,不点击刷新也是不出现粘贴的文件. 网友问:最近又遇到了Win7的 ...
- 下拉推广系统立择火星推荐_下拉词优化不仅仅优化百度,其实还可以优化抖音、京东和阿里巴巴...
金大侠说有人的地方就有江湖,我说有搜索的地方就会有下拉优化.这个搜索不单指大众搜索引擎,包括一切有搜索应用的地方.例如,我们在天猫平台搜索自己需要的商品,我们在微信搜索公众号文章,我们在抖音搜索短视频 ...
最新文章
- qlist length 函数讲解_读《JavaScript 轻量级函数式编程》
- sql server排序慢_用Nginx实现接口慢查询并可示化展示TOP 20
- Webservice 的设计和模式
- NYOJ 73 比大小
- 图片加到json中,提交到服务器端处理异常问题。
- All-In-One Code Framework [一站式示例代码库] 【转】
- input层级高 小程序_获客、引流成本越来越高?开发小程序:低成本获客、引流...
- c55x 汇编语言指令,[转载]关于TMS320C55x的汇编语言中的.sym伪指令
- java springmvc 后台读取文件,springMVC
- fatal: unable to access ‘https://XXXXX‘: : OpenSSL SSL_read: Connection was reset, errno 10054……
- [BZOJ]3727: PA2014 Final Zadanie
- Hive SQL开窗函数详解
- Balanced Array
- PCB设计之线宽、线距规则设置
- oracle 11g从DBF文件恢复数据
- 编写选择结构程序,输入个人月收入总额,计算出他本月应缴税款和税后收入
- 美国总统拜登下令降半旗悼念枪击案遇难者
- Node.js 入门
- 聚观早报 | 科技巨头组建元宇宙组织,苹果缺席;推特董事会批准马斯克收购交易​;TikTok调整欧盟用户相关权利
- enum weekday
热门文章
- 好文推荐 | 缓存与数据库一致性问题深度剖析 (修订)
- CTO要我把这份MySQL规范贴在工位上!
- LiveVideoStack 主编观察 01
- Netflix在安卓移动启用AV1格式 较VP9编码效率提升20%
- 音视频技术开发周刊(第122期)
- BBR及其在实时音视频领域的应用
- 深度学习为图片压缩算法赋能:节省55%带宽
- 「递归」第10集 | 一款“摔”出来的产品
- 腾讯 AI Lab 正式开源PocketFlow自动化深度学习模型压缩与加速框架
- FFmpeg源代码:avcodec_send_packet