SwiftUI @Published 属性不更新视图

时间:2021-04-02 15:32:36

标签: ios swiftui reactive-programming

在下面的代码中,当点击“收藏夹”按钮时,详细信息视图 UI 不会更新。我们知道绑定连接到 ObservableObject,因为 didSet 被调用并打印 foos 数组的更新状态。奇怪的是,我发现在 VStackFooList 中添加 NavigationView 可以解决此实例中的错误,但对于我遇到此问题的更复杂的 UI 却没有。

我是否遗漏了有关如何连接的信息?另一种解决方法是在 State 中切换 FavoriteButton 布尔值,但这似乎与绑定背道而驰。

import SwiftUI

struct Foo: Identifiable {
    var id = UUID()
    var name: String
    var isFavorite: Bool
}

class FooData: ObservableObject {
    @Published var foos = [Foo(name: "Test", isFavorite: true)] {
        didSet {
            print(foos)
        }
    }
}

struct FooList: View {
    @ObservedObject var fooData = FooData()
    
    var body: some View {
        NavigationView {
// Adding a VStack makes the FavoriteButton update correctly, but this doesn't work for more complicated UIs
//            VStack {
                List {
                    ForEach(fooData.foos, id: \.id) { foo in
                        let index = fooData.foos.firstIndex(where: { $0.id == foo.id })!
                        NavigationLink(foo.name, destination: FooDetailView(foo: $fooData.foos[index]))
                    }
                }
//            }
        }
    }
}

struct FooDetailView: View {
    @Binding var foo: Foo
    
    var body: some View {
        FavoriteButton(isFavorite: $foo.isFavorite)
    }
}

struct FavoriteButton: View {
    @Binding var isFavorite: Bool
    
    var body: some View {
        Button(action: {
            isFavorite.toggle()
        }, label: {
            if isFavorite {
                Image(systemName: "heart.fill")
            } else {
                Image(systemName: "heart")
            }
        })
    }
}

更新:

FooData 作为 EnvironmentObject 传递给 List 并在 EnvironmentObject 中声明 FavoriteButton 可以解决该问题。这必须足以告诉 SwiftUI 此 View 关心 FooData 的任何更新,即使没有代码直接引用 EnvironmentObject。这感觉有点像魔术,对我来说绝对不是最直观的行为。希望这可以帮助其他人。

完整的工作代码:

import SwiftUI

struct Foo: Identifiable {
    var id = UUID()
    var name: String
    var isFavorite: Bool
}

class FooData: ObservableObject {
    @Published var foos = [Foo(name: "Test", isFavorite: true)] {
        didSet {
            print(foos)
        }
    }
}

struct FooList: View {
    @ObservedObject var fooData = FooData()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(fooData.foos, id: \.id) { foo in
                    let index = fooData.foos.firstIndex(where: { $0.id == foo.id })!
                    NavigationLink(foo.name, destination: FooDetailView(foo: $fooData.foos[index]))
                }
            }
        }
        .environmentObject(fooData)
    }
}

struct FooDetailView: View {
    @Binding var foo: Foo
    
    var body: some View {
        FavoriteButton(isFavorite: $foo.isFavorite)
    }
}

struct FavoriteButton: View {
    @EnvironmentObject var fooData: FooData
    @Binding var isFavorite: Bool
    
    var body: some View {
        Button(action: {
            isFavorite.toggle()
        }, label: {
            if isFavorite {
                Image(systemName: "heart.fill")
            } else {
                Image(systemName: "heart")
            }
        })
    }
}

2 个答案:

答案 0 :(得分:1)

您可以使用仍然可以访问 NavigationLinkObservableObject 视图的中间“包装器”解决此问题:


struct FooList: View {
    @ObservedObject var fooData = FooData()
    
    var body: some View {
        NavigationView {
                List {
                    ForEach(fooData.foos, id: \.id) { foo in
                        NavigationLink(foo.name, destination: FooWrapper(id: foo.id, fooData: fooData))
                    }
                }
        }
    }
}

struct FooWrapper : View {
    var id : UUID
    @ObservedObject var fooData : FooData
    
    var fooBinding : Binding<Foo> {
        .init { () -> Foo in
            let index = fooData.foos.firstIndex(where: { $0.id == id })!
            return fooData.foos[index]
        } set: { (newValue) in
            let index = fooData.foos.firstIndex(where: { $0.id == id })!
            fooData.foos[index] = newValue
        }
    }
    
    var body: some View {
        FooDetailView(foo: fooBinding)
    }
}

我重用了您的代码来查找 indexfoo -- 就我个人而言,我可能会编写一个在发生崩溃时不强制展开的解决方案,但这是一个不完全相关的问题问题。

答案 1 :(得分:0)

当您有一个 viewModel 发布一个数组时,唯一可以触发视图更新的是数组计数。因此,您需要将数组中的对象设置为 class,将其扩展为 ObservableObject,并将 isFavorite 设置为 @Published var 才能起作用。

class Foo: Identifiable, ObservableObject {
  var id = UUID()
  var name: String
  @Published var isFavorite: Bool

  init(name: String, isFavorite: Bool) {
    self.name = name
    self.isFavorite = isFavorite
  }
}

此外,您应该将 FooData 设为 @StateObject,因为此视图拥有它。

struct ContentView: View {
  @StateObject var fooData = FooData()

  var body: some View {
    NavigationView {
      List {
        ForEach(fooData.foos) { foo in
          NavigationLink(foo.name, destination: FooDetailView(foo: foo))
        }
      }
    }
  }
}

foo 作为 @ObservedObject 向下传递

struct FooDetailView: View {
  @ObservedObject var foo: Foo

  var body: some View {
    Button(action: {
        foo.isFavorite.toggle()
    }, label: {
        if foo.isFavorite {
            Image(systemName: "heart.fill")
        } else {
            Image(systemName: "heart")
        }
    })
  }
}