@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.106 → 0.1.1-beta.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios-simulator-x64/build/entrypoint +25 -5
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +10 -4
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-x64/native-shell/TokamakApp.swift +81 -63
- package/ios-simulator-x64/native-shell/TokamakBindings.swift +347 -0
- package/ios-simulator-x64/native-shell/TokamakControls.swift +98 -0
- package/ios-simulator-x64/native-shell/TokamakOrigin.swift +7 -0
- package/ios-simulator-x64/native-shell/TokamakPlugins.swift +307 -156
- package/ios-simulator-x64/native-shell/TokamakResources.swift +74 -0
- package/ios-simulator-x64/native-shell/TokamakShell.swift +3 -20
- package/ios-simulator-x64/platform-pack.json +1 -0
- package/ios-simulator-x64/tools/tokamak-apple-signing +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// One call or subscription of a generated platform binding. It decodes the
|
|
4
|
+
/// Worker's JSON into native values and encodes native values back, keeping
|
|
5
|
+
/// the native objects it returns in the receiving invocation's scope.
|
|
6
|
+
///
|
|
7
|
+
/// JSON objects tagged `#` carry what JSON lacks: native objects, dates,
|
|
8
|
+
/// bytes, errors, non-finite numbers, and records whose keys include `#`.
|
|
9
|
+
struct TokamakBinding {
|
|
10
|
+
private let resources: TokamakResources
|
|
11
|
+
private let request: Result<[String: Any], TokamakPluginError>
|
|
12
|
+
private let reply: TokamakPluginBytesReply
|
|
13
|
+
|
|
14
|
+
init(
|
|
15
|
+
_ payload: Data, resources: TokamakResources, reply: @escaping TokamakPluginBytesReply
|
|
16
|
+
) {
|
|
17
|
+
self.resources = resources
|
|
18
|
+
self.reply = reply
|
|
19
|
+
request = Result { () throws(TokamakPluginError) in
|
|
20
|
+
guard let request = try decodePluginValue(payload) as? [String: Any] else {
|
|
21
|
+
throw typeError("A binding request is an object")
|
|
22
|
+
}
|
|
23
|
+
return request
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The rejection of a member the running OS version lacks.
|
|
28
|
+
static let unavailable = TokamakPluginError.notSupported(
|
|
29
|
+
"The API is unavailable on this OS version")
|
|
30
|
+
|
|
31
|
+
/// Argument `index` as JSON.
|
|
32
|
+
subscript(index: Int) -> Any {
|
|
33
|
+
get throws {
|
|
34
|
+
let arguments = try request.get()["arguments"] as? [Any] ?? []
|
|
35
|
+
return arguments.indices.contains(index) ? arguments[index] : NSNull()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// The object the call is made on.
|
|
40
|
+
func target<T>(_ type: T.Type) throws -> T {
|
|
41
|
+
try object(request.get()["target"] as Any, as: type)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// The handlers an implementation calls.
|
|
45
|
+
var methods: Set<String> {
|
|
46
|
+
Set((try? request.get()["methods"] as? [String]) ?? [])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private var scope: String {
|
|
50
|
+
(try? request.get()["scope"] as? String) ?? ""
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// MARK: Decoding
|
|
54
|
+
|
|
55
|
+
func decode(_ value: Any, as type: Bool.Type) throws -> Bool {
|
|
56
|
+
guard let value = value as? Bool else { throw mismatch(value, type) }
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func decode<T: BinaryInteger>(_ value: Any, as type: T.Type) throws -> T {
|
|
61
|
+
guard let number = value as? NSNumber, let value = T(exactly: number.doubleValue) else {
|
|
62
|
+
throw mismatch(value, type)
|
|
63
|
+
}
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func decode<T: BinaryFloatingPoint>(_ value: Any, as type: T.Type) throws -> T {
|
|
68
|
+
if let number = value as? NSNumber { return T(number.doubleValue) }
|
|
69
|
+
guard let number = try Double(decode(tagged(value, "number"), as: String.self)) else {
|
|
70
|
+
throw mismatch(value, type)
|
|
71
|
+
}
|
|
72
|
+
return T(number)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func decode(_ value: Any, as type: String.Type) throws -> String {
|
|
76
|
+
guard let value = value as? String else { throw mismatch(value, type) }
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func decode(_ value: Any, as type: NSNumber.Type) throws -> NSNumber {
|
|
81
|
+
guard let value = value as? NSNumber else { throw mismatch(value, type) }
|
|
82
|
+
return value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func decode(_ value: Any, as type: Locale.Type) throws -> Locale {
|
|
86
|
+
Locale(identifier: try decode(value, as: String.self))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func decode(_ value: Any, as type: TimeZone.Type) throws -> TimeZone {
|
|
90
|
+
guard let zone = TimeZone(identifier: try decode(value, as: String.self)) else {
|
|
91
|
+
throw mismatch(value, type)
|
|
92
|
+
}
|
|
93
|
+
return zone
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func decode(_ value: Any, as type: Date.Type) throws -> Date {
|
|
97
|
+
Date(timeIntervalSince1970: try decode(tagged(value, "date"), as: Double.self) / 1000)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func decode(_ value: Any, as type: Data.Type) throws -> Data {
|
|
101
|
+
guard let data = Data(base64Encoded: try decode(tagged(value, "bytes"), as: String.self)) else {
|
|
102
|
+
throw mismatch(value, type)
|
|
103
|
+
}
|
|
104
|
+
return data
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func decode(_ value: Any, as type: URL.Type) throws -> URL {
|
|
108
|
+
guard let url = URL(string: try decode(value, as: String.self)) else {
|
|
109
|
+
throw mismatch(value, type)
|
|
110
|
+
}
|
|
111
|
+
return url
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func decode(_ value: Any, as type: UUID.Type) throws -> UUID {
|
|
115
|
+
guard let uuid = UUID(uuidString: try decode(value, as: String.self)) else {
|
|
116
|
+
throw mismatch(value, type)
|
|
117
|
+
}
|
|
118
|
+
return uuid
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The field `name` of the struct `value`.
|
|
122
|
+
func field(_ value: Any, _ name: String) throws -> Any {
|
|
123
|
+
guard let fields = value as? [String: Any] else { throw mismatch(value, [String: Any].self) }
|
|
124
|
+
return fields[name] ?? NSNull()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// The native object `value` refers to.
|
|
128
|
+
func object<T>(_ value: Any, as type: T.Type) throws -> T {
|
|
129
|
+
try resources.get(tagged(value, "object", key: "id"), as: type)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func optional<T>(_ value: Any, _ decode: (Any) throws -> T) rethrows -> T? {
|
|
133
|
+
value is NSNull ? nil : try decode(value)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func array<T>(_ value: Any, _ decode: (Any) throws -> T) throws -> [T] {
|
|
137
|
+
guard let values = value as? [Any] else { throw mismatch(value, [T].self) }
|
|
138
|
+
return try values.map(decode)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func record<T>(_ value: Any, _ decode: (Any) throws -> T) throws -> [String: T] {
|
|
142
|
+
let fields = (try? tagged(value, "record")) ?? value
|
|
143
|
+
guard let fields = fields as? [String: Any] else { throw mismatch(value, [String: T].self) }
|
|
144
|
+
return try fields.mapValues(decode)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: Encoding
|
|
148
|
+
|
|
149
|
+
func encode(_ value: Bool) -> Any { value }
|
|
150
|
+
|
|
151
|
+
func encode<T: BinaryInteger>(_ value: T) -> Any {
|
|
152
|
+
Int64(exactly: value).map { NSNumber(value: $0) } ?? NSNumber(value: Double(value))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func encode<T: BinaryFloatingPoint>(_ value: T) -> Any {
|
|
156
|
+
let number = Double(value)
|
|
157
|
+
if number.isFinite { return number }
|
|
158
|
+
return ["#": "number", "value": number.isNaN ? "NaN" : number > 0 ? "Infinity" : "-Infinity"]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func encode(_ value: String) -> Any { value }
|
|
162
|
+
|
|
163
|
+
func encode(_ value: NSNumber) -> Any { encode(value.doubleValue) }
|
|
164
|
+
|
|
165
|
+
func encode(_ value: Locale) -> Any { value.identifier }
|
|
166
|
+
|
|
167
|
+
func encode(_ value: TimeZone) -> Any { value.identifier }
|
|
168
|
+
|
|
169
|
+
func encode(_ value: Date) -> Any { ["#": "date", "value": value.timeIntervalSince1970 * 1000] }
|
|
170
|
+
|
|
171
|
+
func encode(_ value: Data) -> Any { ["#": "bytes", "value": value.base64EncodedString()] }
|
|
172
|
+
|
|
173
|
+
func encode(_ value: URL) -> Any { value.absoluteString }
|
|
174
|
+
|
|
175
|
+
func encode(_ value: UUID) -> Any { value.uuidString }
|
|
176
|
+
|
|
177
|
+
func encode(_ value: any Error) -> Any {
|
|
178
|
+
let error = Self.failure(value)
|
|
179
|
+
var encoded: [String: Any] = ["#": "error", "name": error.name, "message": error.message]
|
|
180
|
+
encoded["cause"] = error.cause
|
|
181
|
+
return encoded
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Keeps `value` in the invocation's scope and refers to it, with the
|
|
185
|
+
/// names of its class and superclasses.
|
|
186
|
+
func encode(object value: Any) -> Any {
|
|
187
|
+
["#": "object", "id": resources.retain(value, in: scope), "types": types(of: value)]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func encode<T>(optional value: T?, _ encode: (T) -> Any) -> Any {
|
|
191
|
+
value.map(encode) ?? NSNull()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func encode<S: Sequence>(array values: S, _ encode: (S.Element) -> Any) -> Any {
|
|
195
|
+
values.map(encode)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func encode<T>(record fields: [String: T], _ encode: (T) -> Any) -> Any {
|
|
199
|
+
let encoded = fields.mapValues(encode)
|
|
200
|
+
return encoded["#"] == nil ? encoded : ["#": "record", "value": encoded]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// MARK: Replies
|
|
204
|
+
|
|
205
|
+
/// Replies with the JSON `body` returns, or the error it throws.
|
|
206
|
+
func run(_ body: () throws -> Any?) -> (() -> Void)? {
|
|
207
|
+
do { send(try body(), done: true) } catch { fail(error) }
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Replies with the JSON `body` resolves to on the main actor, or the
|
|
212
|
+
/// error it throws. The returned function cancels its task.
|
|
213
|
+
func task(_ body: @escaping @MainActor () async throws -> Any?) -> () -> Void {
|
|
214
|
+
let task = Task { @MainActor in
|
|
215
|
+
do { send(try await body(), done: true) } catch { fail(error) }
|
|
216
|
+
}
|
|
217
|
+
return { task.cancel() }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Replies with each element of the sequence `body` returns on the main
|
|
221
|
+
/// actor, encoded, then ends. The returned function cancels its task.
|
|
222
|
+
func stream<S: AsyncSequence>(
|
|
223
|
+
_ body: @escaping @MainActor () async throws -> S,
|
|
224
|
+
encode: @escaping (S.Element) -> Any
|
|
225
|
+
) -> () -> Void {
|
|
226
|
+
let task = Task {
|
|
227
|
+
do {
|
|
228
|
+
for try await element in try await body() { send(encode(element), done: false) }
|
|
229
|
+
reply(TokamakPluginResult(payload: Data(), failed: false, done: true))
|
|
230
|
+
} catch { fail(error) }
|
|
231
|
+
}
|
|
232
|
+
return { task.cancel() }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Keeps `type`'s implementation while the subscription lasts, and replies
|
|
236
|
+
/// with its ID.
|
|
237
|
+
func implement(_ type: TokamakBindingImplementation.Type) -> () -> Void {
|
|
238
|
+
let scope = resources.open()
|
|
239
|
+
send(resources.retain(type.init(self), in: scope), done: false)
|
|
240
|
+
return { [resources] in resources.end(scope) }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Rejects the member the plugin does not bind.
|
|
244
|
+
func unsupported(_ member: String) -> (() -> Void)? {
|
|
245
|
+
fail(TokamakPluginError.notSupported("\(member) is not supported"))
|
|
246
|
+
return nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func send(_ value: Any?, done: Bool) {
|
|
250
|
+
reply(TokamakPluginResult(encodePluginValue(value), done: done))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func fail(_ error: any Error) {
|
|
254
|
+
reply(TokamakPluginResult(.failure(Self.failure(error)), done: true))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// `error` for the Worker: an `OperationError` whose cause has the
|
|
258
|
+
/// platform error's domain and code.
|
|
259
|
+
static func failure(_ error: any Error) -> TokamakPluginError {
|
|
260
|
+
if let error = error as? TokamakPluginError { return error }
|
|
261
|
+
let error = error as NSError
|
|
262
|
+
return TokamakPluginError(
|
|
263
|
+
name: "OperationError", message: error.localizedDescription,
|
|
264
|
+
cause: ["domain": error.domain, "code": error.code])
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// The field `key` of `value` tagged `tag`.
|
|
268
|
+
private func tagged(_ value: Any, _ tag: String, key: String = "value") throws -> Any {
|
|
269
|
+
guard let fields = value as? [String: Any], fields["#"] as? String == tag,
|
|
270
|
+
let field = fields[key]
|
|
271
|
+
else { throw typeError("Expected a tagged \(tag)") }
|
|
272
|
+
return field
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// The error of `value`, which is not a `type`.
|
|
276
|
+
func mismatch(_ value: Any, _ type: Any.Type) -> TokamakPluginError {
|
|
277
|
+
typeError("\(Swift.type(of: value)) is not \(type)")
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// The names of `value`'s class and superclasses, then of the Objective-C
|
|
281
|
+
/// protocols they adopt, or of its type: Objective-C names, and
|
|
282
|
+
/// module-qualified Swift names.
|
|
283
|
+
private func types(of value: Any) -> [String] {
|
|
284
|
+
let name = { (type: Any.Type) in
|
|
285
|
+
let name = String(reflecting: type)
|
|
286
|
+
return name.hasPrefix("__C.") ? String(name.dropFirst(4)) : name
|
|
287
|
+
}
|
|
288
|
+
guard let type = Swift.type(of: value) as? AnyClass else {
|
|
289
|
+
return [name(Swift.type(of: value))]
|
|
290
|
+
}
|
|
291
|
+
let classes = Array(sequence(first: type, next: class_getSuperclass))
|
|
292
|
+
let protocols = classes.flatMap(adopted).map(NSStringFromProtocol)
|
|
293
|
+
var seen = Set<String>()
|
|
294
|
+
return (classes.map(name) + protocols).filter { seen.insert($0).inserted }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// The Objective-C protocols `type` adopts, each followed by those it inherits.
|
|
299
|
+
private func adopted(by type: AnyClass) -> [Protocol] {
|
|
300
|
+
protocols { class_copyProtocolList(type, $0) }.flatMap { [$0] + inherited(by: $0) }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private func inherited(by parent: Protocol) -> [Protocol] {
|
|
304
|
+
protocols { protocol_copyProtocolList(parent, $0) }.flatMap { [$0] + inherited(by: $0) }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// The protocols a runtime `copy` function lists.
|
|
308
|
+
private func protocols(
|
|
309
|
+
_ copy: (UnsafeMutablePointer<UInt32>) -> AutoreleasingUnsafeMutablePointer<Protocol>?
|
|
310
|
+
) -> [Protocol] {
|
|
311
|
+
var count: UInt32 = 0
|
|
312
|
+
guard let list = copy(&count) else { return [] }
|
|
313
|
+
defer { free(UnsafeMutableRawPointer(list)) }
|
|
314
|
+
return (0..<Int(count)).map { list[$0] }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// A native object that implements a protocol by calling the JavaScript
|
|
318
|
+
/// handlers of an implementation subscription. A method without a handler
|
|
319
|
+
/// keeps the platform's default.
|
|
320
|
+
class TokamakBindingImplementation: NSObject {
|
|
321
|
+
private let binding: TokamakBinding
|
|
322
|
+
private let handled: Set<String>
|
|
323
|
+
|
|
324
|
+
required init(_ binding: TokamakBinding) {
|
|
325
|
+
self.binding = binding
|
|
326
|
+
handled = binding.methods
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// The handler each optional Objective-C method calls, by selector.
|
|
330
|
+
class var selectors: [Selector: String] { [:] }
|
|
331
|
+
|
|
332
|
+
override func responds(to selector: Selector!) -> Bool {
|
|
333
|
+
guard let method = Self.selectors[selector] else { return super.responds(to: selector) }
|
|
334
|
+
return handled.contains(method)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// Calls the handler `method`, when it has one, with the `arguments` it
|
|
338
|
+
/// encodes.
|
|
339
|
+
func call(_ method: String, _ arguments: (TokamakBinding) -> [Any]) {
|
|
340
|
+
guard handled.contains(method) else { return }
|
|
341
|
+
binding.send(["method": method, "arguments": arguments(binding)], done: false)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private func typeError(_ message: String) -> TokamakPluginError {
|
|
346
|
+
TokamakPluginError(name: "TypeError", message: message)
|
|
347
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Core device operations use the same endpoint and ownership as plugins.
|
|
5
|
+
final class TokamakCorePlugin: TokamakPlugin {
|
|
6
|
+
let id = "tokamak"
|
|
7
|
+
private let host: TokamakHost
|
|
8
|
+
private let queue = DispatchQueue(label: "tokamak.core")
|
|
9
|
+
|
|
10
|
+
init(host: TokamakHost) { self.host = host }
|
|
11
|
+
|
|
12
|
+
/// WebView operations and the presenter run on the main thread, which
|
|
13
|
+
/// WebKit and UIKit require.
|
|
14
|
+
func executor(for method: String) -> DispatchQueue {
|
|
15
|
+
method.hasPrefix("webview.") || method == "resource.root" ? .main : queue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func call(method: String, arguments: Any, reply: @escaping TokamakPluginReply) -> (() -> Void)? {
|
|
19
|
+
switch method {
|
|
20
|
+
case "resource.read":
|
|
21
|
+
reply(
|
|
22
|
+
Result { () throws(TokamakPluginError) -> Any? in
|
|
23
|
+
try host.resources.get(arguments, as: Data.self)
|
|
24
|
+
})
|
|
25
|
+
case "resource.release":
|
|
26
|
+
host.resources.release(arguments)
|
|
27
|
+
reply(.success(nil))
|
|
28
|
+
case "resource.root":
|
|
29
|
+
reply(Result { () throws(TokamakPluginError) -> Any? in try root(arguments) })
|
|
30
|
+
case "webview.getCurrentPage":
|
|
31
|
+
reply(.success(host.webView?.url?.absoluteString))
|
|
32
|
+
case "webview.navigate":
|
|
33
|
+
reply(navigate(to: arguments).map { nil })
|
|
34
|
+
case "webview.postMessage":
|
|
35
|
+
postMessage(arguments, reply: reply)
|
|
36
|
+
default:
|
|
37
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
38
|
+
}
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// `resource.scope` opens a scope for the objects an invocation receives,
|
|
43
|
+
/// which ends with the subscription.
|
|
44
|
+
func subscribe(method: String, arguments: Any, reply: @escaping TokamakPluginReply)
|
|
45
|
+
throws(TokamakPluginError) -> () -> Void
|
|
46
|
+
{
|
|
47
|
+
guard method == "resource.scope" else {
|
|
48
|
+
throw .notSupported("\(id).\(method) is not supported")
|
|
49
|
+
}
|
|
50
|
+
let scope = host.resources.open()
|
|
51
|
+
reply(.success(scope))
|
|
52
|
+
return { [host] in host.resources.end(scope) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Retains the app object `name` in `scope` and returns its ID: the
|
|
56
|
+
/// `presenter` a native screen is presented from.
|
|
57
|
+
private func root(_ arguments: Any) throws(TokamakPluginError) -> String {
|
|
58
|
+
let arguments = arguments as? [String: Any]
|
|
59
|
+
guard let scope = arguments?["scope"] as? String, arguments?["name"] as? String == "presenter"
|
|
60
|
+
else { throw .notSupported("\(id).resource.root has no such object") }
|
|
61
|
+
return try host.resources.retain(host.presenter(), in: scope)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Starts navigation to the app URL `path`, relative to the current page or
|
|
65
|
+
/// the app origin.
|
|
66
|
+
private func navigate(to path: Any) -> Result<Void, TokamakPluginError> {
|
|
67
|
+
guard let view = host.webView else { return .failure(noView) }
|
|
68
|
+
let appHost = Bundle.main.tokamakHost ?? ""
|
|
69
|
+
guard let path = path as? String,
|
|
70
|
+
let base = view.url ?? URL(string: "https://\(appHost)/"),
|
|
71
|
+
let url = URL(string: path, relativeTo: base)?.absoluteURL,
|
|
72
|
+
url.isAppOrigin(appHost)
|
|
73
|
+
else {
|
|
74
|
+
return .failure(.init(name: "TypeError", message: "Navigation requires an app URL"))
|
|
75
|
+
}
|
|
76
|
+
view.load(URLRequest(url: url))
|
|
77
|
+
return .success(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func postMessage(_ data: Any, reply: @escaping TokamakPluginReply) {
|
|
81
|
+
guard let view = host.webView, view.url != nil else {
|
|
82
|
+
return reply(.failure(.init(name: "InvalidStateError", message: "No page is loaded")))
|
|
83
|
+
}
|
|
84
|
+
view.callAsyncJavaScript(
|
|
85
|
+
"window.dispatchEvent(new MessageEvent('message', {data, origin: location.origin})); return true;",
|
|
86
|
+
arguments: ["data": data], in: nil, in: .page
|
|
87
|
+
) { result in
|
|
88
|
+
reply(
|
|
89
|
+
result.map { _ in nil }.mapError {
|
|
90
|
+
TokamakPluginError(name: "OperationError", message: $0.localizedDescription)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private var noView: TokamakPluginError {
|
|
96
|
+
.init(name: "InvalidStateError", message: "The app has no WebView")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -10,6 +10,13 @@ extension URL {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
extension Bundle {
|
|
14
|
+
/// The host of the app origin, `https://<host>`, from the `TokamakHost` key.
|
|
15
|
+
var tokamakHost: String? {
|
|
16
|
+
object(forInfoDictionaryKey: "TokamakHost") as? String
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
extension WKSecurityOrigin {
|
|
14
21
|
/// Whether this is the app origin, `https://<host>`.
|
|
15
22
|
func isAppOrigin(_ host: String) -> Bool {
|