【问题标题】:SwiftUI onChange(of: ...) to update StateObjectSwiftUI onChange(of: ...) 更新 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。为什么这比我拥有它的方式有任何好处?

标签: swift swiftui


【解决方案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;
猜你喜欢
  • 2016-10-03
  • 1970-01-01
  • 2022-07-19
  • 1970-01-01
  • 1970-01-01
  • 2022-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多