如何使用 SwiftUI 将工具栏添加到 macOS 应用程序?
How do I add a toolbar to a macOS app using SwiftUI?
我正在尝试使用 SwiftUI 将标题栏内的工具栏添加到 macOS 应用程序,类似于下图所示。
我无法找到使用 SwiftUI 实现此目的的方法。目前,我的视图中有我的工具栏(只有一个文本字段),但我想将它移到标题栏中。
我当前的代码:
struct TestView: View {
var body: some View {
VStack {
TextField("Placeholder", text: .constant("")).padding()
Spacer()
}
}
}
因此,就我而言,我需要在工具栏中包含文本字段。
方法一:
这是通过添加标题栏附件来完成的。我能够通过修改 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 来完成应用程序的其余部分。这是通过创建一个以故事板作为用户界面的新应用程序来完成的。然后转到故事板并删除默认的 View Controller 并添加一个新的 NSHostingController
。通过设置关系将新添加的 Hosting Controller 连接到主 window。使用界面生成器将工具栏添加到 window。
将自定义 class 附加到您的 NSHostingController
并将您的 SwiftUI 视图加载到其中。
示例代码如下:
import Cocoa
import SwiftUI
class HostingController: NSHostingController<SwiftUIView> {
@objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: SwiftUIView())
}
}
使用这种方法还可以让您自定义工具栏。
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
从 macOS 11 开始,您可能希望使用新的 API,如 WWDC Session 10104 as the new standard. Explicit code examples were provided in 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() {}
}
受您第一种方法的启发,我也设法获得了一个工具栏。当我在其中使用 Divider()s 时,您的 Paddings 对我来说效果不佳。
这个似乎在不同的布局尺寸下工作得更顺畅一些:
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),但我找不到任何不添加数值的看起来不错的方法.
我终于设法做到了这一点,没有任何繁琐的填充,而且在全屏下看起来也很棒。另外,以前的解决方案不允许水平调整大小。
将您的标题视图包裹在 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 设置为顶部。这将允许它随着 window 大小的变化而水平调整大小(前导和左侧将宽度固定为最小值,这让我很痛苦地寻找这个问题的答案。
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 实现此目的的方法。目前,我的视图中有我的工具栏(只有一个文本字段),但我想将它移到标题栏中。
我当前的代码:
struct TestView: View {
var body: some View {
VStack {
TextField("Placeholder", text: .constant("")).padding()
Spacer()
}
}
}
因此,就我而言,我需要在工具栏中包含文本字段。
方法一:
这是通过添加标题栏附件来完成的。我能够通过修改 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 来完成应用程序的其余部分。这是通过创建一个以故事板作为用户界面的新应用程序来完成的。然后转到故事板并删除默认的 View Controller 并添加一个新的 NSHostingController
。通过设置关系将新添加的 Hosting Controller 连接到主 window。使用界面生成器将工具栏添加到 window。
将自定义 class 附加到您的 NSHostingController
并将您的 SwiftUI 视图加载到其中。
示例代码如下:
import Cocoa
import SwiftUI
class HostingController: NSHostingController<SwiftUIView> {
@objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: SwiftUIView())
}
}
使用这种方法还可以让您自定义工具栏。
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
从 macOS 11 开始,您可能希望使用新的 API,如 WWDC Session 10104 as the new standard. Explicit code examples were provided in 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() {}
}
受您第一种方法的启发,我也设法获得了一个工具栏。当我在其中使用 Divider()s 时,您的 Paddings 对我来说效果不佳。
这个似乎在不同的布局尺寸下工作得更顺畅一些:
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),但我找不到任何不添加数值的看起来不错的方法.
我终于设法做到了这一点,没有任何繁琐的填充,而且在全屏下看起来也很棒。另外,以前的解决方案不允许水平调整大小。
将您的标题视图包裹在 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 设置为顶部。这将允许它随着 window 大小的变化而水平调整大小(前导和左侧将宽度固定为最小值,这让我很痛苦地寻找这个问题的答案。
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)