1

我在 SwiftUI 中使用泛型,在尝试利用 ViewBuilder 闭包将数据传递到泛型视图时遇到了数据持久性问题。我的目标是拥有一个管理从 API 接收数据并将其传递给通用视图的 shell 视图,如 ViewBuilder 块中定义的那样。所有数据似乎都成功地传递给了 inits,包括我的 generic BasicListView,但是当body被调用时,没有任何列表数据被持久化。

我认为通过代码来解释这个问题会更容易。为这里的长代码转储道歉:

import SwiftUI
import Combine

// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block

struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
    
    let api = GlobalAPI.shared
    
    @State var list: [ListItem] = []
    
    @State var viewBuilder: ([ListItem]) -> Content // Passing in generic [ListItem] here
    
    init(@ViewBuilder builder: @escaping ([ListItem]) -> Content) {
        self._viewBuilder = State<([ListItem]) -> Content>(initialValue: builder)
    }
    
    var body: some View {
        
        viewBuilder(list) // List contained in Blank View passed into viewBuilder Block here
            .multilineTextAlignment(.center)
            .onReceive(GlobalAPI.shared.listDidChange) { item in
                if let newItem = item as? ListItem {
                    self.list.append(newItem) // Handle API updates here
                }
            }
    }
}

// And Here is the implementation of the Blank View
struct TestView: View {

    public var body: some View {
        BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
            VStack {
                Text("Add a row") // Button to add row via API singleton
                    .onTapGesture {
                        GlobalAPI.shared.addListItem()
                    }
                
                BasicListView(items: items) { // List view init'd with items
                    Text("Hold on to your butts") // Destination
                }
            }
        }
    }
}


// Supporting code

// The generic list view/cell

struct BasicListView<Content: View, ListItem:Listable>: View {
    
    @State var items: [ListItem]
    
    var destination: () -> Content
    
    init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
        self._items = State<[ListItem]>(initialValue: items) // Items successfully init'd here
        self.destination = builder
    }
    
    var body: some View {
        List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
            BasicListCell(item: item, destination: self.destination)
        }
    }
}

struct BasicListCell<Content: View, ListItem:Listable>: View {
    
    @State var item: ListItem
    
    var destination: () -> Content
    
    var body: some View {
        
        NavigationLink(destination: destination()) {
            HStack {
                item.photo
                    .resizable()
                    .frame(width: 50.0, height: 50.0)
                    .font(.largeTitle)
                    .cornerRadius(25.0)
                VStack (alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    Text(item.description)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
        }
    }
}

// The protocol and mock data struct
protocol Listable: Identifiable {
        
    var id: UUID { get set }
    var title: String { get set }
    var description: String { get set }
    var photo: Image { get set }
}

public struct MockListItem: Listable {
    
    public var photo: Image = Image(systemName:"photo")
    public var id = UUID()
    public var title: String = "Title"
    public var description: String = "This is the description"

    static let all = [MockListItem(), MockListItem(), MockListItem(), MockListItem()]
}

// A global API singleton for testing data updates
class GlobalAPI {
    
    static let shared = GlobalAPI()
    
    var listDidChange = PassthroughSubject<MockListItem, Never>()
    
    var newListItem:MockListItem? = nil {
        didSet {
            if let item = newListItem {
                listDidChange.send(item)
            }
        }
    }
    
    func addListItem() {
        newListItem = MockListItem()
    }
}

这是 ViewBuilder 块的正确实现,还是不鼓励尝试通过 View builder 块传递数据?

注意:什么工作

如果我直接传入静态 Mock 数据,视图将正确绘制自己,如下所示:

struct TestView: View {

    public var body: some View {
        BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
            VStack {
                Text("Add a row") // Button to add row via API singleton
                    .onTapGesture {
                        GlobalAPI.shared.addListItem()
                    }
                
                BasicListView(items: MockListItem.all) { // List view init'd with items
                    Text("Hold on to your butts") // Destination
                }
            }
        }
    }
}

有任何想法吗?感谢大家的帮助和反馈。

4

2 回答 2

1

Welp,我想我想出了一个解决方案。

问题似乎BasicListView在于包装中的项目@State而不是@Binding,并且 ViewBuilder 块的类型([ListItem]) -> Content而不是(Binding<[ListItem]>) -> Content. 原始设置用于从MockListItem.all块外部提取的静态数据(相反,我将itemsin更改BasicListView为 @Binding,现在通过传入 in 的绑定来@State var list初始化BlankView。这是更新的代码:


// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block

struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
    
    let api = GlobalAPI.shared
    
    @State var list: [ListItem] = []
    
    var viewBuilder: (Binding<[ListItem]>) -> Content // Now passing Binding into the block instead of an array
    
    init(contentType: ContentType, @ViewBuilder builder: @escaping (Binding<[ListItem]>) -> Content) {
        self.viewBuilder = builder
    }
    
    var body: some View {
        
        viewBuilder($list) // Binding passed into ViewBuilder block
            .multilineTextAlignment(.center)
            .onReceive(GlobalAPI.shared.listDidChange) { item in
                if let newItem = item as? ListItem {
                    self.list.append(newItem) // Handle API updates here
                }
            }
    }
}

// Supporting code

// The generic list view/cell

struct BasicListView<Content: View, ListItem:Listable>: View {
    
    @Binding var items: [ListItem]
    
    var destination: () -> Content
    
    init(items: Binding<[ListItem]>, @ViewBuilder builder: @escaping () -> Content) {
        self._items = items
        self.destination = builder
    }
    
    var body: some View {
        List(items) { item in // Items passed into init now persist and correctly get rendered here, including when API updates the list.
            BasicListCell(item: item, destination: self.destination)
        }
    }
}

希望这可以帮助那里的人。干杯!

于 2020-06-20T17:05:28.683 回答
1

这里是固定视图。您在外部提供模型,但状态用于内部更改,并且一旦创建,它就会持续存在于相同的视图中。所以在这种情况下,状态是错误的——视图重建是由外部注入的数据管理的。

使用 Xcode 11.4 / iOS 13.4 测试

演示

struct BasicListView<Content: View, ListItem:Listable>: View {

    var items: [ListItem]
    var destination: () -> Content

    init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
        self.items = items // Items successfully init'd here
        self.destination = builder
    }

    var body: some View {
        List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
            BasicListCell(item: item, destination: self.destination)
        }
    }
}
于 2020-06-20T17:06:45.233 回答