SwiftUI-ObservableObject性能问题

时间:2019-11-25 20:15:39

标签: ios swift macos swiftui

当SwiftUI View绑定到ObservableObject时,如果在观察到的对象中发生任何更改,则视图将自动重新加载-不管更改是否直接影响视图

对于非平凡的应用,这似乎会导致严重的性能问题。看这个简单的例子:

// Our observed model
class User: ObservableObject {
    @Published var name = "Bob"
    @Published var imageResource = "IMAGE_RESOURCE"
}


// Name view
struct NameView: View {
    @EnvironmentObject var user: User

    var body: some View {
        print("Redrawing name")
        return TextField("Name", text: $user.name)
    }
}

// Image view - elsewhere in the app
struct ImageView: View {
    @EnvironmentObject var user: User

    var body: some View {
        print("Redrawing image")
        return Image(user.imageResource)
    }
}

在这里,我们有两个不相关的视图,分别位于应用程序的不同部分。他们俩都观察到环境所提供的共享User的变化。 NameView允许您通过TextField编辑User的名称。 ImageView显示用户的个人资料图像。

Screenshot

问题:每次在NameView内进行击键操作时,观察此User的所有所有视图都必须重新加载其整个内容。其中包括ImageView,这可能涉及一些昂贵的操作-例如下载/调整大图像的大小。

在上面的示例中可以很容易地证明这一点,因为每次在TextField中输入新字符时都会记录"Redrawing name""Redrawing image"

问题:我们如何改善对Observable / Environment对象的使用,以避免不必要地重绘视图?有没有更好的方法来构建我们的数据模型?

编辑:

为了更好地说明这可能是个问题的原因,假设ImageView所做的不仅仅是显示静态图像。例如,它可能:

  • 通过子视图的initonAppear方法触发的图像异步加载
  • 包含正在运行的动画
  • 支持拖放界面,需要本地状态管理

还有更多示例,但这是我在当前项目中遇到的示例。在上述每种情况下,重新计算视图的body都将导致被丢弃的状态,并取消/重新启动一些昂贵的操作。

并不是说这是SwiftUI中的“错误”-但是,如果有更好的方法来构建我们的应用程序,我还没有看到Apple或任何教程提到的问题。大多数示例似乎都赞成在不解决副作用的情况下自由使用EnvironmentObject。

2 个答案:

答案 0 :(得分:2)

为什么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

答案 1 :(得分:0)

一种快速的解决方案是使用debounce(for:scheduler:options:)

当您要等待上游发布者传递事件的暂停时,请使用此运算符。例如,在发布者上从文本字段调用反跳操作以仅在用户暂停或停止键入时才接收元素。当他们再次开始键入内容时,防跳动会将事件传递一直保持到下一个暂停。

我已经快速完成了这个小例子,展示了使用它的方法。

// 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()
  }
}

希望对您有帮助。