前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >了解 SwiftUI 的 onChange

了解 SwiftUI 的 onChange

作者头像
东坡肘子
发布于 2022-07-28 04:56:53
发布于 2022-07-28 04:56:53
3K00
代码可运行
举报
运行总次数:0
代码可运行

了解 SwiftUI 的 onChange

请访问我的博客 www.fatbobman.com[1] 获得更好的阅读体验

iOS 14 开始,SwiftUI 为视图提供了 onChange 修饰器,通过使用 onChange,我们可以在视图中对特定的值进行观察,并在其更改时触发操作。本文将对 onChange 的特点、用法、注意事项以及替代方案做以介绍。

如何使用 onChange

onChange 的定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func onChange<V>(of value: V, perform action: @escaping (V) -> Void) -> some View where V : Equatable

onChange 在发现特定值发生变化时,将调用闭包中的操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct OnChangeDemo:View{    @State var t = 0    var body: some View{        Button("change"){            t += 1        }        .onChange(of: t, perform: { value in            print(value)        })    }}

点击 Button,t 将加一,onChange 将对 t 值进行比较,如果值发生改变,将调用闭包打印新值。

在闭包中可以进行副作用操作,或者修改视图中的其他可变内容。

传递到闭包中的值(例如上面的 value)是不可变的,如果需要修改,请直接更改视图中的可变值(t)。

onChange 的闭包是运行在主线程上的,应避免在闭包中执行运行时间长的任务。

如何获取被观察值的 OldValue

onChange 允许我们通过闭包捕获的方式获取被观察值的旧值(oldValue)。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct OldValue: View {    @State var t = 1    var body: some View {        Button("change") {            t = Int.random(in: 1...5)        }        .onChange(of: t) { [t] newValue in            let oldValue = t            if newValue % oldValue == 2 {                print("余值为 2")            } else {                print("不满足条件")            }        }    }}

由于闭包中捕获了 t,因此需使用self.t方能调用视图中的 t。

对于结构类型,捕获时需使用结构实例,而不能直接捕获结构中的属性,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct OldValue1:View{    @State var data = MyData()    var body: some View{        Button("change"){            data.t = Int.random(in: 1...5)        }        .onChange(of: data.t){ [data] newValue in            let oldValue = data.t            if newValue % oldValue == 2 {                print("余值为 2")            } else {                print("不满足条件")            }        }    }}struct MyData{    var t = 0}

换成 [data.t] 将提示如下错误:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Fields may only be captured by assigning to a specific name

对于引用类型,捕获时需添加weak

onChange 可以观察哪些值

任何符合 Equatable 协议的类型都可被 onChange 所观察。对于可选值,只要 Wrapped 符合 Equatable 即可。

通常我们会使用 onChange 来观察@State,@StateObject 或@ObservableObject 包装数据的变化。但在某些特定的场景下,我们也可以用 onChange 来观察并非为视图 Source of truth 的数据。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct NonStateDemo: View {    let store = Store.share    @State var id = UUID()    var body: some View {        VStack {            Button("refresh") {                id = UUID()            }            .id(id)            .onChange(of: store.date) { value in                print(value)            }        }    }}class Store {    var date = Date()    var cancellables = Set<AnyCancellable>()    init(){        Timer.publish(every: 3,  on: .current, in: .common)            .autoconnect()            .assign(to: \.date, on: self)            .store(in: &cancellables)    }    static let share = Store()}

Store 并非可以引发视图刷新的元素,通过点击 Button 改变 id 来刷新视图。

本例看起来有些无厘头,但它为揭示 onChange 的特点提供了很好的启示。

onChange 的特点

在 onChange 推出之际,大多数人将其视为@State 的 didSet 实现。但事实上两者间有很大的差异。

didSet 在值发生改变即调用闭包中的操作,无论新值与旧值是否不同。例如

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class MyStore{    var i = 0{        didSet {            print("oldValue:\(oldValue),newValue:\(i)")        }    }}let store = MyStore()store.i = 0//oldValue:0,newValue:0

