肇鑫的技术博客

业精于勤,荒于嬉

Understand TabView in SwiftUI and Adopt a TabView with indicators into Window’s Toolbar

A Simplest TabView Sample

ContentView

struct ContentView: View {
    var body: some View {
        TabView {
            ForEach(1..<4) { id in
                TabItemView(id: id)
            }
        }
    }
}

TabItemView

struct TabItemView: View {
    @State var id:Int
    
    var body: some View {
        Text("Tab \(id)")
            .tabItem {
                Text(String(id))
            }
    }
}

first_tabview_sample

Life Cycle of Tab Item View

TabItemView

struct TabItemView: View {
    @State var id:Int
    
    var body: some View {
        Text("Tab \(id)")
            .tabItem {
                Text(String(id))
            }
            .onAppear(perform: {
                print("I am tab \(id)")
            })
            .onDisappear {
                print("Tab \(id) am quit!")
            }
    }
}

Runs.

I am tab 1
Tab 1 am quit!
I am tab 2
Tab 2 am quit!
I am tab 3
Tab 3 am quit!
I am tab 1

Is the tab item view just hidden or actually quit when you change a tab?

TabItemView

struct TabItemView: View {
    @State var id:Int
    @State private var title = ""
    
    var body: some View {
        HStack {
            Text("Tab \(id)")
                .onAppear(perform: {
                    print("I am tab \(id)")
                })
                .onDisappear {
                    print("Tab \(id) am quit!")
            }
            
            TextField("Title", text: $title)
        }
        .tabItem {
            Text(String(id))
        }
        .padding()
    }
}

Input "Test" in tab 1, and change to other tabs, then change back to tab 1. You will see the "Test" is still there. That means the tab item view was just hidden, not quit.

third_tabview_sample

Change Tab Name

TabItemView

struct TabItemView: View {
    @State var id:Int
    @State private var title = ""
    
    var body: some View {
        HStack {
            Text("Tab \(id)")
                .onAppear(perform: {
                    print("I am tab \(id)")
                })
                .onDisappear {
                    print("Tab \(id) am quit!")
            }
            
            TextField("Title", text: $title)
        }
        .tabItem {
            Text(getTabName())
        }
        .padding()
    }
    
    private func getTabName() -> String {
        if title.isEmpty {
            return String(id)
        }
        
        return title
    }
}

change_tab_name

Put Indicators in Window's Title Bar

ContentView

@main
struct TabView_SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowToolbarStyle(.unifiedCompact(showsTitle: false))
    }
}

tab_in_window_title

The indicators worked. But the tab title no longer worked.

Fix Tab Title

TabItemView

struct TabItemView: View {
    @Binding var tabContent:TabContent
    
    var body: some View {
        HStack {
            Text("Tab \(tabContent.id)")
            
            TextField("Title", text: $tabContent.title)
                .frame(minWidth: 300)
        }
        .tabItem {
            Text(getTabName())
        }
        .padding()
    }
    
    private func getTabName() -> String {
        if tabContent.title.isEmpty {
            return String(tabContent.id)
        }
        
        return tabContent.title
    }
}

struct TabContent:Identifiable, Equatable, Hashable {
    var id = 0
    var title = ""
}

ContentView

struct ContentView: View {
    @State private var tabContents:[TabContent] = {
        (1..<4).map { TabContent(id: $0) }
    }()
    
    @State private var currentTabContent = TabContent(id: 1)
    
    var body: some View {
        ForEach($tabContents) { $tabContent in
            if currentTabContent.id == tabContent.id {
                TabItemView(tabContent: $tabContent)
            }
        }
        .toolbar {
            HStack {
                Picker(String(currentTabContent.title), selection: $currentTabContent) {
                    ForEach(tabContents) { tabContent in
                        Text(tabContent.title.isEmpty ? String(tabContent.id) : tabContent.title).tag(tabContent)
                    }
                }
                .pickerStyle(.segmented)
                .onChange(of: currentTabContent) { newValue in
                    if let index = tabContents.map({$0.id}).firstIndex(of: newValue.id) {
                        tabContents[index] = newValue
                    }
                }
                
                Spacer()
            }
        }
        .frame(width: 400, height: 50)
    }
}

worked_tab_in_window_title