Ios SwiftUI-可观察对象性能问题

Ios SwiftUI-可观察对象性能问题,ios,swift,macos,swiftui,Ios,Swift,Macos,Swiftui,当SwiftUI视图绑定到一个可观察对象时,当观察对象内发生任何更改时,视图将自动重新加载,而不管更改是否直接影响视图 这似乎给非平凡应用程序带来了巨大的性能问题。请参见以下简单示例: //我们观察到的模型 类用户:observeObject{ @已发布的var name=“Bob” @已发布的var imageResource=“图像资源” } //名称视图 结构名称视图:视图{ @环境对象变量用户:用户 var body:一些观点{ 打印(“重画名称”) 返回文本字段(“名称”,文本:$us

当SwiftUI
视图
绑定到一个
可观察对象
时,当观察对象内发生任何更改时,视图将自动重新加载,而不管更改是否直接影响视图

这似乎给非平凡应用程序带来了巨大的性能问题。请参见以下简单示例:

//我们观察到的模型
类用户: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
不改变,它的
主体也不能改变。在这种情况下,