【问题标题】:Create a List with an array of custom classes in SwiftUI在 SwiftUI 中创建一个包含自定义类数组的列表
【发布时间】:2019-07-01 20:08:49
【问题描述】:

我尝试在 SwiftUI 中创建自定义类列表,但在 UI 更新方面一直遇到问题。我让我的类符合BindableObject 并且当属性更改时它正确调用didChange.send() 函数,但是更改似乎没有传递到数组,因为当我更改属性时视图没有更新我的自定义类在数组中。

这是我的意思的一个基本示例:

class Media: BindableObject, Identifiable {
    typealias PublisherType = PassthroughSubject<Void, Never>
    var didChange = PublisherType()

    var id: Int {
        didSet {
            didChange.send()
        }
    }
    var name: String {
        didSet {
            print("Name changed from \(oldValue) to \(self.name)")
            didChange.send()
        }
    }

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

struct ContentView : View {
    @State private var media = [
        Media(id: 0, name: "Name 0"),
        Media(id: 1, name: "Name 1"),
        Media(id: 2, name: "Name 2"),
        Media(id: 3, name: "Name 3"),
    ]

    var body: some View {
        VStack {
            List(media) { media in
                Text(media.name)
            }
            HStack {
                Button(action: {
                    self.media.first!.name = "Name \(Int.random(in: 100...199))"
                }) {
                    Text("Change")
                }
                Button(action: {
                    self.media.append(Media(id: 4, name: "Name 4"))
                }) {
                    Text("Add")
                }
            }
        }
    }
}

当按下“更改”按钮时,它只会更改数组中第一个媒体对象的名称,这不会导致重新渲染视图。当我按下“添加”按钮时,他将一个对象添加到数组中,因此重新渲染 UI 并显示第一个对象的更改名称(通过按下“更改”)。

现在我的问题是,如果有办法将Media 的发布者链接到Array&lt;Media&gt; 的发布者,那么当Media 发布者触发时,Array&lt;Media&gt; 发布者也会触发,从而导致视图重新渲染。

当我将Text(media.name) 移动到单独的视图中时(它将媒体作为@State 属性保存,它按预期工作,因为子视图本身请求重新渲染。

但假设我不想使用自定义视图而只想使用简单的Text 视图,有什么办法可以做到这一点吗?

【问题讨论】:

    标签: swift list swiftui combine


    【解决方案1】:

    你正在混合两种不同的东西。 @State 旨在用于视图内部的任何内容。虽然@BindableObject 是为了保存视图外部的东西(即您的数据模型)。定义@BindableObject 时,不是用@State 引用它,而是用@ObjectBinding。

    不知道是什么情况,但是如果你打算使用@State,实现应该是这样的:

    struct Media {
    
        var id: Int
        var name: String
    
        init(id: Int, name: String) {
            self.id = id
            self.name = name
        }
    }
    
    struct ContentView : View {
        @State var media = [
            Media(id: 0, name: "Name 0"),
            Media(id: 1, name: "Name 1"),
            Media(id: 2, name: "Name 2"),
            Media(id: 3, name: "Name 3"),
        ]
    
        var body: some View {
            VStack {
                List(media.identified(by: \.id)) { media in
                    Text(media.name)
                }
    
                HStack {
                    Button(action: {
                        self.media[0].name = "Name \(Int.random(in: 100...199))"
                    }) {
                        Text("Change")
                    }
                    Button(action: {
                        self.media.append(Media(id: 4, name: "Name 4"))
                    }) {
                        Text("Add")
                    }
                }
            }
        }
    }
    

    您主要将@State 用于视图的某些内部状态,但也用于原型设计。您对 Media 对象的意图是什么?


    更新

    使用@ObjectBinding 实现它:

    假设您在 SceneDelegate 中实例化您的 CustomView,您需要将库传递给它:

            if let windowScene = scene as? UIWindowScene {
    
                let window = UIWindow(windowScene: windowScene)
    
                let library = MediaLibrary(store: [Media(id: 0, name: "Name 0"),
                Media(id: 1, name: "Name 1"),
                Media(id: 2, name: "Name 2"),
                Media(id: 3, name: "Name 3")])
    
                window.rootViewController = UIHostingController(rootView: ContentView(library: library))
                self.window = window
                window.makeKeyAndVisible()
            }
    

    以及实现:

    struct Media {
        let id: Int
        var name: String
    
        init(id: Int, name: String) {
            self.id = id
            self.name = name
        }
    }
    
    class MediaLibrary: BindableObject {
        typealias PublisherType = PassthroughSubject<Void, Never>
        var didChange = PublisherType()
    
        var store: [Media] {
            didSet {
                didChange.send()
            }
        }
    
        init(store: [Media]) {
            self.store = store
        }
    }
    
    struct ContentView : View {
        @ObjectBinding var library: MediaLibrary
    
        var body: some View {
            VStack {
                List(self.library.store.identified(by: \.id)) { media in
                    Text(media.name)
                }
                HStack {
                    Button(action: {
                        self.library.store[0].name = "Name \(Int.random(in: 100...199))"
                    }) {
                        Text("Change")
                    }
                    Button(action: {
                        self.library.store.append(Media(id: 4, name: "Name 4"))
                    }) {
                        Text("Add")
                    }
                }
            }
        }
    }
    

    【讨论】:

    • 好吧,我想要一个 List 显示许多复杂的 Media 对象,并且我希望在更改媒体对象的属性时刷新列表(因为在视图出现后会加载名称通过网络呼叫)。使用@ObjectBinding 仅适用于我的Media 类,因为数组不符合BindableObject。在我的项目中,Media 是一个更大的类,它具有属性,这些属性是从 API 调用中加载的,因此不能直接使用,因此列表必须在加载后刷新。
    • 好的,我更新了数据,使数据位于视图之外。在这种情况下,如 WWDC 会话 226(通过 SwiftUI 的数据流)中所述,您需要通过将模型作为参数传递来实例化视图。
    • 您现在可以进行所有网络调用并更新库。列表将在没有任何额外工作的情况下更新。
    • 嗯,是的,您的解决方案有效,但只是因为Media 现在是struct,因此它总是在属性更改时被替换。当我将Media 设为课程时,您的解决方案将不再有效。可悲的是,在我的情况下,Media 需要是一个类,因为其他类继承自它。我真的希望有某种解决方案,您可以将Media 的发布者与数组“链接”(例如,数组订阅Media 对象并将事件传递给视图)。
    • 然后在更新名称后立即添加对 self.library.didChange.send() 的调用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多