如何使用SwiftUI在macOS应用程序中添加工具栏?

27

我正在尝试使用SwiftUI将工具栏添加到macOS应用程序的标题栏中,类似于下面所示的内容。

Toolbar in the titlebar in macOS app

我无法找到一种使用SwiftUI实现这一目标的方法。 目前,我的工具栏(只有一个文本字段)在我的视图内,但我想将其移动到标题栏中。

我的当前代码:

struct TestView: View {
    var body: some View {
        VStack {
            TextField("Placeholder", text: .constant("")).padding()
            Spacer()
        }
    }
}
所以,在我的情况下,我需要将文本字段放在工具栏中。

1
我在谈论macOS目标中的SwiftUI。 - Bijoy Thangaraj
1
不,navigationBarTitle修饰符在macOS SwiftUI中不可用。 - Bijoy Thangaraj
1
@Asperi 我能够做到这一点 - 请参见下面的答案。工具栏(或标题栏附件)在 macOS 应用程序中仍然被广泛使用,不是吗? - Bijoy Thangaraj
1
@Asperi 你是指使用NSViewRepresentable来使用AppKit中的那个吗?如果是,我尝试过这种方法但没有成功。如果你有解决方案,我很乐意看看。 - Bijoy Thangaraj
1
认为使用AppKit来管理工具栏更容易,因为SwiftUI对窗口“viewContent”之外的UI支持不好。我在这里发布了一个编程式AppKit实现的示例代码:https://github.com/billibala/SUIToolbarPlay - billibala
5个回答

28

在macOS 11中,您可能会希望使用WWDC Session 10104中记录的新API作为新标准。在WWDC Session 10041的12分钟处提供了明确的代码示例。

NSWindowToolbarStyle.unified
或者
NSWindowToolbarStyle.unifiedCompact

在SwiftUI中,你可以使用新的.toolbar { }构建器。

struct ContentView: View {


  var body: some View {
        List {
            Text("Book List")
        }
        .toolbar {
            Button(action: recordProgress) {
                Label("Record Progress", systemImage: "book.circle")
            }
        }
    }

    private func recordProgress() {}
}

1
非常方便的链接!谢谢。 - Dan Korkelia
用户无法使用“查看>自定义工具栏...”选项对此进行自定义,是吗? - No one you know
2
@Nooneyouknow,如果您在ToolbarItem中使用showsByDefault参数,则可以在SwiftUI中拥有可自定义的工具栏。 - hayesk

8

方法1:

通过添加标题栏附件来实现。我能够通过修改AppDelegate.swift文件完成此操作。我不得不应用一些奇怪的填充来使其看起来正确。

AppDelegate.swift

func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Create the titlebar accessory
        let titlebarAccessoryView = TitlebarAccessory().padding([.top, .leading, .trailing], 16.0).padding(.bottom,-8.0).edgesIgnoringSafeArea(.top)

        let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
        accessoryHostingView.frame.size = accessoryHostingView.fittingSize

        let titlebarAccessory = NSTitlebarAccessoryViewController()
        titlebarAccessory.view = accessoryHostingView       

        // 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.setFrameAutosaveName("Main Window")

        // Add the titlebar accessory
        window.addTitlebarAccessoryViewController(titlebarAccessory)

        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    }

TitlebarAccessory.swift

import SwiftUI

struct TitlebarAccessory: View {
    var body: some View {

        TextField("Placeholder", text: .constant(""))

    }
}

结果:

macOS 工具栏内的内容

方法 2(另一种方法):

这里的思路是使用 storyboard 来实现工具栏部分,而其余部分则使用 SwiftUI。首先创建一个新的应用程序并将 storyboard 作为用户界面。然后进入 storyboard,删除默认的视图控制器并添加一个新的 NSHostingController。通过设置其关系,将新添加的 Hosting Controller 连接到主窗口。使用界面生成器将工具栏添加到窗口中。

Storyboard 配置

将自定义类附加到您的 NSHostingController 并将您的 SwiftUI 视图加载到其中。

以下是示例代码:

import Cocoa
import SwiftUI

class HostingController: NSHostingController<SwiftUIView> {

    @objc required dynamic init?(coder: NSCoder) {
        super.init(coder: coder, rootView: SwiftUIView())       

    }

}

使用这种方法还可以让您自定义工具栏。

有没有办法通过右键单击并选择“自定义工具栏”使其可编辑?(类似于Finder、Pages等) - sqwk
2
使用第一种解决方案,不行。但是,请查看我在上面回答中添加的替代方法。这样,您就可以自定义工具栏。 - Bijoy Thangaraj
@BijoyThangaraj 怎样通过工具栏中的按钮改变 SwiftUIViewState 变量?我尝试过了,但状态没有改变。 - user7649191
@Alan,实现这个的一种方式是从你的WindowController发布一个通知,然后在HostingController中观察该通知。在HostingController中,你可以持有一个ObservableObject并基于接收到的通知修改它的属性。最后,你可以像这样将此可观察对象传递给你的SwiftUIView:super.init(coder: coder, rootView: AnyView(SwiftUIView().environmentObject(myObservableObject))) - Bijoy Thangaraj
对于方法1,文本有些错位/错放。有没有办法修复它以使其在视觉上更好?这也会发生在按钮上。 - Aryo

0

