Create a List with an array of custom classes in SwiftUI

iCo*_*eak 1 list swift swiftui combine

I tried creating a list of custom classes in SwiftUI, but keep having problems with the UI updates. I made my class conform to BindableObject and it calls the didChange.send() function correctly when a property changes, but the change doesn't seem to get passed through to the array, as the view does not update, when I change a property of my custom class in the array.

Here is a basic example of what I mean:

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")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

When pressing the "Change" button, it just changes the name of the first media object in the array, which does not result in a re-rendering of the view. When I press the "Add" button, he adds an object to the array, therefore re-rendering the UI and also displaying the changed name of the first object (from pressing "Change").

Now my question is, if there is a way to link the publisher of Media to the publisher of Array<Media>, so that when the Media Publisher fires, the Array<Media> Publisher fires too and therefore causes the view to re-render.

When I move the Text(media.name) in a separate view (which holds the media as a @State property, it works as intended, as the subview itself request the re-render.

但是,假设我不想使用自定义视图,而只是使用简单Text视图,是否有任何方法可以实现此目的?

kon*_*iki 5

您正在混合两种不同的东西。@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")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您通常将@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()
        }
Run Code Online (Sandbox Code Playgroud)

并执行:

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")
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)