You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()
}

关键修改点

  1. 使用原生TabView:用TabView承载子视图,通过tag绑定TabBarItem,利用原生TabView的生命周期管理,确保视图仅初始化一次。
  2. 隐藏原生TabBar:添加.tabBarStyle(.hidden)隐藏原生TabBar的外观。
  3. 添加自定义TabBar:在ZStack底部放置自定义的CustomTabBarView,通过绑定selection控制TabView的选中状态,实现自定义外观。

这样修改后,切换标签时HomeView和SecondView的init方法只会调用一次,和原生TabBar的行为一致,同时保留了自定义TabBar的样式。

内容的提问来源于stack exchange,提问作者Whirlwind

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 10:34:55