Skip to content

Commit 6ddeccd

Browse files
authored
Merge pull request #2 from EdgarDegas/develop
Improve Async Function Declaration
2 parents 044ce9b + e4f5762 commit 6ddeccd

File tree

7 files changed

+666
-109
lines changed

7 files changed

+666
-109
lines changed

README.md

Lines changed: 151 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,36 @@
88

99
[^1]: Designed by [Freepik](https://freepik.com)
1010

11+
[简体中文版](https://github.com/EdgarDegas/DSBridge-Swift/blob/main/README.zh-Hans.md)
12+
1113
DSBridge-Swift is a [DSBridge-iOS](https://github.com/wendux/DSBridge-IOS) fork in Swift. It allows developers to send method calls back and forth between Swift and JavaScript.
1214

15+
# Installation
16+
17+
DSBridge is available on both iOS and Android.
18+
19+
This repo is a pure Swift version. You can integrate it with Swift Package Manager.
20+
21+
> 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](https://github.com/wendux/DSBridge-IOS).
22+
23+
For Android, see [DSBridge-Android](https://github.com/wendux/DSBridge-Android).
24+
25+
You can link the JavaScript with CDN:
26+
27+
```html
28+
<script src="https://cdn.jsdelivr.net/npm/dsbridge@3.1.4/dist/dsbridge.js"></script>
29+
```
30+
31+
Or install with npm:
32+
33+
```shell
34+
npm install dsbridge@3.1.4
35+
```
36+
1337
# Usage
38+
39+
## Brief
40+
1441
First of all, use `DSBridge.WebView` instead of `WKWebView`:
1542
```swift
1643
import class DSBridge.WebView
@@ -23,23 +50,31 @@ class ViewController: UIViewController {
2350
}
2451
```
2552

26-
DSBridge-Swift does not rely on Objective-C runtime. Thus you can declare your interface with pure Swift types:
53+
Declare your `Interface` with the `@Exposed` annotation. All the functions will be exposed to JavaScript:
2754
```swift
28-
2955
import Foundation
3056
import typealias DSBridge.Exposed
3157
import protocol DSBridge.ExposedInterface
3258

3359
@Exposed
3460
class MyInterface {
35-
func returnValue() -> Int { 101 }
61+
func addingOne(to input: Int) -> Int {
62+
input + 1
63+
}
64+
}
65+
```
66+
For functions you do not want to expose, add `@unexposed` to it:
67+
68+
```swift
69+
@Exposed
70+
class MyInterface {
3671
@unexposed
3772
func localMethod()
3873
}
3974
```
40-
Mark your interface `@Exposed` and that's it. Add `@unexposed` annotation to any function you don't want to expose.
4175

42-
If you don't need to declare it as a class, why not use a struct? Or, even further, an enum!
76+
Aside from `class`, you can declare your `Interface` in `struct` or `enum`:
77+
4378
```swift
4479
@Exposed
4580
enum EnumInterface {
@@ -57,45 +92,53 @@ enum EnumInterface {
5792
}
5893
```
5994

60-
You then add your interfaces into `DSBridge.WebView`, with or without a namespace:
95+
You then add your interfaces into `DSBridge.WebView`.
96+
97+
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.
98+
6199
```swift
62-
webView.addInterface(Interface(), by: nil) // `nil` works the same as ""
100+
webView.addInterface(MyInterface(), by: nil) // `nil` works the same as ""
63101
webView.addInterface(EnumInterface.onStreet, by: "street")
64102
webView.addInterface(EnumInterface.inSchool, by: "school")
65103
```
66104

67-
Done. You can call them from JavaScript now:
105+
Done. You can call them from JavaScript now. Do prepend the namespace before the method names:
68106
```javascript
69-
bridge.call('returnValue') // returns 101
107+
bridge.call('addingOne', 5) // returns 6
70108
bridge.call('street.getName') // returns Heisenberg
71109
bridge.call('school.getName') // returns Walter White
72110
```
73111

74-
Asynchronous functions are a bit more complicated. You have to use a completion handler, whose second parameter is a `Bool`.
112+
> DSBridge supports multi-level namespaces, like `a.b.c`.
113+
114+
Asynchronous functions are a little bit different. You have to use a completion handler to send your response:
115+
75116
```swift
76117
@Exposed
77118
class MyInterface {
78-
func asyncStyledFunction(callback: (String, Bool) -> Void) {
79-
callback("Async response", true)
119+
func asyncStyledFunction(callback: (String) -> Void) {
120+
callback("Async response")
80121
}
81122
}
82123
```
83124

84-
Call from JavaScript with a function as the last parameter:
125+
Call from JavaScript with a function accordingly:
85126
```javascript
86127
bridge.call('asyncStyledFunction', function(v) { console.log(v) });
87128
// ""
88129
// Async response
89130
```
90131
As you can see, there is a empty string returned. The response we sent in the interface is printed by the `function`.
91132

92-
OK, we send async response with the completion in its first parameter. What does the second parameter, the `Bool` do then?
133+
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:
93134

94-
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:
95135
```swift
96136
@Exposed
97137
class MyInterface {
98-
func asyncFunction(input: Int, completion: @escaping (Int, Bool) -> Void) {
138+
func asyncFunction(
139+
input: Int,
140+
completion: @escaping (Int, Bool) -> Void
141+
) {
99142
// Use `false` to ask JS to keep the callback
100143
completion(input + 1, false)
101144
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -122,7 +165,6 @@ bridge.call('asyncFunction', 1, function(v) { console.log(v) });
122165
// 4
123166
```
124167

125-
What will happen if we remove the `Bool` from the completion, you might ask. It won't compile. It might shock you how rough the `Exposed` macro is implemented if you click the Xcode error.
126168
# Declaration Rules
127169
## Allowed Interface Types
128170
You can declare your interface as these types:
@@ -134,39 +176,93 @@ You can declare your interface as these types:
134176
## Allowed Data Types
135177
You can receive or send the following types:
136178
- String
137-
- Int, Double
179+
- Int, Double (types toll-free bridged to NSNumber)
138180
- Bool
181+
- Standard JSON top-level objects:
182+
183+
- Dictionary that's encodable
184+
185+
- Array that's encodable
139186

140-
And standard JSON top-level objects:
141-
- Dictionary that's encodable
142-
- Array that's encodable
143187

144188
## Allowed Function Declarations
189+
DSBridge-Swift ignores argument labels and parameter names of your functions. Thus you can name your parameters whatever you want.
190+
191+
#### Synchronous Functions
192+
193+
About parameters, synchronous functions can have:
194+
195+
- 1 parameter, which is one of the above-mentioned *Allowed Data Types*
196+
- no parameter
197+
198+
About return value, synchronous functions can have:
199+
200+
- return value that's one of the above-mentioned *Allowed Data Types*
201+
- no return value
202+
145203
For simplicity, we use `Allowed` to represent the before-mentioned Allowed Data Types.
146-
You can define your synchronous functions in three ways:
147204

148205
```swift
149206
func name()
150207
func name(Allowed)
151208
func name(Allowed) -> Allowed
152209
```
153-
You can have at most one parameter. You can name it with anything, `func name(_ input: Int)`, `func name(using input: Int)`, or whatever you want.
210+
#### Asynchronous Functions
211+
212+
Asynchronous functions are allowed to have 1 or 2 parameters and no return value.
213+
214+
If there are 2 parameters, the first one must be one of the above-mentioned *Allowed Data Types*.
215+
216+
The last parameter has to be a closure that returns nothing (i.e., `Void`). For parameters, the closure can have:
217+
218+
- 1 parameter, one of the above-mentioned *Allowed Data Types*
219+
- 2 parameters, the first one is one of the above-mentioned *Allowed Data Types* and the second one is a `Bool`
154220

155-
For asynchronous functions:
156221
```swift
157-
typealias Completion = (Allowed, Bool) -> Bool
222+
typealias Completion = (Allowed) -> Void
223+
typealias RepeatableCompletion = (Allowed, Bool) -> Void
224+
158225
func name(Completion)
226+
func name(RepeatableCompletion)
159227
func name(Allowed, Completion)
228+
func name(Allowed, RepeatableCompletion)
160229
```
161-
You can have your completion attributed with `@ecaping` if you need it to persist longer than the function call.
162-
163-
Like the parameter, you can name the completion whatever you like.
164-
# JavaScript side
165-
Check out [the original repo](https://github.com/wendux/DSBridge-IOS) for how to use the JavaScript DSBridge.
230+
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.
166231

167232
# Differences with DSBridge-iOS
233+
234+
## Seamless `WKWebView` Experience
235+
236+
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`.
237+
238+
The old `dsuiDelegate` does not respond to new APIs, such as one that's released on iOS 16.4:
239+
240+
```swift
241+
@available(iOS 16.4, *)
242+
func webView(
243+
_ webView: WKWebView,
244+
willPresentEditMenuWithAnimator animator: any UIEditMenuInteractionAnimating
245+
) {
246+
247+
}
248+
```
249+
250+
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`.
251+
252+
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.
253+
254+
On the contrary, you'd have to do the dialog thing yourself. And all the dialog related APIs are removed, along with the `dsuiDelegate`.
255+
256+
## Static instead of Dynamic
257+
258+
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`.
259+
260+
## Customizable
261+
262+
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.
263+
168264
## API Changes
169-
### Newly added:
265+
### Newly added
170266
A new calling method that allows you to specify the expected return type and returns a `Result<T, Error>` instead of an `Any`.
171267
```swift
172268
call<T>(
@@ -176,62 +272,49 @@ call<T>(
176272
completion: @escaping (Result<T, any Swift.Error>) -> Void
177273
)
178274
```
179-
### Renamed:
275+
### Renamed
180276
- `callHandler` is renamed to `call`
181277
- `setJavascriptCloseWindowListener` to `dismissalHandler`
182278
- `addJavascriptObject` to `addInterface`
183279
- `removeJavascriptObject` to `removeInterface`
184280

185-
### Removed:
186-
`loadUrl(_: String)` is removed. Define your own one if you need it.
187-
188-
`onMessage`, as a private method marked public, is removed.
189-
190-
The old DSBridge-iOS relay all the `WKUIDelegate` calls for you, which cause it to suffer from iOS iteration. Especially that it crashes when it tries to show the deprecated `UIAlertView`.
281+
### Removed
282+
- `loadUrl(_: String)` is removed. Define your own one if you need it
191283

192-
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 the bare `WKWebView`.
284+
- `onMessage`, a public method that's supposed to be private, is removed
193285

194-
On the contrary, you'd have to do the dialog thing yourself. And all the dialog related APIs are removed, along with the `dsuiDelegate`:
195286
- `dsuiDelegate`
196287
- `disableJavascriptDialogBlock`
197288
- `customJavascriptDialogLabelTitles`
198289
- and all the `WKUIDelegate` implementations
199-
## Other minor differences
290+
### Not Implemented
200291

201-
- Does not require `NSObjectProtocol` for interfaces and `@objc` for functions.
202292
- Debug mode not implemented yet.
203293

204-
# Customization
294+
# Open / Close Principle
205295

206-
DSBridge-Swift really shines on how it allows you to customize it.
207-
## Resolving Incoming Calls
208-
This is how a synchronous method call comes in and returns back:
296+
DSBridge-Swift has a Keystone that holds everything together.
297+
298+
> 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
299+
300+
Here is how a synchronous method call comes in and returns back:
209301

210302
<img src="https://github.com/EdgarDegas/DSBridge-Swift/blob/main/assets/image-20240326210400582.png?raw=true" width="500" />
211303

304+
## Resolving Incoming Calls
305+
212306
The `Keystone` converts raw text into an `Invocation`. You can change how it resolves raw text by changing `methodResolver` or `jsonSerializer` of `WebView.keystone`.
307+
213308
```swift
214309
import class DSBridge.Keystone
215310
// ...
216311
(webView.keystone as! Keystone).jsonSerializer = MyJSONSerializer()
217312
// ...
218313
```
219314

220-
Your own jsonSerializer has to implement a two-method protocol `JSONSerializing`. Keep in mind that DSBridge needs it to encode the response into JSON on the way back. It's really ease, though, and you can then use SwiftyJSON or whatever you want:
221-
```swift
222-
struct MyJSONSerializer: JSONSerializing {
223-
func readParamters(
224-
from text: JSON?
225-
) throws -> IncomingInvocation.Signature {
226-
227-
}
228-
func serialize(_ object: Any) throws -> JSON {
229-
230-
}
231-
}
232-
```
315+
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`.
233316

234-
`methodResolver: any MethodResolving` is even easier, it's a one-method protocol. You just read a text and return a `Method`:
317+
`methodResolver` is even easier. It simply reads a text and finds the namespace and method name:
235318

236319
```swift
237320
(webView.keystone as! Keystone).methodResolver = MyMethodResolver()
@@ -246,23 +329,22 @@ You can, of course, replace the dispatcher. Then you would have to manage interf
246329
(webView.keystone as! Keystone).invocationDispatcher = MyInvocationDispatcher()
247330
```
248331

249-
## JavaScriptEvaluation
332+
## JavaScript Evaluation
250333

251-
This is how an asynchronous method call works:
334+
To explain to you how we can customize JavaScript evaluation, here's how an asynchronous invocation works.
335+
336+
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.
252337

253338
<img src="https://github.com/EdgarDegas/DSBridge-Swift/blob/main/assets/image-20240326210427127.png?raw=true" width="500" />
254339

255-
An empty response is returned immediately when it reaches the dispatcher. After that, the `Dispatcher` continues to dispatch the method call:
340+
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:
256341

257342
<img src="https://github.com/EdgarDegas/DSBridge-Swift/blob/main/assets/image-20240326210448065.png?raw=true" width="500" />
258343

259-
After the interface returned, the data is wrapped into an `AsyncResponse` and delivered to the JavaScript evaluator.
344+
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.
260345

261-
Guess what, you can substitute it with your own.
346+
If you need further optimization or you just want the vanilla experience instead, you can simply replace the `Keystone.javaScriptEvaluator`.
262347

263348
## Keystone
264-
As you can see from all above, the keystone is what holds everything together.
265-
266-
> A keystone is a stone at the top of an arch, which [keeps](https://www.collinsdictionary.com/dictionary/english/keep "Definition of keeps") the other stones in place by its [weight](https://www.collinsdictionary.com/dictionary/english/weight "Definition of weight") and position. -- Collins Dictionary
267349

268-
You can 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.
350+
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.

0 commit comments

Comments
 (0)