snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
65
crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
Normal file
65
crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
let CHANNEL_PREFIX = "__CHANNEL__:"
|
||||
let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")!
|
||||
|
||||
public class Channel: Decodable {
|
||||
public let id: UInt64
|
||||
let handler: (UInt64, String) -> Void
|
||||
|
||||
public required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let channelDef = try container.decode(String.self)
|
||||
|
||||
let components = channelDef.components(separatedBy: CHANNEL_PREFIX)
|
||||
if components.count < 2 {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container,
|
||||
debugDescription: "Invalid channel definition from \(channelDef)"
|
||||
)
|
||||
|
||||
}
|
||||
guard let channelId = UInt64(components[1]) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container,
|
||||
debugDescription: "Invalid channel ID from \(channelDef)"
|
||||
)
|
||||
}
|
||||
|
||||
guard let handler = decoder.userInfo[channelDataKey] as? (UInt64, String) -> Void else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container,
|
||||
debugDescription: "missing userInfo for Channel handler. This is a Tauri issue"
|
||||
)
|
||||
}
|
||||
|
||||
self.id = channelId
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
func serialize(_ data: JsonValue) -> String {
|
||||
do {
|
||||
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
|
||||
} catch {
|
||||
return "\"\(error)\""
|
||||
}
|
||||
}
|
||||
|
||||
public func send(_ data: JsonObject) {
|
||||
send(.dictionary(data))
|
||||
}
|
||||
|
||||
public func send(_ data: JsonValue) {
|
||||
handler(id, serialize(data))
|
||||
}
|
||||
|
||||
public func send<T: Encodable>(_ data: T) throws {
|
||||
let json = try JSONEncoder().encode(data)
|
||||
handler(id, String(decoding: json, as: UTF8.self))
|
||||
}
|
||||
|
||||
}
|
||||
118
crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal file
118
crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@objc public class Invoke: NSObject {
|
||||
public let command: String
|
||||
let callback: UInt64
|
||||
let error: UInt64
|
||||
let data: String
|
||||
let sendResponse: (UInt64, String?) -> Void
|
||||
let sendChannelData: (UInt64, String) -> Void
|
||||
|
||||
public init(
|
||||
command: String, callback: UInt64, error: UInt64,
|
||||
sendResponse: @escaping (UInt64, String?) -> Void,
|
||||
sendChannelData: @escaping (UInt64, String) -> Void, data: String
|
||||
) {
|
||||
self.command = command
|
||||
self.callback = callback
|
||||
self.error = error
|
||||
self.data = data
|
||||
self.sendResponse = sendResponse
|
||||
self.sendChannelData = sendChannelData
|
||||
}
|
||||
|
||||
public func getRawArgs() -> String {
|
||||
return self.data
|
||||
}
|
||||
|
||||
public func getArgs() throws -> JSObject {
|
||||
let jsonData = self.data.data(using: .utf8)!
|
||||
let data = try JSONSerialization.jsonObject(with: jsonData, options: [])
|
||||
return JSTypes.coerceDictionaryToJSObject(
|
||||
(data as! NSDictionary), formattingDatesAsStrings: true)!
|
||||
}
|
||||
|
||||
public func parseArgs<T: Decodable>(_ type: T.Type) throws -> T {
|
||||
let jsonData = self.data.data(using: .utf8)!
|
||||
let decoder = JSONDecoder()
|
||||
decoder.userInfo[channelDataKey] = sendChannelData
|
||||
return try decoder.decode(type, from: jsonData)
|
||||
}
|
||||
|
||||
func serialize(_ data: JsonValue) -> String {
|
||||
do {
|
||||
return try data.jsonRepresentation() ?? "\"Failed to serialize payload\""
|
||||
} catch {
|
||||
return "\"\(error)\""
|
||||
}
|
||||
}
|
||||
|
||||
public func resolve() {
|
||||
sendResponse(callback, nil)
|
||||
}
|
||||
|
||||
public func resolve(_ data: JsonObject) {
|
||||
resolve(.dictionary(data))
|
||||
}
|
||||
|
||||
public func resolve(_ data: JsonValue) {
|
||||
sendResponse(callback, serialize(data))
|
||||
}
|
||||
|
||||
public func resolve<T: Encodable>(_ data: T) {
|
||||
do {
|
||||
let json = try JSONEncoder().encode(data)
|
||||
sendResponse(callback, String(decoding: json, as: UTF8.self))
|
||||
} catch {
|
||||
sendResponse(self.error, "\"\(error)\"")
|
||||
}
|
||||
}
|
||||
|
||||
public func reject(
|
||||
_ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil
|
||||
) {
|
||||
let payload: NSMutableDictionary = [
|
||||
"message": message
|
||||
]
|
||||
|
||||
if let code = code {
|
||||
payload["code"] = code
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
payload["error"] = error
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
switch data {
|
||||
case .dictionary(let dict):
|
||||
for entry in dict {
|
||||
payload[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendResponse(self.error, serialize(.dictionary(payload as! JsonObject)))
|
||||
}
|
||||
|
||||
public func unimplemented() {
|
||||
unimplemented("not implemented")
|
||||
}
|
||||
|
||||
public func unimplemented(_ message: String) {
|
||||
reject(message)
|
||||
}
|
||||
|
||||
public func unavailable() {
|
||||
unavailable("not available")
|
||||
}
|
||||
|
||||
public func unavailable(_ message: String) {
|
||||
reject(message)
|
||||
}
|
||||
}
|
||||
123
crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal file
123
crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
// declare our empty protocol, and conformance, for typing
|
||||
public protocol JSValue {}
|
||||
extension String: JSValue {}
|
||||
extension Bool: JSValue {}
|
||||
extension Int: JSValue {}
|
||||
extension Float: JSValue {}
|
||||
extension Double: JSValue {}
|
||||
extension NSNumber: JSValue {}
|
||||
extension NSNull: JSValue {}
|
||||
extension Array: JSValue {}
|
||||
extension Date: JSValue {}
|
||||
extension Dictionary: JSValue where Key == String, Value == JSValue {}
|
||||
|
||||
// convenience aliases
|
||||
public typealias JSObject = [String: JSValue]
|
||||
public typealias JSArray = [JSValue]
|
||||
|
||||
extension Dictionary where Key == String, Value == JSValue {
|
||||
public func getValue(_ key: String) -> JSValue? {
|
||||
return self[key]
|
||||
}
|
||||
|
||||
public func getString(_ key: String) -> String? {
|
||||
return self[key] as? String
|
||||
}
|
||||
|
||||
public func getBool(_ key: String) -> Bool? {
|
||||
return self[key] as? Bool
|
||||
}
|
||||
|
||||
public func getInt(_ key: String) -> Int? {
|
||||
return self[key] as? Int
|
||||
}
|
||||
|
||||
public func getFloat(_ key: String) -> Float? {
|
||||
if let floatValue = self[key] as? Float {
|
||||
return floatValue
|
||||
} else if let doubleValue = self[key] as? Double {
|
||||
return Float(doubleValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getDouble(_ key: String) -> Double? {
|
||||
return self[key] as? Double
|
||||
}
|
||||
|
||||
public func getArray(_ key: String) -> JSArray? {
|
||||
return self[key] as? JSArray
|
||||
}
|
||||
|
||||
public func getObject(_ key: String) -> JSObject? {
|
||||
return self[key] as? JSObject
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Simply casting objects from foundation class clusters (such as __NSArrayM)
|
||||
doesn't work with the JSValue protocol and will always fail. So we need to
|
||||
recursively and explicitly convert each value in the dictionary.
|
||||
*/
|
||||
public enum JSTypes {}
|
||||
extension JSTypes {
|
||||
public static func coerceDictionaryToJSObject(
|
||||
_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false
|
||||
) -> JSObject? {
|
||||
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
|
||||
}
|
||||
|
||||
public static func coerceDictionaryToJSObject(
|
||||
_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false
|
||||
) -> JSObject? {
|
||||
return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject
|
||||
}
|
||||
}
|
||||
|
||||
private let dateStringFormatter = ISO8601DateFormatter()
|
||||
|
||||
// We need a large switch statement because we have a lot of types.
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? {
|
||||
guard let value = value else {
|
||||
return nil
|
||||
}
|
||||
switch value {
|
||||
case let stringValue as String:
|
||||
return stringValue
|
||||
case let numberValue as NSNumber:
|
||||
return numberValue
|
||||
case let boolValue as Bool:
|
||||
return boolValue
|
||||
case let intValue as Int:
|
||||
return intValue
|
||||
case let floatValue as Float:
|
||||
return floatValue
|
||||
case let doubleValue as Double:
|
||||
return doubleValue
|
||||
case let dateValue as Date:
|
||||
if formattingDates {
|
||||
return dateStringFormatter.string(from: dateValue)
|
||||
}
|
||||
return dateValue
|
||||
case let nullValue as NSNull:
|
||||
return nullValue
|
||||
case let arrayValue as NSArray:
|
||||
return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) }
|
||||
case let dictionaryValue as NSDictionary:
|
||||
let keys = dictionaryValue.allKeys.compactMap { $0 as? String }
|
||||
var result: JSObject = [:]
|
||||
for key in keys {
|
||||
result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
58
crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal file
58
crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias JsonObject = [String: Any?]
|
||||
|
||||
public enum JsonValue {
|
||||
case dictionary(JsonObject)
|
||||
|
||||
enum SerializationError: Error {
|
||||
case invalidObject
|
||||
}
|
||||
|
||||
public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? {
|
||||
switch self {
|
||||
case .dictionary(var dictionary):
|
||||
if let fields = includingFields {
|
||||
dictionary.merge(fields) { (current, _) in current }
|
||||
}
|
||||
dictionary = prepare(dictionary: dictionary)
|
||||
guard JSONSerialization.isValidJSONObject(dictionary) else {
|
||||
throw SerializationError.invalidObject
|
||||
}
|
||||
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
private static let formatter = ISO8601DateFormatter()
|
||||
|
||||
private func prepare(dictionary: JsonObject) -> JsonObject {
|
||||
return dictionary.mapValues { (value) -> Any in
|
||||
if let date = value as? Date {
|
||||
return JsonValue.formatter.string(from: date)
|
||||
} else if let aDictionary = value as? JsonObject {
|
||||
return prepare(dictionary: aDictionary)
|
||||
} else if let anArray = value as? [Any] {
|
||||
return prepare(array: anArray)
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private func prepare(array: [Any]) -> [Any] {
|
||||
return array.map { (value) -> Any in
|
||||
if let date = value as? Date {
|
||||
return JsonValue.formatter.string(from: date)
|
||||
} else if let aDictionary = value as? JsonObject {
|
||||
return prepare(dictionary: aDictionary)
|
||||
} else if let anArray = value as? [Any] {
|
||||
return prepare(array: anArray)
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
139
crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift
Normal file
139
crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
class StdoutRedirector {
|
||||
private var originalStdout: Int32 = -1
|
||||
private var originalStderr: Int32 = -1
|
||||
private var stdoutPipe: [Int32] = [-1, -1]
|
||||
private var stderrPipe: [Int32] = [-1, -1]
|
||||
private var stdoutReadSource: DispatchSourceRead?
|
||||
private var stderrReadSource: DispatchSourceRead?
|
||||
|
||||
func start() {
|
||||
originalStdout = dup(STDOUT_FILENO)
|
||||
originalStderr = dup(STDERR_FILENO)
|
||||
|
||||
guard Darwin.pipe(&stdoutPipe) == 0,
|
||||
Darwin.pipe(&stderrPipe) == 0 else {
|
||||
Logger.error("Failed to create stdout/stderr pipes")
|
||||
return
|
||||
}
|
||||
|
||||
dup2(stdoutPipe[1], STDOUT_FILENO)
|
||||
dup2(stderrPipe[1], STDERR_FILENO)
|
||||
close(stdoutPipe[1])
|
||||
close(stderrPipe[1])
|
||||
|
||||
stdoutReadSource = createReader(
|
||||
readPipe: stdoutPipe[0],
|
||||
writeToOriginal: originalStdout,
|
||||
label: "stdout"
|
||||
)
|
||||
|
||||
stderrReadSource = createReader(
|
||||
readPipe: stderrPipe[0],
|
||||
writeToOriginal: originalStderr,
|
||||
label: "stderr"
|
||||
)
|
||||
}
|
||||
|
||||
private func createReader(
|
||||
readPipe: Int32,
|
||||
writeToOriginal: Int32,
|
||||
label: String
|
||||
) -> DispatchSourceRead {
|
||||
let source = DispatchSource.makeReadSource(
|
||||
fileDescriptor: readPipe,
|
||||
queue: .global(qos: .utility)
|
||||
)
|
||||
|
||||
source.setEventHandler {
|
||||
let bufferSize = 4096
|
||||
var buffer = [UInt8](repeating: 0, count: bufferSize)
|
||||
let bytesRead = read(readPipe, &buffer, bufferSize)
|
||||
|
||||
if bytesRead > 0 {
|
||||
let output = String(
|
||||
bytes: buffer[0..<bytesRead],
|
||||
encoding: .utf8
|
||||
) ?? ""
|
||||
|
||||
let trimmed = output.trimmingCharacters(in: .newlines)
|
||||
if !trimmed.isEmpty {
|
||||
// we're sending stderr to oslog, so we need to avoid recursive calls
|
||||
if trimmed.hasPrefix("OSLOG-") {
|
||||
// make sure the system can parse the oslogs
|
||||
write(writeToOriginal, &buffer, bytesRead)
|
||||
} else {
|
||||
Logger.info("[\(label)] \(trimmed)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source.setCancelHandler {
|
||||
close(readPipe)
|
||||
}
|
||||
|
||||
source.resume()
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper class for os_log function
|
||||
public class Logger {
|
||||
private static var _enabled = false
|
||||
public static var enabled: Bool {
|
||||
get {
|
||||
#if DEBUG
|
||||
return true
|
||||
#else
|
||||
return _enabled
|
||||
#endif
|
||||
}
|
||||
set {
|
||||
Logger._enabled = newValue
|
||||
}
|
||||
}
|
||||
|
||||
static func log(_ items: [Any], category: String, type: OSLogType) {
|
||||
if Logger.enabled {
|
||||
var message = ""
|
||||
let last = items.count - 1
|
||||
for (index, item) in items.enumerated() {
|
||||
message += "\(item)"
|
||||
if index != last {
|
||||
message += " "
|
||||
}
|
||||
}
|
||||
|
||||
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category)
|
||||
os_log("%{public}@", log: log, type: type, String(message.prefix(4068)))
|
||||
}
|
||||
}
|
||||
|
||||
public static func debug(_ items: Any..., category: String = "app") {
|
||||
#if DEBUG
|
||||
Logger.log(items, category: category, type: OSLogType.default)
|
||||
#else
|
||||
Logger.log(items, category: category, type: OSLogType.debug)
|
||||
#endif
|
||||
}
|
||||
|
||||
public static func info(_ items: Any..., category: String = "app") {
|
||||
#if DEBUG
|
||||
Logger.log(items, category: category, type: OSLogType.default)
|
||||
#else
|
||||
Logger.log(items, category: category, type: OSLogType.info)
|
||||
#endif
|
||||
}
|
||||
|
||||
public static func error(_ items: Any..., category: String = "app") {
|
||||
Logger.log(items, category: category, type: OSLogType.error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import WebKit
|
||||
import os.log
|
||||
|
||||
struct RegisterListenerArgs: Decodable {
|
||||
let event: String
|
||||
let handler: Channel
|
||||
}
|
||||
|
||||
struct RemoveListenerArgs: Decodable {
|
||||
let event: String
|
||||
let channelId: UInt64
|
||||
}
|
||||
|
||||
open class Plugin: NSObject {
|
||||
public let manager: PluginManager = PluginManager.shared
|
||||
var config: String = "{}"
|
||||
private var listeners = [String: [Channel]]()
|
||||
|
||||
internal func setConfig(_ config: String) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
public func parseConfig<T: Decodable>(_ type: T.Type) throws -> T {
|
||||
let jsonData = self.config.data(using: .utf8)!
|
||||
let decoder = JSONDecoder()
|
||||
return try decoder.decode(type, from: jsonData)
|
||||
}
|
||||
|
||||
@objc open func load(webview: WKWebView) {}
|
||||
|
||||
@objc open func checkPermissions(_ invoke: Invoke) {
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc open func requestPermissions(_ invoke: Invoke) {
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
public func trigger(_ event: String, data: JSObject) {
|
||||
if let eventListeners = listeners[event] {
|
||||
for channel in eventListeners {
|
||||
channel.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func trigger<T: Encodable>(_ event: String, data: T) throws {
|
||||
if let eventListeners = listeners[event] {
|
||||
for channel in eventListeners {
|
||||
try channel.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func registerListener(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(RegisterListenerArgs.self)
|
||||
|
||||
if var eventListeners = listeners[args.event] {
|
||||
eventListeners.append(args.handler)
|
||||
listeners[args.event] = eventListeners
|
||||
} else {
|
||||
listeners[args.event] = [args.handler]
|
||||
}
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc func removeListener(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(RemoveListenerArgs.self)
|
||||
|
||||
if let eventListeners = listeners[args.event] {
|
||||
|
||||
listeners[args.event] = eventListeners.filter { $0.id != args.channelId }
|
||||
}
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
157
crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal file
157
crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import Foundation
|
||||
import SwiftRs
|
||||
import UIKit
|
||||
import WebKit
|
||||
import os.log
|
||||
|
||||
class PluginHandle {
|
||||
var instance: Plugin
|
||||
var loaded = false
|
||||
|
||||
init(plugin: Plugin) {
|
||||
instance = plugin
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginManager {
|
||||
static let shared: PluginManager = PluginManager()
|
||||
public var viewController: UIViewController?
|
||||
var plugins: [String: PluginHandle] = [:]
|
||||
var ipcDispatchQueue = DispatchQueue(label: "ipc")
|
||||
public var isSimEnvironment: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
public func assetUrl(fromLocalURL url: URL?) -> URL? {
|
||||
guard let inputURL = url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path)
|
||||
}
|
||||
|
||||
func onWebviewCreated(_ webview: WKWebView) {
|
||||
for (_, handle) in plugins {
|
||||
if !handle.loaded {
|
||||
handle.instance.load(webview: webview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func load<P: Plugin>(name: String, plugin: P, config: String, webview: WKWebView?) {
|
||||
plugin.setConfig(config)
|
||||
let handle = PluginHandle(plugin: plugin)
|
||||
if let webview = webview {
|
||||
handle.instance.load(webview: webview)
|
||||
handle.loaded = true
|
||||
}
|
||||
plugins[name] = handle
|
||||
}
|
||||
|
||||
func invoke(name: String, invoke: Invoke) {
|
||||
if let plugin = plugins[name] {
|
||||
ipcDispatchQueue.async {
|
||||
let selectorWithCompletionHandler = Selector(("\(invoke.command):completionHandler:"))
|
||||
let selectorWithThrows = Selector(("\(invoke.command):error:"))
|
||||
|
||||
if plugin.instance.responds(to: selectorWithCompletionHandler) {
|
||||
let completion: @convention(block) (NSError?) -> Void = { error in
|
||||
if let error = error {
|
||||
invoke.reject("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
let blockObj: AnyObject = unsafeBitCast(completion, to: AnyObject.self)
|
||||
let imp = plugin.instance.method(for: selectorWithCompletionHandler)
|
||||
|
||||
typealias Fn = @convention(c) (AnyObject, Selector, Invoke, AnyObject) -> Void
|
||||
let fn = unsafeBitCast(imp, to: Fn.self)
|
||||
fn(plugin.instance, selectorWithCompletionHandler, invoke, blockObj)
|
||||
} else if plugin.instance.responds(to: selectorWithThrows) {
|
||||
var error: NSError? = nil
|
||||
withUnsafeMutablePointer(to: &error) {
|
||||
let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows)
|
||||
unsafeBitCast(
|
||||
methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)(
|
||||
plugin.instance, selectorWithThrows, invoke, OpaquePointer($0))
|
||||
}
|
||||
if let error = error {
|
||||
invoke.reject("\(error)")
|
||||
// TODO: app crashes without this leak
|
||||
let _ = Unmanaged.passRetained(error)
|
||||
}
|
||||
} else {
|
||||
let selector = Selector(("\(invoke.command):"))
|
||||
if plugin.instance.responds(to: selector) {
|
||||
plugin.instance.perform(selector, with: invoke)
|
||||
} else {
|
||||
invoke.reject("No command \(invoke.command) found for plugin \(name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Plugin \(name) not initialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager: NSCopying {
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
private var stdoutRedirector: StdoutRedirector?
|
||||
|
||||
@_cdecl("log_stdout")
|
||||
func logStdout() {
|
||||
stdoutRedirector = StdoutRedirector()
|
||||
stdoutRedirector!.start()
|
||||
}
|
||||
|
||||
@_cdecl("register_plugin")
|
||||
func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) {
|
||||
PluginManager.shared.load(
|
||||
name: name.toString(),
|
||||
plugin: plugin as! Plugin,
|
||||
config: config.toString(),
|
||||
webview: webview
|
||||
)
|
||||
}
|
||||
|
||||
@_cdecl("on_webview_created")
|
||||
func onWebviewCreated(webview: WKWebView, viewController: UIViewController) {
|
||||
PluginManager.shared.viewController = viewController
|
||||
PluginManager.shared.onWebviewCreated(webview)
|
||||
}
|
||||
|
||||
@_cdecl("run_plugin_command")
|
||||
func runCommand(
|
||||
id: Int,
|
||||
name: SRString,
|
||||
command: SRString,
|
||||
data: SRString,
|
||||
callback: @escaping @convention(c) (Int, Bool, UnsafePointer<CChar>) -> Void,
|
||||
sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer<CChar>) -> Void
|
||||
) {
|
||||
let callbackId: UInt64 = 0
|
||||
let errorId: UInt64 = 1
|
||||
let invoke = Invoke(
|
||||
command: command.toString(), callback: callbackId, error: errorId,
|
||||
sendResponse: { (fn: UInt64, payload: String?) -> Void in
|
||||
let success = fn == callbackId
|
||||
callback(id, success, payload ?? "null")
|
||||
},
|
||||
sendChannelData: { (id: UInt64, payload: String) -> Void in
|
||||
sendChannelData(id, payload)
|
||||
}, data: data.toString())
|
||||
PluginManager.shared.invoke(name: name.toString(), invoke: invoke)
|
||||
}
|
||||
15
crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift
Normal file
15
crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import UIKit
|
||||
|
||||
public class UIUtils {
|
||||
public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) {
|
||||
if let viewController = rootViewController {
|
||||
popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0)
|
||||
popoverController.popoverPresentationController?.sourceView = viewController.view
|
||||
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user