SwiftUI 已经迭代了好几个版本,但是目前很多的功能只用 SwiftUI 还是无法实现,需要使用 AppKit 中的功能实现,SwiftUI 的macOS view不够的时候,要用之前的 AppKit 中的 组件,需要把 AppKit 的view或 Controller 镶嵌到 SwiftUI 视图中,SwiftUI 的窗口管理功能不够,要用NSWindow,这时需要把 SwiftUI 中的View 用到 AppKit 中,总的来说分2种:1.SwiftUI view 中使用 AppKit 的 NSView 或 NSController 2. Appkit 的 视图或控制器中使用 是SwiftUI 的View
1. 在SwiftUI 中使用 AppKit中NSView 或NSController
NSViewRepresentable
NSViewRepresentable
用于将 NSView
嵌入到 SwiftUI 中,用于简单的 AppKit 控件嵌入 SwiftUI,比如按钮、文本框、图像视图等。例如 NSTextField
、NSButton
等 macOS 视图组件。
NSViewRepresentable
协议要求你实现两个方法:
makeNSView(context: Context) -> NSViewType
updateNSView(_ nsView: NSViewType, context: Context)
import AppKit
import SwiftUI
// 自定义 NSViewRepresentable 来封装 NSButton
struct NSButtonWrapper: NSViewRepresentable {
var title: String
var action: () -> Void
// 创建 NSButton
func makeNSView(context: Context) -> NSButton {
let button = NSButton(title: title, target: context.coordinator, action: #selector(Coordinator.buttonPressed))
button.bezelStyle = .rounded
button.wantsLayer = true // 允许修改背景颜色等
// 设置背景颜色和其他外观
button.layer?.backgroundColor = NSColor.systemBlue.cgColor
button.layer?.cornerRadius = 10
button.frame.size = CGSize(width: 200, height: 50)
return button
}
// 更新 NSButton 的属性
func updateNSView(_ nsView: NSButton, context: Context) {
nsView.title = title
}
// Coordinator 用于处理按钮的点击事件
class Coordinator: NSObject {
var parent: NSButtonWrapper
init(parent: NSButtonWrapper) {
self.parent = parent
}
@objc func buttonPressed() {
parent.action()
}
}
// 创建 Coordinator
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
}
// 在 SwiftUI 中使用 NSButton
NSButtonWrapper(title: "Click Me") {
print("Button Clicked!")
}
.frame(width: 300, height: 60)
其中,Context
类型实际上是 NSViewRepresentableContext
,它包含了一些对你有用的信息和功能,例如 SwiftUI 的环境(environment
),以及用于协调 SwiftUI 和 AppKit 的 Coordinator
。
NSViewRepresentableContext
的组成部分:
environment
: SwiftUI 环境变量的集合。你可以通过context.environment
来访问当前视图的环境值,例如颜色方案、尺寸类别等。这允许你在NSView
中根据 SwiftUI 环境值做出相应的调整。常用的环境值可以包括:
.colorScheme
: 检查当前是否处于深色模式或浅色模式。.sizeCategory
: 确定用户是否使用了较大的字体或更高的可访问性文本设置。
coordinator
:Coordinator
是你自定义的一个类,用于在 SwiftUI 和 AppKit 之间处理更复杂的交互,例如代理或事件处理。context.coordinator
可以让你访问这个协调器,以便在 SwiftUI 和 AppKit 之间传递状态和操作。
NSViewControllerRepresentable
NSViewControllerRepresentable
用于将完整的NSViewController
嵌入到 SwiftUI 中。这适用于复杂场景,如需要NSViewController
管理多个视图、处理控制逻辑或导航。它特别适合需要管理视图层次结构、处理复杂逻辑或生命周期事件的情况。此协议管理NSViewController
及其视图,包括生命周期方法(如viewDidLoad
、viewWillAppear
等)。
import SwiftUI
import AppKit
// 1. 创建自定义 NSViewController
class MyCustomViewController: NSViewController {
override func loadView() {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
let label = NSTextField(labelWithString: "Hello from NSViewController!")
label.frame = NSRect(x: 20, y: 20, width: 200, height: 20)
view.addSubview(label)
self.view = view
}
}
// 2. 使用 NSViewControllerRepresentable 将 NSViewController 嵌入 SwiftUI
struct MyCustomViewControllerRepresentable: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> MyCustomViewController {
return MyCustomViewController()
}
func updateNSViewController(_ nsViewController: MyCustomViewController, context: Context) {
// 更新 NSViewController 的逻辑
}
}
// 3. 在 SwiftUI 中使用 MyCustomViewControllerRepresentable
struct ContentView: View {
var body: some View {
MyCustomViewControllerRepresentable()
.frame(width: 300, height: 200)
}
}
SwiftUI 通过布局修饰符(如 .frame()、.padding()、.offset() 等)来管理视图的大小和位置。因此,应该避免手动修改 NSView 的 frame 和 bounds,而是通过 SwiftUI 的布局机制进行调整。如果手动修改了 NSView 的 frame 或 bounds,就会和 SwiftUI 自己的布局计算冲突,导致布局行为不可预测的结果(即“未定义行为”)。
import SwiftUI
import AppKit
// 1. 创建自定义 NSViewController
class MyCustomViewController: NSViewController {
override func loadView() {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
let label = NSTextField(labelWithString: "Hello from NSViewController!")
label.frame = NSRect(x: 20, y: 20, width: 200, height: 20)
view.addSubview(label)
self.view = view
}
}
// 2. 使用 NSViewControllerRepresentable 将 NSViewController 嵌入 SwiftUI
struct MyCustomViewControllerRepresentable: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> MyCustomViewController {
return MyCustomViewController()
}
func updateNSViewController(_ nsViewController: MyCustomViewController, context: Context) {
// 更新 NSViewController 的逻辑
}
}
// 3. 在 SwiftUI 中使用 MyCustomViewControllerRepresentable
struct ContentView: View {
var body: some View {
MyCustomViewControllerRepresentable()
.frame(width: 300, height: 200)
}
}
其中makeNSViewController 和 updateNSViewController 中的 Context 是 NSViewControllerRepresentableContext ,他有2个部分组成:
environment
:SwiftUI 的环境变量集合,包含当前视图的一些全局设置。例如,深色模式、可访问性设置、布局方向等。这允许你根据 SwiftUI 的环境状态来动态调整NSViewController
的行为和外观。coordinator
:自定义的Coordinator
,用于处理 SwiftUI 和 AppKit 之间更复杂的交互。通过context.coordinator
,可以管理代理、事件处理、状态传递等。
2 在appkit 中使用 SwiftUI view
在 AppKit 中使用 SwiftUI View 对应 view 和controller有两个分别是NSHostingView 和 NSHostingController ,另外还有一个 NSHostingMenu 来处理菜单
NSHostingController
NSHostingController
是一个工具,用于在 macOS 的 AppKit 应用中使用 SwiftUI 视图。它把 SwiftUI 视图放入一个NSViewController
中,这样你就可以在现有的 AppKit 界面里使用 SwiftUI 设计的界面了。
使用NSHostingController
的主要好处是:
- 你可以在老的 AppKit 项目中逐步引入 SwiftUI。
- 你可以用 SwiftUI 设计新的界面部分,同时保留现有的 AppKit 结构。
- 你可以同时享受 SwiftUI 的简单性和 AppKit 的功能性。
如果你正在开发一个 AppKit 应用,想要慢慢转向使用 SwiftUI,或者只想在某些地方使用 SwiftUI,NSHostingController
就是一个很好的选择。它让你能够保持现有项目的结构,同时利用 SwiftUI 的优势来构建新的界面。
import SwiftUI
import AppKit
// 定义一个简单的 SwiftUI 视图
struct MySwiftUIView: View {
var body: some View {
Text("Hello from SwiftUI!")
.padding()
}
}
// 在 AppKit 应用的某个 NSViewController 中嵌入 SwiftUI 视图
class MyViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 创建 NSHostingController 并传入 SwiftUI 视图
let swiftUIView = MySwiftUIView()
let hostingController = NSHostingController(rootView: swiftUIView)
// 将 NSHostingController 的视图添加为子视图
addChild(hostingController)
view.addSubview(hostingController.view)
// 设置 SwiftUI 视图的布局
hostingController.view.frame = view.bounds
hostingController.view.autoresizingMask = [.width, .height]
}
func updateSwiftUIView() {
// 更新 SwiftUI 视图内容
let updatedView = MySwiftUIView()
hostingController.rootView = updatedView
}
}
其他应用场景- 将 SwiftUI 视图嵌入 NSWindow
你可以直接将 SwiftUI 视图嵌入到 NSWindow
中,而不需要创建整个 NSViewController
。
import SwiftUI
import AppKit
// 定义一个简单的 SwiftUI 视图
struct MySwiftUIView: View {
var body: some View {
Text("SwiftUI in NSWindow")
.padding()
}
}
// 在 NSAppDelegate 中使用
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ notification: Notification) {
let swiftUIView = MySwiftUIView()
// 创建一个 NSHostingController 并将其作为 window 的 contentViewController
let hostingController = NSHostingController(rootView: swiftUIView)
// 创建并配置 NSWindow
window = NSWindow(contentViewController: hostingController)
window.setContentSize(NSSize(width: 400, height: 300))
window.makeKeyAndOrderFront(nil)
}
}
这个例子展示了如何将 SwiftUI 视图直接设置为 NSWindow
的内容视图,而不是通过 NSViewController
。这对想创建完全基于 SwiftUI 的 macOS 界面非常有用。
NSHostingView
NSHostingView
是 SwiftUI 和 AppKit 之间的桥梁之一,允许你在 macOS 应用中将 SwiftUI 视图嵌入到现有的 AppKit 视图层次结构中。与 NSHostingController
类似,NSHostingView
负责将 SwiftUI 视图渲染为 AppKit 的 NSView
,但它直接生成一个 NSView
,而不是通过 NSViewController
来管理视图。NSHostingView
适用于那些你想要直接在现有的 NSView
结构中嵌入 SwiftUI 视图的场景。它更简单,不需要控制器管理视图的生命周期,而是将 SwiftUI 的 View
转换成一个 AppKit 的 NSView
。
代码示例
import SwiftUI
import AppKit
// 创建一个 SwiftUI 视图
struct MySwiftUIView: View {
var body: some View {
Text("Hello from SwiftUI!")
.padding()
}
}
class MyViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 创建 NSHostingView,将 SwiftUI 视图传入
let swiftUIView = MySwiftUIView()
let hostingView = NSHostingView(rootView: swiftUIView)
// 将 NSHostingView 添加为子视图
view.addSubview(hostingView)
// 设置 SwiftUI 视图的布局
hostingView.frame = view.bounds
hostingView.autoresizingMask = [.width, .height]
}
func updateSwiftUIView() {
// 更新 rootView 以更改 SwiftUI 视图内容
hostingView.rootView = MySwiftUIView()
}
}
NSHostingView
vs NSHostingController
NSHostingView
: 适用于将 SwiftUI 视图嵌入到单个 AppKit 视图的场景。它非常轻量,不依赖控制器,因此适合嵌入到已有的复杂NSView
结构中。对于简单的布局嵌入,NSHostingView
提供了一个方便的解决方案,使 SwiftUI 视图能够无缝地融入 AppKit 应用。NSHostingController
: 通常用于需要视图控制器管理 SwiftUI 界面的场景。它不仅生成NSView
,还管理视图的生命周期,比如viewDidAppear
、viewWillDisappear
等。
AppKit 中使用 SwiftUI View 还有2个枚举类:
1 NSHostingSizingOptions
NSHostingSizingOptions
是一个枚举类型,用于控制 NSHostingView
或 NSHostingController
在 macOS 上如何根据其 SwiftUI 内容调整大小。它提供了不同的选项,帮助你确定 SwiftUI 视图是按照其内容大小自动调整,还是保持固定尺寸。这在处理自适应布局或弹性界面时非常有用。它有三个选项:
- .preferredContentSize: 这个选项让
NSHostingView
或NSHostingController
的大小匹配 SwiftUI 视图的内容大小。这意味着 SwiftUI 视图的尺寸会根据其内部内容动态变化。 - .minSize: 设置 SwiftUI 视图的最小大小。当
NSHostingView
或NSHostingController
包含内容时,视图将不会比内容指定的最小尺寸小。 - .maxSize: 设置 SwiftUI 视图的最大大小。即便内容较大,视图的尺寸也不会超过指定的最大大小。
import SwiftUI
import AppKit
// SwiftUI 视图
struct MyDynamicView: View {
var body: some View {
Text("This is a dynamically sized SwiftUI view")
.padding()
}
}
class MyWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// 创建 NSHostingView 并使用 preferredContentSize 调整大小
let hostingView = NSHostingView(rootView: MyDynamicView())
// **自动调整窗口大小**:
hostingView.sizingOptions = .preferredContentSize
//或设置最小和最大大小
hostingView.sizingOptions = [.minSize, .maxSize]
hostingView.minSize = NSSize(width: 200, height: 150)
hostingView.maxSize = NSSize(width: 600, height: 400)
window?.contentView = hostingView
}
}
2 NSHostingSceneBridgingOptions
在 SwiftUI 6 中,NSHostingSceneBridgingOptions
枚举有三个选项,分别为 all
、title
和 toolbars
,这些选项用来控制 SwiftUI 与 AppKit 场景之间在标题和工具栏方面的桥接行为。通过 NSHostingSceneBridgingOptions
,你可以细粒度地控制 SwiftUI 和 AppKit 场景之间的交互,确保混合界面应用中的一致性和流畅体验。下面我会详细解释每个选项的作用及其应用场景。
.all
- 含义: 表示将 SwiftUI 和 AppKit 之间的所有场景元素(包括标题和工具栏)进行桥接。这意味着 SwiftUI 场景中的标题和工具栏会和 AppKit 场景同步,互相反映任何变化。
- 应用场景: 你希望 SwiftUI 和 AppKit 的窗口元素(如标题栏和工具栏)保持完全一致时,可以使用此选项。例如,在 AppKit 应用中使用 SwiftUI 管理窗口,但希望这些窗口与原生 AppKit 窗口的表现一致。
.title
- 含义: 只桥接 SwiftUI 场景的标题和 AppKit 场景的标题。如果 SwiftUI 界面中的标题发生变化,那么 AppKit 窗口的标题会自动同步更新。
- 应用场景: 当你在 SwiftUI 场景中展示的内容需要动态调整窗口标题时,此选项会非常有用。它确保 AppKit 窗口的标题会反映 SwiftUI 界面中的标题变化。
.toolbars
- 含义: 只桥接工具栏。SwiftUI 场景中的工具栏与 AppKit 窗口的工具栏保持同步,任何一个工具栏的变化都会反映在另一个工具栏上。
应用场景: 如果你的 SwiftUI 界面包含了工具栏,而你希望该工具栏与 AppKit 的工具栏整合并保持一致,可以使用此选项。这通常用于你需要统一工具栏行为的应用中,例如在 SwiftUI 中更新或自定义工具栏按钮,同时确保 AppKit 工具栏随之变化。
使用示例
import SwiftUI
import AppKit
class MyWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// 创建一个 SwiftUI 视图
let swiftUIView = MySwiftUIView()
// 创建 NSHostingView 并使用 NSHostingSceneBridgingOptions 进行桥接
let hostingView = NSHostingView(rootView: swiftUIView, bridgingOptions: [.title, .toolbars])
// 将 SwiftUI 视图添加到窗口中
window?.contentView = hostingView
// 设置窗口标题和工具栏
window?.title = "My App Title"
let toolbar = NSToolbar(identifier: "MyToolbar")
window?.toolbar = toolbar
}
}
总结
SwiftUI 和 AppKit 可以互相嵌入使用,主要分为两种情况:SwiftUI 中使用 AppKit 组件,以及 AppKit 中使用 SwiftUI 视图。在 SwiftUI 中使用 AppKit 时,NSViewRepresentable 用于将简单的 NSView 嵌入 SwiftUI,适用于按钮、文本框等基本控件;NSViewControllerRepresentable 用于将完整的 NSViewController 嵌入 SwiftUI,适用于复杂场景和视图层次结构。在 AppKit 中使用 SwiftUI 时,NSHostingController 用于在 AppKit 应用中使用 SwiftUI 视图,将其封装在 NSViewController 中;NSHostingView 则直接将 SwiftUI 视图转换为 NSView,适用于简单嵌入场景。此外,NSHostingSizingOptions 用于控制 SwiftUI 视图在 AppKit 中的大小调整行为,而 NSHostingSceneBridgingOptions 用于控制 SwiftUI 和 AppKit 场景之间的标题和工具栏桥接。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。