我终于成功地完成了这一点,而且没有使用任何麻烦的填充方式,以一种在全屏模式下看起来很棒的方式。此外,先前的解决方案不允许水平调整大小。

对齐标题栏

将您的标题视图包装在HStack()中,并添加一个不可见的文本视图,该文本视图允许无限展开高度。这似乎是保持一切居中的方法。现在忽略顶部的安全区域,以使其在标题栏的全高度中心对齐。

       struct TitleView : View {
   
            var body: some View {
                HStack {
                    Text("").font(.system(size: 0, weight: .light, design: .default)).frame(maxHeight: .infinity)
                    Text("This is my Title")
                }.edgesIgnoringSafeArea(.top)
            }
        }

在您的应用程序委托中,当您添加NSTitlebarAccessoryViewController()时,请将layoutAttribute设置为top。这将允许它在窗口大小更改时水平调整大小(前导和左侧将宽度固定为最小值,并且这已经给我带来了很多痛苦,寻找答案)。

    let titlebarAccessoryView = TitleView()

    let accessoryHostingView = NSHostingView(rootView: titlebarAccessoryView)
    accessoryHostingView.frame.size = accessoryHostingView.fittingSize
    
    let titlebarAccessory = NSTitlebarAccessoryViewController()
    titlebarAccessory.view = accessoryHostingView
    titlebarAccessory.layoutAttribute = .top

在我的情况下,我还希望一些独立于标题其余部分的按钮位于最右侧,因此我选择单独添加它们,利用添加多个视图控制器的能力。
    let titlebarAccessoryRight = NSTitlebarAccessoryViewController()
    titlebarAccessoryRight.view = accessoryHostingRightView
    titlebarAccessoryRight.layoutAttribute = .trailing
    
    window.toolbar = NSToolbar()
    window.toolbar?.displayMode = .iconOnly
    window.addTitlebarAccessoryViewController(titlebarAccessory)
    window.addTitlebarAccessoryViewController(titlebarAccessoryRight)

0

https://developer.apple.com/documentation/uikit/uititlebar

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: windowScene)

        if let titlebar = windowScene.titlebar {

            //toolbar
            let identifier = NSToolbar.Identifier(toolbarIdentifier)
            let toolbar = NSToolbar(identifier: identifier)
            toolbar.allowsUserCustomization = true
            toolbar.centeredItemIdentifier = NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier)
            titlebar.toolbar = toolbar
            titlebar.toolbar?.delegate = self

            titlebar.titleVisibility = .hidden
            titlebar.autoHidesToolbarInFullScreen = true
        }

        window.makeKeyAndVisible()

    }

#if targetEnvironment(macCatalyst)
let toolbarIdentifier = "com.example.apple-samplecode.toolbar"
let centerToolbarIdentifier = "com.example.apple-samplecode.centerToolbar"
let addToolbarIdentifier = "com.example.apple-samplecode.add"

extension SceneDelegate: NSToolbarDelegate {

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        if itemIdentifier == NSToolbarItem.Identifier(rawValue: toolbarIdentifier) {
            let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: toolbarIdentifier), titles: ["Solver", "Resistance", "Settings"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))

            group.setSelected(true, at: 0)

            return group
        }

        if itemIdentifier == NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier) {
            let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), titles: ["Solver1", "Resistance1", "Settings1"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))

            group.setSelected(true, at: 0)

            return group
        }

        if itemIdentifier == NSToolbarItem.Identifier(rawValue: addToolbarIdentifier) {
            let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(self.add(sender:)))
            let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem)
            return button
        }

        return nil
    }

    @objc func toolbarGroupSelectionChanged(sender: NSToolbarItemGroup) {
        print("selection changed to index: \(sender.selectedIndex)")
    }

    @objc func add(sender: UIBarButtonItem) {
        print("add clicked")
    }

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        [NSToolbarItem.Identifier(rawValue: toolbarIdentifier), NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), NSToolbarItem.Identifier.flexibleSpace,
            NSToolbarItem.Identifier(rawValue: addToolbarIdentifier),
            NSToolbarItem.Identifier(rawValue: addToolbarIdentifier)]
    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        self.toolbarDefaultItemIdentifiers(toolbar)
    }
    
}
#endif

1
这个答案可以通过在问题中解释如何解决OP的问题来改进。仅有代码的答案通常被认为是Stack Overflow上质量较低的答案,而仅有链接也不足以作为解释(链接可能随着时间的推移而失效)。 - codewario
这个解决方案适用于Mac Catalyst。问题是关于一个macOS应用程序。 - soundflix

0
受您第一次尝试的启发,我也成功地得到了一个工具栏。由于我在其中使用了Divider(),所以您的Paddings对我来说效果不佳。

Picture showing wrong alignment

这个似乎在不同的布局尺寸下运行得更加流畅:
 let titlebarAccessoryView = TitlebarAccessory().padding([.leading, .trailing], 10).edgesIgnoringSafeArea(.top)

    let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
    accessoryHostingView.frame.size.height = accessoryHostingView.fittingSize.height+16
    accessoryHostingView.frame.size.width = accessoryHostingView.fittingSize.width

Result with the right paddings

也许有更顺畅的方法来消除这个+16和填充尾部和前导(除了fittingSize还有其他几个选项),但我没有找到任何一个看起来很好而不添加数值的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接