Skip to content

JavaScript Calling Native APIs

iMoeNya edited this page Apr 23, 2024 · 2 revisions

Brief

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

Check Whether An API Exists

You can check whether there is some API in native:

bridge.hasNativeMethod('test')  // true

Specify if the API is synchronous / asynchronous:

bridge.hasNativeMethod('test', 'syn')  // true
bridge.hasNativeMethod('test', 'asyn')  // false

Declaration Rules

Allowed Interface Types

You can declare your interface as these types:

  • class

  • enum

  • struct

    actors are not supported yet. Please file up your ideas about it.

Allowed Data Types

You can receive or send the following types:

  • String

  • Int, Double (types toll-free bridged to NSNumber)

  • Bool

  • Standard JSON top-level objects:

    • Dictionary that's encodable

    • Array that's encodable

Allowed Function Declarations

DSBridge-Swift ignores argument labels and parameter names of your functions. Thus you can name your parameters whatever you want.

Synchronous Functions

About parameters, synchronous functions can have:

  • 1 parameter, which is one of the above-mentioned Allowed Data Types
  • no parameter

About return value, synchronous functions can have:

  • return value that's one of the above-mentioned Allowed Data Types
  • no return value

For simplicity, we use Allowed to represent the before-mentioned Allowed Data Types.

func name()
func name(Allowed)
func name(Allowed) -> Allowed

Asynchronous Functions

Asynchronous functions are allowed to have 1 or 2 parameters and no return value.

If there are 2 parameters, the first one must be one of the above-mentioned Allowed Data Types.

The last parameter has to be a closure that returns nothing (i.e., Void). For parameters, the closure can have:

  • 1 parameter, one of the above-mentioned Allowed Data Types
  • 2 parameters, the first one is one of the above-mentioned Allowed Data Types and the second one is a Bool
typealias Completion = (Allowed) -> Void
typealias RepeatableCompletion = (Allowed, Bool) -> Void

func name(Completion)
func name(RepeatableCompletion)
func name(Allowed, Completion)
func name(Allowed, RepeatableCompletion)

Attribute your closure with @ecaping if needed. Otherwise, keep in mind that your functions run on the main thread and try not to block it.

Clone this wiki locally