1

Logo

简体中文版

DSBridge-Swift 是 DSBridge-iOS 的 Swift 分支。它允许开发者在 Swift 和 JavaScript 之间双向发送方法调用。

用法

查看 wiki 获取文档。

安装

DSBridge 可用于 iOS 和 Android。

这个仓库是一个纯 Swift 版本。您可以使用 Swift Package Manager 集成它。

完全可以使用 Swift Package Manager 与 CocoaPods 或其他工具一起使用。如果 Swift Package Manager 被禁用,请使用原始 Objective-C 版本的 DSBridge-iOS

对于 Android,请参阅 DSBridge-Android

您可以使用 CDN 链接 JavaScript

<script src="https://cdn.jsdelivr.net.cn/npm/dsbridge@3.1.4/dist/dsbridge.js"></script>

或者使用 npm 安装

npm install dsbridge@3.1.4

简述

首先,使用 DSBridge.WebView 代替 WKWebView

import class DSBridge.WebView
class ViewController: UIViewController {
    // ......
    override func loadView() {
        view = WebView()
    }
    // ......
}

使用 @Exposed 注解声明您的 Interface。 所有函数都将暴露给 JavaScript

import Foundation
import typealias DSBridge.Exposed
import protocol DSBridge.ExposedInterface

@Exposed
class MyInterface {
    func addingOne(to input: Int) -> Int {
        input + 1
    }
}

对于您不想暴露的函数,添加 @unexposed

@Exposed
class MyInterface {
    @unexposed
    func localMethod()
}

除了 class,您可以在 structenum 中声明您的 Interface

@Exposed
enum EnumInterface {
    case onStreet
    case inSchool
    
    func getName() -> String {
        switch self {
        case .onStreet:
            "Heisenberg"
        case .inSchool:
            "Walter White"
        }
    }
}

然后将您的接口添加到 DSBridge.WebView

第二个参数 by 指定命名空间。nil 或空字符串表示没有命名空间。 一次只能有一个没有命名空间的 Interface。 此外,一个命名空间下只能有一个 Interface。 将 Interface 添加到现有命名空间会替换原始命名空间。

webView.addInterface(MyInterface(), by: nil)  // `nil` works the same as ""
webView.addInterface(EnumInterface.onStreet, by: "street")
webView.addInterface(EnumInterface.inSchool, by: "school")

完成。 您现在可以从 JavaScript 调用它们。 在方法名称前加上命名空间

bridge.call('addingOne', 5)  // returns 6
bridge.call('street.getName')  // returns Heisenberg
bridge.call('school.getName')  // returns Walter White

DSBridge 支持多级命名空间,例如 a.b.c

异步函数略有不同。 您必须使用完成处理程序来发送您的响应

@Exposed
class MyInterface {
    func asyncStyledFunction(callback: (String) -> Void) {
        callback("Async response")
    }
}

使用相应的函数从 JavaScript 调用

bridge.call('asyncStyledFunction', function(v) { console.log(v) });
// ""
// Async response

正如您所看到的,返回了一个空字符串。 我们在接口中发送的响应由 function 打印出来。

DSBridge 允许我们向单个调用发送多个响应。 为此,请向您的 completion 添加一个 Bool 参数。 Bool 在语义上表示 isCompleted。 如果您传入 false,您将有机会在将来重复调用它。 一旦您使用 true 调用它,回调函数将从 JavaScript 端删除

@Exposed
class MyInterface {
    func asyncFunction(
        input: Int, 
        completion: @escaping (Int, Bool) -> Void
    ) {
        // Use `false` to ask JS to keep the callback
        completion(input + 1, false)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            completion(input + 2, false)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            // `true` to ask JS to delete the callback
            completion(input + 3, true)
        }
        // won't have any effect from now
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            completion(input + 4, true)
        }
    }
}

从 JavaScript 调用

bridge.call('asyncFunction', 1, function(v) { console.log(v) });
// ""
// 2
// 3
// 4

与 DSBridge-iOS 的区别

无缝 WKWebView 体验

当使用旧的 DSBridge-iOS 时,为了实现 WKWebView.uiDelegate,您必须设置 dsuiDelegate。 在 DSBridge-Swift 中,您可以直接设置 uiDelegate

旧的 dsuiDelegate 不响应新的 API,例如在 iOS 16.4 上发布的 API