onChange 有其自身的运行逻辑。

在上节的例子中,尽管 Store 中的 date 每三秒会发生一次改变,但并不会引起视图的重新绘制。通过点击按钮强制重绘视图,onChange 才会被触发。

如果在三秒之内多次点击按钮,控制台并不会打印更多的时间信息。

被观察值的变化并不会触发 onChange,只有在每次视图重绘时 onChnage 才会触发。onChange 触发后会比较被观察值的变化,只有新旧值不一致时,才会调用 onChange 闭包中的操作。

关于 onChange 的 FAQ

视图中可以放置多少个 onChange

任意多个。不过由于 onChange 的闭包运行在主线程中,因此最好限制 onChange 的使用量,避免影响视图的渲染效率。

多个 onChange 的执行顺行

严格按照视图树的渲染顺序,下面的代码中,onChange 的执行顺序为从内到外:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct ContentView: View {    @State var text = ""    var body: some View {        VStack {            Button("Change") {                text += "1"            }            .onChange(of: text) { _ in                print("TextField1")            }            .onChange(of: text) { _ in                print("TextField2")            }        }        .onChange(of: text, perform: { _ in            print("VStack")        })    }}// Output:// TextField1// TextField2// VStack

多个 onChange 观察同一个值

在一个渲染周期内,观察同一个值的 onChange,无论顺序与否,获得的被观察值的新旧值均相同。不会因为更早顺序前的 onChange 对值的内容进行更改而变化。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct InOneLoop: View {    @State var t = 0    var body: some View {        VStack {            Button("change") {                t += 1 // t = 1            }            // onChange1            .onChange(of: t) { [t] newValue in                print("onChange1: old:\(t) new:\(newValue)")                    self.t += 1            }            // onChange2            .onChange(of: t) { [t] newValue in                print("onChange2 old:\(t) new:\(newValue)")            }        }    }}

输出为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
render looponChange1: old:3 new:4onChange2 old:3 new:4render looponChange1: old:4 new:5onChange2 old:4 new:5render looponChange(of: Int) action tried to update multiple times per frame.

在每个 loop 循环中,onChange2 的内容并没有因为 onChange1 对 t 进行了修改而变化。

为什么 onChange 会报错

在上面的代码中,在输出的最后,我们获得了onChange(of: Int) action tried to update multiple times per frame.的错误提示。

这是因为,由于我们在 onChange 中对被观察值进行了修改,而修改将再次刷新视图,从而导致了无限循环。SwiftUI 为了避免 app 锁死而采取的保护机制——强制中断了 onChange 的继续执行。

至于允许的循环次数没有明确的约定,上面例子中由 Button 激发的变化通常会限制在 2 次,而由 onAppear 激发的变化则可能在 6-7 次。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct LoopTest: View {    @State var t = 0    var body: some View {        let _ = print("frame")        VStack {            Text("\(t)")                .onChange(of: t) { _ in                    t += 1                    print(t)                }                .onAppear(perform: { t += 1 })        }    }}

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 frame 2 frame 3 frame 4 frame 5 frame 6 frame 7 frame onChange(of: Int) action tried to update multiple times per frame.

因此我们需要尽量避免在 onChange 中对被观察值进行修改,如确有必要,请使用条件判断语句来限制更改次数,保证程序按预期执行。

onChange 的替代方案

本节中我们将介绍几个同 onChange 类似的实现,它们同 onChange 的行为并不完全一样,有各自的特点和合适的场景。

task(id:)

SwiftUI 3.0 中新增了 task 修饰器,task 将在视图出现时以异步的方式运行闭包中的内容,同时在 id 值发生变化时,重启任务。

在 task 闭包中的任务单元足够简单时,其表现同 onChange 类似,相当于 onAppear + onChange 的组合。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct AsyncTest: View {    @State var t: CGFloat = 0    var body: some View {        let _ = print("frame")        VStack {            Text("\(t)")                .task(id: t) {                    t += 1                    print(t)                }        }    }}

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
frame1.0frame2.0...

但有一点需要特别注意,由于 task 的闭包是异步运行的,理论上其并不会对视图的渲染造成影响,因此 SwiftUI 将不会限制它的执行次数。本例中,task 的闭包中的任务将不断运行,Text 中的内容也将不断变化(如果将 task 换成 onChange 则会被 SwiftUI 自动中断)。

Combine 版本的 onChange

在 onChange 没有推出之前,多数人会利用 Combine 框架来实现类似 onChange 的效果。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import Combinestruct CombineVersion: View {    @State var t = 0    var body: some View {        VStack {            Button("change") {                t += 1            }        }        .onAppearAndOnChange(of: t, perform: { value in            print(value)        })    }}public extension View {    func onAppearAndOnChange<V>(of value: V, perform action: @escaping (_ newValue: V) -> Void) -> some View where V: Equatable {        onReceive(Just(value), perform: action)    }}

它的行为类似 onAppear + onChange 的组合。最大的不同是,此种方案并不会比较被观察值是否发生改变(新旧值不一样)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct CombineVersion: View {    @State var t = 0    @State var n = 0    var body: some View {        VStack {            Text("\(n)")            Button("change n"){                n += 1                t += 0            }        }        .onAppearAndOnChange(of: t, perform: { value in            print("combine \(t)")        })        .onChange(of: t){ value in            print("onChange \(t)")        }    }}

onChange 的闭包因为 t 的内容没有发生变化将不会被调用,而 onAppearAndOnChange 的闭包将在每次 t 赋值时均被调用。

有的时候,这种行为恰是我们所需的。

Binding 版本的 onChange

此种方式只能针对 Binding 类型的数据,通过在 Binding 的 Set 中添加一层逻辑,实现对内容变化的响应。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension Binding {    func didSet(_ didSet: @escaping (Value) -> Void) -> Binding<Value> {        Binding(get: { wrappedValue },                set: { newValue in                    self.wrappedValue = newValue                    didSet(newValue)                })    }}struct BindingVersion2: View {    @State var text = ""    var body: some View {        Form {            TextField("text:", text: $text.didSet { print($0) })        }    }}

可能你会觉得多此一举,完全可以使用 onChange 来实现,但采用 Binding 的方式让我们有了在数据修改前进行判断操作的可能,使用得当将极大地减少视图的刷新。

例如,我们还可以对新数据进行提前判断以决定是否更改原值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension Binding {    func conditionSet(_ condition: @escaping (Value) -> Bool) -> Binding<Value> {        Binding(get: { wrappedValue },                set: { newValue in                    if condition(newValue) {                        self.wrappedValue = newValue                    }                })    }}

请注意,此种方式并不能同支持 Binding 的系统控件很好的配合使用,因为系统控件并不会因为我们限制了数值的修改而产生对应的效果(系统控件中还保留了一套自己的数据,除非强制刷新视图,否则并不会保证同外部的数据完全同步)。例如下面的代码,表现同你预期的行为会有出入。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct BindingVersion3: View {    @State var text = ""    var body: some View {        Form {            Text(text)            TextField("text:", text: $text.conditionSet { text in                return text.count < 5            })        }    }}

总结

onChange 为我们在视图中进行逻辑处理提供了便利,了解它的特点与限制,选择合适的场景使用它。在必要的情况下,将逻辑处理与视图分离,以保证视图的渲染效率。

希望本文对你有所帮助。

引用链接

[1] www.fatbobman.com: http://www.fatbobman.com

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
实例二是完整代码吗?
实例二是完整代码吗?
回复回复点赞举报
推荐阅读
使用 Python 进行 Windows GUI 自动化
在今天的文章中,我们将探讨如何使用 Python 进行 Windows GUI 自动化。GUI 自动化可以帮助我们自动执行许多与操作系统交互的任务,比如移动鼠标、点击按钮、输入文本、移动窗口等。Python 提供了两个强大的库:pyautogui 和 pywinauto,使得 GUI 自动化变得简单。接下来我们详细介绍。
somenzz
2023/08/22
1.9K0
使用 Python 进行 Windows GUI 自动化
让所有GUI都自动化-PyAutoGUI(GUI自动化工具)
在使用 Selenium 进行自动化测试时,鼠标事件可以用 ActionChains 类,键盘事件可以用 Keys 类。本篇将介绍一款自动化工具-PyAutoGUI,除了可以满足鼠标、键盘事件操作外,还可以进行消息弹窗、截屏等操作。
wangmcn
2022/10/30
6.3K0
让所有GUI都自动化-PyAutoGUI(GUI自动化工具)
python有意思的库PyAutoGUI详解
在现代数字化世界中,自动化成为提高效率、减轻工作负担的关键手段之一。而在自动化的领域中,Python语言一直以其简洁、灵活的特性受到广泛青睐。在Python的自动化工具中,PyAutoGUI是一款备受推崇的库,它为用户提供了在桌面环境中进行自动化操作的便捷方式。
Michel_Rolle
2024/01/20
2.7K0
Python自动化办公-自动录入表单数据
图片展示的是如何用 Python 将销售交易数据填充到在线发票生成器中,可以自动生成并下载所有客户的发票。
somenzz
2021/08/19
2.7K0
python自动化办公--pyautogui控制鼠标和键盘操作
在公司某些工作场景下,需要大量重复的工作,重复的工作完全可以通过python软件的自动化实现,省时省力。本文分享python自动化办公的利器之一--pyautogui,通过pyautogui可以轻松控制鼠标和键盘操作。
用户9925864
2022/07/27
2.2K0
python自动化办公--pyautogui控制鼠标和键盘操作
Python: 基于Pyautogui模块的自动填表程序
文章背景:最近在学习pyautogui模块,有一个项目是自动填表程序,它能够实现将字典中的数据重复输入到表单界面中。我的环境:win10 + Chrome浏览器。
Exploring
2022/08/10
9100
AI炒股:批量下载东方财富choice中的投资数据
你是一个Python编程专家,写一个关于键盘鼠标自动化操作的Python脚本,具体步骤如下:
AIGC部落
2024/06/24
1750
AI炒股:批量下载东方财富choice中的投资数据
python---很多行代码做一个自动打开软件的程序
这里我要推荐一个可以控制鼠标和键盘的库,pyautogui,据它的官方文档,PyAutoGUI的目的是为人类的 GUI自动化提供跨平台的Python模块。
sjw1998
2019/09/28
3.9K0
自动化之图形界面库pyautogui
开篇言:py的库真的是多,封装相关功能的库真的是各具神通,里面的轮子很多。前几天一直回去看基础的语法函数这些,以及c语言数据结构这些,扎实自己的基础。 学习是一件很麻烦但是很有意思的事情(我指的是码代码),今天为大家介绍这个自动化库,提高趣味性,当然python的自动化操作还是有好多,后面会为大家继续推出。
兰舟千帆
2022/07/17
2.2K0
自动化之图形界面库pyautogui
女朋友让我深夜十二点催她睡觉,我有Python我就不干
不过,可是我实在太困了,熬不下去…… 是吧?女朋友哪有睡觉重要? 但,女朋友的命令,我是不敢违抗的…… 但是睡觉也不能缺!
全栈程序员站长
2022/07/31
7230
女朋友让我深夜十二点催她睡觉,我有Python我就不干
用python如何控制你的鼠标和键盘
要用 Python 控制鼠标和键盘,常用的库有 pyautogui 和 pynput。下面我将通过 pyautogui 库来演示如何控制鼠标和键盘,每一步操作都将提供详细的代码解析。
golang开发者
2024/10/30
5080
如何每天自动发送微信消息给女朋友说晚安
经常晚上打王者到凌晨,老是忘记给女朋友说晚安。这次我们来做一个自动发送微信的程序,在晚上22点左右给女朋友发去消息,也好表现我执着的形象,以及早睡早起的良好生活习惯。
用户10002156
2023/08/07
6390
如何每天自动发送微信消息给女朋友说晚安
python selenium 关于将网页打包为静态网页(mhtml)下载。
需求:单纯的将page.source写入文件的方式,会导致一些图片无法显示,对于google浏览器,直接将页面打包下载成一个mhtml格式的文件,则可以进行离线下载。对应python selenium 微信公众号历史文章随手一点就返回首页?郁闷之下只好将他们都下载下来。:https://www.cnblogs.com/cycxtz/p/13416245.html 后续
forxtz
2020/10/10
3.3K0
python selenium 关于将网页打包为静态网页(mhtml)下载。
python控制鼠标键盘,解放你的双手~
上次研究了python程序如何控制鼠标(python自动播放网课),今天我们接着来聊聊,python如何控制键盘,结合上次的内容你就可以解放你的双手了。
生信交流平台
2020/08/06
1.1K0
python控制鼠标键盘,解放你的双手~
Python PyAutoGUI是什么?
Python的pyautogui库是一种用于自动化任务的强大工具,它可以模拟鼠标和键盘操作,执行各种GUI任务。无论是进行屏幕截图、自动填写表单、自动化测试还是进行GUI操作,pyautogui都可以派上用场。
闻说社
2024/06/19
2460
Python PyAutoGUI是什么?
python pyautogui 键盘鼠标自动化
1、安装模块: 在Windows 上,不需要安装其他模块。  在OS X 上,运行sudo pip3 install pyobjc-framework-Quartz,sudo pip3 install pyobjc-core,然后sudo pip3 install pyobjc。  在Linux 上,运行sudo pip3 install python3-xlib,sudo apt-get install scrot,sudo apt-get install python3-tk,以及sudo apt-get install python3-dev(Scrot 是 PyAutoGUI 使用的屏幕快照程序)。 在这些依赖安装后,运行pip install pyautogu(i 或在OS X和Linux上运行pip3), 安装pyautogui。 2、pyautogui执行时,如果鼠标移到屏幕左上角,将导致pyautogui产生pyautogui.FailSafeException异常。如果设置FAILSAEF=False将禁止这项功能。
用户5760343
2022/05/13
1.4K0
使用 PyAutoGUI 库在 Python 中自动化 GUI 交互
PyAutoGUI是一个很棒的模块,用于自动化Python应用程序中的图形用户界面交互。它使开发人员能够模仿用户输入并自动执行重复操作,使其成为测试、数据输入和其他需要与 GUI 交互的工作的理想选择。PyAutoGUI是一个跨平台的库,支持所有主要的操作系统,如Windows,Linux和macOS。
很酷的站长
2023/08/11
8360
使用 PyAutoGUI 库在 Python 中自动化 GUI 交互
自动输入表单-以录入学生平时成绩到教务系统为例
STEP2:安装pyautogui,命令行输入pip install pyautogui
周星星9527
2019/07/30
7750
Python实战03:实现一键自动登录
我平时在办公室的工作之一是在公司生产管理系统上查看和审核文档。要进入公司生产管理系统,我通常的操作是:
fanjy
2020/01/14
2.2K0
Python实战03:实现一键自动登录
Pyautogui实现自动化办公-RPA小case
1. 安装python3.4以上版本,并配置环境变量(目前有装3.9遇到坑的,我个人用的3.7.6)
用户9925864
2022/07/27
1.1K0
Pyautogui实现自动化办公-RPA小case
推荐阅读
相关推荐
使用 Python 进行 Windows GUI 自动化
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 了解 SwiftUI 的 onChange
    • 如何使用 onChange
    • 如何获取被观察值的 OldValue
    • onChange 可以观察哪些值
    • onChange 的特点
    • 关于 onChange 的 FAQ
      • 视图中可以放置多少个 onChange
      • 多个 onChange 的执行顺行
      • 多个 onChange 观察同一个值
      • 为什么 onChange 会报错
    • onChange 的替代方案
      • task(id:)
      • Combine 版本的 onChange
      • Binding 版本的 onChange
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档