NG235
NG235

Reputation: 1281

macOS SwiftUI App TabView with Segmented Control in Toolbar

I am trying to create a macOS app with SwiftUI. I need a TabView or something similar, but when I use TabView the segmented control is not in the macOS Toolbar.
Click here to see an example of what I would like

My current code is:

import SwiftUI

struct ContentView: View {
    var body: some View {
        TabView {
            Text("1")
                .tabItem {
                    Text("1")
            }
        }
    }
}

The result is here as an image

The segmented control needs to be in the toolbar and not the view.

Upvotes: 11

Views: 5352

Answers (3)

Raimundas Sakalauskas
Raimundas Sakalauskas

Reputation: 2294

It unfortunately seems that as of macOS 13.0 and SwiftUI 4.0 the only way for this to work is if your app is SwiftUI app.

@main
struct SettingsViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        Settings {
            SettingsView()
        }
    }
}

Source: https://serialcoder.dev/text-tutorials/macos-tutorials/presenting-the-preferences-window-on-macos-using-swiftui/

Upvotes: 1

Denise
Denise

Reputation: 370

I stumbled upon your question when I wanted to build something similar on macOS BigSur. I am using Xcode 12.2.

Here is what my solution would look like inspired by the answer from Asperi. It was important to set the title of the window group to an empty string "" otherwise it look weird.

Note that it only works when you run the app, not in the preview!

TabBar Example

App File

import SwiftUI

@main
struct SegmentedToolbarApp: App {
    var body: some Scene {
        WindowGroup("") {
            ToolbarItemPlacement()
        }
    }
}

ToolbarItemPlacement View

The important part was the placement with principal.

It was also important to set a bigger minWidth - otherwise the toolbar would disappear!

import SwiftUI

struct ToolbarItemPlacement: View {
    
    private let tabs = ["Watch Now", "Movies", "TV Shows", "Kids", "Library"]
    @State private var selectedTab = 0
    
    var body: some View {
        VStack {
            ChildTabView(title: self.tabs[self.selectedTab], index: self.selectedTab)
        }
        .toolbar {
            ToolbarItem(placement: .principal) {
                
                Picker("", selection: $selectedTab) {
                    ForEach(tabs.indices) { i in
                        Text(self.tabs[i]).tag(i)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .padding(.top, 8)
            }
        }
        .frame(minWidth: 800, minHeight: 400)
    }
}

ChildTabView

struct ChildTabView: View {
    var title: String
    var index: Int

    var body: some View {
        Text("\(title) - Index \(index)")
            .padding()
    }
}

Upvotes: 5

Asperi
Asperi

Reputation: 258461

Here is a simplified demo of possible approach to achieve this. Tested & works with Xcode 11.2.

demo

1) Prepare window to have needed style and background in AppDelegate

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView()
        .edgesIgnoringSafeArea(.top)
        .frame(minWidth: 480, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)

    // Create the window and set the content view. 
    window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered, defer: false)
    window.center()
    window.titlebarAppearsTransparent = true
    window.titleVisibility = .hidden

    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: contentView)
    window.makeKeyAndOrderFront(nil)
}

2) Prepare window content view to have needed behavior

struct ContentView: View {
    private let tabs = ["Watch Now", "Movies", "TV Shows", "Kids", "Library"]
    @State private var selectedTab = 0
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Picker("", selection: $selectedTab) {
                    ForEach(tabs.indices) { i in
                        Text(self.tabs[i]).tag(i)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .padding(.top, 8)
                Spacer()
            }
            .padding(.horizontal, 100)
            Divider()
            GeometryReader { gp in
                VStack {
                    ChildTabView(title: self.tabs[self.selectedTab], index: self.selectedTab)
                }
            }
        }
    }
}

struct ChildTabView: View {
    var title: String
    var index: Int

    var body: some View {
        Text("\(title)")
    }
}

Upvotes: 2

Related Questions