Ios SwiftUI-可观察对象性能问题
当SwiftUIIos SwiftUI-可观察对象性能问题,ios,swift,macos,swiftui,Ios,Swift,Macos,Swiftui,当SwiftUI视图绑定到一个可观察对象时,当观察对象内发生任何更改时,视图将自动重新加载,而不管更改是否直接影响视图 这似乎给非平凡应用程序带来了巨大的性能问题。请参见以下简单示例: //我们观察到的模型 类用户:observeObject{ @已发布的var name=“Bob” @已发布的var imageResource=“图像资源” } //名称视图 结构名称视图:视图{ @环境对象变量用户:用户 var body:一些观点{ 打印(“重画名称”) 返回文本字段(“名称”,文本:$us
视图
绑定到一个可观察对象
时,当观察对象内发生任何更改时,视图将自动重新加载,而不管更改是否直接影响视图
这似乎给非平凡应用程序带来了巨大的性能问题。请参见以下简单示例:
//我们观察到的模型
类用户:observeObject{
@已发布的var name=“Bob”
@已发布的var imageResource=“图像资源”
}
//名称视图
结构名称视图:视图{
@环境对象变量用户:用户
var body:一些观点{
打印(“重画名称”)
返回文本字段(“名称”,文本:$user.Name)
}
}
//图像视图-应用程序中的其他位置
结构图像视图:视图{
@环境对象变量用户:用户
var body:一些观点{
打印(“重画图像”)
返回图像(user.imageResource)
}
}
这里我们有两个不相关的视图,位于应用程序的不同部分。它们都观察到环境提供的共享用户的更改NameView
允许您通过文本字段编辑用户的姓名<代码>图像视图
显示用户的配置文件图像
问题:每次在NameView
内击键时,所有观察到该用户的视图都将被迫重新加载其整个正文内容。这包括ImageView
,这可能涉及一些昂贵的操作,比如下载/调整大图像的大小
在上面的示例中可以很容易地证明这一点,因为每次在文本字段中输入新字符时都会记录“重画名称”
和“重画图像”
问题:我们如何改进可观察/环境对象的使用,以避免不必要的视图重画?有没有更好的方法来构建我们的数据模型
编辑:
为了更好地说明这可能是一个问题的原因,假设ImageView
不仅仅显示静态图像。例如,它可能:
- 异步加载由子视图的
init
或onAppear
方法触发的图像
- 包含跑步动画
- 支持拖放界面,需要本地状态管理
还有很多例子,但这些都是我在当前项目中遇到的。在每种情况下,重新计算视图的主体
都会导致丢弃状态,并取消/重新启动一些昂贵的操作
这并不是说这是SwiftUI中的一个“bug”——但如果有更好的方法来设计我们的应用程序,我还没有看到苹果或任何教程提到它。大多数示例似乎倾向于在不解决副作用的情况下自由使用EnvironmentObject。一个快速解决方案是使用debounce(for:scheduler:options:)
如果要等待来自上游发布服务器的事件传递暂停,请使用此运算符。例如,从文本字段在发布服务器上调用debounce,以便仅在用户暂停或停止键入时接收元素。当他们再次开始输入时,解盎司将保持事件传递,直到下一次暂停
我已经快速地完成了这个小示例,以展示如何使用它
// UserViewModel
import Foundation
import Combine
class UserViewModel: ObservableObject {
// input
@Published var temporaryUsername = ""
// output
@Published var username = ""
private var temporaryUsernamePublisher: AnyPublisher<Bool, Never> {
$temporaryUsername
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.eraseToAnyPublisher()
}
init() {
temporaryUsernamePublisher
.receive(on: RunLoop.main)
.assign(to: \.username, on: self)
}
}
// View
import SwiftUI
struct ContentView: View {
@ObservedObject private var userViewModel = UserViewModel()
var body: some View {
TextField("Username", text: $userViewModel.temporaryUsername)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//用户视图模型
进口基金会
进口联合收割机
类UserViewModel:observeObject{
//输入
@已发布的var temporaryUsername=“”
//输出
@已发布的var username=“”
private var temporaryUsernamePublisher:AnyPublisher{
$temporaryUsername
.debounce(对于:0.5,调度程序:RunLoop.main)
.removeDuplicates()
.删除任何发布者()
}
init(){
临时用户名发布者
.receive(打开:RunLoop.main)
.assign(收件人:\.username,在:self上)
}
}
//看法
导入快捷键
结构ContentView:View{
@ObservedObject私有变量userViewModel=userViewModel()
var body:一些观点{
文本字段(“用户名”,文本:$userViewModel.temporaryUsername)
}
}
结构内容视图\u预览:PreviewProvider{
静态var预览:一些视图{
ContentView()
}
}
我希望它能有所帮助。为什么ImageView
需要整个User
对象
回答:没有
将其更改为仅获取所需内容:
struct ImageView: View {
var imageName: String
var body: some View {
print("Redrawing image")
return Image(imageName)
}
}
struct ContentView: View {
@EnvironmentObject var user: User
var body: some View {
VStack {
NameView()
ImageView(imageName: user.imageResource)
}
}
}
轻触键盘键时输出:
Redrawing name
Redrawing image
Redrawing name
Redrawing name
Redrawing name
Redrawing name
这就是ObsarvableObjects
和environmentobjects
背后的全部要点。结构的重量非常轻,因此需要对其进行重新初始化和重新评估。在幕后也有相当复杂的扩散,所以除非你滥用AnyView
s,否则大多数东西都不会被重新绘制。在您的示例中,只有TextField
被重新绘制,其他所有内容都被重新评估。所发生的是受更改影响的视图被重新评估。然后将结果与以前版本的视图进行差异和比较。只有在视图定义中发生更改时,才会将其绘制到屏幕上。生成视图并比较它们非常简单,而且可以非常快速地完成。@LuLuGaGa我编辑了我的问题,以提供一些更好的示例。我知道差异化通常很快,有些情况似乎无法正确处理。好吧,这很有趣-如果我们使用您的示例并向ContentView添加日志,我们会重复“重画内容”和“重画名称”。如果重新计算ContentView的正文
,这意味着ImageView也正在重新初始化。如何重新初始化结构,但不重新计算其主体
?在SwiftUI中,主体
本质上是一个函数,具有一个输入(self
)和一个输出(一些视图
)。它应该是一个纯函数,这意味着它的输出只依赖于它的输入(不依赖于任何全局变量),并且没有副作用。基于这种纯度假设,SwiftUI知道如果self
不改变,它的主体也不能改变。在这种情况下,