这个答案是对@oivvio在@arsenius的答案中提供的关于如何使用填充了UIHostingController的UITabController的解决方案的补充。
链接中的答案有一个问题:如果子视图有外部的SwiftUI依赖,那些子视图将不会被更新。这对于大多数情况下子视图只有内部状态的情况来说是可以接受的。然而,如果你像我一样是一个喜欢全局Redux系统的React开发者,那么你就会遇到麻烦。
为了解决这个问题,关键是每次调用updateUIViewController时更新每个UIHostingController的rootView。我的代码还避免创建不必要的UIView或UIViewControllers:如果您不将它们添加到视图层次结构中,它们并不那么昂贵,但是仍然越少浪费越好。
警告:该代码不支持动态选项卡视图列表。为了正确支持它,我们需要识别每个子选项卡视图,并进行数组差异以正确添加、排序或删除它们。原则上可以做到,但超出了我的需求。
首先,我们需要一个 TabItem
。这样做是为了使控制器能够获取所有信息,而不创建任何 UITabBarItem
:
struct XNTabItem: View {
let title: String
let image: UIImage?
let body: AnyView
public init<Content: View>(title: String, image: UIImage?, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.body = AnyView(content())
}
}
我们接下来有控制器:
struct XNTabView: UIViewControllerRepresentable {
let tabItems: [XNTabItem]
func makeUIViewController(context: UIViewControllerRepresentableContext<XNTabView>) -> UITabBarController {
let rootController = UITabBarController()
rootController.viewControllers = tabItems.map {
let host = UIHostingController(rootView: $0.body)
host.tabBarItem = UITabBarItem(title: $0.title, image: $0.image, selectedImage: $0.image)
return host
}
return rootController
}
func updateUIViewController(_ rootController: UITabBarController, context: UIViewControllerRepresentableContext<XNTabView>) {
let children = rootController.viewControllers as! [UIHostingController<AnyView>]
for (newTab, host) in zip(self.tabItems, children) {
host.rootView = newTab.body
if host.tabBarItem.title != host.tabBarItem.title {
host.tabBarItem.title = host.tabBarItem.title
}
if host.tabBarItem.image != host.tabBarItem.image {
host.tabBarItem.image = host.tabBarItem.image
}
}
}
}
子控制器在makeUIViewController
中初始化。每当调用updateUIViewController
时,我们更新每个子控制器的根视图。我没有对rootView
进行比较,因为我认为在框架级别上会执行相同的检查,根据苹果关于如何更新视图的描述。但我可能是错误的。
使用它非常简单。下面是我从一个模拟项目中获取的部分代码:
class Model: ObservableObject {
@Published var allHouseInfo = HouseInfo.samples
public func flipFavorite(for id: Int) {
if let index = (allHouseInfo.firstIndex { $0.id == id }) {
allHouseInfo[index].isFavorite.toggle()
}
}
}
struct FavoritesView: View {
let favorites: [HouseInfo]
var body: some View {
if favorites.count > 0 {
return AnyView(ScrollView {
ForEach(favorites) {
CardContent(info: $0)
}
})
} else {
return AnyView(Text("No Favorites"))
}
}
}
struct ContentView: View {
static let housingTabImage = UIImage(systemName: "house.fill")
static let favoritesTabImage = UIImage(systemName: "heart.fill")
@ObservedObject var model = Model()
var favorites: [HouseInfo] {
get { model.allHouseInfo.filter { $0.isFavorite } }
}
var body: some View {
XNTabView(tabItems: [
XNTabItem(title: "Housing", image: Self.housingTabImage) {
NavigationView {
ScrollView {
ForEach(model.allHouseInfo) {
CardView(info: $0)
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
}.navigationBarTitle("Housing")
}
},
XNTabItem(title: "Favorites", image: Self.favoritesTabImage) {
NavigationView {
FavoritesView(favorites: favorites).navigationBarTitle("Favorites")
}
}
]).environmentObject(model)
}
}
状态被提升到根级别作为Model,并携带变异辅助工具。在CardContent中,您可以通过EnvironmentObject访问状态和辅助工具。更新将在Model对象中完成,传播到ContentView,通知我们的XNTabView并更新其每个UIHostController。
编辑:
- 结果发现.environmentObject可以放置在顶层。
ScrollView
在如何使SwiftUI列表自动滚动?中非常有用。 - Asperi