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,比如按钮、文本框、图像视图等。例如 NSTextFieldNSButton 等 macOS 视图组件。

NSViewRepresentable 协议要求你实现两个方法:

  1. makeNSView(context: Context) -> NSViewType
  2. 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 的组成部分:

  1. environment: SwiftUI 环境变量的集合。你可以通过 context.environment 来访问当前视图的环境值,例如颜色方案、尺寸类别等。这允许你在 NSView 中根据 SwiftUI 环境值做出相应的调整。

    常用的环境值可以包括:

    • .colorScheme: 检查当前是否处于深色模式或浅色模式。
    • .sizeCategory: 确定用户是否使用了较大的字体或更高的可访问性文本设置。
  2. coordinator: Coordinator 是你自定义的一个类,用于在 SwiftUI 和 AppKit 之间处理更复杂的交互,例如代理或事件处理。context.coordinator 可以让你访问这个协调器,以便在 SwiftUI 和 AppKit 之间传递状态和操作。

NSViewControllerRepresentable

NSViewControllerRepresentable用于将完整的NSViewController嵌入到 SwiftUI 中。这适用于复杂场景,如需要NSViewController管理多个视图、处理控制逻辑或导航。它特别适合需要管理视图层次结构、处理复杂逻辑或生命周期事件的情况。此协议管理NSViewController及其视图,包括生命周期方法(如viewDidLoadviewWillAppear等)。

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的主要好处是:

  1. 你可以在老的 AppKit 项目中逐步引入 SwiftUI。
  2. 你可以用 SwiftUI 设计新的界面部分,同时保留现有的 AppKit 结构。
  3. 你可以同时享受 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,还管理视图的生命周期,比如 viewDidAppearviewWillDisappear 等。

AppKit 中使用 SwiftUI View 还有2个枚举类:

1 NSHostingSizingOptions

NSHostingSizingOptions 是一个枚举类型,用于控制 NSHostingViewNSHostingController 在 macOS 上如何根据其 SwiftUI 内容调整大小。它提供了不同的选项,帮助你确定 SwiftUI 视图是按照其内容大小自动调整,还是保持固定尺寸。这在处理自适应布局或弹性界面时非常有用。它有三个选项:

  1. .preferredContentSize: 这个选项让 NSHostingViewNSHostingController 的大小匹配 SwiftUI 视图的内容大小。这意味着 SwiftUI 视图的尺寸会根据其内部内容动态变化。
  2. .minSize: 设置 SwiftUI 视图的最小大小。当 NSHostingViewNSHostingController 包含内容时,视图将不会比内容指定的最小尺寸小。
  3. .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 枚举有三个选项,分别为 alltitletoolbars,这些选项用来控制 SwiftUI 与 AppKit 场景之间在标题和工具栏方面的桥接行为。通过 NSHostingSceneBridgingOptions,你可以细粒度地控制 SwiftUI 和 AppKit 场景之间的交互,确保混合界面应用中的一致性和流畅体验。下面我会详细解释每个选项的作用及其应用场景。

  1. .all
  • 含义: 表示将 SwiftUI 和 AppKit 之间的所有场景元素(包括标题和工具栏)进行桥接。这意味着 SwiftUI 场景中的标题和工具栏会和 AppKit 场景同步,互相反映任何变化。
  • 应用场景: 你希望 SwiftUI 和 AppKit 的窗口元素(如标题栏和工具栏)保持完全一致时,可以使用此选项。例如,在 AppKit 应用中使用 SwiftUI 管理窗口,但希望这些窗口与原生 AppKit 窗口的表现一致。
  1. .title
  • 含义: 只桥接 SwiftUI 场景的标题和 AppKit 场景的标题。如果 SwiftUI 界面中的标题发生变化,那么 AppKit 窗口的标题会自动同步更新。
  • 应用场景: 当你在 SwiftUI 场景中展示的内容需要动态调整窗口标题时,此选项会非常有用。它确保 AppKit 窗口的标题会反映 SwiftUI 界面中的标题变化。
  1. .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 场景之间的标题和工具栏桥接。


today
890 声望41 粉丝