SwiftUI onChange(of: ...) 更新 StateObject

Posted

技术标签:

【中文标题】SwiftUI onChange(of: ...) 更新 StateObject【英文标题】:SwiftUI onChange(of: ...) to update StateObject 【发布时间】:2021-09-06 00:28:41 【问题描述】:

我构建了一个自定义AsyncImage 视图,其中ImageLoader 类获取图像并在检索图像时更新视图。由于 @StateObject 仅在第一次构建视图时被初始化,如果从父级提供的 url 发生更改,我需要一种方法让 ImageLoader 获取新图像,我在底部使用 onChange(of: url)... 执行此操作。如果传递的父参数发生更改,是否可以这样做或者是否有更好的机制来更新 @StateObjects 参数?

struct URLImage: View, Equatable   
  @StateObject private var loader = ImageLoader()
  private var url: URL?
  private var placeholder: UIImage?
  
  
  init(url: URL?, placeholder: UIImage? = nil) 
    self.url = url
    self.placeholder = placeholder
  
  
  
  var body: some View 
    Image(uiImage: loader.image ?? UIImage())
      .resizable()
      .opacity(loader.isLoading ? 0 : 1)
      .zIndex(5)
      .overlay(
        VStack 
          if loader.isLoading 
            ProgressView()
              .progressViewStyle(CircularProgressViewStyle())
          
        
      )
      .background(
        VStack 
          if placeholder != nil && loader.isLoading == false && loader.image == nil 
            GeometryReader  geo in
              VStack(alignment: .center) 
                Image(uiImage: placeholder!)
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .frame(width: geo.size.width / 3)
                  .foregroundColor(Theme.Colors.primary400)
              
              .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            
          
        
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Theme.Colors.primary200)
      )
      .onAppear 
        loader.url = url
      
      .onChange(of: url)  newValue in
        if loader.url != newValue 
          loader.url = newValue
        
      
  
  
  static func == (lhs: Self, rhs: Self) -> Bool 
    lhs.url == rhs.url
  

【问题讨论】:

即使使用绑定,我也必须定义一个自定义 Binding,它有一个 set,它会在更改时更新 @StateObject。为什么这比我拥有它的方式有任何好处? 【参考方案1】:

您应该将传入 URL 设为 Binding。然后,它将更改,并随后更新视图主体。当重新计算视图主体时,onChange(of:perform:) 将被触发,因此您可以对新的urlnewValue 进行任何操作。

struct URLImage: View, Equatable   
  @StateObject private var loader = ImageLoader()
  @Binding private var url: URL?
  private var placeholder: UIImage?
  
  
  init(url: Binding<URL?>, placeholder: UIImage? = nil) 
    _url = url
    self.placeholder = placeholder
  
  
  
  var body: some View 
    Image(uiImage: loader.image ?? UIImage())
      .resizable()
      .opacity(loader.isLoading ? 0 : 1)
      .zIndex(5)
      .overlay(
        VStack 
          if loader.isLoading 
            ProgressView()
              .progressViewStyle(CircularProgressViewStyle())
          
        
      )
      .background(
        VStack 
          if placeholder != nil && loader.isLoading == false && loader.image == nil 
            GeometryReader  geo in
              VStack(alignment: .center) 
                Image(uiImage: placeholder!)
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .frame(width: geo.size.width / 3)
                  .foregroundColor(Theme.Colors.primary400)
              
              .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            
          
        
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Theme.Colors.primary200)
      )
      .onAppear 
        loader.url = url
      
      .onChange(of: url)  newValue in
        if loader.url != newValue 
          loader.url = newValue
        
      
  
  
  static func == (lhs: Self, rhs: Self) -> Bool 
    lhs.url == rhs.url
  

另一种方法是将url 传递到ImageLoader,如果您创建一个接受Binding&lt;URL?&gt; 的init。我认为这是更好的方法,因为它更简单,重复的代码更少。这意味着您还可以删除所有 onAppearonChange 内容:

struct URLImage: View, Equatable   
  @StateObject private var loader: ImageLoader
  private var placeholder: UIImage?
  
  
  init(url: Binding<URL?>, placeholder: UIImage? = nil) 
    _loader = StateObject(wrappedValue: ImageLoader(url: url))
    self.placeholder = placeholder
  
  
  
  var body: some View 
    Image(uiImage: loader.image ?? UIImage())
      .resizable()
      .opacity(loader.isLoading ? 0 : 1)
      .zIndex(5)
      .overlay(
        VStack 
          if loader.isLoading 
            ProgressView()
              .progressViewStyle(CircularProgressViewStyle())
          
        
      )
      .background(
        VStack 
          if placeholder != nil && loader.isLoading == false && loader.image == nil 
            GeometryReader  geo in
              VStack(alignment: .center) 
                Image(uiImage: placeholder!)
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .frame(width: geo.size.width / 3)
                  .foregroundColor(Theme.Colors.primary400)
              
              .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            
          
        
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Theme.Colors.primary200)
      )
  
  
  static func == (lhs: Self, rhs: Self) -> Bool 
    lhs.loader.url == rhs.loader.url
  

旁注:我不建议您在此处使用此 Equatable 实现。当您添加其他变量和对象时,这将在将来导致许多问题 - 您只是不需要它。 SwiftUI 通过检查 Equatable 的所有内容来隐式计算视图是否是可等价的。我建议您只删除此视图的一致性。

【讨论】:

我在使用绑定时遇到的问题是创建@State 对象并不总是很方便。例如,如果我有一个 Person 模型并且该人模型具有图像属性,例如let image: URL,如果我要从这个模型创建一个视图,我需要在视图中创建一个@State 变量并将图像属性分配给@State 对象,而使用我的方法你可以直接传递属性到视图。并同意 Equatable。只是我试图解决另一个问题并搞砸了:) @Darkisa 如果你有一个ObservableObject,你可以直接使用$objectName.propertyName访问一个属性的绑定。所以你说你有一个Person 模型,所以你会做类似$person.image 的事情,它的类型是Binding&lt;URL&gt;

以上是关于SwiftUI onChange(of: ...) 更新 StateObject的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 无法添加 onChange 事件

SwiftUI 2 .onChange() 中的选取器不会更改 UINavigationBar.appearance()

SwiftUI 工作表中未触发深层链接 onChange

在 SwiftUI 视图中结合 onChange 和 onAppear 事件?

Django:jQuery触发表单提交onchange of checkbox并保留重新加载时的值

SwiftUI 中的可选状态或绑定