SwiftUI/UIKit如何实现类Vue Slots与Slot Props的功能?
不管是SwiftUI还是UIKit,都能实现你要的带过滤、排序逻辑,支持自定义列表项(默认用内置视图)的通用组件,对应前端的Slots和作用域插槽概念。下面分别给出两种框架的实现方案,同时解决你遇到的编译错误和参数冗余问题。
SwiftUI 实现方案
SwiftUI的@ViewBuilder特性天然适合实现类似插槽的功能,结合泛型可以轻松打造通用列表组件。核心思路是给组件的自定义列表项闭包设置默认值,这样用户调用时可以省略该参数,自动使用内置视图;需要自定义时,闭包能接收当前列表项的数据(对应作用域插槽)。
完整代码示例
import SwiftUI // 通用过滤排序列表组件 struct FilteredSortedList<Item: Identifiable, Content: View>: View { let items: [Item] let filter: (Item) -> Bool let sort: (Item, Item) -> Bool @ViewBuilder let itemContent: (Item) -> Content // 带默认参数的初始化器,省略itemContent时自动用默认视图 init( items: [Item], filter: @escaping (Item) -> Bool = { _ in true }, sort: @escaping (Item, Item) -> Bool = { _, _ in false }, @ViewBuilder itemContent: @escaping (Item) -> Content = { item in DefaultListItemView(item: item) } ) { self.items = items self.filter = filter self.sort = sort self.itemContent = itemContent } // 预处理:过滤+排序后的数据源 private var processedItems: [Item] { items.filter(filter).sorted(by: sort) } var body: some View { List(processedItems) { item in // 调用自定义闭包或默认视图 itemContent(item) } } } // 内置默认列表项视图 struct DefaultListItemView<Item: Identifiable>: View { let item: Item var body: some View { Text("\(item.id)") // 可扩展:如果Item有通用属性(如title),可以改为Text(item.title),或用Mirror做更灵活的默认展示 } } // 使用示例 struct TestView: View { // 测试数据模型 struct User: Identifiable { let id: Int let name: String let age: Int } let userList = [ User(id: 1, name: "Alice", age: 25), User(id: 2, name: "Bob", age: 30), User(id: 3, name: "Charlie", age: 22) ] var body: some View { VStack(spacing: 20) { // 1. 省略自定义视图,使用默认样式 FilteredSortedList(items: userList) .navigationTitle("默认视图") // 2. 自定义列表项,同时配置过滤、排序规则 FilteredSortedList( items: userList, filter: { $0.age > 23 }, // 只显示年龄>23的用户 sort: { $0.age < $1.age } // 按年龄升序排列 ) { user in // 作用域插槽:拿到当前user数据,自定义视图 HStack { Text(user.name) .font(.headline) Spacer() Text("\(user.age)岁") .foregroundColor(.secondary) } .padding(.vertical, 8) } .navigationTitle("自定义视图") } } }
解决你遇到的编译错误
你之前遇到的Generic parameter 'Content' could not be inferred错误,本质是编译器无法推断泛型Content的类型——因为你没有给自定义视图闭包提供默认值,导致省略该参数时,编译器不知道Content应该对应什么视图类型。
上面的代码通过在初始化器中给itemContent设置默认的@ViewBuilder闭包(返回DefaultListItemView),让编译器能明确推断出Content的类型,从而解决编译错误。同时默认参数也让你可以按需省略过滤、排序、自定义视图等参数,避免代码杂乱。
UIKit 实现方案
UIKit没有@ViewBuilder,但可以通过配置闭包实现类似逻辑:定义一个可选闭包,用于自定义cell的内容,默认值为nil;当闭包存在时调用它自定义cell,否则使用内置默认配置。
完整代码示例
import UIKit // 通用过滤排序列表控制器 class FilteredSortedListVC<Item: Identifiable>: UIViewController { // 自定义cell的配置闭包类型:接收当前item和cell typealias CellConfigHandler = (Item, UITableViewCell) -> Void private let items: [Item] private let filter: (Item) -> Bool private let sort: (Item, Item) -> Bool private let cellConfig: CellConfigHandler? private let tableView = UITableView() // 预处理后的数据源 private var processedItems: [Item] { items.filter(filter).sorted(by: sort) } // 带默认参数的初始化器 init( items: [Item], filter: @escaping (Item) -> Bool = { _ in true }, sort: @escaping (Item, Item) -> Bool = { _, _ in false }, cellConfig: CellConfigHandler? = nil ) { self.items = items self.filter = filter self.sort = sort self.cellConfig = cellConfig super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupTableView() } private func setupTableView() { tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) tableView.register(UITableViewCell.self, forCellReuseIdentifier: "DefaultCell") tableView.dataSource = self } } // 实现UITableViewDataSource extension FilteredSortedListVC: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { processedItems.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "DefaultCell", for: indexPath) let item = processedItems[indexPath.row] if let configHandler = cellConfig { // 使用自定义配置闭包 configHandler(item, cell) } else { // 默认cell配置 cell.textLabel?.text = "\(item.id)" cell.selectionStyle = .none } return cell } } // 使用示例 class TestVC: UIViewController { // 测试数据模型 struct User: Identifiable { let id: Int let name: String let age: Int } let userList = [ User(id: 1, name: "Alice", age: 25), User(id: 2, name: "Bob", age: 30), User(id: 3, name: "Charlie", age: 22) ] override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground // 1. 使用默认cell配置 let defaultListVC = FilteredSortedListVC(items: userList) addChild(defaultListVC) defaultListVC.view.frame = CGRect(x: 0, y: 88, width: view.bounds.width, height: view.bounds.height/2 - 44) view.addSubview(defaultListVC.view) defaultListVC.didMove(toParent: self) // 2. 自定义cell配置,同时设置过滤、排序 let customListVC = FilteredSortedListVC( items: userList, filter: { $0.age > 23 }, sort: { $0.age < $1.age } ) { user, cell in // 作用域插槽:拿到user数据,自定义cell内容 cell.textLabel?.text = user.name cell.detailTextLabel?.text = "Age: \(user.age)" cell.accessoryType = .disclosureIndicator cell.selectionStyle = .gray } addChild(customListVC) customListVC.view.frame = CGRect(x: 0, y: view.bounds.height/2 + 44, width: view.bounds.width, height: view.bounds.height/2 - 44) view.addSubview(customListVC.view) customListVC.didMove(toParent: self) } }
说明
UIKit的方案通过可选闭包实现了类似插槽的功能,默认闭包为nil时使用内置cell配置;需要自定义时,闭包接收当前列表项的数据和cell实例,让你可以灵活配置cell内容,对应前端的作用域插槽。
内容的提问来源于stack exchange,提问作者Yauheni Platau

