不得不说,在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(下拉刷新)相关推荐

  1. android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件

    下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的PullToRefresh,或是修改自该框架的其他库.而到现在已经有了更多的选择,github上还是有很多体验 ...

  2. Android 怎么实现支持所有View的通用的下拉刷新控件

    转载请标明出处: http://blog.csdn.net/u010386612/article/details/51372696 本文出自:[AItsuki的博客] 下拉刷新对于一个app来说是必不 ...

  3. 中改变了值但是数据没有刷新_SwiftUI数据流

    SwiftUI 中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作. 数据处理的基本原则 Data Access as a Dependency:在 ...

  4. 【Android 性能优化】布局渲染优化 ( GPU 过度绘制优化总结 | CPU 渲染过程 | Layout Inspector 工具 | View Tree 分析 | 布局组件层级分析 )

    文章目录 一. GPU 过度绘制优化总结 二. CPU 渲染过程 三. CPU 渲染性能调试工具 Layout Inspector 四. Layout Inspector 组件树 DecorView ...

  5. element做树形下拉_一个基于 elementUi的vue树形下拉框组件

    # wl-vue-select,wl-tree-selectcss # 简介vue 用于vue框架的树形下拉框及带全选的普通下拉框.   node Tree drop-down box for vue ...

  6. html下拉框代码默认选中状态,@Html.DropDownListFor 下拉框绑定(选择默认值)

    首先先构建绑定下拉框的数据源 private void GetSalesList() { var userList = _rmaExpressAppService.GetUserList(); Tem ...

  7. 两个下拉框相关联ajax,触发第二个下拉框以显示基于从第一个下拉框中选择的值的值ajax...

    我有两个引导程序下拉框.当我们点击另一个下拉菜单时,其中一个会根据用户选择的国家显示来自数据库的所有国家名称,另一个下拉菜单应该选择状态. 当我点击一个下拉菜单时,我做了一个ajax请求来显示国家名称 ...

  8. win7 win10 win8系统文件夹重命名要刷新下文件名才会改变,桌面也不会自动刷新...

    网友问:WIN7系统文件夹重命名后要点击刷新后才能显示新的文件名.还有就是我删除一个文件也要点击刷新再删掉文件图标.粘贴文件也是这样的,不点击刷新也是不出现粘贴的文件. 网友问:最近又遇到了Win7的 ...

  9. 下拉推广系统立择火星推荐_下拉词优化不仅仅优化百度,其实还可以优化抖音、京东和阿里巴巴...

    金大侠说有人的地方就有江湖,我说有搜索的地方就会有下拉优化.这个搜索不单指大众搜索引擎,包括一切有搜索应用的地方.例如,我们在天猫平台搜索自己需要的商品,我们在微信搜索公众号文章,我们在抖音搜索短视频 ...

最新文章

  1. qlist length 函数讲解_读《JavaScript 轻量级函数式编程》
  2. sql server排序慢_用Nginx实现接口慢查询并可示化展示TOP 20
  3. Webservice 的设计和模式
  4. NYOJ 73 比大小
  5. 图片加到json中,提交到服务器端处理异常问题。
  6. All-In-One Code Framework [一站式示例代码库] 【转】
  7. input层级高 小程序_获客、引流成本越来越高?开发小程序:低成本获客、引流...
  8. c55x 汇编语言指令,[转载]关于TMS320C55x的汇编语言中的.sym伪指令
  9. java springmvc 后台读取文件,springMVC
  10. fatal: unable to access ‘https://XXXXX‘: : OpenSSL SSL_read: Connection was reset, errno 10054……
  11. [BZOJ]3727: PA2014 Final Zadanie
  12. Hive SQL开窗函数详解
  13. Balanced Array
  14. PCB设计之线宽、线距规则设置
  15. oracle 11g从DBF文件恢复数据
  16. 编写选择结构程序,输入个人月收入总额,计算出他本月应缴税款和税后收入
  17. 美国总统拜登下令降半旗悼念枪击案遇难者
  18. Node.js 入门
  19. 聚观早报 | 科技巨头组建元宇宙组织,苹果缺席;推特董事会批准马斯克收购交易​;TikTok调整欧盟用户相关权利
  20. enum weekday

热门文章

  1. 好文推荐 | 缓存与数据库一致性问题深度剖析 (修订)
  2. CTO要我把这份MySQL规范贴在工位上!
  3. LiveVideoStack 主编观察 01
  4. Netflix在安卓移动启用AV1格式 较VP9编码效率提升20%
  5. 音视频技术开发周刊(第122期)
  6. BBR及其在实时音视频领域的应用
  7. 深度学习为图片压缩算法赋能:节省55%带宽
  8. 「递归」第10集 | 一款“摔”出来的产品
  9. 腾讯 AI Lab 正式开源PocketFlow自动化深度学习模型压缩与加速框架
  10. FFmpeg源代码:avcodec_send_packet