原生代码与 Electron:Swift (macOS)
本教程建立在 原生代码和 Electron 的通用介绍 的基础上,重点介绍使用 Swift 为 macOS 创建原生插件。
Swift 是一种现代、强大的语言,专为安全性和性能而设计。虽然您不能直接将 Swift 与 Electron 使用的 Node.js N-API 一起使用,但您可以使用 Objective-C++ 创建一个桥梁,将 Swift 与 Electron 应用程序中的 JavaScript 连接起来。
为了说明如何在 Electron 应用中嵌入 macOS 原生代码,我们将构建一个基本的 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 窗口。
- 激活应用程序以将窗口带到前面。
实现待办事项
接下来,我们将定义一个 TodoItem
模型,包含 ID、文本和日期。
// 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++ 在 Objective-C 和 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('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 中 require 并调用它来测试您的代码。
结论
您现在已经使用 Swift 和 SwiftUI 构建了一个完整的 macOS 原生 Node.js 插件。这为在 Electron 应用中构建更复杂的 macOS 特定功能提供了基础,让您能够兼顾两全其美:Web 技术的便利性与 macOS 原生代码的强大功能。
此处演示的方法使您能够:
- 设置一个桥接 Swift、Objective-C 和 JavaScript 的项目结构。
- 使用 SwiftUI 实现 Swift 代码以实现原生 UI。
- 创建 Objective-C 桥接以连接 Swift 和 Node.js。
- 使用回调和事件设置双向通信。
- 配置自定义构建过程来编译 Swift 代码。
有关使用 Swift 和 Swift 进行开发的更多信息,请参阅 Apple 的开发者文档: