原生代码与 Electron:Swift (macOS)
本教程建立在原生代码与 Electron 的通用介绍的基础上,专注于使用 Swift 为 macOS 创建原生插件。
Swift 是一种现代、强大的语言,旨在提供安全性和高性能。虽然你无法像使用 Electron 的 Node.js N-API 那样直接使用 Swift,但你可以使用 Objective-C++ 创建一个桥接,将 Swift 与 Electron 应用程序中的 JavaScript 连接起来。
为了说明如何将原生 macOS 代码嵌入到你的 Electron 应用中,我们将构建一个基本的原生 macOS GUI(使用 SwiftUI),该 GUI 与 Electron 的 JavaScript 进行通信。
本教程对于那些已经熟悉 Objective-C、Swift 和 SwiftUI 开发的人最有帮助。你应该理解 Swift 语法、可选类型、闭包、SwiftUI 视图、属性包装器等基本概念,以及 Objective-C/Swift 互操作机制,例如 @objc 属性和桥接头文件。
如果你还不熟悉这些概念,Apple 的Swift 编程语言指南、SwiftUI 文档和Swift 和 Objective-C 互操作性指南是极好的起点。
要求
与我们原生代码与 Electron 的通用介绍一样,本教程假定你已安装 Node.js 和 npm,以及在 macOS 上编译原生代码所需的基本工具。你需要
- 安装 Xcode(可从 Mac App Store 获取)
- Xcode 命令行工具(可以通过在终端中运行
xcode-select --install来安装)
1) 创建一个包
你可以重用我们在 原生代码和 Electron 教程中创建的包。本教程不会重复其中描述的步骤。让我们首先设置我们的基本插件文件夹结构。
swift-native-addon/
├── binding.gyp # Build configuration
├── include/
│ └── SwiftBridge.h # Objective-C header for the bridge
├── js/
│ └── index.js # JavaScript interface
├── package.json # Package configuration
└── src/
├── SwiftCode.swift # Swift implementation
├── SwiftBridge.m # Objective-C bridge implementation
└── swift_addon.mm # Node.js addon implementation
我们的 package.json 应该如下所示
{
"name": "swift-macos",
"version": "1.0.0",
"description": "A demo module that exposes Swift code to Electron",
"main": "js/index.js",
"scripts": {
"clean": "rm -rf build",
"build-electron": "electron-rebuild",
"build": "node-gyp configure && node-gyp build"
},
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^8.3.0"
},
"devDependencies": {
"node-gyp": "^11.1.0"
}
}
2) 配置构建设置
在我们关注其他原生语言的其他教程中,我们可以使用 node-gyp 来构建所有代码。对于 Swift,情况有点棘手:我们需要先构建,然后链接我们的 Swift 代码。这是因为 Swift 有自己的编译模型和运行时要求,它们不直接集成到 node-gyp 以 C/C++ 为中心的构建系统中。
该过程包括
- 将 Swift 代码单独编译为静态库(.a 文件)
- 创建 Objective-C 桥接,以暴露 Swift 功能
- 将编译后的 Swift 库与我们的 Node.js 插件链接
- 管理 Swift 运行时依赖项
这种两步编译过程确保了 Swift 的高级语言特性和运行时得到正确处理,同时仍允许我们通过 Node.js 的原生插件系统将功能暴露给 JavaScript。
让我们开始添加一个基本结构
{
"targets": [{
"target_name": "swift_addon",
"conditions": [
['OS=="mac"', {
"sources": [
"src/swift_addon.mm",
"src/SwiftBridge.m",
"src/SwiftCode.swift"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include",
"build_swift"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"libraries": [
"<(PRODUCT_DIR)/libSwiftCode.a"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_ENABLE_OBJC_ARC": "YES",
"SWIFT_OBJC_BRIDGING_HEADER": "include/SwiftBridge.h",
"SWIFT_VERSION": "5.0",
"SWIFT_OBJC_INTERFACE_HEADER_NAME": "swift_addon-Swift.h",
"MACOSX_DEPLOYMENT_TARGET": "11.0",
"OTHER_CFLAGS": [
"-ObjC++",
"-fobjc-arc"
],
"OTHER_LDFLAGS": [
"-Wl,-rpath,@loader_path",
"-Wl,-install_name,@rpath/libSwiftCode.a"
],
"HEADER_SEARCH_PATHS": [
"$(SRCROOT)/include",
"$(CONFIGURATION_BUILD_DIR)",
"$(SRCROOT)/build/Release",
"$(SRCROOT)/build_swift"
]
},
"actions": []
}]
]
}]
}
我们包含了 Objective-C++ 文件(sources),指定了必要的 macOS 框架,并设置了 C++ 异常和 ARC。我们还设置了各种 Xcode 标志
GCC_ENABLE_CPP_EXCEPTIONS:在原生代码中启用 C++ 异常处理。CLANG_ENABLE_OBJC_ARC:为 Objective-C 内存管理启用自动引用计数。SWIFT_OBJC_BRIDGING_HEADER:指定桥接 Swift 和 Objective-C 代码的头文件。SWIFT_VERSION:将 Swift 语言版本设置为 5.0。SWIFT_OBJC_INTERFACE_HEADER_NAME:命名自动生成的头文件,该文件将 Swift 代码暴露给 Objective-C。MACOSX_DEPLOYMENT_TARGET:设置所需的最低 macOS 版本(11.0/Big Sur)。OTHER_CFLAGS:其他编译器标志:-ObjC++指定 Objective-C++ 模式。-fobjc-arc在编译器级别启用 ARC。
然后,使用 OTHER_LDFLAGS,我们设置链接器标志
-Wl,-rpath,@loader_path:设置库的运行时搜索路径-Wl,-install_name,@rpath/libSwiftCode.a:配置库的安装名称HEADER_SEARCH_PATHS:编译期间搜索头文件的目录。
你可能还会注意到,我们在 JSON 中添加了一个当前为空的 actions 数组。在下一步中,我们将编译 Swift。
配置 Swift 构建设置
我们将添加两个操作:一个用于编译我们的 Swift 代码(以便进行链接),另一个用于将其复制到一个文件夹以供使用。将上面的 actions 数组替换为以下 JSON
{
// ...other code
"actions": [
{
"action_name": "build_swift",
"inputs": [
"src/SwiftCode.swift"
],
"outputs": [
"build_swift/libSwiftCode.a",
"build_swift/swift_addon-Swift.h"
],
"action": [
"swiftc",
"src/SwiftCode.swift",
"-emit-objc-header-path", "./build_swift/swift_addon-Swift.h",
"-emit-library", "-o", "./build_swift/libSwiftCode.a",
"-emit-module", "-module-name", "swift_addon",
"-module-link-name", "SwiftCode"
]
},
{
"action_name": "copy_swift_lib",
"inputs": [
"<(module_root_dir)/build_swift/libSwiftCode.a"
],
"outputs": [
"<(PRODUCT_DIR)/libSwiftCode.a"
],
"action": [
"sh",
"-c",
"cp -f <(module_root_dir)/build_swift/libSwiftCode.a <(PRODUCT_DIR)/libSwiftCode.a && install_name_tool -id @rpath/libSwiftCode.a <(PRODUCT_DIR)/libSwiftCode.a"
]
}
]
// ...other code
}
这些操作
- 使用
swiftc将 Swift 代码编译为静态库 - 从 Swift 代码生成 Objective-C 头文件
- 将编译后的 Swift 库复制到输出目录
- 使用
install_name_tool修复库路径,通过设置正确的安装名称来确保动态链接器可以在运行时找到该库
3) 创建 Objective-C 桥接头文件
我们需要设置一个桥梁,连接 Swift 代码和原生的 Node.js C++ 插件。让我们从在 include/SwiftBridge.h 中创建一个头文件开始
#ifndef SwiftBridge_h
#define SwiftBridge_h
#import <Foundation/Foundation.h>
@interface SwiftBridge : NSObject
+ (NSString*)helloWorld:(NSString*)input;
+ (void)helloGui;
+ (void)setTodoAddedCallback:(void(^)(NSString* todoJson))callback;
+ (void)setTodoUpdatedCallback:(void(^)(NSString* todoJson))callback;
+ (void)setTodoDeletedCallback:(void(^)(NSString* todoId))callback;
@end
#endif
此头文件定义了我们将用于桥接 Swift 代码和 Node.js 插件的 Objective-C 接口。它包括
- 一个简单的
helloWorld方法,它接受一个字符串输入并返回一个字符串 - 一个
helloGui方法,它将显示一个原生的 SwiftUI 界面 - 用于设置待办事项操作(添加、更新、删除)回调的方法
4) 实现 Objective-C 桥接
现在,让我们在 src/SwiftBridge.m 中创建 Objective-C 桥接本身
#import "SwiftBridge.h"
#import "swift_addon-Swift.h"
#import <Foundation/Foundation.h>
@implementation SwiftBridge
static void (^todoAddedCallback)(NSString*);
static void (^todoUpdatedCallback)(NSString*);
static void (^todoDeletedCallback)(NSString*);
+ (NSString*)helloWorld:(NSString*)input {
return [SwiftCode helloWorld:input];
}
+ (void)helloGui {
[SwiftCode helloGui];
}
+ (void)setTodoAddedCallback:(void(^)(NSString*))callback {
todoAddedCallback = callback;
[SwiftCode setTodoAddedCallback:callback];
}
+ (void)setTodoUpdatedCallback:(void(^)(NSString*))callback {
todoUpdatedCallback = callback;
[SwiftCode setTodoUpdatedCallback:callback];
}
+ (void)setTodoDeletedCallback:(void(^)(NSString*))callback {
todoDeletedCallback = callback;
[SwiftCode setTodoDeletedCallback:callback];
}
@end
这个桥接
- 导入 Swift 生成的头文件(
swift_addon-Swift.h) - 实现头文件中定义的方法
- 简单地将调用转发给 Swift 代码
- 将回调存储在静态变量中以供以后使用,从而允许它们在应用程序的整个生命周期中持久存在。这确保了当待办事项被添加、更新或删除时,可以随时调用 JavaScript 回调。
5) 实现 Swift 代码
现在,让我们在 src/SwiftCode.swift 中实现我们的 Objective-C 代码。这是我们将使用 SwiftUI 创建原生 macOS GUI 的地方。
为了使本教程更容易理解,我们将从基本结构开始,然后逐步添加功能。
配置基本结构
让我们从基本结构开始。这里,我们只是设置变量、一些基本的回调方法和一个简单的辅助方法,我们稍后将用它来将数据转换为准备好进入 JavaScript 世界的格式。
import Foundation
import SwiftUI
@objc
public class SwiftCode: NSObject {
private static var windowController: NSWindowController?
private static var todoAddedCallback: ((String) -> Void)?
private static var todoUpdatedCallback: ((String) -> Void)?
private static var todoDeletedCallback: ((String) -> Void)?
@objc
public static func helloWorld(_ input: String) -> String {
return "Hello from Swift! You said: \(input)"
}
@objc
public static func setTodoAddedCallback(_ callback: @escaping (String) -> Void) {
todoAddedCallback = callback
}
@objc
public static func setTodoUpdatedCallback(_ callback: @escaping (String) -> Void) {
todoUpdatedCallback = callback
}
@objc
public static func setTodoDeletedCallback(_ callback: @escaping (String) -> Void) {
todoDeletedCallback = callback
}
private static func encodeToJson<T: Encodable>(_ item: T) -> String? {
let encoder = JSONEncoder()
// Encode date as milliseconds since 1970, which is what the JS side expects
encoder.dateEncodingStrategy = .custom { date, encoder in
let milliseconds = Int64(date.timeIntervalSince1970 * 1000)
var container = encoder.singleValueContainer()
try container.encode(milliseconds)
}
guard let jsonData = try? encoder.encode(item),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
// More code to follow...
}
我们 Swift 代码的第一部分
- 声明一个带有
@objc属性的类,使其可以从 Objective-C 访问 - 实现
helloWorld方法 - 添加待办事项操作的回调设置器
- 包含一个辅助方法,用于将 Swift 对象编码为 JSON 字符串
实现 helloGui()
让我们继续 helloGui 方法和 SwiftUI 实现。这是我们开始在屏幕上添加用户界面元素的地方。
// Other code...
@objc
public class SwiftCode: NSObject {
// Other code...
@objc
public static func helloGui() -> Void {
let contentView = NSHostingView(rootView: ContentView(
onTodoAdded: { todo in
if let jsonString = encodeToJson(todo) {
todoAddedCallback?(jsonString)
}
},
onTodoUpdated: { todo in
if let jsonString = encodeToJson(todo) {
todoUpdatedCallback?(jsonString)
}
},
onTodoDeleted: { todoId in
todoDeletedCallback?(todoId.uuidString)
}
))
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 500),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Todo List"
window.contentView = contentView
window.center()
windowController = NSWindowController(window: window)
windowController?.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
}
这个 helloGui 方法
- 创建了一个托管在
NSHostingView中的 SwiftUI 视图。这是一个关键的桥接组件,允许在 AppKit 应用程序中使用 SwiftUI 视图。NSHostingView充当一个容器,它包装我们的 SwiftUIContentView并处理 SwiftUI 的声明式 UI 系统与 AppKit 的命令式 UI 系统之间的转换。这使我们能够利用 SwiftUI 的现代 UI 框架,同时仍然与传统的 macOS 窗口管理系统集成。 - 设置回调以在待办事项发生变化时通知 JavaScript。我们将在稍后设置实际的回调,现在我们只会调用它们(如果存在)。
- 创建并显示一个原生的 macOS 窗口。
- 激活应用程序以将窗口带到前面。
实现待办事项
接下来,我们将定义一个带有 ID、文本和日期的 TodoItem 模型。
// Other code...
@objc
public class SwiftCode: NSObject {
// Other code...
private struct TodoItem: Identifiable, Codable {
let id: UUID
var text: String
var date: Date
init(id: UUID = UUID(), text: String, date: Date) {
self.id = id
self.text = text
self.date = date
}
}
}
实现视图
接下来,我们可以实现实际的视图。Swift 在这方面相当冗长,所以下面的代码如果你是 Swift 新手可能会看起来很吓人。大量的代码掩盖了其简单性——我们只是在设置一些 UI 元素。这里没有什么是 Electron 特有的。
// Other code...
@objc
public class SwiftCode: NSObject {
// Other code...
private struct ContentView: View {
@State private var todos: [TodoItem] = []
@State private var newTodo: String = ""
@State private var newTodoDate: Date = Date()
@State private var editingTodo: UUID?
@State private var editedText: String = ""
@State private var editedDate: Date = Date()
let onTodoAdded: (TodoItem) -> Void
let onTodoUpdated: (TodoItem) -> Void
let onTodoDeleted: (UUID) -> Void
private func todoTextField(_ text: Binding<String>, placeholder: String, maxWidth: CGFloat? = nil) -> some View {
TextField(placeholder, text: text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: maxWidth ?? .infinity)
}
private func todoDatePicker(_ date: Binding<Date>) -> some View {
DatePicker("Due date", selection: date, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
.frame(width: 100)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
var body: some View {
VStack(spacing: 16) {
HStack(spacing: 12) {
todoTextField($newTodo, placeholder: "New todo")
todoDatePicker($newTodoDate)
Button(action: {
if !newTodo.isEmpty {
let todo = TodoItem(text: newTodo, date: newTodoDate)
todos.append(todo)
onTodoAdded(todo)
newTodo = ""
newTodoDate = Date()
}
}) {
Text("Add")
.frame(width: 50)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
List {
ForEach(todos) { todo in
if editingTodo == todo.id {
HStack(spacing: 12) {
todoTextField($editedText, placeholder: "Edit todo", maxWidth: 250)
todoDatePicker($editedDate)
Button(action: {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
let updatedTodo = TodoItem(id: todo.id, text: editedText, date: editedDate)
todos[index] = updatedTodo
onTodoUpdated(updatedTodo)
editingTodo = nil
}
}) {
Text("Save")
.frame(width: 60)
}
}
.padding(.vertical, 4)
} else {
HStack(spacing: 12) {
Text(todo.text)
.lineLimit(1)
.truncationMode(.tail)
Spacer()
Text(todo.date.formatted(date: .abbreviated, time: .shortened))
.foregroundColor(.gray)
Button(action: {
editingTodo = todo.id
editedText = todo.text
editedDate = todo.date
}) {
Image(systemName: "pencil")
}
.buttonStyle(BorderlessButtonStyle())
Button(action: {
todos.removeAll(where: { $0.id == todo.id })
onTodoDeleted(todo.id)
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
.buttonStyle(BorderlessButtonStyle())
}
.padding(.vertical, 4)
}
}
}
}
}
}
}
代码的这一部分
- 创建一个 SwiftUI 视图,其中包含一个用于添加待办事项的表单,包含一个待办事项描述的文本字段、一个用于设置截止日期的日期选择器,以及一个“添加”按钮,该按钮会验证输入、创建一个新的 TodoItem、将其添加到本地数组、触发
onTodoAdded回调以通知 JavaScript,然后重置下一个条目的输入字段。 - 实现一个列表来显示待办事项,并支持编辑和删除功能
- 当待办事项被添加、更新或删除时调用相应回调
最终文件应如下所示
import Foundation
import SwiftUI
@objc
public class SwiftCode: NSObject {
private static var windowController: NSWindowController?
private static var todoAddedCallback: ((String) -> Void)?
private static var todoUpdatedCallback: ((String) -> Void)?
private static var todoDeletedCallback: ((String) -> Void)?
@objc
public static func helloWorld(_ input: String) -> String {
return "Hello from Swift! You said: \(input)"
}
@objc
public static func setTodoAddedCallback(_ callback: @escaping (String) -> Void) {
todoAddedCallback = callback
}
@objc
public static func setTodoUpdatedCallback(_ callback: @escaping (String) -> Void) {
todoUpdatedCallback = callback
}
@objc
public static func setTodoDeletedCallback(_ callback: @escaping (String) -> Void) {
todoDeletedCallback = callback
}
private static func encodeToJson<T: Encodable>(_ item: T) -> String? {
let encoder = JSONEncoder()
// Encode date as milliseconds since 1970, which is what the JS side expects
encoder.dateEncodingStrategy = .custom { date, encoder in
let milliseconds = Int64(date.timeIntervalSince1970 * 1000)
var container = encoder.singleValueContainer()
try container.encode(milliseconds)
}
guard let jsonData = try? encoder.encode(item),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
@objc
public static func helloGui() -> Void {
let contentView = NSHostingView(rootView: ContentView(
onTodoAdded: { todo in
if let jsonString = encodeToJson(todo) {
todoAddedCallback?(jsonString)
}
},
onTodoUpdated: { todo in
if let jsonString = encodeToJson(todo) {
todoUpdatedCallback?(jsonString)
}
},
onTodoDeleted: { todoId in
todoDeletedCallback?(todoId.uuidString)
}
))
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 500),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Todo List"
window.contentView = contentView
window.center()
windowController = NSWindowController(window: window)
windowController?.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
private struct TodoItem: Identifiable, Codable {
let id: UUID
var text: String
var date: Date
init(id: UUID = UUID(), text: String, date: Date) {
self.id = id
self.text = text
self.date = date
}
}
private struct ContentView: View {
@State private var todos: [TodoItem] = []
@State private var newTodo: String = ""
@State private var newTodoDate: Date = Date()
@State private var editingTodo: UUID?
@State private var editedText: String = ""
@State private var editedDate: Date = Date()
let onTodoAdded: (TodoItem) -> Void
let onTodoUpdated: (TodoItem) -> Void
let onTodoDeleted: (UUID) -> Void
private func todoTextField(_ text: Binding<String>, placeholder: String, maxWidth: CGFloat? = nil) -> some View {
TextField(placeholder, text: text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(maxWidth: maxWidth ?? .infinity)
}
private func todoDatePicker(_ date: Binding<Date>) -> some View {
DatePicker("Due date", selection: date, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
.frame(width: 100)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
var body: some View {
VStack(spacing: 16) {
HStack(spacing: 12) {
todoTextField($newTodo, placeholder: "New todo")
todoDatePicker($newTodoDate)
Button(action: {
if !newTodo.isEmpty {
let todo = TodoItem(text: newTodo, date: newTodoDate)
todos.append(todo)
onTodoAdded(todo)
newTodo = ""
newTodoDate = Date()
}
}) {
Text("Add")
.frame(width: 50)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
List {
ForEach(todos) { todo in
if editingTodo == todo.id {
HStack(spacing: 12) {
todoTextField($editedText, placeholder: "Edit todo", maxWidth: 250)
todoDatePicker($editedDate)
Button(action: {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
let updatedTodo = TodoItem(id: todo.id, text: editedText, date: editedDate)
todos[index] = updatedTodo
onTodoUpdated(updatedTodo)
editingTodo = nil
}
}) {
Text("Save")
.frame(width: 60)
}
}
.padding(.vertical, 4)
} else {
HStack(spacing: 12) {
Text(todo.text)
.lineLimit(1)
.truncationMode(.tail)
Spacer()
Text(todo.date.formatted(date: .abbreviated, time: .shortened))
.foregroundColor(.gray)
Button(action: {
editingTodo = todo.id
editedText = todo.text
editedDate = todo.date
}) {
Image(systemName: "pencil")
}
.buttonStyle(BorderlessButtonStyle())
Button(action: {
todos.removeAll(where: { $0.id == todo.id })
onTodoDeleted(todo.id)
}) {
Image(systemName: "trash")
.foregroundColor(.red)
}
.buttonStyle(BorderlessButtonStyle())
}
.padding(.vertical, 4)
}
}
}
}
}
}
}
6) 创建 Node.js 插件桥接
我们现在有了工作的 Objective-C 代码,它又能够调用工作的 Swift 代码。为了确保它能够从 JavaScript 世界中安全、正确地调用,我们需要构建一个 Objective-C 和 C++ 之间的桥梁,我们可以用 Objective-C++ 来完成。我们将在 src/swift_addon.mm 中这样做。
#import <Foundation/Foundation.h>
#import "SwiftBridge.h"
#include <napi.h>
class SwiftAddon : public Napi::ObjectWrap<SwiftAddon> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "SwiftAddon", {
InstanceMethod("helloWorld", &SwiftAddon::HelloWorld),
InstanceMethod("helloGui", &SwiftAddon::HelloGui),
InstanceMethod("on", &SwiftAddon::On)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("SwiftAddon", func);
return exports;
}
// More code to follow...
这第一部分
- 定义一个继承自
Napi::ObjectWrap的 C++ 类 - 创建一个静态
Init方法来将我们的类注册到 Node.js - 定义三个方法:
helloWorld、helloGui和on
回调机制
接下来,让我们实现回调机制
// Previous code...
struct CallbackData {
std::string eventType;
std::string payload;
SwiftAddon* addon;
};
SwiftAddon(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<SwiftAddon>(info)
, env_(info.Env())
, emitter(Napi::Persistent(Napi::Object::New(info.Env())))
, callbacks(Napi::Persistent(Napi::Object::New(info.Env())))
, tsfn_(nullptr) {
napi_status status = napi_create_threadsafe_function(
env_,
nullptr,
nullptr,
Napi::String::New(env_, "SwiftCallback"),
0,
1,
nullptr,
nullptr,
this,
[](napi_env env, napi_value js_callback, void* context, void* data) {
auto* callbackData = static_cast<CallbackData*>(data);
if (!callbackData) return;
Napi::Env napi_env(env);
Napi::HandleScope scope(napi_env);
auto addon = static_cast<SwiftAddon*>(context);
if (!addon) {
delete callbackData;
return;
}
try {
auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
if (callback.IsFunction()) {
callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
}
} catch (...) {}
delete callbackData;
},
&tsfn_
);
if (status != napi_ok) {
Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
return;
}
这一部分
- 定义一个结构体来在线程之间传递数据
- 设置插件的构造函数
- 创建线程安全函数来处理来自 Swift 的回调
让我们继续设置 Swift 回调
// Previous code...
auto makeCallback = [this](const char* eventType) {
return ^(NSString* payload) {
if (tsfn_ != nullptr) {
auto* data = new CallbackData{
eventType,
std::string([payload UTF8String]),
this
};
napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
}
};
};
[SwiftBridge setTodoAddedCallback:makeCallback("todoAdded")];
[SwiftBridge setTodoUpdatedCallback:makeCallback("todoUpdated")];
[SwiftBridge setTodoDeletedCallback:makeCallback("todoDeleted")];
}
~SwiftAddon() {
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
tsfn_ = nullptr;
}
}
这一部分
- 创建一个辅助函数来生成 Objective-C 块,这些块将用作 Swift 事件的回调。这个 lambda 函数
makeCallback接受一个事件类型字符串并返回一个 Objective-C 块,该块捕获事件类型和有效负载。当 Swift 调用此块时,它会创建一个包含事件信息的 CallbackData 结构体,并将其传递给线程安全函数,该函数在 Swift 线程和 Node.js 事件循环之间安全地桥接。 - 设置精心构造的待办事项操作回调
- 实现析构函数以清理资源
实例方法
最后,让我们实现实例方法
// Previous code...
private:
Napi::Env env_;
Napi::ObjectReference emitter;
Napi::ObjectReference callbacks;
napi_threadsafe_function tsfn_;
Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}
std::string input = info[0].As<Napi::String>();
NSString* nsInput = [NSString stringWithUTF8String:input.c_str()];
NSString* result = [SwiftBridge helloWorld:nsInput];
return Napi::String::New(env, [result UTF8String]);
}
void HelloGui(const Napi::CallbackInfo& info) {
[SwiftBridge helloGui];
}
Napi::Value On(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) {
Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return SwiftAddon::Init(env, exports);
}
NODE_API_MODULE(swift_addon, Init)
这最后一部分做了几件事
- 代码定义了环境、事件发射器、回调存储和线程安全函数私有成员变量,这些变量对于插件的运行至关重要。
- HelloWorld 方法的实现接受来自 JavaScript 的字符串输入,将其传递给 Swift 代码,并将处理后的结果返回给 JavaScript 环境。
HelloGui方法的实现提供了一个简单的包装器,它调用 Swift UI 创建函数来显示原生的 macOS 窗口。On方法的实现允许 JavaScript 代码注册将在原生 Swift 代码中发生特定事件时调用的回调函数。- 代码设置了模块初始化过程,该过程将插件注册到 Node.js 并使其功能可供 JavaScript 使用。
最终完整的 src/swift_addon.mm 应如下所示
#import <Foundation/Foundation.h>
#import "SwiftBridge.h"
#include <napi.h>
class SwiftAddon : public Napi::ObjectWrap<SwiftAddon> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "SwiftAddon", {
InstanceMethod("helloWorld", &SwiftAddon::HelloWorld),
InstanceMethod("helloGui", &SwiftAddon::HelloGui),
InstanceMethod("on", &SwiftAddon::On)
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("SwiftAddon", func);
return exports;
}
struct CallbackData {
std::string eventType;
std::string payload;
SwiftAddon* addon;
};
SwiftAddon(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<SwiftAddon>(info)
, env_(info.Env())
, emitter(Napi::Persistent(Napi::Object::New(info.Env())))
, callbacks(Napi::Persistent(Napi::Object::New(info.Env())))
, tsfn_(nullptr) {
napi_status status = napi_create_threadsafe_function(
env_,
nullptr,
nullptr,
Napi::String::New(env_, "SwiftCallback"),
0,
1,
nullptr,
nullptr,
this,
[](napi_env env, napi_value js_callback, void* context, void* data) {
auto* callbackData = static_cast<CallbackData*>(data);
if (!callbackData) return;
Napi::Env napi_env(env);
Napi::HandleScope scope(napi_env);
auto addon = static_cast<SwiftAddon*>(context);
if (!addon) {
delete callbackData;
return;
}
try {
auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
if (callback.IsFunction()) {
callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
}
} catch (...) {}
delete callbackData;
},
&tsfn_
);
if (status != napi_ok) {
Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
return;
}
auto makeCallback = [this](const char* eventType) {
return ^(NSString* payload) {
if (tsfn_ != nullptr) {
auto* data = new CallbackData{
eventType,
std::string([payload UTF8String]),
this
};
napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
}
};
};
[SwiftBridge setTodoAddedCallback:makeCallback("todoAdded")];
[SwiftBridge setTodoUpdatedCallback:makeCallback("todoUpdated")];
[SwiftBridge setTodoDeletedCallback:makeCallback("todoDeleted")];
}
~SwiftAddon() {
if (tsfn_ != nullptr) {
napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
tsfn_ = nullptr;
}
}
private:
Napi::Env env_;
Napi::ObjectReference emitter;
Napi::ObjectReference callbacks;
napi_threadsafe_function tsfn_;
Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}
std::string input = info[0].As<Napi::String>();
NSString* nsInput = [NSString stringWithUTF8String:input.c_str()];
NSString* result = [SwiftBridge helloWorld:nsInput];
return Napi::String::New(env, [result UTF8String]);
}
void HelloGui(const Napi::CallbackInfo& info) {
[SwiftBridge helloGui];
}
Napi::Value On(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) {
Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
return env.Undefined();
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return SwiftAddon::Init(env, exports);
}
NODE_API_MODULE(swift_addon, Init)
6) 创建 JavaScript 包装器
你离成功非常近了!我们现在有了工作的 Objective-C、Swift 和线程安全的方式来将方法和事件暴露给 JavaScript。在最后一步中,让我们在 js/index.js 中创建一个 JavaScript 包装器,以提供更友好的 API
const EventEmitter = require('node:events')
class SwiftAddon extends EventEmitter {
constructor () {
super()
if (process.platform !== 'darwin') {
throw new Error('This module is only available on macOS')
}
const native = require('bindings')('swift_addon')
this.addon = new native.SwiftAddon()
this.addon.on('todoAdded', (payload) => {
this.emit('todoAdded', this.parse(payload))
})
this.addon.on('todoUpdated', (payload) => {
this.emit('todoUpdated', this.parse(payload))
})
this.addon.on('todoDeleted', (payload) => {
this.emit('todoDeleted', this.parse(payload))
})
}
helloWorld (input = '') {
return this.addon.helloWorld(input)
}
helloGui () {
this.addon.helloGui()
}
parse (payload) {
const parsed = JSON.parse(payload)
return { ...parsed, date: new Date(parsed.date) }
}
}
if (process.platform === 'darwin') {
module.exports = new SwiftAddon()
} else {
module.exports = {}
}
这个包装器
- 扩展 EventEmitter 以提供事件支持
- 检查我们是否在 macOS 上运行
- 加载原生插件
- 设置事件监听器并转发它们
- 为我们的函数提供干净的 API
- 解析 JSON 有效负载并将时间戳转换为 JavaScript Date 对象
7) 构建和测试插件
在所有文件都就位后,你可以构建插件。
npm run build
请注意,你无法直接从 Node.js 调用此脚本,因为 Node.js 在 macOS 眼中并未设置“应用程序”。但 Electron 可以,所以你可以通过从 Electron 中要求并调用它来测试你的代码。
结论
你现在已经使用 Swift 和 SwiftUI 构建了一个完整的原生 Node.js macOS 插件。这为在你的 Electron 应用中构建更复杂的 macOS 特定功能奠定了基础,让你两全其美:Web 技术的便捷性与原生 macOS 代码的强大功能。
此处演示的方法允许你
- 配置一个项目结构,该结构桥接 Swift、Objective-C 和 JavaScript
- 使用 SwiftUI 实现 Swift 代码以实现原生 UI
- 创建 Objective-C 桥接以连接 Swift 和 Node.js
- 使用回调和事件设置双向通信
- 配置自定义构建过程来编译 Swift 代码
有关使用 Swift 和 Swift 开发的更多信息,请参阅 Apple 的开发者文档