DSBridge-Swift is a DSBridge-iOS fork in Swift. It allows developers to send method calls back and forth between Swift and JavaScript.
Check out wiki for docs.
DSBridge is available on both iOS and Android.
This repo is a pure Swift version. You can integrate it with Swift Package Manager.
It's totally OK to use Swift Package Manager together with CocoaPods or other tools. If Swift Package Manager is banned, use the original Objective-C version DSBridge-iOS.
For Android, see DSBridge-Android.
You can link the JavaScript with CDN:
<script src="https://cdn.jsdelivr.net/npm/dsbridge@3.1.4/dist/dsbridge.js"></script>
Or install with npm:
npm install dsbridge@3.1.4
First of all, use DSBridge.WebView
instead of WKWebView
:
import class DSBridge.WebView
class ViewController: UIViewController {
// ......
override func loadView() {
view = WebView()
}
// ......
}
Declare your Interface
with the @Exposed
annotation. All the functions will be exposed to JavaScript:
import Foundation
import typealias DSBridge.Exposed
import protocol DSBridge.ExposedInterface
@Exposed
class MyInterface {
func addingOne(to input: Int) -> Int {
input + 1
}
}
For functions you do not want to expose, add @unexposed
to it:
@Exposed
class MyInterface {
@unexposed
func localMethod()
}
Aside from class
, you can declare your Interface
in struct
or enum
:
@Exposed
enum EnumInterface {
case onStreet
case inSchool
func getName() -> String {
switch self {
case .onStreet:
"Heisenberg"
case .inSchool:
"Walter White"
}
}
}
You then add your interfaces into DSBridge.WebView
.
The second parameter by
specifies namespace. nil
or an empty string indicates no namespace. There can be only one non-namespaced Interface
at once. Also, there can be only one Interface
under a namespace. Adding an Interface
to an existing namespace replaces the original one.
webView.addInterface(MyInterface(), by: nil) // `nil` works the same as ""
webView.addInterface(EnumInterface.onStreet, by: "street")
webView.addInterface(EnumInterface.inSchool, by: "school")
Done. You can call them from JavaScript now. Do prepend the namespace before the method names:
bridge.call('addingOne', 5) // returns 6
bridge.call('street.getName') // returns Heisenberg
bridge.call('school.getName') // returns Walter White
DSBridge supports multi-level namespaces, like
a.b.c
.
Asynchronous functions are a little bit different. You have to use a completion handler to send your response:
@Exposed
class MyInterface {
func asyncStyledFunction(callback: (String) -> Void) {
callback("Async response")
}
}
Call from JavaScript with a function accordingly:
bridge.call('asyncStyledFunction', function(v) { console.log(v) });
// ""
// Async response
As you can see, there is a empty string returned. The response we sent in the interface is printed by the function
.
DSBridge allows us to send multiple responses to a single invocation. To do so, add a Bool
parameter to your completion. The Bool
means isCompleted
semantically. If you pass in a false
, you get the chance to repeatedly call it in future. Once you call it with true
, the callback function will be deleted from the JavaScript side:
@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)
}
}
}
Call from JavaScript:
bridge.call('asyncFunction', 1, function(v) { console.log(v) });
// ""
// 2
// 3
// 4
When using the old DSBridge-iOS, in order to implement WKWebView.uiDelegate
, you'd have to set dsuiDelegate
instead. In DSBridge-Swift, you can just set uiDelegate
.
The old dsuiDelegate
does not respond to new APIs, such as one that's released on iOS 16.4:
@available(iOS 16.4, *)
func webView(
_ webView: WKWebView,
willPresentEditMenuWithAnimator animator: any UIEditMenuInteractionAnimating
) {
}
Even if your dsuiDelegate
does implement it, it won't get called on text selections or editing menu animations. The reason is that the old DSBridge-iOS relay those API calls to you by implementing them ahead of time and calling dsuiDelegate
inside those implementations. This causes it to suffer from iOS iterations. Especially that it crashes when it tries to use the deprecated UIAlertView
.
DSBridge-Swift, instead, makes better use of iOS Runtime features to avoid standing between you and the web view. You can set the uiDelegate
to your own object just like what you do with bare WKWebView
and all the delegation methods will work as if DSBridge is not there.
On the contrary, you'd have to do the dialog thing yourself. And all the dialog related APIs are removed, along with the dsuiDelegate
.
When using the old DSBridge-iOS, your JavaScript Object has to be an NSObject
subclass. Functions in it have to be prefixed with @objc
. DSBridge-Swift, however, is much more Swift-ish. You can use pure Swift types like class
or even struct
and enum
.
DSBridge-Swift provides highly customizable flexibility which allows you to change almost any part of it. You can even extends it to use it with another piece of completely different JavaScript. See section Open / Close Principle below.
A new calling method that allows you to specify the expected return type and returns a Result<T, Error>
instead of an Any
.
call<T>(
_: String,
with: [Any],
thatReturns: T.Type,
completion: @escaping (Result<T, any Swift.Error>) -> Void
)
callHandler
is renamed tocall
setJavascriptCloseWindowListener
todismissalHandler
addJavascriptObject
toaddInterface
removeJavascriptObject
toremoveInterface
-
loadUrl(_: String)
is removed. Define your own one if you need it -
onMessage
, a public method that's supposed to be private, is removed -
dsuiDelegate
-
disableJavascriptDialogBlock
-
customJavascriptDialogLabelTitles
-
and all the
WKUIDelegate
implementations
- Debug mode not implemented yet.
DSBridge-Swift has a Keystone that holds everything together.
A keystone is a stone at the top of an arch, which keeps the other stones in place by its weight and position. -- Collins Dictionary
Here is how a synchronous method call comes in and returns back:
The Keystone
converts raw text into an Invocation
. You can change how it resolves raw text by changing methodResolver
or jsonSerializer
of WebView.keystone
.
import class DSBridge.Keystone
// ...
(webView.keystone as! Keystone).jsonSerializer = MyJSONSerializer()
// ...
There might be something you don't want in the built-in JSON serializer. For example it won't log details about an object or text in production environment. You can change this behavior by defining your own errors instead of using the ones defined in DSBridge.Error.JSON
.
methodResolver
is even easier. It simply reads a text and finds the namespace and method name:
(webView.keystone as! Keystone).methodResolver = MyMethodResolver()
After being resolved into an Invocation
, the method call is sent to Dispather
, where all the interfaces you added are registered and indexed.
You can, of course, replace the dispatcher. Then you would have to manage interfaces and dispatch calls to different interfaces in your own InvocationDispatching
implementation.
(webView.keystone as! Keystone).invocationDispatcher = MyInvocationDispatcher()
To explain to you how we can customize JavaScript evaluation, here's how an asynchronous invocation works.
Everything is the same before the invocation reaches the dispatcher. The dispatcher returns an empty response immediately after it gets the invocation, so that the webpage gets to continue running. From now on, the synchronous chain breaks.
Dispatcher sends the invocation to Interface
at the same time. But since the way back no longer exists, DSBridge-Swift has to send the repsonse by evaluating JavaScript:
The JavaScriptEvaluator
is in charge of all the messages towards JavaScript, including method calls initiated from native. The default evaluator evaluates JavaScript every 50ms to avoid getting dropped by iOS for evaluating too frequently.
If you need further optimization or you just want the vanilla experience instead, you can simply replace the Keystone.javaScriptEvaluator
.
As you can see from all above, the keystone is what holds everything together. You can even change the keystone, with either a Keystone
subclass or a completely different KeystoneProtocol
. Either way, you will be able to use DSBridge-Swift with any JavaScript.