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

Posted

技术标签:

【中文标题】如何使用 SwiftUI 向 macOS 应用程序添加工具栏?【英文标题】:How do I add a toolbar to a macOS app using SwiftUI? 【发布时间】:2020-02-29 12:00:30 【问题描述】:

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

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

我当前的代码:

struct TestView: View 
    var body: some View 
        VStack 
            TextField("Placeholder", text: .constant("")).padding()
            Spacer()
        
    

因此,就我而言,我需要在工具栏中包含文本字段。

【问题讨论】:

我说的是 macOS 目标中的 SwiftUI。 否,navigationBarTitle 修饰符在 macOS SwiftUI 中不可用。 目前 SwiftUI 不支持工具栏。如果你真的需要一个(为什么?)然后使用 AppKit 中的一个,它仍然存在......真的。 @Asperi 我能够做到这一点 - 请参阅下面的答案。工具栏(或标题栏附件)仍然在 macOS 应用程序中广泛使用,不是吗? @Asperi 要使用 AppKit 中的那个,您的意思是为 NSToolbar 使用 NSViewRepresentable 吗?如果是这样,我尝试了该方法但没有成功。如果你有这样的解决方案,我很乐意去看看。 【参考方案1】:

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

【讨论】:

非常方便的链接!谢谢 用户无法使用“视图”>“自定义工具栏...”选项来自定义它,对吧? @Linus 你应该能够使用 UIKit 而不是 SwiftUI。事实上,我有一些代码在 NSWindow 的 NSToolbar 中托管 SwiftUI 视图。 @Nooneyouknow ,如果您使用 ToolbarItem 的 showsByDefault 参数,您可以在 SwiftUI 中拥有可自定义的工具栏。【参考方案2】:

方法一:

这是通过添加标题栏附件来完成的。我可以通过修改 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(""))

    

结果:

方法 2(替代方法):

这里的想法是使用故事板来完成工具栏部分,而应用程序的其余部分则使用 SwiftUI。这是通过创建一个以故事板为用户界面的新应用程序来完成的。然后去storyboard删除默认的View Controller,添加一个新的NSHostingController。通过设置关系,将新添加的 Hosting Controller 连接到主窗口。使用界面生成器将工具栏添加到窗口。

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

示例代码如下:

import Cocoa
import SwiftUI

class HostingController: NSHostingController<SwiftUIView> 

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

    


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

【讨论】:

有没有办法通过右键单击并选择“自定义工具栏”使其可编辑? (类似于 Finder、Pages 等) 第一个解决方案,没有。但是,看看我在上面的回复中添加的替代方法。这样,您就可以选择自定义工具栏。 @BijoyThangaraj 如何使用工具栏中的按钮更改SwiftUIViewState 变量?我试过了,但状态没有改变。 @Alan,一种方法是从 WindowController 发布通知,您可以在 HostingController 中观察到该通知。在您的 HostingController 中,您可以持有一个 ObservableObject 并根据收到的通知修改其属性。最后,您可以像这样将这个 observable 对象传递给您的 SwiftUIView:super.init(coder: coder, rootView: AnyView(SwiftUIView().environmentObject(myObservableObject))) 对于方法 1,文本有些错位/错位。有没有办法解决这个问题,让它在视觉上更好?这在某种程度上也发生在按钮上。【参考方案3】:

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

【讨论】:

【参考方案4】:

受您第一种方法的启发,我也设法获得了一个工具栏。由于我在其中使用了 Divider(),因此您的 Paddings 对我来说效果不佳。

对于不同的 Layout-Sizes,这似乎更流畅一些:

 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

也许有一种更流畅的方法可以摆脱 +16 和填充尾随和前导(还有其他几个选项而不是 FittingSize),但是如果不添加数值,我找不到任何看起来不错的方法。

【讨论】:

【参考方案5】:

我终于设法做到了,没有任何繁琐的填充,而且在全屏时看起来也很棒。此外,以前的解决方案不允许水平调整大小。

将您的标题视图包装在 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 设置为顶部。这将允许它随着窗口大小的变化而水平调整大小(前导和左将宽度固定为最小值,这让我在寻找答案时感到很痛苦。

    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)

【讨论】:

以上是关于如何使用 SwiftUI 向 macOS 应用程序添加工具栏?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:如何在 macOS 应用程序中实现编辑菜单

(SwiftUI, MacOS 11.0) 如何在 SecureTextFeild 中检测焦点?

如何在 SwiftUI MacOS App 中使用 NSTouchBar?

SwiftUI:如何在 macOS 11 上设置工具栏的标题?

SwiftUI - 如何在 macOS 上隐藏窗口标题

Swiftui: MacOS App: Fetching Core Data in ContentView OK,但是如何使用 AppDelegate 的结果呢?