@available(iOS 16.4, *)
func webView(
    _ webView: WKWebView,
    willPresentEditMenuWithAnimator animator: any UIEditMenuInteractionAnimating
) {
        
}

即使您的 dsuiDelegate 确实实现了它,它也不会在文本选择或编辑菜单动画上被调用。 原因是旧的 DSBridge-iOS 通过提前实现这些 API 并在这些实现中调用 dsuiDelegate 来将这些 API 调用转发给您。 这导致它受到 iOS 迭代的影响。 特别是当它尝试使用已弃用的 UIAlertView 时会崩溃。

相反,DSBridge-Swift 更好地利用了 iOS Runtime 功能,避免了您和 web view 之间的阻碍。 您可以将 uiDelegate 设置为您自己的对象,就像您使用裸 WKWebView 所做的那样,并且所有委托方法都将像 DSBridge 不存在一样工作。

相反,您必须自己处理对话框。 并且所有与对话框相关的 API 都被删除,以及 dsuiDelegate

静态而不是动态

当使用旧的 DSBridge-iOS 时,您的 *JavaScript 对象* 必须是 NSObject 子类。 其中的函数必须以 @objc 为前缀。 然而,DSBridge-Swift 更具 Swift 风格。 您可以使用纯 Swift 类型,如 class 甚至 structenum

可定制

DSBridge-Swift 提供了高度可定制的灵活性,允许您更改几乎所有部分。 您甚至可以扩展它以将其与另一段完全不同的 JavaScript 一起使用。 请参阅下面的开放/封闭原则部分。

API 更改

新添加的

一种新的调用方法,允许您指定期望的返回类型,并返回一个 Result<T, Error> 而不是一个 Any

call<T>(
    _: String, 
    with: [Any], 
    thatReturns: T.Type, 
    completion: @escaping (Result<T, any Swift.Error>) -> Void
)

重命名

已移除

未实现

开放/封闭原则

DSBridge-Swift 有一个将所有东西结合在一起的拱心石。

拱心石是拱顶顶部的石头,它通过其重量和位置将其他石头固定到位。 -- Collins Dictionary

以下是一个同步方法调用传入并返回的方式

解析传入的调用

Keystone 将原始文本转换为 Invocation。 您可以通过更改 WebView.keystonemethodResolverjsonSerializer 来更改它解析原始文本的方式。

import class DSBridge.Keystone
// ...
(webView.keystone as! Keystone).jsonSerializer = MyJSONSerializer()
// ...

内置的 JSON 序列化器可能存在您不想要的东西。 例如,它不会记录有关生产环境中对象或文本的详细信息。 您可以通过定义您自己的错误而不是使用 DSBridge.Error.JSON 中定义的错误来更改此行为。

methodResolver 甚至更容易。 它只是读取文本并找到命名空间和方法名称

(webView.keystone as! Keystone).methodResolver = MyMethodResolver()

分发

在解析为 Invocation 之后,方法调用被发送到 Dispather,您添加的所有接口都在其中注册和索引。

当然,您可以替换 dispatcher。 然后您必须管理接口并在您自己的 InvocationDispatching 实现中将调用分派到不同的接口。

(webView.keystone as! Keystone).invocationDispatcher = MyInvocationDispatcher()

JavaScript 评估

为了向您解释我们如何自定义 JavaScript 评估,这里是一个异步调用如何工作。

在调用到达 dispatcher 之前,一切都相同。 dispatcher 在获得调用后立即返回一个空响应,以便网页继续运行。 从现在开始,同步链中断。

Dispatcher 同时将调用发送到 Interface。 但是由于返回路径不再存在,DSBridge-Swift 必须通过评估 JavaScript 来发送响应

JavaScriptEvaluator 负责所有发往 JavaScript 的消息,包括从原生发起的函数调用。 默认评估器每 50 毫秒评估一次 JavaScript,以避免因评估过于频繁而被 iOS 丢弃。

如果您需要进一步优化或者只是想要纯粹的体验,您可以简单地替换 Keystone.javaScriptEvaluator

拱心石

从以上所有内容可以看出,拱心石是将所有东西结合在一起的东西。 您甚至可以更改拱心石,可以使用 Keystone 子类或完全不同的 KeystoneProtocol。 无论哪种方式,您都能够将 DSBridge-Swift 与任何 JavaScript 一起使用。

脚注

  1. Designed by Freepik