在 SwiftUI 中创建一个包含自定义类数组的列表

Posted

技术标签:

【中文标题】在 SwiftUI 中创建一个包含自定义类数组的列表【英文标题】:Create a List with an array of custom classes in 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 视图,有什么办法可以做到这一点吗?

【问题讨论】:

【参考方案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() 的调用。

以上是关于在 SwiftUI 中创建一个包含自定义类数组的列表的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中创建一个获取 View 并返回自定义结果的函数?

从数组中创建集合,其中包含自定义对象的数组

在 SwiftUI 的自定义视图中访问图像的修饰符

SwiftUI 中的 ForEach 问题

在 magento 1.9.2 中创建自定义顶部菜单

在 C++ 中的类中创建类对象的动态数组