SwiftUI自定义TabBar切换时视图重复初始化问题求助
解决自定义TabBar切换时视图重复初始化的问题
问题描述
使用自定义TabBar时,每次切换标签,HomeView和SecondView的init方法都会被调用(视图重复创建),但原生TabBar仅会初始化一次视图。以下是可复现问题的代码:
import SwiftUI // MARK: - Extensions extension View { func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View { self.modifier(TabBarItemViewModifier(tab: tab, selection: selection)) } } // MARK: - Preference Keys struct TabBarItemPreferenceKey: PreferenceKey { static var defaultValue: [TabBarItem] = [] static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) { value += nextValue() } } // MARK: - View Modifiers struct TabBarItemViewModifier: ViewModifier { let tab: TabBarItem @Binding var selection: TabBarItem func body(content: Content) -> some View { content.preference(key: TabBarItemPreferenceKey.self, value: [tab]) .opacity(selection == tab ? 1.0 : 0.0) .disabled(selection == tab ? false : true) } } // MARK: - ViewModels final class ContentViewModel: ObservableObject { @Published var selection: TabBarItem = .home } // MARK: - Content View (starting view) struct ContentView: View { @StateObject private var viewModel = ContentViewModel() var body: some View { CustomTabBarContainerView(selection: $viewModel.selection) { NavigationStack { HomeView() .navigationTitle("Home") } .tabBarItem(tab: .home, selection: $viewModel.selection) NavigationStack { SecondView() .navigationTitle("Second") } .tabBarItem(tab: .second, selection: $viewModel.selection) } } } // Custom TabBar views (container and tabbar view) struct CustomTabBarContainerView<Content: View>: View { // MARK: - Private Properties private let content: Content @State private var tabs: [TabBarItem] = [] @Binding private var selection: TabBarItem var body: some View { VStack(spacing: 0) { ZStack { content } CustomTabBarView(tabs: tabs, selection: $selection) } .onPreferenceChange(TabBarItemPreferenceKey.self, perform: { tabs in self.tabs = tabs }) } init(selection: Binding<TabBarItem>, @ViewBuilder content: () -> Content) { self._selection = selection self.content = content() } } struct CustomTabBarView: View { let tabs: [TabBarItem] @Binding var selection: TabBarItem var body: some View { HStack { ForEach(tabs, id: \.self) { tab in tabView(tab: tab) .onTapGesture { switchTo(tab: tab) } } } } func tabView(tab: TabBarItem) -> some View { VStack { Image(systemName: tab.iconName) .font(.system(size: 12)) Text(tab.title) .font(.system(size: 12)) } .padding(.vertical, 8) .frame(maxWidth: .infinity) .foregroundStyle(selection == tab ? tab.color : .gray) } func switchTo(tab: TabBarItem) { selection = tab } } // MARK: - TabBarItem enum TabBarItem: Hashable, CaseIterable { case home, second var iconName: String { switch self { case .home: return "house.fill" case .second: return "waveform" } } var title: String { switch self { case .home: return "Home" case .second: return "Second" } } var color: Color { Color.purple } } // MARK: - HomeView struct HomeView: View { init() {print("HomeView init")} var body: some View { ZStack { Color.purple.ignoresSafeArea().opacity(0.4) Text("Home view") } } } // MARK: - SecondView struct SecondView: View { init() {print("SecondView init")} var body: some View { Text("Second view") } } #Preview { ContentView() }
原因分析
当前实现通过ZStack承载所有子视图,并用opacity(selection == tab ? 1 : 0)控制显示/隐藏。SwiftUI会对完全不可见(opacity=0)的视图进行优化销毁,导致每次切换标签时,未选中的视图会被销毁,再次选中时重新初始化。而原生TabView内部会维护每个标签页的视图实例,即使切换也会保留状态,不会重复创建。
解决方案
基于原生TabView改造,隐藏原生TabBar,添加自定义TabBar来控制选中状态,既保留原生的生命周期管理,又实现自定义外观:
修改后的完整代码
import SwiftUI // MARK: - ViewModels final class ContentViewModel: ObservableObject { @Published var selection: TabBarItem = .home } // MARK: - Content View (starting view) struct ContentView: View { @StateObject private var viewModel = ContentViewModel() var body: some View { ZStack(alignment: .bottom) { TabView(selection: $viewModel.selection) { NavigationStack { HomeView() .navigationTitle("Home") } .tag(TabBarItem.home) NavigationStack { SecondView() .navigationTitle("Second") } .tag(TabBarItem.second) } // 隐藏原生TabBar .tabBarStyle(.hidden) // 自定义TabBar CustomTabBarView(selection: $viewModel.selection) .padding(.horizontal) .padding(.bottom, 8) .background(Color.white) .cornerRadius(12) .shadow(radius: 4) .padding(.horizontal, 16) } } } // MARK: - Custom TabBar View struct CustomTabBarView: View { @Binding var selection: TabBarItem var body: some View { HStack { ForEach(TabBarItem.allCases, id: \.self) { tab in tabView(tab: tab) .onTapGesture { selection = tab } } } } func tabView(tab: TabBarItem) -> some View { VStack { Image(systemName: tab.iconName) .font(.system(size: 12)) Text(tab.title) .font(.system(size: 12)) } .padding(.vertical, 8) .frame(maxWidth: .infinity) .foregroundStyle(selection == tab ? tab.color : .gray) } } // MARK: - TabBarItem enum TabBarItem: Hashable, CaseIterable { case home, second var iconName: String { switch self { case .home: return "house.fill" case .second: return "waveform" } } var title: String { switch self { case .home: return "Home" case .second: return "Second" } } var color: Color { Color.purple } } // MARK: - HomeView struct HomeView: View { init() {print("HomeView init")} var body: some View { ZStack { Color.purple.ignoresSafeArea().opacity(0.4) Text("Home view") } } } // MARK: - SecondView struct SecondView: View { init() {print("SecondView init")} var body: some View { Text("Second view") } } #Preview { ContentView() }
关键修改点
- 使用原生TabView:用
TabView承载子视图,通过tag绑定TabBarItem,利用原生TabView的生命周期管理,确保视图仅初始化一次。 - 隐藏原生TabBar:添加
.tabBarStyle(.hidden)隐藏原生TabBar的外观。 - 添加自定义TabBar:在ZStack底部放置自定义的
CustomTabBarView,通过绑定selection控制TabView的选中状态,实现自定义外观。
这样修改后,切换标签时HomeView和SecondView的init方法只会调用一次,和原生TabBar的行为一致,同时保留了自定义TabBar的样式。
内容的提问来源于stack exchange,提问作者Whirlwind
相关产品推荐
相关产品推荐

