SwiftUI:ui 插槽和插槽道具?

问题描述 投票:0回答:1

我从 Swift(来自前端)开始测试项目,并对视图和视图构建器有疑问

供参考:在前端框架 Vue.js 中,有一个 slot 和 slot props 的概念 https://vuejs.org/guide/components/slots.html#scoped-slots
Svelte https://svelte.dev/docs/special-elements#slot 也是如此,并且在 React 中也可以实现相同的功能

例如,我有一些通用列表,它具有一些基本布局并在内部进行一些逻辑(过滤、排序等),我想用它来传递列表并显示列表项,但是我想要,如果我不提供构建器,该列表中将指定一些默认视图

在 SwiftUI/UIKit 中有什么方法可以实现这样的功能吗?

我尝试做的类似于下一个代码,我简化了它以显示总体思路

import SwiftUI

struct ListTestView\<T: Hashable, Content: View\>: View {
    private let items: \[T\]
    private let listItemBuilder: (ListItemBuilderParams) -\> Content

    init(
        _ list: [T],
        @ViewBuilder listItem: @escaping (_ params: ListItemBuilderParams) -> Content
    ) {
        self.items = list
        self.listItemBuilder = listItem
    }
    
    var body: some View {
        VStack {
            ForEach(filteredItems, id: \.self) { item in
                listItemBuilder(.init(item1: item, item2: item, item3: item)) // ?? Text(String(item))
            }
        }
    }
    
    private var filteredItems: [T] {
        // ...some filtration
        return items
    }
    
    struct ListItemBuilderParams {
        let item1: T
        let item2: T
        let item3: T
        // ... and possible more
    }
}

struct ViewWithList: View {
    var body: some View {
        ListTestView(\[1, 2, 3, 4\]) { params in
            Text(String(params.item1))
            Button(String(params.item2)) {}
            Text(String(params.item3))
        }
    }
}

但它不会编译,因为在

ViewWithList
中我收到错误
Generic parameter 'Content' could not be inferred

但是如果我将所有参数作为普通参数列表传递,没有结构,它会编译,但它可能会变得非常混乱,因为没有办法省略一些特定的参数,我必须在闭包中将它们全部列出

import SwiftUI

struct ListTestView<T: Hashable, Content: View>: View {
    private let items: [T]
    private let listItemBuilder: (T, T, T) -> Content

    init(
        _ list: [T],
        @ViewBuilder listItem: @escaping (T, T, T) -> Content
    ) {
        self.items = list
        self.listItemBuilder = listItem
    }

    var body: some View {
        VStack {
            ForEach(filteredItems, id: \.self) { item in
                listItemBuilder(item, item, item) // ?? Text(String(item))
            }
        }
    }

    private var filteredItems: [T] {
        // ...some filtration
        return items
    }
}

struct ViewWithList: View {
    var body: some View {
        ListTestView([1, 2, 3, 4]) { item1, item2, item3 in
            Text(String(item1))
            Button(String(item2)) {}
            Text(String(item3))
        }
    }
}
swift swiftui
1个回答
0
投票

我建议遵循“单一责任”原则,这会让事情变得更简单/更容易实现:

  1. 列表视图负责根据提供的项目构建和显示视图列表,可能带有过滤/排序按钮。它不会有过滤器逻辑,或任何其他功能逻辑:
// Notice that T was changed to Identifiable as recommended for ForEach
struct ListTestView<T: Identifiable, Item: View>: View {
    
    // Items provided and linked to a parent
    @Binding var items: [T]
    
    // Functions can be a constant
    @ViewBuilder let itemView: (T) -> Item
    let filter: () -> Void
    let sort: () -> Void
    
    var body: some View {
        VStack {
            HStack {
                Button("Filter", action: filter)
                Button("Sort", action: sort)
            }
            LazyVStack {
                ForEach(items) { item in
                    itemView(item)
                }
            }
        }
    }
}
  1. 该项目是任何可识别的结构。例如,根据您的示例,它可能是:
struct MyItem: Identifiable {
    let id = UUID() // example, could be set to something more consistent in reality
    let field1: String
    let field2: String
    let field3: String
}
  1. Item View(它的每个具体实现)负责显示单个项目。它不需要灵活性,因为它将直接接收需要显示的内容:
struct MyItemView: View {
    
    let myItem: MyItem
    
    let buttonAction: (MyItem) -> ()
    
    var body: some View {
        HStack {
            Text(myItem.field1)
            Button(myItem.field2, action: callAction)
            Text(myItem.field3)
        }
    }
    
    // Calls buttonAction with the MyItem 
    func callAction() {
        buttonAction(myItem)
    }
}
  1. 现在父视图(负责包含具体项目的具体列表的视图)是保存有关如何对它们进行排序/过滤并将它们提供给视图模型的逻辑的视图。
struct ContentView: View {
    
    // Those can also move to ViewModel
    @State var items: [MyItem]
    @State var clickedText: String = ""
    
    var body: some View {
        VStack {
            Text("Clicked \(clickedText)")
            ListTestView(
                items: $items,
                itemView: itemViewBuilder,
                filter: filter,
                sort: sort
            )
        }
    }
    
    func itemViewBuilder(_ item: MyItem) -> some View {
        MyItemView(
            myItem: item,
            buttonAction: performAction
        )
    }
    
    func performAction(_ item: MyItem) {
        clickedText = "\(item.field1), \(item.field3)"
    }
    
    func filter() {
        // For example
        items = [items[0]]
    }
    
    func sort() {
        // For example
        items = items.reversed()
    }
}

此外,您还可以实现过滤、排序等可重用过程,但要在单独的类中实现,甚至是

Array

的扩展
© www.soinside.com 2019 - 2024. All rights reserved.