mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-25 03:33:36 +03:00
feat: initial work for iOS plugins (#6205)
This commit is contained in:
parent
f379e2f3da
commit
05dad08768
6
.changes/android-apis-runtime.md
Normal file
6
.changes/android-apis-runtime.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-runtime": minor
|
||||
"tauri-runtime-wry": minor
|
||||
---
|
||||
|
||||
Add `find_class`, `run_on_android_context` on `RuntimeHandle`.
|
6
.changes/cli-mobile-plugin.md
Normal file
6
.changes/cli-mobile-plugin.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"cli.rs": minor
|
||||
"cli.js": minor
|
||||
---
|
||||
|
||||
Add commands to add native Android and iOS functionality to plugins.
|
7
.changes/invoke-return-bool.md
Normal file
7
.changes/invoke-return-bool.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"tauri-macros": major
|
||||
"tauri-codegen": major
|
||||
"tauri": major
|
||||
---
|
||||
|
||||
Return `bool` in the invoke handler.
|
5
.changes/mobile-plugins.md
Normal file
5
.changes/mobile-plugins.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Run Android and iOS native plugins on the invoke handler if a Rust plugin command is not found.
|
5
.changes/plugin-init-fns.md
Normal file
5
.changes/plugin-init-fns.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": minor
|
||||
---
|
||||
|
||||
Added `initialize_android_plugin` and `initialize_ios_plugin` APIs on `AppHandle`.
|
5
.changes/tauri-build-mobile.md
Normal file
5
.changes/tauri-build-mobile.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-build": minor
|
||||
---
|
||||
|
||||
Add `mobile::PluginBuilder` for running build tasks related to Tauri plugins.
|
7
.changes/with-webview.md
Normal file
7
.changes/with-webview.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"tauri": minor
|
||||
"tauri-runtime": minor
|
||||
"tauri-runtime-wry": minor
|
||||
---
|
||||
|
||||
Implemented `with_webview` on Android and iOS.
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -87,3 +87,7 @@ test_video.mp4
|
||||
# old cli directories
|
||||
/tooling/cli.js
|
||||
/tooling/cli.rs
|
||||
|
||||
# Swift
|
||||
Package.resolved
|
||||
.build
|
||||
|
28
Package.swift
Normal file
28
Package.swift
Normal file
@ -0,0 +1,28 @@
|
||||
// swift-tools-version:5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TauriWorkspace",
|
||||
products: [
|
||||
.library(name: "Tauri", targets: ["Tauri"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Brendonovich/swift-rs", revision: "eb6de914ad57501da5019154d476d45660559999"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Tauri",
|
||||
dependencies: [
|
||||
.product(name: "SwiftRs", package: "swift-rs"),
|
||||
],
|
||||
path: "core/tauri/ios/Sources/Tauri"
|
||||
),
|
||||
.testTarget(
|
||||
name: "TauriTests",
|
||||
dependencies: ["Tauri"],
|
||||
path: "core/tauri/ios/Tests/TauriTests"
|
||||
),
|
||||
]
|
||||
)
|
@ -27,6 +27,9 @@ heck = "0.4"
|
||||
json-patch = "0.2"
|
||||
walkdir = "2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs", rev = "eb6de914ad57501da5019154d476d45660559999", features = ["build"] }
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
winres = "0.1"
|
||||
semver = "1"
|
||||
|
@ -9,6 +9,7 @@ use anyhow::Result;
|
||||
#[derive(Default)]
|
||||
pub struct PluginBuilder {
|
||||
android_path: Option<PathBuf>,
|
||||
ios_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl PluginBuilder {
|
||||
@ -23,53 +24,79 @@ impl PluginBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the iOS project path.
|
||||
pub fn ios_path<P: Into<PathBuf>>(mut self, ios_path: P) -> Self {
|
||||
self.ios_path.replace(ios_path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Injects the mobile templates in the given path relative to the manifest root.
|
||||
pub fn run(self) -> Result<()> {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
if target_os == "android" {
|
||||
if let Some(path) = self.android_path {
|
||||
let manifest_dir = var("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap();
|
||||
if let Ok(project_dir) = var("TAURI_ANDROID_PROJECT_PATH") {
|
||||
let source = manifest_dir.join(path);
|
||||
let pkg_name = var("CARGO_PKG_NAME").unwrap();
|
||||
match target_os.as_str() {
|
||||
"android" => {
|
||||
if let Some(path) = self.android_path {
|
||||
let manifest_dir = var("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap();
|
||||
if let Ok(project_dir) = var("TAURI_ANDROID_PROJECT_PATH") {
|
||||
let source = manifest_dir.join(path);
|
||||
let pkg_name = var("CARGO_PKG_NAME").unwrap();
|
||||
|
||||
println!("cargo:rerun-if-env-changed=TAURI_ANDROID_PROJECT_PATH");
|
||||
println!("cargo:rerun-if-env-changed=TAURI_ANDROID_PROJECT_PATH");
|
||||
|
||||
let project_dir = PathBuf::from(project_dir);
|
||||
let project_dir = PathBuf::from(project_dir);
|
||||
|
||||
inject_android_project(source, project_dir.join("tauri-plugins").join(&pkg_name))?;
|
||||
inject_android_project(source, project_dir.join("tauri-plugins").join(&pkg_name))?;
|
||||
|
||||
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
|
||||
let gradle_settings = fs::read_to_string(&gradle_settings_path)?;
|
||||
let include = format!(
|
||||
"include ':{pkg_name}'
|
||||
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
|
||||
let gradle_settings = fs::read_to_string(&gradle_settings_path)?;
|
||||
let include = format!(
|
||||
"include ':{pkg_name}'
|
||||
project(':{pkg_name}').projectDir = new File('./tauri-plugins/{pkg_name}')"
|
||||
);
|
||||
if !gradle_settings.contains(&include) {
|
||||
fs::write(
|
||||
&gradle_settings_path,
|
||||
format!("{gradle_settings}\n{include}"),
|
||||
)?;
|
||||
}
|
||||
);
|
||||
if !gradle_settings.contains(&include) {
|
||||
fs::write(
|
||||
&gradle_settings_path,
|
||||
format!("{gradle_settings}\n{include}"),
|
||||
)?;
|
||||
}
|
||||
|
||||
let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
|
||||
let app_build_gradle = fs::read_to_string(&app_build_gradle_path)?;
|
||||
let implementation = format!(r#"implementation(project(":{pkg_name}"))"#);
|
||||
let target = "dependencies {";
|
||||
if !app_build_gradle.contains(&implementation) {
|
||||
fs::write(
|
||||
&app_build_gradle_path,
|
||||
app_build_gradle.replace(target, &format!("{target}\n {implementation}")),
|
||||
)?
|
||||
let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
|
||||
let app_build_gradle = fs::read_to_string(&app_build_gradle_path)?;
|
||||
let implementation = format!(r#"implementation(project(":{pkg_name}"))"#);
|
||||
let target = "dependencies {";
|
||||
if !app_build_gradle.contains(&implementation) {
|
||||
fs::write(
|
||||
&app_build_gradle_path,
|
||||
app_build_gradle.replace(target, &format!("{target}\n {implementation}")),
|
||||
)?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
"ios" => {
|
||||
if let Some(path) = self.ios_path {
|
||||
link_swift_library(&std::env::var("CARGO_PKG_NAME").unwrap(), path);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[doc(hidden)]
|
||||
pub fn link_swift_library(name: &str, source: impl AsRef<Path>) {
|
||||
let source = source.as_ref();
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
swift_rs::build::SwiftLinker::new("10.13")
|
||||
.with_ios("11")
|
||||
.with_package(name, source)
|
||||
.link();
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn inject_android_project(source: impl AsRef<Path>, target: impl AsRef<Path>) -> Result<()> {
|
||||
let source = source.as_ref();
|
||||
|
@ -105,9 +105,7 @@ type FileDropHandler = dyn Fn(&Window, WryFileDropEvent) -> bool + 'static;
|
||||
#[cfg(all(desktop, feature = "system-tray"))]
|
||||
pub use tauri_runtime::TrayId;
|
||||
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
mod webview;
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
pub use webview::Webview;
|
||||
|
||||
#[cfg(all(desktop, feature = "system-tray"))]
|
||||
@ -1028,7 +1026,6 @@ pub enum ApplicationMessage {
|
||||
}
|
||||
|
||||
pub enum WindowMessage {
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
WithWebview(Box<dyn FnOnce(Webview) + Send>),
|
||||
AddEventListener(Uuid, Box<dyn Fn(&WindowEvent) + Send>),
|
||||
AddMenuEventListener(Uuid, Box<dyn Fn(&MenuEvent) + Send>),
|
||||
@ -1205,7 +1202,6 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
|
||||
id
|
||||
}
|
||||
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()> {
|
||||
send_user_message(
|
||||
&self.context,
|
||||
@ -2305,7 +2301,6 @@ fn handle_user_message<T: UserEvent>(
|
||||
});
|
||||
if let Some((Some(window), window_event_listeners, menu_event_listeners)) = w {
|
||||
match window_message {
|
||||
#[cfg(any(target_os = "android", desktop))]
|
||||
WindowMessage::WithWebview(f) => {
|
||||
if let WindowHandle::Webview { inner: w, .. } = &window {
|
||||
#[cfg(any(
|
||||
@ -2328,6 +2323,14 @@ fn handle_user_message<T: UserEvent>(
|
||||
ns_window: w.ns_window(),
|
||||
});
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use wry::webview::WebviewExtIOS;
|
||||
f(Webview {
|
||||
webview: w.webview(),
|
||||
manager: w.manager(),
|
||||
});
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
f(Webview {
|
||||
|
@ -26,6 +26,16 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
mod imp {
|
||||
use cocoa::base::id;
|
||||
|
||||
pub struct Webview {
|
||||
pub webview: id,
|
||||
pub manager: id,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller;
|
||||
|
@ -505,7 +505,7 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
|
||||
/// Registers a window event handler.
|
||||
fn on_menu_event<F: Fn(&window::MenuEvent) + Send + 'static>(&self, f: F) -> Uuid;
|
||||
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
/// Runs a closure with the platform webview object as argument.
|
||||
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()>;
|
||||
|
||||
/// Open the web inspector which is usually called devtools.
|
||||
|
@ -115,6 +115,9 @@ jni = "0.20"
|
||||
[target."cfg(target_os = \"ios\")".dependencies]
|
||||
log = "0.4"
|
||||
libc = "0.2"
|
||||
objc = "0.2"
|
||||
cocoa = "0.24"
|
||||
swift-rs = { git = "https://github.com/Brendonovich/swift-rs", rev = "eb6de914ad57501da5019154d476d45660559999" }
|
||||
|
||||
[build-dependencies]
|
||||
heck = "0.4"
|
||||
|
@ -154,6 +154,13 @@ fn main() {
|
||||
.expect("failed to copy tauri-api Android project");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if target_os == "ios" {
|
||||
tauri_build::mobile::link_swift_library("Tauri", "./mobile/ios-api");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create aliases for the given module with its apis.
|
||||
|
9
core/tauri/mobile/ios-api/.gitignore
vendored
Normal file
9
core/tauri/mobile/ios-api/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
34
core/tauri/mobile/ios-api/Package.swift
Normal file
34
core/tauri/mobile/ios-api/Package.swift
Normal file
@ -0,0 +1,34 @@
|
||||
// swift-tools-version: 5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Tauri",
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "Tauri",
|
||||
type: .static,
|
||||
targets: ["Tauri"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
.package(url: "https://github.com/Brendonovich/swift-rs", revision: "eb6de914ad57501da5019154d476d45660559999"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "Tauri",
|
||||
dependencies: [
|
||||
.product(name: "SwiftRs", package: "swift-rs"),
|
||||
],
|
||||
path: "Sources"
|
||||
),
|
||||
.testTarget(
|
||||
name: "TauriTests",
|
||||
dependencies: ["Tauri"]
|
||||
),
|
||||
]
|
||||
)
|
3
core/tauri/mobile/ios-api/README.md
Normal file
3
core/tauri/mobile/ios-api/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Tauri
|
||||
|
||||
Tauri iOS API.
|
57
core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal file
57
core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import MetalKit
|
||||
|
||||
@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer {
|
||||
public var jsObjectRepresentation: JSObject {
|
||||
return data as? JSObject ?? [:]
|
||||
}
|
||||
|
||||
public var dictionaryRepresentation: NSDictionary {
|
||||
return data as NSDictionary
|
||||
}
|
||||
|
||||
public static var jsDateFormatter: ISO8601DateFormatter = {
|
||||
return ISO8601DateFormatter()
|
||||
}()
|
||||
|
||||
var sendResponse: (JsonValue?, JsonValue?) -> Void
|
||||
var data: NSDictionary
|
||||
|
||||
public init(sendResponse: @escaping (JsonValue?, JsonValue?) -> Void, data: NSDictionary) {
|
||||
self.sendResponse = sendResponse
|
||||
self.data = data
|
||||
}
|
||||
|
||||
public func resolve(_ data: JsonValue? = nil) {
|
||||
sendResponse(data, nil)
|
||||
}
|
||||
|
||||
public func reject(_ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil) {
|
||||
let payload: NSMutableDictionary = ["message": message, "code": code ?? "", "error": error ?? ""]
|
||||
if let data = data {
|
||||
switch data {
|
||||
case .dictionary(let dict):
|
||||
for entry in dict {
|
||||
payload[entry.key] = entry.value
|
||||
}
|
||||
}
|
||||
}
|
||||
sendResponse(nil, .dictionary(payload as! JsonObject))
|
||||
}
|
||||
|
||||
public func unimplemented() {
|
||||
unimplemented("not implemented")
|
||||
}
|
||||
|
||||
public func unimplemented(_ message: String) {
|
||||
sendResponse(nil, .dictionary(["message": message]))
|
||||
}
|
||||
|
||||
public func unavailable() {
|
||||
unavailable("not available")
|
||||
}
|
||||
|
||||
public func unavailable(_ message: String) {
|
||||
sendResponse(nil, .dictionary(["message": message]))
|
||||
}
|
||||
}
|
238
core/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal file
238
core/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift
Normal file
@ -0,0 +1,238 @@
|
||||
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]
|
||||
|
||||
// string types
|
||||
public protocol JSStringContainer {
|
||||
func getString(_ key: String, _ defaultValue: String) -> String
|
||||
func getString(_ key: String) -> String?
|
||||
}
|
||||
|
||||
extension JSStringContainer {
|
||||
public func getString(_ key: String, _ defaultValue: String) -> String {
|
||||
return getString(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// boolean types
|
||||
public protocol JSBoolContainer {
|
||||
func getBool(_ key: String, _ defaultValue: Bool) -> Bool
|
||||
func getBool(_ key: String) -> Bool?
|
||||
}
|
||||
|
||||
extension JSBoolContainer {
|
||||
public func getBool(_ key: String, _ defaultValue: Bool) -> Bool {
|
||||
return getBool(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// integer types
|
||||
public protocol JSIntContainer {
|
||||
func getInt(_ key: String, _ defaultValue: Int) -> Int
|
||||
func getInt(_ key: String) -> Int?
|
||||
}
|
||||
|
||||
extension JSIntContainer {
|
||||
public func getInt(_ key: String, _ defaultValue: Int) -> Int {
|
||||
return getInt(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// float types
|
||||
public protocol JSFloatContainer {
|
||||
func getFloat(_ key: String, _ defaultValue: Float) -> Float
|
||||
func getFloat(_ key: String) -> Float?
|
||||
}
|
||||
|
||||
extension JSFloatContainer {
|
||||
public func getFloat(_ key: String, _ defaultValue: Float) -> Float {
|
||||
return getFloat(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// double types
|
||||
public protocol JSDoubleContainer {
|
||||
func getDouble(_ key: String, _ defaultValue: Double) -> Double
|
||||
func getDouble(_ key: String) -> Double?
|
||||
}
|
||||
|
||||
extension JSDoubleContainer {
|
||||
public func getDouble(_ key: String, _ defaultValue: Double) -> Double {
|
||||
return getDouble(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// date types
|
||||
public protocol JSDateContainer {
|
||||
func getDate(_ key: String, _ defaultValue: Date) -> Date
|
||||
func getDate(_ key: String) -> Date?
|
||||
}
|
||||
|
||||
extension JSDateContainer {
|
||||
public func getDate(_ key: String, _ defaultValue: Date) -> Date {
|
||||
return getDate(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// array types
|
||||
public protocol JSArrayContainer {
|
||||
func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray
|
||||
func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]?
|
||||
func getArray(_ key: String) -> JSArray?
|
||||
}
|
||||
|
||||
extension JSArrayContainer {
|
||||
public func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray {
|
||||
return getArray(key) ?? defaultValue
|
||||
}
|
||||
|
||||
public func getArray<T>(_ key: String, _ ofType: T.Type) -> [T]? {
|
||||
return getArray(key) as? [T]
|
||||
}
|
||||
}
|
||||
|
||||
// dictionary types
|
||||
public protocol JSObjectContainer {
|
||||
func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject
|
||||
func getObject(_ key: String) -> JSObject?
|
||||
}
|
||||
|
||||
extension JSObjectContainer {
|
||||
public func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject {
|
||||
return getObject(key) ?? defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
public protocol JSValueContainer: JSStringContainer, JSBoolContainer, JSIntContainer, JSFloatContainer,
|
||||
JSDoubleContainer, JSDateContainer, JSArrayContainer, JSObjectContainer {
|
||||
static var jsDateFormatter: ISO8601DateFormatter { get }
|
||||
var jsObjectRepresentation: JSObject { get }
|
||||
}
|
||||
|
||||
extension JSValueContainer {
|
||||
public func getValue(_ key: String) -> JSValue? {
|
||||
return jsObjectRepresentation[key]
|
||||
}
|
||||
|
||||
public func getString(_ key: String) -> String? {
|
||||
return jsObjectRepresentation[key] as? String
|
||||
}
|
||||
|
||||
public func getBool(_ key: String) -> Bool? {
|
||||
return jsObjectRepresentation[key] as? Bool
|
||||
}
|
||||
|
||||
public func getInt(_ key: String) -> Int? {
|
||||
return jsObjectRepresentation[key] as? Int
|
||||
}
|
||||
|
||||
public func getFloat(_ key: String) -> Float? {
|
||||
if let floatValue = jsObjectRepresentation[key] as? Float {
|
||||
return floatValue
|
||||
} else if let doubleValue = jsObjectRepresentation[key] as? Double {
|
||||
return Float(doubleValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getDouble(_ key: String) -> Double? {
|
||||
return jsObjectRepresentation[key] as? Double
|
||||
}
|
||||
|
||||
public func getDate(_ key: String) -> Date? {
|
||||
if let isoString = jsObjectRepresentation[key] as? String {
|
||||
return Self.jsDateFormatter.date(from: isoString)
|
||||
}
|
||||
return jsObjectRepresentation[key] as? Date
|
||||
}
|
||||
|
||||
public func getArray(_ key: String) -> JSArray? {
|
||||
return jsObjectRepresentation[key] as? JSArray
|
||||
}
|
||||
|
||||
public func getObject(_ key: String) -> JSObject? {
|
||||
return jsObjectRepresentation[key] as? JSObject
|
||||
}
|
||||
}
|
||||
|
||||
@objc protocol BridgedJSValueContainer: NSObjectProtocol {
|
||||
static var jsDateFormatter: ISO8601DateFormatter { get }
|
||||
var dictionaryRepresentation: NSDictionary { get }
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
public static func coerceArrayToJSArray(_ array: [Any]?, formattingDatesAsStrings: Bool = false) -> JSArray? {
|
||||
return array?.compactMap { coerceToJSValue($0, formattingDates: formattingDatesAsStrings) }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
54
core/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal file
54
core/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift
Normal file
@ -0,0 +1,54 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import WebKit
|
||||
import os.log
|
||||
|
||||
@objc public protocol Plugin {
|
||||
@objc func load(webview: WKWebView)
|
||||
}
|
95
core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal file
95
core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift
Normal file
@ -0,0 +1,95 @@
|
||||
import SwiftRs
|
||||
import MetalKit
|
||||
import WebKit
|
||||
import os.log
|
||||
|
||||
class PluginHandle {
|
||||
var instance: NSObject
|
||||
var loaded = false
|
||||
|
||||
init(plugin: NSObject) {
|
||||
instance = plugin
|
||||
}
|
||||
}
|
||||
|
||||
class PluginManager {
|
||||
static var shared: PluginManager = PluginManager()
|
||||
var plugins: [String:PluginHandle] = [:]
|
||||
|
||||
func onWebviewCreated(_ webview: WKWebView) {
|
||||
for (_, handle) in plugins {
|
||||
if (!handle.loaded) {
|
||||
handle.instance.perform(#selector(Plugin.load), with: webview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func load<P: Plugin & NSObject>(webview: WKWebView?, name: String, plugin: P) {
|
||||
let handle = PluginHandle(plugin: plugin)
|
||||
if let webview = webview {
|
||||
handle.instance.perform(#selector(Plugin.load), with: webview)
|
||||
handle.loaded = true
|
||||
}
|
||||
plugins[name] = handle
|
||||
}
|
||||
|
||||
func invoke(name: String, methodName: String, invoke: Invoke) {
|
||||
if let plugin = plugins[name] {
|
||||
let selectorWithThrows = Selector(("\(methodName):error:"))
|
||||
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, selectorWithThrows, invoke, OpaquePointer($0))
|
||||
}
|
||||
if let error = error {
|
||||
invoke.reject("\(error)")
|
||||
toRust(error) // TODO app is crashing without this memory leak (when an error is thrown)
|
||||
}
|
||||
} else {
|
||||
let selector = Selector(("\(methodName):"))
|
||||
if plugin.instance.responds(to: selector) {
|
||||
plugin.instance.perform(selector, with: invoke)
|
||||
} else {
|
||||
invoke.reject("No method \(methodName) found for plugin \(name)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Plugin \(name) not initialized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PluginManager: NSCopying {
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func registerPlugin<P: Plugin & NSObject>(webview: WKWebView?, name: String, plugin: P) {
|
||||
PluginManager.shared.load(
|
||||
webview: webview,
|
||||
name: name,
|
||||
plugin: plugin
|
||||
)
|
||||
}
|
||||
|
||||
@_cdecl("on_webview_created")
|
||||
func onWebviewCreated(webview: WKWebView) {
|
||||
PluginManager.shared.onWebviewCreated(webview)
|
||||
}
|
||||
|
||||
@_cdecl("invoke_plugin")
|
||||
func invokePlugin(webview: WKWebView, name: UnsafePointer<SRString>, methodName: UnsafePointer<SRString>, data: NSDictionary, callback: UInt, error: UInt) {
|
||||
let invoke = Invoke(sendResponse: { (successResult: JsonValue?, errorResult: JsonValue?) -> Void in
|
||||
let (fn, payload) = errorResult == nil ? (callback, successResult) : (error, errorResult)
|
||||
var payloadJson: String
|
||||
do {
|
||||
try payloadJson = payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`"
|
||||
} catch {
|
||||
payloadJson = "`\(error)`"
|
||||
}
|
||||
webview.evaluateJavaScript("window['_\(fn)'](\(payloadJson))")
|
||||
}, data: data)
|
||||
PluginManager.shared.invoke(name: name.pointee.to_string(), methodName: methodName.pointee.to_string(), invoke: invoke)
|
||||
}
|
11
core/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift
Normal file
11
core/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import XCTest
|
||||
@testable import Tauri
|
||||
|
||||
final class TauriTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
XCTAssertEqual(Tauri().text, "Hello, World!")
|
||||
}
|
||||
}
|
@ -384,6 +384,22 @@ impl<R: Runtime> AppHandle<R> {
|
||||
self.runtime_handle.create_proxy()
|
||||
}
|
||||
|
||||
/// Initializes an iOS plugin.
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn initialize_ios_plugin(
|
||||
&self,
|
||||
init_fn: unsafe extern "C" fn(cocoa::base::id),
|
||||
) -> crate::Result<()> {
|
||||
if let Some(window) = self.windows().values().next() {
|
||||
window.with_webview(move |w| {
|
||||
unsafe { init_fn(w.inner()) };
|
||||
})?;
|
||||
} else {
|
||||
unsafe { init_fn(cocoa::base::nil) };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes an Android plugin.
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn initialize_android_plugin(
|
||||
|
15
core/tauri/src/ios.rs
Normal file
15
core/tauri/src/ios.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use cocoa::base::id;
|
||||
use swift_rs::SRString;
|
||||
|
||||
extern "C" {
|
||||
pub fn invoke_plugin(
|
||||
webview: id,
|
||||
name: &SRString,
|
||||
method: &SRString,
|
||||
data: id,
|
||||
callback: usize,
|
||||
error: usize,
|
||||
);
|
||||
|
||||
pub fn on_webview_created(webview: id);
|
||||
}
|
@ -162,6 +162,9 @@
|
||||
#![warn(missing_docs, rust_2018_idioms)]
|
||||
#![cfg_attr(doc_cfg, feature(doc_cfg))]
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[doc(hidden)]
|
||||
pub use cocoa;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[doc(hidden)]
|
||||
pub use embed_plist;
|
||||
@ -188,6 +191,8 @@ mod pattern;
|
||||
pub mod plugin;
|
||||
pub mod window;
|
||||
use tauri_runtime as runtime;
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios;
|
||||
#[cfg(target_os = "android")]
|
||||
mod jni_helpers;
|
||||
/// The allowlist scopes.
|
||||
|
@ -1400,6 +1400,15 @@ impl<R: Runtime> WindowManager<R> {
|
||||
.created(window_);
|
||||
});
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
window
|
||||
.with_webview(|w| {
|
||||
unsafe { crate::ios::on_webview_created(w.inner()) };
|
||||
})
|
||||
.expect("failed to run on_webview_created hook");
|
||||
}
|
||||
|
||||
window
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
|
||||
Uuid::new_v4()
|
||||
}
|
||||
|
||||
#[cfg(any(desktop, target_os = "android"))]
|
||||
fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -673,14 +673,11 @@ impl<'de, R: Runtime> CommandArg<'de, R> for Window<R> {
|
||||
}
|
||||
|
||||
/// The platform webview handle. Accessed with [`Window#method.with_webview`];
|
||||
#[cfg(all(any(desktop, target_os = "android"), feature = "wry"))]
|
||||
#[cfg_attr(
|
||||
doc_cfg,
|
||||
doc(cfg(all(any(desktop, target_os = "android"), feature = "wry")))
|
||||
)]
|
||||
#[cfg(feature = "wry")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))]
|
||||
pub struct PlatformWebview(tauri_runtime_wry::Webview);
|
||||
|
||||
#[cfg(all(any(desktop, target_os = "android"), feature = "wry"))]
|
||||
#[cfg(feature = "wry")]
|
||||
impl PlatformWebview {
|
||||
/// Returns [`webkit2gtk::WebView`] handle.
|
||||
#[cfg(any(
|
||||
@ -716,8 +713,8 @@ impl PlatformWebview {
|
||||
/// Returns the [WKWebView] handle.
|
||||
///
|
||||
/// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
|
||||
pub fn inner(&self) -> cocoa::base::id {
|
||||
self.0.webview
|
||||
}
|
||||
@ -725,8 +722,8 @@ impl PlatformWebview {
|
||||
/// Returns WKWebView [controller] handle.
|
||||
///
|
||||
/// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", target_os = "ios"))))]
|
||||
pub fn controller(&self) -> cocoa::base::id {
|
||||
self.0.manager
|
||||
}
|
||||
@ -869,11 +866,8 @@ impl<R: Runtime> Window<R> {
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(all(feature = "wry", any(desktop, target_os = "android")))]
|
||||
#[cfg_attr(
|
||||
doc_cfg,
|
||||
doc(all(feature = "wry", any(desktop, target_os = "android")))
|
||||
)]
|
||||
#[cfg(all(feature = "wry"))]
|
||||
#[cfg_attr(doc_cfg, doc(all(feature = "wry")))]
|
||||
pub fn with_webview<F: FnOnce(PlatformWebview) + Send + 'static>(
|
||||
&self,
|
||||
f: F,
|
||||
@ -1352,12 +1346,161 @@ impl<R: Runtime> Window<R> {
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[cfg(mobile)]
|
||||
let (message, resolver) = (invoke.message.clone(), invoke.resolver.clone());
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let handled = manager.extend_api(plugin, invoke);
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
if !handled {
|
||||
let plugin = plugin.to_string();
|
||||
self.with_webview(move |webview| {
|
||||
use cocoa::base::{id, nil, NO, YES};
|
||||
use objc::*;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
const UTF8_ENCODING: usize = 4;
|
||||
|
||||
struct NSString(id);
|
||||
|
||||
impl NSString {
|
||||
fn new(s: &str) -> Self {
|
||||
// Safety: objc runtime calls are unsafe
|
||||
NSString(unsafe {
|
||||
let ns_string: id = msg_send![class!(NSString), alloc];
|
||||
let ns_string: id = msg_send![ns_string,
|
||||
initWithBytes:s.as_ptr()
|
||||
length:s.len()
|
||||
encoding:UTF8_ENCODING];
|
||||
|
||||
// The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
|
||||
// or it can not be released correctly in OC runtime
|
||||
let _: () = msg_send![ns_string, autorelease];
|
||||
|
||||
ns_string
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn add_json_value_to_array(array: id, value: JsonValue) {
|
||||
match value {
|
||||
JsonValue::Null => {
|
||||
let null: id = msg_send![class!(NSNull), null];
|
||||
let () = msg_send![array, addObject: null];
|
||||
}
|
||||
JsonValue::Bool(val) => {
|
||||
let value = if val { YES } else { NO };
|
||||
let v: id = msg_send![class!(NSNumber), numberWithBool: value];
|
||||
let () = msg_send![array, addObject: v];
|
||||
}
|
||||
JsonValue::Number(val) => {
|
||||
let number: id = if let Some(v) = val.as_i64() {
|
||||
msg_send![class!(NSNumber), numberWithInteger: v]
|
||||
} else if let Some(v) = val.as_u64() {
|
||||
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
|
||||
} else if let Some(v) = val.as_f64() {
|
||||
msg_send![class!(NSNumber), numberWithDouble: v]
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
let () = msg_send![array, addObject: number];
|
||||
}
|
||||
JsonValue::String(val) => {
|
||||
let () = msg_send![array, addObject: NSString::new(&val)];
|
||||
}
|
||||
JsonValue::Array(val) => {
|
||||
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
|
||||
let inner_array: id = msg_send![nsarray, init];
|
||||
for value in val {
|
||||
add_json_value_to_array(inner_array, value);
|
||||
}
|
||||
let () = msg_send![array, addObject: inner_array];
|
||||
}
|
||||
JsonValue::Object(val) => {
|
||||
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
|
||||
let data: id = msg_send![dictionary, init];
|
||||
for (key, value) in val {
|
||||
add_json_entry_to_dictionary(data, key, value);
|
||||
}
|
||||
let () = msg_send![array, addObject: data];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn add_json_entry_to_dictionary(data: id, key: String, value: JsonValue) {
|
||||
let key = NSString::new(&key);
|
||||
match value {
|
||||
JsonValue::Null => {
|
||||
let null: id = msg_send![class!(NSNull), null];
|
||||
let () = msg_send![data, setObject:null forKey: key];
|
||||
}
|
||||
JsonValue::Bool(val) => {
|
||||
let value = if val { YES } else { NO };
|
||||
let () = msg_send![data, setObject:value forKey: key];
|
||||
}
|
||||
JsonValue::Number(val) => {
|
||||
let number: id = if let Some(v) = val.as_i64() {
|
||||
msg_send![class!(NSNumber), numberWithInteger: v]
|
||||
} else if let Some(v) = val.as_u64() {
|
||||
msg_send![class!(NSNumber), numberWithUnsignedLongLong: v]
|
||||
} else if let Some(v) = val.as_f64() {
|
||||
msg_send![class!(NSNumber), numberWithDouble: v]
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
let () = msg_send![data, setObject:number forKey: key];
|
||||
}
|
||||
JsonValue::String(val) => {
|
||||
let () = msg_send![data, setObject:NSString::new(&val) forKey: key];
|
||||
}
|
||||
JsonValue::Array(val) => {
|
||||
let nsarray: id = msg_send![class!(NSMutableArray), alloc];
|
||||
let array: id = msg_send![nsarray, init];
|
||||
for value in val {
|
||||
add_json_value_to_array(array, value);
|
||||
}
|
||||
let () = msg_send![data, setObject:array forKey: key];
|
||||
}
|
||||
JsonValue::Object(val) => {
|
||||
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
|
||||
let inner_data: id = msg_send![dictionary, init];
|
||||
for (key, value) in val {
|
||||
add_json_entry_to_dictionary(inner_data, key, value);
|
||||
}
|
||||
let () = msg_send![data, setObject:inner_data forKey: key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data = if let JsonValue::Object(map) = message.payload {
|
||||
unsafe {
|
||||
let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
|
||||
let data: id = msg_send![dictionary, init];
|
||||
for (key, value) in map {
|
||||
add_json_entry_to_dictionary(data, key, value);
|
||||
}
|
||||
data
|
||||
}
|
||||
} else {
|
||||
nil
|
||||
};
|
||||
|
||||
unsafe {
|
||||
crate::ios::invoke_plugin(
|
||||
webview.inner(),
|
||||
&plugin.as_str().into(),
|
||||
&message.command.as_str().into(),
|
||||
data,
|
||||
resolver.callback.0,
|
||||
resolver.error.0,
|
||||
)
|
||||
};
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if !handled {
|
||||
|
13
examples/api/src-tauri/Cargo.lock
generated
13
examples/api/src-tauri/Cargo.lock
generated
@ -2896,6 +2896,16 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/Brendonovich/swift-rs?rev=eb6de914ad57501da5019154d476d45660559999#eb6de914ad57501da5019154d476d45660559999"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.107"
|
||||
@ -3036,6 +3046,7 @@ dependencies = [
|
||||
"serialize-to-javascript",
|
||||
"shared_child",
|
||||
"state",
|
||||
"swift-rs",
|
||||
"tar",
|
||||
"tauri-build",
|
||||
"tauri-macros",
|
||||
@ -3066,6 +3077,7 @@ dependencies = [
|
||||
"quote",
|
||||
"semver 1.0.16",
|
||||
"serde_json",
|
||||
"swift-rs",
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
"walkdir",
|
||||
@ -3130,6 +3142,7 @@ dependencies = [
|
||||
name = "tauri-plugin-sample"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
]
|
||||
|
@ -8,5 +8,5 @@ fn main() {
|
||||
codegen = codegen.dev();
|
||||
}
|
||||
codegen.build();
|
||||
tauri_build::build()
|
||||
tauri_build::build();
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tauri = { path = "../../../../core/tauri" }
|
||||
log = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { path = "../../../../core/tauri-build/" }
|
||||
|
@ -3,6 +3,7 @@ use std::process::exit;
|
||||
fn main() {
|
||||
if let Err(error) = tauri_build::mobile::PluginBuilder::new()
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.run()
|
||||
{
|
||||
println!("{error:#}");
|
||||
|
10
examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore
vendored
Normal file
10
examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
34
examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift
Normal file
34
examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift
Normal file
@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-sample",
|
||||
platforms: [
|
||||
.iOS(.v11),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-sample",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-sample"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
//.package(url: "https://github.com/tauri-apps/tauri", branch: "next"),
|
||||
.package(name: "Tauri", path: "../../../../../core/tauri/mobile/ios-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-sample",
|
||||
dependencies: [
|
||||
//.product(name: "Tauri", package: "tauri"),
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
3
examples/api/src-tauri/tauri-plugin-sample/ios/README.md
Normal file
3
examples/api/src-tauri/tauri-plugin-sample/ios/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Tauri Plugin sample
|
||||
|
||||
A description of this package.
|
@ -0,0 +1,17 @@
|
||||
import MetalKit
|
||||
import WebKit
|
||||
import Tauri
|
||||
|
||||
class ExamplePlugin: NSObject, Plugin {
|
||||
@objc func load(webview: WKWebView) {}
|
||||
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let value = invoke.getString("value")
|
||||
invoke.resolve(.dictionary(["value": value as Any]))
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_sample")
|
||||
func initPlugin(webview: WKWebView?) {
|
||||
Tauri.registerPlugin(webview: webview, name: "sample", plugin: ExamplePlugin())
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
@ -7,15 +7,19 @@ const PLUGIN_NAME: &str = "sample";
|
||||
#[cfg(target_os = "android")]
|
||||
const PLUGIN_IDENTIFIER: &str = "com.plugin.test";
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = Builder::new(PLUGIN_NAME);
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
builder = builder.setup(|app| {
|
||||
app.initialize_android_plugin(PLUGIN_NAME, PLUGIN_IDENTIFIER, "ExamplePlugin")?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
builder.build()
|
||||
#[cfg(target_os = "ios")]
|
||||
extern "C" {
|
||||
fn init_plugin_sample(webview: tauri::cocoa::base::id);
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new(PLUGIN_NAME)
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "android")]
|
||||
app.initialize_android_plugin(PLUGIN_NAME, PLUGIN_IDENTIFIER, "ExamplePlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
app.initialize_ios_plugin(init_plugin_sample)?;
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
|
||||
configure_cargo(app, Some((&mut env, config)))?;
|
||||
|
||||
// run an initial build to initialize plugins
|
||||
Target::all().first_key_value().unwrap().1.build(
|
||||
Target::all().values().next().unwrap().build(
|
||||
config,
|
||||
metadata,
|
||||
&env,
|
||||
|
@ -117,7 +117,7 @@ fn run_dev(
|
||||
let target_triple = device
|
||||
.as_ref()
|
||||
.map(|d| d.target().triple.to_string())
|
||||
.unwrap_or_else(|| Target::all().first_key_value().unwrap().1.triple.into());
|
||||
.unwrap_or_else(|| Target::all().values().next().unwrap().triple.into());
|
||||
dev_options.target = Some(target_triple.clone());
|
||||
let mut interface = crate::dev::setup(&mut dev_options, true)?;
|
||||
|
||||
@ -138,7 +138,7 @@ fn run_dev(
|
||||
let target = Target::all()
|
||||
.values()
|
||||
.find(|t| t.triple == target_triple)
|
||||
.unwrap_or(Target::all().first_key_value().unwrap().1);
|
||||
.unwrap_or(Target::all().values().next().unwrap());
|
||||
target.build(config, metadata, &env, noise_level, true, Profile::Debug)?;
|
||||
|
||||
let open = options.open;
|
||||
|
@ -22,6 +22,7 @@ use std::{
|
||||
const BACKEND_PLUGIN_DIR: Dir<'_> = include_dir!("templates/plugin/backend");
|
||||
const API_PLUGIN_DIR: Dir<'_> = include_dir!("templates/plugin/with-api");
|
||||
const ANDROID_PLUGIN_DIR: Dir<'_> = include_dir!("templates/plugin/android");
|
||||
const IOS_PLUGIN_DIR: Dir<'_> = include_dir!("templates/plugin/ios");
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Initializes a Tauri plugin project")]
|
||||
@ -48,6 +49,9 @@ pub struct Options {
|
||||
/// Adds native Android support.
|
||||
#[clap(long)]
|
||||
android: bool,
|
||||
/// Adds native iOS support.
|
||||
#[clap(long)]
|
||||
ios: bool,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@ -163,7 +167,17 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_context(|| "failed to render Tauri template")?;
|
||||
.with_context(|| "failed to render plugin Android template")?;
|
||||
}
|
||||
|
||||
if options.ios {
|
||||
template::render(
|
||||
&handlebars,
|
||||
&data,
|
||||
&IOS_PLUGIN_DIR,
|
||||
template_target_path.join("ios"),
|
||||
)
|
||||
.with_context(|| "failed to render plugin iOS template")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
8
tooling/cli/templates/mobile/ios/Sources/Tauri.swift
Normal file
8
tooling/cli/templates/mobile/ios/Sources/Tauri.swift
Normal file
@ -0,0 +1,8 @@
|
||||
//
|
||||
// Tauri.swift
|
||||
// api_iOS
|
||||
//
|
||||
// Created by Lucas Nogueira on 29/01/23.
|
||||
//
|
||||
|
||||
import Foundation
|
@ -72,9 +72,9 @@ targets:
|
||||
ENABLE_BITCODE: false
|
||||
ARCHS: [{{join ios-valid-archs}}]
|
||||
VALID_ARCHS: {{~#each ios-valid-archs}} {{this}} {{/each}}
|
||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64-apple-ios/$(CONFIGURATION)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/aarch64-apple-ios/$(CONFIGURATION)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64-sim]: $(inherited) $(PROJECT_DIR)/Externals/aarch64-apple-ios-sim/$(CONFIGURATION)
|
||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64-apple-ios/$(CONFIGURATION) $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/aarch64-apple-ios/$(CONFIGURATION) $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64-sim]: $(inherited) $(PROJECT_DIR)/Externals/aarch64-apple-ios-sim/$(CONFIGURATION) $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||
groups: [app]
|
||||
dependencies:
|
||||
|
10
tooling/cli/templates/plugin/ios/.gitignore
vendored
Normal file
10
tooling/cli/templates/plugin/ios/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
32
tooling/cli/templates/plugin/ios/Package.swift
Normal file
32
tooling/cli/templates/plugin/ios/Package.swift
Normal file
@ -0,0 +1,32 @@
|
||||
// swift-tools-version:5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-{{ plugin_name }}",
|
||||
platforms: [
|
||||
.iOS(.v11),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-{{ plugin_name }}",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-{{ plugin_name }}"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
.package(url: "https://github.com/tauri-apps/tauri", branch: "next"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-{{ plugin_name }}",
|
||||
dependencies: [
|
||||
.product(name: "Tauri", package: "tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
3
tooling/cli/templates/plugin/ios/README.md
Normal file
3
tooling/cli/templates/plugin/ios/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Tauri Plugin {{ plugin_name_original }}
|
||||
|
||||
A description of this package.
|
17
tooling/cli/templates/plugin/ios/Sources/ExamplePlugin.swift
Normal file
17
tooling/cli/templates/plugin/ios/Sources/ExamplePlugin.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import MetalKit
|
||||
import WebKit
|
||||
import Tauri
|
||||
|
||||
class ExamplePlugin: NSObject, Plugin {
|
||||
@objc func load(webview: WKWebView) {}
|
||||
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let value = invoke.getString("value")
|
||||
invoke.resolve(.dictionary(["value": value as Any]))
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_{{ plugin_name_snake_case }}")
|
||||
func initPlugin(webview: WKWebView?) {
|
||||
Tauri.registerPlugin(webview: webview, name: "{{plugin_name}}", plugin: ExamplePlugin())
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user