-iOS Player Plugins are very similar to core and react plugins in both their composition and use. The `PlayerUI/Core` subspec exposes an interface `NativePlugin` that, much like the core `PlayerPlugin` interfaces, provides the necessary attributes that are required for an iOS Player plugin. A `pluginName` attributed is required, and a function `apply` is required that takes an instance of a Player implementation. Similarly to core plugins, in the `apply` function you have access to the Player object and access to the hooks. `apply` uses generics to future proof so plugins can be used for multiple Player implementations should they be created.
+iOS Player Plugins are very similar to core and react plugins in both their composition and use.
+
+### NativePlugin
+
+The `PlayerUI/Core` subspec exposes an interface `NativePlugin` that, much like the core `PlayerPlugin` interfaces, provides the necessary attributes that are required for an iOS Player plugin. A `pluginName` attributed is required, and a function `apply` is required that takes an instance of a Player implementation. Similarly to core plugins, in the `apply` function you have access to the Player object and access to the hooks. `apply` uses generics to future proof so plugins can be used for multiple Player implementations should they be created.
The `player` passed to `apply` exposes hooks from the core player, as well as hooks specific to that player implementation. For the current state of this project, the `SwiftUIPlayer` is the primary iOS Player, and exposes two hooks for the SwiftUI layer specifically:
- The `view` hook allows you to modify the root view that will be displayed in the SwiftUIPlayer body. This is useful for applying changes to the environment for the SwiftUI view tree, or apply ViewModifiers and such.
- The `transition` hook allows you to specify a `PlayerViewTransition` object to be applied when the flow transitions from one view to another, to animate the transition.
+
+#### Basic Example
Below is an example of a basic `NativePlugin` that sets a value in the EnvironmentValues when the plugin is included:
```swift
@@ -107,5 +134,76 @@ class EnvironmentPlugin: NativePlugin {
}
}
```
+#### Asset Registration
+Likely the most common usecase for plugins is to register assets:
+
+```swift
+import PlayerUI
+
+class ExampleAssetPlugin: NativePlugin {
+ let pluginName = "ExampleAssetPlugin"
+
+ func apply(player: P) where P: HeadlessPlayer {
+ guard let player = player as? SwiftUIPlayer else { return }
+ player.assetRegistry.register("text", asset: TextAsset.self)
+ player.assetRegistry.register("action", asset: ActionAsset.self)
+ }
+}
+```
+
+### JSBasePlugin
+Building native features on top of shared functionality is one of the primary benefits of using player. As such we expose convenience utilities to enable loading JavaScript Player Plugins as the base for your `NativePlugin`.
+
+
+#### Basic Setup
+This example will load the `SharedJSPlugin` in the JavaScript layer when included as a plugin to `SwiftUIPlayer`.
+```swift
+import PlayerUI
+
+class SharedJSPlugin: JSBasePlugin {
+ convenience init() {
+ // pluginName must match the exported class name in the JavaScript plugin
+ self.init(fileName: 'shared-js-plugin-bundle', pluginName: 'SharedJSPlugin')
+ }
+
+ // Construct the URL to load the JS bundle
+ override open func getUrlForFile(fileName: String) -> URL? {
+ ResourceUtilities.urlForFile(
+ fileName: fileName,
+ ext: "js",
+ bundle: Bundle(for: YourPlugin.self),
+ pathComponent: "YOUR_POD.bundle"
+ )
+ }
+}
+```
+#### Arguments for constructing the JavaScript plugins
+To simplify the ease of use, the `JSContext` for `JSBasePlugin` implementations is provided when the underlying resources for the core `player` are being setup. This means that we need to provide the arguments for the JavaScript constructor late. To do this, override the `getArguments` function:
+
+```swift
+import PlayerUI
+
+class SharedJSPlugin: JSBasePlugin {
+ var option: Boolean
+
+ convenience init(option: Boolean) {
+ // pluginName must match the exported class name in the JavaScript plugin
+ self.init(fileName: 'shared-js-plugin-bundle', pluginName: 'SharedJSPlugin')
+ self.option = option
+ }
+
+ override open func getArguments() -> [Any] {
+ // plugin just takes a boolean arguments
+ return [option]
+ // More common in JavaScript, constructor takes an object
+ return [["enable": option]]
+ }
+}
+```
+
+**Note**: As `JavaScriptCore` cannot resolve dependencies at runtime, using a module bundler such as [tsup](https://github.com/egoist/tsup) is required.
+
+**Note**: `JSBasePlugin` implementations do not necessarily need to be a `PlayerPlugin`, for example, the [BeaconPlugin](https://github.com/player-ui/player/blob/main/ios/plugins/BaseBeaconPlugin/Sources/BaseBeaconPlugin.swift#L63) can take plugins in it's constructor, that are not `PlayerPlugin`.
+
diff --git a/docs/site/plugins/mdx-link-append-loader.js b/docs/site/plugins/mdx-link-append-loader.js
new file mode 100644
index 000000000..f27e598b5
--- /dev/null
+++ b/docs/site/plugins/mdx-link-append-loader.js
@@ -0,0 +1,9 @@
+const path = require('path');
+
+module.exports = function (source) {
+ const filePath = this.resourcePath;
+ const rootDir = this.rootContext;
+ const relativePath = path.relative(rootDir, filePath);
+ const newContent = `${source}\n\n---\n\n[Help to improve this page](https://github.dev/player-ui/player/blob/main/docs/site/${relativePath})`;
+ return newContent;
+};
diff --git a/docs/storybook/src/flows/managed.ts b/docs/storybook/src/flows/managed.ts
index 1f4ea8f48..fc8de3ce1 100644
--- a/docs/storybook/src/flows/managed.ts
+++ b/docs/storybook/src/flows/managed.ts
@@ -1,38 +1,8 @@
-import { FlowManager, Flow, CompletedState } from "@player-ui/react";
-import { makeFlow } from "@player-ui/make-flow";
-
-const firstFlow = makeFlow({
- id: "text-1",
- type: "action",
- value: "Flow 1",
- label: {
- asset: { id: "action-label-1", type: "text", value: "End Flow 1" },
- },
-});
-
-const secondFlow = makeFlow({
- id: "text-2",
- type: "action",
- value: "Flow 2",
- label: {
- asset: { id: "action-label-2", type: "text", value: "End Flow 2" },
- },
-});
-
-const errorFlow = makeFlow({
- id: "text-2",
- type: "action",
- value: "Flow Error",
- exp: "{{foo.bar..}",
- label: {
- asset: { id: "action-label-2", type: "text", value: "End Flow 2" },
- },
-});
-
-const assetErrorFlow = makeFlow({
- id: "text-3",
- type: "error",
-});
+import { FlowManager, Flow, CompletedState } from '@player-ui/react';
+import firstFlow from '@player-ui/reference-assets-plugin-mocks/flow-manager/first-flow.json';
+import secondFlow from '@player-ui/reference-assets-plugin-mocks/flow-manager/second-flow.json';
+import errorFlow from '@player-ui/reference-assets-plugin-mocks/flow-manager/error-flow.json';
+import assetErrorFlow from '@player-ui/reference-assets-plugin-mocks/flow-manager/asset-error-flow.json';
export const SIMPLE_FLOWS = [firstFlow, secondFlow];
export const ERROR_CONTENT_FLOW = [firstFlow, errorFlow];
diff --git a/ios/core/Sources/CorePlugins/JSBasePlugin.swift b/ios/core/Sources/CorePlugins/JSBasePlugin.swift
index 46eedce74..f3537f06e 100644
--- a/ios/core/Sources/CorePlugins/JSBasePlugin.swift
+++ b/ios/core/Sources/CorePlugins/JSBasePlugin.swift
@@ -80,7 +80,7 @@ open class JSBasePlugin {
*/
private func getPlugin(context: JSContext, fileName: String, pluginName: String, arguments: [Any] = []) -> JSValue {
guard
- let plugin = context.getClassReference(pluginName, load: {loadJSResource(into: $0, fileName: fileName)}),
+ let plugin = context.getClassReference(pluginName, load: {loadJSResource(into: $0, fileName: fileName)}), !plugin.isUndefined,
let pluginValue = plugin.construct(withArguments: arguments)
else {
fatalError("Unable To Construct \(pluginName)")
diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift
index f8857efa3..3399f340a 100644
--- a/ios/core/Sources/Player/HeadlessPlayer.swift
+++ b/ios/core/Sources/Player/HeadlessPlayer.swift
@@ -187,7 +187,7 @@ public extension HeadlessPlayer {
let warn = JSValue(object: logger.getJSLogFor(level: .warning), in: context)
let error = JSValue(object: logger.getJSLogFor(level: .error), in: context)
for plugin in plugins {
- if let plugin = plugin as? JSBasePlugin {
+ if let plugin = plugin as? JSBasePlugin, plugin.context != context {
plugin.context = context
}
}
diff --git a/ios/core/Sources/Types/Core/BindingParser.swift b/ios/core/Sources/Types/Core/BindingParser.swift
index e59e32829..349455047 100644
--- a/ios/core/Sources/Types/Core/BindingParser.swift
+++ b/ios/core/Sources/Types/Core/BindingParser.swift
@@ -34,7 +34,7 @@ public class BindingParser {
}
/// A path in the data model
-public class BindingInstance {
+public class BindingInstance: Decodable {
internal var value: JSValue?
/// Create a BindingInstance, a path to data in the data model
@@ -53,6 +53,11 @@ public class BindingInstance {
self.value = value
}
+ ///Create a BindingInstance from a decoder
+ required public init(from decoder: Decoder) throws {
+ value = try decoder.getJSValue()
+ }
+
/// Retrieve this Binding as an array
/// - Returns: The array of segments in the binding
public func asArray() -> [String]? {
diff --git a/ios/core/Sources/Types/Core/DataController.swift b/ios/core/Sources/Types/Core/DataController.swift
index 74b5c2787..65eb1cab4 100644
--- a/ios/core/Sources/Types/Core/DataController.swift
+++ b/ios/core/Sources/Types/Core/DataController.swift
@@ -16,6 +16,9 @@ open class BaseDataController {
/// The JSValue that backs this wrapper
public let value: JSValue
+ /// The hooks that can be tapped into
+ public let hooks: DataControllerHooks
+
/**
Construct a DataController from a JSValue
- parameters:
@@ -23,6 +26,9 @@ open class BaseDataController {
*/
public init(_ value: JSValue) {
self.value = value
+ self.hooks = DataControllerHooks(
+ onUpdate: HookDecode<[Update]>(baseValue: self.value, name: "onUpdate")
+ )
}
/**
diff --git a/ios/core/Sources/Types/Core/Flow.swift b/ios/core/Sources/Types/Core/Flow.swift
index ed101cddd..ae31cb619 100644
--- a/ios/core/Sources/Types/Core/Flow.swift
+++ b/ios/core/Sources/Types/Core/Flow.swift
@@ -1,6 +1,6 @@
//
// Flow.swift
-//
+//
//
// Created by Borawski, Harris on 2/13/20.
//
diff --git a/ios/core/Sources/Types/Core/FlowController.swift b/ios/core/Sources/Types/Core/FlowController.swift
index 9eb5f249c..1c192ff7b 100644
--- a/ios/core/Sources/Types/Core/FlowController.swift
+++ b/ios/core/Sources/Types/Core/FlowController.swift
@@ -52,4 +52,3 @@ public class FlowController: CreatedFromJSValue {
try self.value.objectForKeyedSubscript("transition").tryCatch(args: [action])
}
}
-
diff --git a/ios/core/Sources/Types/Core/NavigationStates.swift b/ios/core/Sources/Types/Core/NavigationStates.swift
index 0e167a80d..cc65b05a4 100644
--- a/ios/core/Sources/Types/Core/NavigationStates.swift
+++ b/ios/core/Sources/Types/Core/NavigationStates.swift
@@ -43,8 +43,8 @@ public class NavigationFlowViewState: NavigationFlowTransitionableState {
public var ref: String { rawValue.objectForKeyedSubscript("ref").toString() }
/// View meta-properties
- public var attributes: [String: String]? {
- rawValue.objectForKeyedSubscript("attributes").toObject() as? [String: String]
+ public var attributes: [String: Any]? {
+ rawValue.objectForKeyedSubscript("attributes").toObject() as? [String: Any]
}
public subscript