feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,34 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A result builder that captures the ``ClassDefinition`` elements such as functions, constants and properties.
*/
@resultBuilder
public struct ClassDefinitionBuilder<OwnerType> {
public static func buildBlock(_ elements: AnyClassDefinitionElement...) -> [AnyClassDefinitionElement] {
return elements
}
/**
Default implementation without any constraints that just returns type-erased element.
*/
public static func buildExpression<ElementType: AnyClassDefinitionElement>(
_ element: ElementType
) -> AnyClassDefinitionElement {
return element
}
/**
In case the element's owner type matches builder's generic type,
we need to instruct the function to pass `this` to the closure
as the first argument and deduct it from `argumentsCount`.
*/
public static func buildExpression<ElementType: ClassDefinitionElement>(
_ element: ElementType
) -> AnyClassDefinitionElement where ElementType.OwnerType == OwnerType {
if var function = element as? AnyFunctionDefinition {
function.takesOwner = true
}
return element
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022-present 650 Industries. All rights reserved.
public protocol AnyObjectDefinitionElement: AnyDefinition {}
@resultBuilder
public struct ObjectDefinitionBuilder {
public static func buildBlock(_ elements: AnyObjectDefinitionElement...) -> [AnyObjectDefinitionElement] {
return elements
}
/**
Default implementation without any constraints that just returns type-erased element.
*/
public static func buildExpression<ElementType: AnyObjectDefinitionElement>(_ element: ElementType) -> AnyObjectDefinitionElement {
return element
}
}
extension SyncFunctionDefinition: AnyObjectDefinitionElement {}
extension AsyncFunctionDefinition: AnyObjectDefinitionElement {}
extension PropertyDefinition: AnyObjectDefinitionElement {}
extension ConstantsDefinition: AnyObjectDefinitionElement {}

View File

@@ -0,0 +1,53 @@
/**
A result builder for the view elements such as prop setters or view events.
*/
@resultBuilder
public struct ViewDefinitionBuilder<ViewType: UIView> {
public static func buildBlock(_ elements: AnyViewDefinitionElement...) -> [AnyViewDefinitionElement] {
return elements
}
/**
Accepts `Events` definition element of `View`.
*/
public static func buildExpression(_ element: EventsDefinition) -> AnyViewDefinitionElement {
return element
}
/**
Accepts `Prop` definition element and lets to skip defining the view type it's inferred from the `View` definition.
*/
public static func buildExpression<PropType: AnyArgument>(_ element: ConcreteViewProp<ViewType, PropType>) -> AnyViewDefinitionElement {
return element
}
/**
Accepts lifecycle methods (such as `OnViewDidUpdateProps`) as a definition element.
*/
public static func buildExpression(_ element: ViewLifecycleMethod<ViewType>) -> AnyViewDefinitionElement {
return element
}
/**
Accepts functions as a view definition elements.
*/
public static func buildExpression<ElementType: ViewDefinitionFunctionElement>(
_ element: ElementType
) -> AnyViewDefinitionElement {
return element
}
/**
Accepts functions that take the owner as a view definition elements.
*/
public static func buildExpression<ElementType: ViewDefinitionFunctionElement>(
_ element: ElementType
) -> AnyViewDefinitionElement where ElementType.ViewType == ViewType {
// Enforce async functions to run on the main queue
if var function = element as? AnyAsyncFunctionDefinition {
function.runOnQueue(.main)
function.takesOwner = true
}
return element
}
}

View File

@@ -0,0 +1,173 @@
/**
Asynchronous function without arguments.
*/
public func AsyncFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> AsyncFunctionDefinition<(), Void, R> {
return AsyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Asynchronous function with one argument.
*/
public func AsyncFunction<R, A0: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0) throws -> R
) -> AsyncFunctionDefinition<(A0), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self],
closure
)
}
/**
Asynchronous function with two arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1) throws -> R
) -> AsyncFunctionDefinition<(A0, A1), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self, ~A1.self],
closure
)
}
/**
Asynchronous function with three arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self
],
closure
)
}
/**
Asynchronous function with four arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2, A3), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self
],
closure
)
}
/**
Asynchronous function with five arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2, A3, A4), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self
],
closure
)
}
/**
Asynchronous function with six arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2, A3, A4, A5), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self
],
closure
)
}
/**
Asynchronous function with seven arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument, A6: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self
],
closure
)
}
/**
Asynchronous function with eight arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument, A6: AnyArgument, A7: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6, A7) throws -> R
) -> AsyncFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6, A7), A0, R> {
return AsyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self,
~A7.self
],
closure
)
}

View File

@@ -0,0 +1,96 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Class constructor without arguments.
*/
public func Constructor<R>(
@_implicitSelfCapture _ body: @escaping () throws -> R
) -> SyncFunctionDefinition<(), Void, R> {
return Function("constructor", body)
}
/**
Class constructor with one argument.
*/
public func Constructor<R, A0: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0) throws -> R
) -> SyncFunctionDefinition<(A0), A0, R> {
return Function("constructor", body)
}
/**
Class constructor with two arguments.
*/
public func Constructor<R, A0: AnyArgument, A1: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, A1) throws -> R
) -> SyncFunctionDefinition<(A0, A1), A0, R> {
return Function("constructor", body)
}
/**
Class constructor with three arguments.
*/
public func Constructor<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, A1, A2) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2), A0, R> {
return Function("constructor", body)
}
/**
Class constructor with four arguments.
*/
public func Constructor<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, A1, A2, A3) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3), A0, R> {
return Function("constructor", body)
}
/**
Class constructor with five arguments.
*/
public func Constructor<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, A1, A2, A3, A4) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4), A0, R> {
return Function("constructor", body)
}
/**
Class constructor with six arguments.
*/
public func Constructor<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument>(
@_implicitSelfCapture _ body: @escaping (A0, A1, A2, A3, A4, A5) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4, A5), A0, R> {
return Function("constructor", body)
}
/**
Creates the definition describing a JavaScript class.
*/
public func Class(
_ name: String,
@ClassDefinitionBuilder<JavaScriptObject> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: name, associatedType: JavaScriptObject.self, elements: elements())
}
/**
Creates the definition describing a JavaScript class with an associated native shared object class.
*/
public func Class<SharedObjectType: SharedObject>(
_ name: String = String(describing: SharedObjectType.self),
_ sharedObjectType: SharedObjectType.Type,
@ClassDefinitionBuilder<SharedObjectType> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: name, associatedType: SharedObjectType.self, elements: elements())
}
/**
Creates the definition describing a JavaScript class with an associated native shared object class
and with the name that is inferred from the shared object type.
*/
public func Class<SharedObjectType: SharedObject>(
_ sharedObjectType: SharedObjectType.Type,
@ClassDefinitionBuilder<SharedObjectType> @_implicitSelfCapture _ elements: () -> [AnyClassDefinitionElement]
) -> ClassDefinition {
return ClassDefinition(name: String(describing: SharedObjectType.self), associatedType: SharedObjectType.self, elements: elements())
}

View File

@@ -0,0 +1,183 @@
/**
Concurrently-executing asynchronous function without arguments.
*/
public func AsyncFunction<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () async throws -> R
) -> ConcurrentFunctionDefinition<(), Void, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Concurrently-executing asynchronous function with one argument.
*/
public func AsyncFunction<R, A0: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0) async throws -> R
) -> ConcurrentFunctionDefinition<(A0), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self],
closure
)
}
/**
Concurrently-executing asynchronous function with two arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self, ~A1.self],
closure
)
}
/**
Concurrently-executing asynchronous function with three arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self
],
closure
)
}
/**
Concurrently-executing asynchronous function with four arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2, A3), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self
],
closure
)
}
/**
Concurrently-executing asynchronous function with five arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2, A3, A4), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self
],
closure
)
}
/**
Concurrently-executing asynchronous function with six arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2, A3, A4, A5), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self
],
closure
)
}
/**
Concurrently-executing asynchronous function with seven arguments.
*/
public func AsyncFunction<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument, A6: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self
],
closure
)
}
/**
Concurrently-executing asynchronous function with eight arguments.
*/
public func AsyncFunction<
R,
A0: AnyArgument,
A1: AnyArgument,
A2: AnyArgument,
A3: AnyArgument,
A4: AnyArgument,
A5: AnyArgument,
A6: AnyArgument,
A7: AnyArgument
>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6, A7) async throws -> R
) -> ConcurrentFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6, A7), A0, R> {
return ConcurrentFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self,
~A7.self
],
closure
)
}

View File

@@ -0,0 +1,41 @@
/**
Creates module's lifecycle listener that is called right after module initialization.
*/
public func OnCreate(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.moduleCreate, closure)
}
/**
Creates module's lifecycle listener that is called when the module is about to be deallocated.
*/
public func OnDestroy(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.moduleDestroy, closure)
}
/**
Creates module's lifecycle listener that is called when the app context owning the module is about to be deallocated.
*/
public func OnAppContextDestroys(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appContextDestroys, closure)
}
/**
Creates a listener that is called when the app is about to enter the foreground mode.
*/
public func OnAppEntersForeground(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appEntersForeground, closure)
}
/**
Creates a listener that is called when the app becomes active again.
*/
public func OnAppBecomesActive(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appBecomesActive, closure)
}
/**
Creates a listener that is called when the app enters the background mode.
*/
public func OnAppEntersBackground(@_implicitSelfCapture _ closure: @escaping () -> Void) -> AnyDefinition {
return EventListener(.appEntersBackground, closure)
}

View File

@@ -0,0 +1,6 @@
/**
Sets the name of the module that is exported to the JavaScript world.
*/
public func Name(_ name: String) -> AnyDefinition {
return ModuleNameDefinition(name: name)
}

View File

@@ -0,0 +1,54 @@
/// This file implements factories for definitions that are allowed in any object-based definition `ObjectDefinition`.
/// So far only constants and functions belong to plain object.
// MARK: - Object
public func Object(@ObjectDefinitionBuilder @_implicitSelfCapture _ body: () -> [AnyDefinition]) -> ObjectDefinition {
return ObjectDefinition(definitions: body())
}
// MARK: - Constants
/**
Definition function setting the module's constants to export.
*/
public func Constants(@_implicitSelfCapture _ body: @escaping () -> [String: Any?]) -> AnyDefinition {
return ConstantsDefinition(body: body)
}
/**
Definition function setting the module's constants to export.
*/
public func Constants(@_implicitSelfCapture _ body: @autoclosure @escaping () -> [String: Any?]) -> AnyDefinition {
return ConstantsDefinition(body: body)
}
// MARK: - Events
/**
Defines event names that the object can send to JavaScript.
*/
public func Events(_ names: String...) -> EventsDefinition {
return EventsDefinition(names: names)
}
/**
Defines event names that the object can send to JavaScript.
*/
public func Events(_ names: [String]) -> EventsDefinition {
return EventsDefinition(names: names)
}
/**
Function that is invoked when the first event listener is added.
*/
public func OnStartObserving(_ event: String? = nil, @_implicitSelfCapture _ closure: @escaping () -> Void) -> EventObservingDefinition {
return EventObservingDefinition(type: .startObserving, event: event, closure)
}
/**
Function that is invoked when all event listeners are removed.
*/
public func OnStopObserving(_ event: String? = nil, @_implicitSelfCapture _ closure: @escaping () -> Void) -> EventObservingDefinition {
return EventObservingDefinition(type: .stopObserving, event: event, closure)
}

View File

@@ -0,0 +1,50 @@
/**
Creates the property with given name. The definition is basically no-op if you don't call `.get(_:)` or `.set(_:)` on it.
*/
public func Property(_ name: String) -> PropertyDefinition<Void> {
return PropertyDefinition(name: name)
}
/**
Creates the read-only property whose getter doesn't take the owner as an argument.
*/
public func Property<Value: AnyArgument>(_ name: String, @_implicitSelfCapture get: @escaping () -> Value) -> PropertyDefinition<Void> {
return PropertyDefinition(name: name, getter: get)
}
/**
Creates the read-only property whose getter takes the owner as an argument.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
@_implicitSelfCapture get: @escaping (_ this: OwnerType) -> Value
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name, getter: get)
}
/**
Creates the property that references to an immutable property of the owner object using the key path.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
_ keyPath: KeyPath<OwnerType, Value>
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name) { owner in
return owner[keyPath: keyPath]
}
}
/**
Creates the property that references to a mutable property of the owner object using the key path.
*/
public func Property<Value: AnyArgument, OwnerType>(
_ name: String,
_ keyPath: ReferenceWritableKeyPath<OwnerType, Value>
) -> PropertyDefinition<OwnerType> {
return PropertyDefinition<OwnerType>(name: name) { owner in
return owner[keyPath: keyPath]
}
.set { owner, newValue in
owner[keyPath: keyPath] = newValue
}
}

View File

@@ -0,0 +1,173 @@
/**
Synchronous function without arguments.
*/
public func Function<R>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping () throws -> R
) -> SyncFunctionDefinition<(), Void, R> {
return SyncFunctionDefinition(
name,
firstArgType: Void.self,
dynamicArgumentTypes: [],
closure
)
}
/**
Synchronous function with one argument.
*/
public func Function<R, A0: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0) throws -> R
) -> SyncFunctionDefinition<(A0), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self],
closure
)
}
/**
Synchronous function with two arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1) throws -> R
) -> SyncFunctionDefinition<(A0, A1), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [~A0.self, ~A1.self],
closure
)
}
/**
Synchronous function with three arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self
],
closure
)
}
/**
Synchronous function with four arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self
],
closure
)
}
/**
Synchronous function with five arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self
],
closure
)
}
/**
Synchronous function with six arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4, A5), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self
],
closure
)
}
/**
Synchronous function with seven arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument, A6: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self
],
closure
)
}
/**
Synchronous function with eight arguments.
*/
public func Function<R, A0: AnyArgument, A1: AnyArgument, A2: AnyArgument, A3: AnyArgument, A4: AnyArgument, A5: AnyArgument, A6: AnyArgument, A7: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ closure: @escaping (A0, A1, A2, A3, A4, A5, A6, A7) throws -> R
) -> SyncFunctionDefinition<(A0, A1, A2, A3, A4, A5, A6, A7), A0, R> {
return SyncFunctionDefinition(
name,
firstArgType: A0.self,
dynamicArgumentTypes: [
~A0.self,
~A1.self,
~A2.self,
~A3.self,
~A4.self,
~A5.self,
~A6.self,
~A7.self
],
closure
)
}

View File

@@ -0,0 +1,38 @@
/// Here we implement factories for the definitions exclusive for native views.
/**
Creates a view definition describing the native view exported to React.
*/
public func View<ViewType: UIView>(
_ viewType: ViewType.Type,
@ViewDefinitionBuilder<ViewType> _ elements: @escaping () -> [AnyViewDefinitionElement]
) -> ViewDefinition<ViewType> {
return ViewDefinition(viewType, elements: elements())
}
// MARK: Props
/**
Creates a view prop that defines its name and setter.
*/
public func Prop<ViewType: UIView, PropType: AnyArgument>(
_ name: String,
@_implicitSelfCapture _ setter: @escaping (ViewType, PropType) -> Void
) -> ConcreteViewProp<ViewType, PropType> {
return ConcreteViewProp(
name: name,
propType: ~PropType.self,
setter: setter
)
}
// MARK: - View lifecycle
/**
Defines the view lifecycle method that is called when the view finished updating all props.
*/
public func OnViewDidUpdateProps<ViewType: UIView>(
@_implicitSelfCapture _ closure: @escaping (_ view: ViewType) -> Void
) -> ViewLifecycleMethod<ViewType> {
return ViewLifecycleMethod(type: .didUpdateProps, closure: closure)
}

View File

@@ -0,0 +1,21 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/Platform.h>
#import <ExpoModulesCore/EXReactDelegateWrapper.h>
#if __has_include(<React-RCTAppDelegate/RCTAppDelegate.h>)
#import <React-RCTAppDelegate/RCTAppDelegate.h>
#elif __has_include(<React_RCTAppDelegate/RCTAppDelegate.h>)
// for importing the header from framework, the dash will be transformed to underscore
#import <React_RCTAppDelegate/RCTAppDelegate.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@interface EXAppDelegateWrapper : RCTAppDelegate
@property (nonatomic, strong, readonly) EXReactDelegateWrapper *reactDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,118 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXAppDelegateWrapper.h>
#import <ExpoModulesCore/EXReactDelegateWrapper+Private.h>
#import <ExpoModulesCore/EXReactRootViewFactory.h>
#import <ExpoModulesCore/Swift.h>
#if __has_include(<React-RCTAppDelegate/RCTRootViewFactory.h>)
#import <React-RCTAppDelegate/RCTRootViewFactory.h>
#elif __has_include(<React_RCTAppDelegate/RCTRootViewFactory.h>)
// for importing the header from framework, the dash will be transformed to underscore
#import <React_RCTAppDelegate/RCTRootViewFactory.h>
#endif
#import <ReactCommon/RCTTurboModuleManager.h>
@interface RCTAppDelegate () <RCTTurboModuleManagerDelegate>
@end
@interface RCTRootViewFactoryConfiguration ()
- (void)setCustomizeRootView:(void (^)(UIView *rootView))customizeRootView;
@end
@interface EXAppDelegateWrapper()
@property (nonatomic, strong) EXReactDelegateWrapper *reactDelegate;
@end
@implementation EXAppDelegateWrapper {
EXExpoAppDelegate *_expoAppDelegate;
}
// Synthesize window, so the AppDelegate can synthesize it too.
@synthesize window = _window;
- (instancetype)init
{
if (self = [super init]) {
_expoAppDelegate = [[EXExpoAppDelegate alloc] init];
_reactDelegate = [[EXReactDelegateWrapper alloc] initWithExpoReactDelegate:_expoAppDelegate.reactDelegate];
}
return self;
}
// This needs to be implemented, otherwise forwarding won't be called.
// When the app starts, `UIApplication` uses it to check beforehand
// which `UIApplicationDelegate` selectors are implemented.
- (BOOL)respondsToSelector:(SEL)selector
{
return [super respondsToSelector:selector]
|| [_expoAppDelegate respondsToSelector:selector];
}
// Forwards all invocations to `ExpoAppDelegate` object.
- (id)forwardingTargetForSelector:(SEL)selector
{
return _expoAppDelegate;
}
#if !TARGET_OS_OSX
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[super application:application didFinishLaunchingWithOptions:launchOptions];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-result"
[_expoAppDelegate application:application didFinishLaunchingWithOptions:launchOptions];
#pragma clang diagnostic pop
return YES;
}
#endif // !TARGET_OS_OSX
- (UIViewController *)createRootViewController
{
return [self.reactDelegate createRootViewController];
}
- (RCTRootViewFactory *)createRCTRootViewFactory
{
__weak __typeof(self) weakSelf = self;
RCTBundleURLBlock bundleUrlBlock = ^{
RCTAppDelegate *strongSelf = weakSelf;
return strongSelf.bundleURL;
};
RCTRootViewFactoryConfiguration *configuration =
[[RCTRootViewFactoryConfiguration alloc] initWithBundleURLBlock:bundleUrlBlock
newArchEnabled:self.fabricEnabled
turboModuleEnabled:self.turboModuleEnabled
bridgelessEnabled:self.bridgelessEnabled];
configuration.createRootViewWithBridge = ^UIView *(RCTBridge *bridge, NSString *moduleName, NSDictionary *initProps)
{
return [weakSelf createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
};
configuration.createBridgeWithDelegate = ^RCTBridge *(id<RCTBridgeDelegate> delegate, NSDictionary *launchOptions)
{
return [weakSelf createBridgeWithDelegate:delegate launchOptions:launchOptions];
};
// TODO(kudo,20240706): Remove respondsToSelector and set the property directly when we upgrade to react-native 0.75
if ([configuration respondsToSelector:@selector(setCustomizeRootView:)]) {
[configuration setCustomizeRootView:^(UIView *_Nonnull rootView) {
[weakSelf customizeRootView:(RCTRootView *)rootView];
}];
}
return [[EXReactRootViewFactory alloc] initWithReactDelegate:self.reactDelegate configuration:configuration turboModuleManagerDelegate:self];
}
- (void)customizeRootView:(UIView *)rootView {
[_expoAppDelegate customizeRootView:rootView];
}
@end

View File

@@ -0,0 +1,15 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
Loads `ExpoAppDelegate` subscribers based on
the list from the generated `ExpoModulesProvider`.
*/
@interface EXAppDelegatesLoader : NSObject
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXLegacyAppDelegateWrapper.h>
#import <ExpoModulesCore/EXAppDelegatesLoader.h>
#import <ExpoModulesCore/Swift.h>
// Make the legacy wrapper conform to the protocol for subscribers.
@interface EXLegacyAppDelegateWrapper () <EXAppDelegateSubscriberProtocol>
@end
@implementation EXAppDelegatesLoader
// App delegate providers must be registered before any `AppDelegate` life-cycle event is called.
// Unfortunately it's not possible in Swift to run code right after the binary is loaded
// and before any code is executed, so we switch back to Objective-C just to do this one thing.
+ (void)load
{
ModulesProvider *modulesProvider = [EXAppContext modulesProviderWithName:@"ExpoModulesProvider"];
[EXExpoAppDelegate registerSubscriber:[[EXLegacyAppDelegateWrapper alloc] init]];
[EXExpoAppDelegate registerSubscribersFromModulesProvider:modulesProvider];
[EXExpoAppDelegate registerReactDelegateHandlersFromModulesProvider:modulesProvider];
}
@end

View File

@@ -0,0 +1,19 @@
// Copyright © 2018 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
The legacy wrapper is still used to forward app delegate calls to singleton modules.
See `EXAppDelegatesLoader.m` which registers this class as a subscriber of `ExpoAppDelegate`.
*/
#if TARGET_OS_OSX
@interface EXLegacyAppDelegateWrapper : NSResponder <NSApplicationDelegate>
#else
@interface EXLegacyAppDelegateWrapper : UIResponder <UIApplicationDelegate>
#endif
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,271 @@
// Copyright © 2018 650 Industries. All rights reserved.
#import <Foundation/FoundationErrors.h>
#import <ExpoModulesCore/EXSingletonModule.h>
#import <ExpoModulesCore/Platform.h>
#import <ExpoModulesCore/EXModuleRegistryProvider.h>
#import <ExpoModulesCore/EXLegacyAppDelegateWrapper.h>
#if !TARGET_OS_OSX
static NSMutableArray<id<UIApplicationDelegate>> *subcontractors;
static NSMutableDictionary<NSString *,NSArray<id<UIApplicationDelegate>> *> *subcontractorsForSelector;
static dispatch_once_t onceToken;
#endif
@implementation EXLegacyAppDelegateWrapper
// The legacy app delegate wrapper is not supported on macOS, but we keep it no-op for convenience.
#if !TARGET_OS_OSX
@synthesize window = _window;
- (void)forwardInvocation:(NSInvocation *)invocation {
#if DEBUG
SEL selector = [invocation selector];
NSArray<id<UIApplicationDelegate>> *delegatesToBeCalled = [self getSubcontractorsImplementingSelector:selector];
NSString *selectorName = NSStringFromSelector(selector);
if ([delegatesToBeCalled count] > 0) {
[NSException raise:@"Method not implemented in UIApplicationDelegate" format:@"Some modules: %@ have registered for `%@` UIApplicationDelegate's callback, however, neither your AppDelegate nor %@ can handle this method. You'll need to either implement this method in your AppDelegate or submit a pull request to handle it in %@.", delegatesToBeCalled, selectorName, NSStringFromClass([self class]), NSStringFromClass([self class])];
}
#endif
[super forwardInvocation:invocation];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
BOOL answer = NO;
SEL selector = @selector(application:didFinishLaunchingWithOptions:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
BOOL subcontractorAnswer = NO;
subcontractorAnswer = [subcontractor application:application didFinishLaunchingWithOptions:launchOptions];
answer |= subcontractorAnswer;
}
return answer;
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
SEL selector = @selector(applicationWillEnterForeground:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor applicationWillEnterForeground:application];
}
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
SEL selector = @selector(application:openURL:options:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
if ([subcontractor application:app openURL:url options:options]) {
return YES;
}
}
return NO;
}
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
SEL selector = @selector(application:performFetchWithCompletionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block UIBackgroundFetchResult fetchResult = UIBackgroundFetchResultNoData;
__block NSObject *lock = [NSObject new];
void (^handler)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) {
@synchronized (lock) {
if (result == UIBackgroundFetchResultFailed) {
fetchResult = UIBackgroundFetchResultFailed;
} else if (fetchResult != UIBackgroundFetchResultFailed && result == UIBackgroundFetchResultNewData) {
fetchResult = UIBackgroundFetchResultNewData;
}
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
}
}
};
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
} else {
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application performFetchWithCompletionHandler:handler];
}
}
}
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
SEL selector = @selector(application:continueUserActivity:restorationHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSMutableArray<id<UIUserActivityRestoring>> * _Nullable mergedParams = [NSMutableArray new];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block NSObject *lock = [NSObject new];
void (^handler)(NSArray<id<UIUserActivityRestoring>> * _Nullable) = ^(NSArray<id<UIUserActivityRestoring>> * _Nullable param) {
@synchronized (lock) {
[mergedParams addObjectsFromArray:param];
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
restorationHandler(mergedParams);
}
}
};
BOOL result = NO;
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
result = result || [subcontractor application:application continueUserActivity:userActivity restorationHandler:handler];
}
return result;
}
#pragma mark - BackgroundSession
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
SEL selector = @selector(application:handleEventsForBackgroundURLSession:completionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block BOOL delegatingCompleted = NO;
__block int delegatesCompleted = 0;
__block unsigned long allDelegates = subcontractorsArray.count;
__block void (^completionHandlerCaller)(void) = ^ {
if (delegatesCompleted && delegatingCompleted == allDelegates) {
completionHandler();
}
};
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application handleEventsForBackgroundURLSession:identifier completionHandler:^(){
@synchronized (self) {
delegatesCompleted += 1;
completionHandlerCaller();
}
}];
}
@synchronized (self) {
delegatingCompleted = YES;
completionHandlerCaller();
}
}
#pragma mark - Notifications
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token
{
SEL selector = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didRegisterForRemoteNotificationsWithDeviceToken:token];
}
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)err
{
SEL selector = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
for(id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didFailToRegisterForRemoteNotificationsWithError:err];
}
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler
{
SEL selector = @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
NSArray<id<UIApplicationDelegate>> *subcontractorsArray = [self getSubcontractorsImplementingSelector:selector];
__block NSUInteger subcontractorsLeft = [subcontractorsArray count];
__block UIBackgroundFetchResult fetchResult = UIBackgroundFetchResultNoData;
__block NSObject *lock = [NSObject new];
void (^handler)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result) {
@synchronized (lock) {
if (result == UIBackgroundFetchResultFailed) {
fetchResult = UIBackgroundFetchResultFailed;
} else if (fetchResult != UIBackgroundFetchResultFailed && result == UIBackgroundFetchResultNewData) {
fetchResult = UIBackgroundFetchResultNewData;
}
subcontractorsLeft--;
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
}
}
};
if (subcontractorsLeft == 0) {
completionHandler(fetchResult);
} else {
for (id<UIApplicationDelegate> subcontractor in subcontractorsArray) {
[subcontractor application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:handler];
}
}
}
#pragma mark - Subcontractors
- (void)ensureSubcontractorsAreInitializedAndSorted {
dispatch_once(&onceToken, ^{
subcontractors = [[NSMutableArray alloc] init];
subcontractorsForSelector = [NSMutableDictionary new];
NSArray<EXSingletonModule*> * singletonModules = [[EXModuleRegistryProvider singletonModules] allObjects];
for (EXSingletonModule *singletonModule in singletonModules) {
if ([singletonModule conformsToProtocol:@protocol(UIApplicationDelegate)]) {
[subcontractors addObject:(id<UIApplicationDelegate>)singletonModule];
}
}
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"priority"
ascending:NO];
[subcontractors sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
});
}
- (NSArray<id<UIApplicationDelegate>> *)getSubcontractorsImplementingSelector:(SEL)selector {
[self ensureSubcontractorsAreInitializedAndSorted];
NSString *selectorKey = NSStringFromSelector(selector);
if (subcontractorsForSelector[selectorKey]) {
return subcontractorsForSelector[selectorKey];
}
NSMutableArray<id<UIApplicationDelegate>> *result = [NSMutableArray new];
for (id<UIApplicationDelegate> subcontractor in subcontractors) {
if ([subcontractor respondsToSelector:selector]) {
[result addObject:subcontractor];
}
}
subcontractorsForSelector[selectorKey] = result;
return result;
}
#endif // !TARGET_OS_OSX
@end

View File

@@ -0,0 +1,389 @@
import Dispatch
import Foundation
var subscribers = [ExpoAppDelegateSubscriberProtocol]()
var reactDelegateHandlers = [ExpoReactDelegateHandler]()
/**
Allows classes extending `ExpoAppDelegateSubscriber` to hook into project's app delegate
by forwarding `UIApplicationDelegate` events to the subscribers.
Keep functions and markers in sync with https://developer.apple.com/documentation/uikit/uiapplicationdelegate
*/
@objc(EXExpoAppDelegate)
open class ExpoAppDelegate: UIResponder, UIApplicationDelegate {
open var window: UIWindow?
@objc
public let reactDelegate = ExpoReactDelegate(handlers: reactDelegateHandlers)
#if os(iOS) || os(tvOS)
// MARK: - Initializing the App
open func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
let parsedSubscribers = subscribers.filter {
$0.responds(to: #selector(application(_:willFinishLaunchingWithOptions:)))
}
// If we can't find a subscriber that implements `willFinishLaunchingWithOptions`, we will delegate the decision if we can handel the passed URL to
// the `didFinishLaunchingWithOptions` method by returning `true` here.
// You can read more about how iOS handles deep links here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application#discussion
if parsedSubscribers.isEmpty {
return true
}
return parsedSubscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, willFinishLaunchingWithOptions: launchOptions) ?? false || result
}
}
open func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
return subscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false || result
}
}
// TODO: - Configuring and Discarding Scenes
// MARK: - Responding to App Life-Cycle Events
@objc
open func applicationDidBecomeActive(_ application: UIApplication) {
subscribers.forEach { $0.applicationDidBecomeActive?(application) }
}
@objc
open func applicationWillResignActive(_ application: UIApplication) {
subscribers.forEach { $0.applicationWillResignActive?(application) }
}
@objc
open func applicationDidEnterBackground(_ application: UIApplication) {
subscribers.forEach { $0.applicationDidEnterBackground?(application) }
}
open func applicationWillEnterForeground(_ application: UIApplication) {
subscribers.forEach { $0.applicationWillEnterForeground?(application) }
}
open func applicationWillTerminate(_ application: UIApplication) {
subscribers.forEach { $0.applicationWillTerminate?(application) }
}
@objc public func customizeRootView(_ rootView: UIView) {
subscribers.forEach { $0.customizeRootView?(rootView) }
}
// TODO: - Responding to Environment Changes
// TODO: - Managing App State Restoration
// MARK: - Downloading Data in the Background
open func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
let selector = #selector(application(_:handleEventsForBackgroundURLSession:completionHandler:))
let subs = subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.handleBackgroundEvents")
let handler = {
dispatchQueue.sync {
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler()
}
}
}
if subs.isEmpty {
completionHandler()
} else {
subs.forEach {
$0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: handler)
}
}
}
// MARK: - Handling Remote Notification Registration
open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
subscribers.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}
open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
subscribers.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
}
open func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
let subs = subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.remoteNotification", qos: .userInteractive)
var failedCount = 0
var newDataCount = 0
let handler = { (result: UIBackgroundFetchResult) in
dispatchQueue.sync {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
if subs.isEmpty {
completionHandler(.noData)
} else {
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: handler)
}
}
}
// MARK: - Continuing User Activity and Handling Quick Actions
open func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
return subscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
}
}
open func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let selector = #selector(application(_:continue:restorationHandler:))
let subs = subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.continueUserActivity", qos: .userInteractive)
var allRestorableObjects = [UIUserActivityRestoring]()
let handler = { (restorableObjects: [UIUserActivityRestoring]?) in
dispatchQueue.sync {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: handler) ?? false || result
}
}
open func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
return subscribers.forEach { $0.application?(application, didUpdate: userActivity) }
}
open func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
return subscribers.forEach {
$0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
}
}
#if !os(tvOS)
open func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
let selector = #selector(application(_:performActionFor:completionHandler:))
let subs = subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var result: Bool = false
let dispatchQueue = DispatchQueue(label: "expo.application.performAction", qos: .userInteractive)
let handler = { (succeeded: Bool) in
dispatchQueue.sync {
result = result || succeeded
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler(result)
}
}
}
if subs.isEmpty {
completionHandler(result)
} else {
subs.forEach { subscriber in
subscriber.application?(application, performActionFor: shortcutItem, completionHandler: handler)
}
}
}
#endif
// MARK: - Background Fetch
open func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let selector = #selector(application(_:performFetchWithCompletionHandler:))
let subs = subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
let dispatchQueue = DispatchQueue(label: "expo.application.performFetch", qos: .userInteractive)
var failedCount = 0
var newDataCount = 0
let handler = { (result: UIBackgroundFetchResult) in
dispatchQueue.sync {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
if subs.isEmpty {
completionHandler(.noData)
} else {
subs.forEach { subscriber in
subscriber.application?(application, performFetchWithCompletionHandler: handler)
}
}
}
// TODO: - Interacting With WatchKit
// TODO: - Interacting With HealthKit
// MARK: - Opening a URL-Specified Resource
open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return subscribers.contains { subscriber in
return subscriber.application?(app, open: url, options: options) ?? false
}
}
// TODO: - Disallowing Specified App Extension Types
// TODO: - Handling SiriKit Intents
// TODO: - Handling CloudKit Invitations
// MARK: - Managing Interface Geometry
/**
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
* a different orientation.
*/
#if !os(tvOS)
public func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
let universalOrientationMask = allowedOrientations(for: .unspecified)
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
let parsedSubscribers = subscribers.filter {
$0.responds(to: #selector(application(_:supportedInterfaceOrientationsFor:)))
}
// We want to create an intersection of all orientations set by subscribers.
let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
return result
}
return requestedOrientation.intersection(result)
}
return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
}
#endif
#endif // os(iOS)
// MARK: - Statics
@objc
public static func registerSubscribersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getAppDelegateSubscribers().forEach { subscriberType in
registerSubscriber(subscriberType.init())
}
}
@objc
public static func registerSubscriber(_ subscriber: ExpoAppDelegateSubscriberProtocol) {
if subscribers.contains(where: { $0 === subscriber }) {
fatalError("Given app delegate subscriber `\(String(describing: subscriber))` is already registered.")
}
subscribers.append(subscriber)
}
@objc
public static func getSubscriber(_ name: String) -> ExpoAppDelegateSubscriberProtocol? {
return subscribers.first { String(describing: $0) == name }
}
public static func getSubscriberOfType<Subscriber>(_ type: Subscriber.Type) -> Subscriber? {
return subscribers.first { $0 is Subscriber } as? Subscriber
}
@objc
public static func registerReactDelegateHandlersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getReactDelegateHandlers()
.sorted { tuple1, tuple2 -> Bool in
return ModulePriorities.get(tuple1.packageName) > ModulePriorities.get(tuple2.packageName)
}
.forEach { handlerTuple in
reactDelegateHandlers.append(handlerTuple.handler.init())
}
}
}
#if os(iOS)
private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
// For now only iPad-specific orientations are supported
let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
var mask: UIInterfaceOrientationMask = []
guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
return mask
}
for orientation in orientations {
switch orientation {
case "UIInterfaceOrientationPortrait":
mask.insert(.portrait)
case "UIInterfaceOrientationLandscapeLeft":
mask.insert(.landscapeLeft)
case "UIInterfaceOrientationLandscapeRight":
mask.insert(.landscapeRight)
case "UIInterfaceOrientationPortraitUpsideDown":
mask.insert(.portraitUpsideDown)
default:
break
}
}
return mask
}
#endif // os(iOS)

View File

@@ -0,0 +1,33 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
Base class for app delegate subscribers. Ensures the class
inherits from `UIResponder` and has `required init()` initializer.
*/
@objc(EXBaseAppDelegateSubscriber)
open class BaseExpoAppDelegateSubscriber: UIResponder {
public override required init() {
super.init()
}
#if os(macOS)
@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#endif // os(macOS)
}
/**
Typealias to `UIApplicationDelegate` protocol.
Might be useful for compatibility reasons if we decide to add more things here.
*/
@objc(EXAppDelegateSubscriberProtocol)
public protocol ExpoAppDelegateSubscriberProtocol: UIApplicationDelegate {
@objc optional func customizeRootView(_ rootView: UIView)
}
/**
Typealias merging the base class for app delegate subscribers and protocol inheritance to `UIApplicationDelegate`.
*/
public typealias ExpoAppDelegateSubscriber = BaseExpoAppDelegateSubscriber & ExpoAppDelegateSubscriberProtocol

View File

@@ -0,0 +1,475 @@
import React
/**
The app context is an interface to a single Expo app.
*/
@objc(EXAppContext)
public final class AppContext: NSObject {
internal static func create() -> AppContext {
let appContext = AppContext()
appContext._runtime = ExpoRuntime()
return appContext
}
/**
The app context configuration.
*/
public let config: AppContextConfig
/**
The module registry for the app context.
*/
public private(set) lazy var moduleRegistry: ModuleRegistry = {
isModuleRegistryInitialized = true
return ModuleRegistry(appContext: self)
}()
/**
Whether the module registry for this app context has already been initialized.
*/
private var isModuleRegistryInitialized: Bool = false
/**
The legacy module registry with modules written in the old-fashioned way.
*/
@objc
public weak var legacyModuleRegistry: EXModuleRegistry?
@objc
public weak var legacyModulesProxy: LegacyNativeModulesProxy?
/**
React bridge of the context's app. Can be `nil` when the bridge
hasn't been propagated to the bridge modules yet (see ``ExpoBridgeModule``),
or when the app context is "bridgeless" (for example in native unit tests).
*/
@objc
public weak var reactBridge: RCTBridge?
/**
Underlying JSI runtime of the running app.
*/
@objc
public var _runtime: ExpoRuntime? {
didSet {
if _runtime == nil {
// When the runtime is unpinned from the context (e.g. deallocated),
// we should make sure to release all JS objects from the memory.
// Otherwise the JSCRuntime asserts may fail on deallocation.
releaseRuntimeObjects()
} else if _runtime != oldValue {
// Try to install the core object automatically when the runtime changes.
try? prepareRuntime()
}
}
}
/**
JSI runtime of the running app.
*/
public var runtime: ExpoRuntime {
get throws {
if let runtime = _runtime {
return runtime
}
throw Exceptions.RuntimeLost()
}
}
/**
The core module that defines the `expo` object in the global scope of Expo runtime.
*/
internal private(set) lazy var coreModule = CoreModule(appContext: self)
/**
The module holder for the core module.
*/
internal private(set) lazy var coreModuleHolder = ModuleHolder(appContext: self, module: coreModule)
/**
Designated initializer without modules provider.
*/
public init(config: AppContextConfig = .default) {
self.config = config
super.init()
listenToClientAppNotifications()
}
public convenience init(legacyModulesProxy: Any, legacyModuleRegistry: Any, config: AppContextConfig = .default) {
self.init(config: config)
self.legacyModulesProxy = legacyModulesProxy as? LegacyNativeModulesProxy
self.legacyModuleRegistry = legacyModuleRegistry as? EXModuleRegistry
}
@objc
public convenience override init() {
self.init(config: .default)
}
@objc
@discardableResult
public func useModulesProvider(_ providerName: String) -> Self {
return useModulesProvider(Self.modulesProvider(withName: providerName))
}
@discardableResult
public func useModulesProvider(_ provider: ModulesProvider) -> Self {
moduleRegistry.register(fromProvider: provider)
return self
}
// MARK: - UI
public func findView<ViewType>(withTag viewTag: Int, ofType type: ViewType.Type) -> ViewType? {
return reactBridge?.uiManager.view(forReactTag: NSNumber(value: viewTag)) as? ViewType
}
// MARK: - Running on specific queues
/**
Runs a code block on the JavaScript thread.
*/
public func executeOnJavaScriptThread(runBlock: @escaping (() -> Void)) {
reactBridge?.dispatchBlock(runBlock, queue: RCTJSThread)
}
// MARK: - Classes
internal lazy var sharedObjectRegistry = SharedObjectRegistry(appContext: self)
/**
A registry containing references to JavaScript classes.
- ToDo: Make one registry per module, not the entire app context.
Perhaps it should be kept by the `ModuleHolder`.
*/
internal let classRegistry = ClassRegistry()
/**
Creates a new JavaScript object with the class prototype associated with the given native class.
- ToDo: Move this to `ModuleHolder` along the `classRegistry` property.
*/
internal func newObject(nativeClassId: ObjectIdentifier) throws -> JavaScriptObject? {
guard let jsClass = classRegistry.getJavaScriptClass(nativeClassId: nativeClassId) else {
// TODO: Define a JS class for SharedRef in the CoreModule and then use it here instead of a raw object (?)
return try runtime.createObject()
}
let prototype = try jsClass.getProperty("prototype").asObject()
let object = try runtime.createObject(withPrototype: prototype)
return object
}
// MARK: - Legacy modules
/**
Returns a legacy module implementing given protocol/interface.
*/
public func legacyModule<ModuleProtocol>(implementing moduleProtocol: Protocol) -> ModuleProtocol? {
return legacyModuleRegistry?.getModuleImplementingProtocol(moduleProtocol) as? ModuleProtocol
}
/**
Provides access to app's constants from legacy module registry.
*/
public var constants: EXConstantsInterface? {
return legacyModule(implementing: EXConstantsInterface.self)
}
/**
Provides access to the file system manager from legacy module registry.
*/
public var fileSystem: EXFileSystemInterface? {
return legacyModule(implementing: EXFileSystemInterface.self)
}
/**
Provides access to the permissions manager from legacy module registry.
*/
public var permissions: EXPermissionsInterface? {
return legacyModule(implementing: EXPermissionsInterface.self)
}
/**
Provides access to the image loader from legacy module registry.
*/
public var imageLoader: EXImageLoaderInterface? {
return legacyModule(implementing: EXImageLoaderInterface.self)
}
/**
Provides access to the utilities from legacy module registry.
*/
public var utilities: EXUtilitiesInterface? {
return legacyModule(implementing: EXUtilitiesInterface.self)
}
/**
Provides an event emitter that is compatible with the legacy interface.
*/
public var eventEmitter: EXEventEmitterService? {
return LegacyEventEmitterCompat(appContext: self)
}
/**
Starts listening to `UIApplication` notifications.
*/
private func listenToClientAppNotifications() {
#if os(iOS) || os(tvOS)
let notifications = [
UIApplication.willEnterForegroundNotification,
UIApplication.didBecomeActiveNotification,
UIApplication.didEnterBackgroundNotification
]
#elseif os(macOS)
let notifications = [
NSApplication.willUnhideNotification,
NSApplication.didBecomeActiveNotification,
NSApplication.didHideNotification
]
#endif
notifications.forEach { name in
NotificationCenter.default.addObserver(self, selector: #selector(handleClientAppNotification(_:)), name: name, object: nil)
}
}
/**
Handles app's (`UIApplication`) lifecycle notifications and posts appropriate events to the module registry.
*/
@objc
private func handleClientAppNotification(_ notification: Notification) {
switch notification.name {
#if os(iOS) || os(tvOS)
case UIApplication.willEnterForegroundNotification:
moduleRegistry.post(event: .appEntersForeground)
case UIApplication.didBecomeActiveNotification:
moduleRegistry.post(event: .appBecomesActive)
case UIApplication.didEnterBackgroundNotification:
moduleRegistry.post(event: .appEntersBackground)
#elseif os(macOS)
case NSApplication.willUnhideNotification:
moduleRegistry.post(event: .appEntersForeground)
case NSApplication.didBecomeActiveNotification:
moduleRegistry.post(event: .appBecomesActive)
case NSApplication.didHideNotification:
moduleRegistry.post(event: .appEntersBackground)
#endif
default:
return
}
}
// MARK: - Interop with NativeModulesProxy
/**
Returns view modules wrapped by the base `ViewModuleWrapper` class.
*/
@objc
public func getViewManagers() -> [ViewModuleWrapper] {
return moduleRegistry.compactMap { holder in
if holder.definition.view != nil {
return ViewModuleWrapper(holder)
} else {
return nil
}
}
}
/**
Returns a bool whether the module with given name is registered in this context.
*/
@objc
public func hasModule(_ moduleName: String) -> Bool {
return moduleRegistry.has(moduleWithName: moduleName)
}
/**
Returns an array of names of the modules registered in the module registry.
*/
@objc
public func getModuleNames() -> [String] {
return moduleRegistry.getModuleNames()
}
/**
Returns a JavaScript object that represents a module with given name.
When remote debugging is enabled, this will always return `nil`.
*/
@objc
public func getNativeModuleObject(_ moduleName: String) -> JavaScriptObject? {
return moduleRegistry.get(moduleHolderForName: moduleName)?.javaScriptObject
}
/**
Returns an array of event names supported by all Swift modules.
*/
@objc
public func getSupportedEvents() -> [String] {
return moduleRegistry.reduce(into: [String]()) { events, holder in
events.append(contentsOf: holder.definition.eventNames)
}
}
/**
Modifies listeners count for module with given name. Depending on the listeners count,
`onStartObserving` and `onStopObserving` are called.
*/
@objc
public func modifyEventListenersCount(_ moduleName: String, count: Int) {
moduleRegistry
.get(moduleHolderForName: moduleName)?
.modifyListenersCount(count)
}
/**
Asynchronously calls module's function with given arguments.
*/
@objc
public func callFunction(
_ functionName: String,
onModule moduleName: String,
withArgs args: [Any],
resolve: @escaping EXPromiseResolveBlock,
reject: @escaping EXPromiseRejectBlock
) {
moduleRegistry
.get(moduleHolderForName: moduleName)?
.call(function: functionName, args: args) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, error)
case .success(let value):
resolve(value)
}
}
}
@objc
public final lazy var expoModulesConfig = ModulesProxyConfig(constants: self.exportedModulesConstants(),
methodNames: self.exportedFunctionNames(),
viewManagers: self.viewManagersMetadata())
private func exportedFunctionNames() -> [String: [[String: Any]]] {
var constants = [String: [[String: Any]]]()
for holder in moduleRegistry {
constants[holder.name] = holder.definition.functions.map({ functionName, function in
return [
"name": functionName,
"argumentsCount": function.argumentsCount,
"key": functionName
]
})
}
return constants
}
private func exportedModulesConstants() -> [String: Any] {
return moduleRegistry
// prevent infinite recursion - exclude NativeProxyModule constants
.filter { $0.name != NativeModulesProxyModule.moduleName }
.reduce(into: [String: Any]()) { acc, holder in
acc[holder.name] = holder.getConstants()
}
}
private func viewManagersMetadata() -> [String: Any] {
return moduleRegistry.reduce(into: [String: Any]()) { acc, holder in
if let viewDefinition = holder.definition.view {
acc[holder.name] = [
"propsNames": viewDefinition.props.map { $0.name }
]
}
}
}
// MARK: - Runtime
internal func prepareRuntime() throws {
let runtime = try runtime
let coreObject = runtime.createObject()
try coreModuleHolder.definition.decorate(object: coreObject, appContext: self)
// Initialize `global.expo`.
try runtime.initializeCoreObject(coreObject)
// Install `global.expo.EventEmitter`.
EXJavaScriptRuntimeManager.installEventEmitterClass(runtime)
// Install `global.expo.SharedObject`.
EXJavaScriptRuntimeManager.installSharedObjectClass(runtime) { [weak sharedObjectRegistry] objectId in
sharedObjectRegistry?.delete(objectId)
}
// Install `global.expo.NativeModule`.
EXJavaScriptRuntimeManager.installNativeModuleClass(runtime)
// Install the modules host object as the `global.expo.modules`.
EXJavaScriptRuntimeManager.installExpoModulesHostObject(self)
}
/**
Unsets runtime objects that we hold for each module.
*/
private func releaseRuntimeObjects() {
sharedObjectRegistry.clear()
classRegistry.clear()
for module in moduleRegistry {
module.javaScriptObject = nil
}
}
// MARK: - Deallocation
/**
Cleans things up before deallocation.
*/
deinit {
NotificationCenter.default.removeObserver(self)
// Post an event to the registry only if it was already created.
// If we let it to lazy-load here, that would crash since the module registry
// has a weak reference to the app context which is being deallocated.
if isModuleRegistryInitialized {
moduleRegistry.post(event: .appContextDestroys)
}
}
// MARK: - Statics
/**
Returns an instance of the generated Expo modules provider.
The provider is usually generated in application's `ExpoModulesProviders` files group.
*/
@objc
public static func modulesProvider(withName providerName: String = "ExpoModulesProvider") -> ModulesProvider {
// [0] When ExpoModulesCore is built as separated framework/module,
// we should explicitly load main bundle's `ExpoModulesProvider` class.
if let bundleName = Bundle.main.infoDictionary?["CFBundleName"],
let providerClass = NSClassFromString("\(bundleName).\(providerName)") as? ModulesProvider.Type {
return providerClass.init()
}
// [1] Fallback to `ExpoModulesProvider` class from the current module.
if let providerClass = NSClassFromString(providerName) as? ModulesProvider.Type {
return providerClass.init()
}
// [2] Fallback to an empty `ModulesProvider` if `ExpoModulesProvider` was not generated
return ModulesProvider()
}
}
// MARK: - Public exceptions
// Deprecated since v1.0.0
@available(*, deprecated, renamed: "Exceptions.AppContextLost")
public typealias AppContextLostException = Exceptions.AppContextLost
// Deprecated since v1.0.0
@available(*, deprecated, renamed: "Exceptions.RuntimeLost")
public typealias RuntimeLostException = Exceptions.RuntimeLost

View File

@@ -0,0 +1,13 @@
// Copyright 2023-present 650 Industries. All rights reserved.
public struct AppContextConfig {
public static var `default` = AppContextConfig()
public let documentDirectory: URL?
public let cacheDirectory: URL?
public init(documentDirectory: URL? = nil, cacheDirectory: URL? = nil) {
self.documentDirectory = documentDirectory ?? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
self.cacheDirectory = cacheDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A protocol for classes/structs accepted as an argument of functions.
*/
public protocol AnyArgument {
static func getDynamicType() -> AnyDynamicType
}
extension AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicRawType(innerType: Self.self)
}
}
// Extend the primitive types these may come from React Native bridge.
extension Bool: AnyArgument {}
extension Int: AnyArgument {}
extension Int8: AnyArgument {}
extension Int16: AnyArgument {}
extension Int32: AnyArgument {}
extension Int64: AnyArgument {}
extension UInt: AnyArgument {}
extension UInt8: AnyArgument {}
extension UInt16: AnyArgument {}
extension UInt32: AnyArgument {}
extension UInt64: AnyArgument {}
extension Float32: AnyArgument {}
extension Double: AnyArgument {}
extension CGFloat: AnyArgument {}
extension String: AnyArgument {}
extension Optional: AnyArgument where Wrapped: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicOptionalType(wrappedType: ~Wrapped.self)
}
}
extension Dictionary: AnyArgument where Key: Hashable {
public static func getDynamicType() -> AnyDynamicType {
return DynamicDictionaryType(valueType: ~Value.self)
}
}
extension Array: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicArrayType(elementType: ~Element.self)
}
}
extension Data: AnyArgument {
public static func getDynamicType() -> AnyDynamicType {
return DynamicDataType()
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
A protocol that allows custom classes or structs to be used as function arguments.
It requires static `convert(from:appContext:)` function that knows how to convert incoming
value of `Any` type to the type implemented by this protocol. It should throw an error
when the value is not recognized, is invalid or doesn't meet type requirements.
*/
public protocol Convertible: AnyArgument {
/**
Converts any value to the instance of its class (or struct) in the given app context.
Throws an error when given value cannot be converted.
*/
static func convert(from value: Any?, appContext: AppContext) throws -> Self
}
extension Convertible {
public static func getDynamicType() -> AnyDynamicType {
return DynamicConvertibleType(innerType: Self.self)
}
}
@available(*, deprecated, renamed: "Convertible")
public typealias ConvertibleArgument = Convertible

View File

@@ -0,0 +1,115 @@
// Copyright 2018-present 650 Industries. All rights reserved.
import CoreGraphics
// Here we extend some common iOS types to implement `Convertible` protocol and
// describe how they can be converted from primitive types received from JavaScript runtime.
// This allows these types to be used as argument types of functions callable from JavaScript.
// As an example, when the `CGPoint` type is used as an argument type, its instance can be
// created from an array of two doubles or an object with `x` and `y` fields.
// MARK: - Foundation
extension URL: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let value = value as? String else {
throw Conversions.ConvertingException<URL>(value)
}
// First we try to create a URL without extra encoding, as it came.
if let url = convertToUrl(string: value) {
return url
}
// File path doesn't need to be percent-encoded.
if isFileUrlPath(value) {
return URL(fileURLWithPath: value)
}
// If we get here, the string is not the file url and may require percent-encoding characters that are not URL-safe according to RFC 3986.
if let encodedValue = percentEncodeUrlString(value), let url = convertToUrl(string: encodedValue) {
return url
}
// If it still fails to create the URL object, the string possibly contains characters that must be explicitly percent-encoded beforehand.
throw UrlContainsInvalidCharactersException()
}
}
internal class UrlContainsInvalidCharactersException: Exception {
override var reason: String {
return "Unable to create a URL object from the given string, make sure to percent-encode these characters: \(urlAllowedCharacters)"
}
}
// MARK: - CoreGraphics
extension CGPoint: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGPoint {
if let value = value as? [Double], value.count == 2 {
return CGPoint(x: value[0], y: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["x", "y"], as: Double.self)
return CGPoint(x: args[0], y: args[1])
}
throw Conversions.ConvertingException<CGPoint>(value)
}
}
extension CGSize: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGSize {
if let value = value as? [Double], value.count == 2 {
return CGSize(width: value[0], height: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["width", "height"], as: Double.self)
return CGSize(width: args[0], height: args[1])
}
throw Conversions.ConvertingException<CGSize>(value)
}
}
extension CGVector: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGVector {
if let value = value as? [Double], value.count == 2 {
return CGVector(dx: value[0], dy: value[1])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["dx", "dy"], as: Double.self)
return CGVector(dx: args[0], dy: args[1])
}
throw Conversions.ConvertingException<CGVector>(value)
}
}
extension CGRect: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> CGRect {
if let value = value as? [Double], value.count == 4 {
return CGRect(x: value[0], y: value[1], width: value[2], height: value[3])
}
if let value = value as? [String: Any] {
let args = try Conversions.pickValues(from: value, byKeys: ["x", "y", "width", "height"], as: Double.self)
return CGRect(x: args[0], y: args[1], width: args[2], height: args[3])
}
throw Conversions.ConvertingException<CGRect>(value)
}
}
extension Date: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Date {
if let value = value as? String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
guard let date = formatter.date(from: value) else {
throw Conversions.ConvertingException<Date>(value)
}
return date
}
// For converting the value from `Date.now()`
if let value = value as? Int {
return Date(timeIntervalSince1970: Double(value) / 1000.0)
}
throw Conversions.ConvertingException<Date>(value)
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A protocol that allows converting raw values to enum cases.
*/
public protocol Enumerable: AnyArgument, CaseIterable {
/**
Tries to create an enum case using given raw value.
May throw errors, e.g. when the raw value doesn't match any case.
*/
static func create<RawValueType>(fromRawValue rawValue: RawValueType) throws -> Self
/**
Returns an array of all raw values available in the enum.
*/
static var allRawValues: [Any] { get }
/**
Type-erased enum's raw value.
*/
var anyRawValue: Any { get }
}
@available(*, deprecated, renamed: "Enumerable")
public typealias EnumArgument = Enumerable
extension Enumerable {
public static func getDynamicType() -> AnyDynamicType {
return DynamicEnumType(innerType: Self.self)
}
}
/**
Extension for `Enumerable` that also conforms to `RawRepresentable`.
This constraint allows us to reference the associated `RawValue` type.
*/
public extension Enumerable where Self: RawRepresentable, Self: Hashable {
static func create<ArgType>(fromRawValue rawValue: ArgType) throws -> Self {
guard let rawValue = rawValue as? RawValue else {
throw EnumCastingException((type: RawValue.self, value: rawValue))
}
guard let enumCase = Self.init(rawValue: rawValue) else {
throw EnumNoSuchValueException((type: Self.self, value: rawValue))
}
return enumCase
}
var anyRawValue: Any {
rawValue
}
static var allRawValues: [Any] {
return allCases.map { $0.rawValue }
}
}
/**
An error that is thrown when the value cannot be cast to associated `RawValue`.
*/
internal class EnumCastingException: GenericException<(type: Any.Type, value: Any)> {
override var reason: String {
"Unable to cast '\(param.value)' to expected type \(param.type)"
}
}
/**
An error that is thrown when the value doesn't match any available case.
*/
internal class EnumNoSuchValueException: GenericException<(type: Enumerable.Type, value: Any)> {
var allRawValuesFormatted: String {
return param.type.allRawValues
.map { "'\($0)'" }
.joined(separator: ", ")
}
override var reason: String {
"'\(param.value)' is not present in \(param.type) enum, it must be one of: \(allRawValuesFormatted)"
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A type-erased protocol that must be implemented by the definitions passed as ``ClassDefinition`` elements.
*/
public protocol AnyClassDefinitionElement: AnyDefinition {}
/**
Class definition element with an associated owner type. The `OwnerType` should refer to
the type that the parent `Class` definition is associated with (e.g. the shared object type).
*/
public protocol ClassDefinitionElement: AnyClassDefinitionElement {
associatedtype OwnerType
}
// MARK: - Conformance
// Allow some other definitions to be used as the class definition elements.
extension SyncFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension AsyncFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension ConcurrentFunctionDefinition: ClassDefinitionElement {
public typealias OwnerType = FirstArgType
}
extension PropertyDefinition: ClassDefinitionElement {
// It already has the `OwnerType`
}
extension ConstantsDefinition: ClassDefinitionElement {
public typealias OwnerType = Void
}

View File

@@ -0,0 +1,108 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Represents a JavaScript class.
*/
public final class ClassDefinition: ObjectDefinition {
/**
Name of the class.
*/
let name: String
/**
A synchronous function that gets called when the object of this class is initializing.
*/
let constructor: AnySyncFunctionDefinition?
/**
A dynamic type for the associated object class.
*/
let associatedType: AnyDynamicType?
init<AssociatedObject: ClassAssociatedObject>(
name: String,
associatedType: AssociatedObject.Type,
elements: [AnyClassDefinitionElement] = []
) {
self.name = name
self.constructor = elements.first(where: isConstructor) as? AnySyncFunctionDefinition
self.associatedType = ~AssociatedObject.self
// Constructors can't be passed down to the object definition
// as we shouldn't override the default `<Class>.prototype.constructor`.
let elementsWithoutConstructors = elements.filter({ !isConstructor($0) })
super.init(definitions: elementsWithoutConstructors)
}
// MARK: - JavaScriptObjectBuilder
public override func build(appContext: AppContext) throws -> JavaScriptObject {
let klass = try appContext.runtime.createSharedObjectClass(name) { [weak self, weak appContext] this, arguments in
guard let self = self, let appContext else {
// TODO: Throw an exception? (@tsapeta)
return
}
// Call the native constructor when defined.
let result = try? self.constructor?.call(by: this, withArguments: arguments, appContext: appContext)
// Register the shared object if returned by the constructor.
if let result = result as? SharedObject {
appContext.sharedObjectRegistry.add(native: result, javaScript: this)
}
}
try decorate(object: klass, appContext: appContext)
// Register the JS class and its associated native type.
if let sharedObjectType = associatedType as? DynamicSharedObjectType {
appContext.classRegistry.register(nativeClassId: sharedObjectType.typeIdentifier, javaScriptClass: klass)
}
return klass
}
public override func decorate(object: JavaScriptObject, appContext: AppContext) throws {
// Here we actually don't decorate the input object (constructor) but its prototype.
// Properties are intentionally skipped here they have to decorate an instance instead of the prototype.
let prototype = object.getProperty("prototype").getObject()
decorateWithConstants(object: prototype)
try decorateWithFunctions(object: prototype, appContext: appContext)
try decorateWithClasses(object: prototype, appContext: appContext)
try decorateWithProperties(object: prototype, appContext: appContext)
}
}
// MARK: - ClassAssociatedObject
/**
A protocol for types that can be used an associated type of the ``ClassDefinition``.
*/
internal protocol ClassAssociatedObject {}
// Basically we only need these two
extension JavaScriptObject: ClassAssociatedObject, AnyArgument, AnyJavaScriptValue {
internal static func convert(from value: JavaScriptValue, appContext: AppContext) throws -> Self {
guard value.kind == .object else {
throw Conversions.ConvertingException<JavaScriptObject>(value)
}
return value.getObject() as! Self
}
}
extension SharedObject: ClassAssociatedObject {}
// MARK: - Privates
/**
Checks whether the definition item is a constructor a synchronous function whose name is "constructor".
We do it that way for the following two reasons:
- It's easier to reuse existing `SyncFunctionDefinition`.
- Redefining prototype's `constructor` is a bad idea so a function with this name
needs to be filtered out when decorating the prototype.
*/
fileprivate func isConstructor(_ item: AnyDefinition) -> Bool {
return (item as? AnySyncFunctionDefinition)?.name == "constructor"
}

View File

@@ -0,0 +1,31 @@
// Copyright 2023-present 650 Industries. All rights reserved.
internal final class ClassRegistry {
var nativeToJS = [ObjectIdentifier: JavaScriptWeakObject]()
// MARK: - Accessing
func getJavaScriptClass(nativeClassId: ObjectIdentifier) -> JavaScriptObject? {
return nativeToJS[nativeClassId]?.lock()
}
func getJavaScriptClass(nativeClass: SharedObject.Type) -> JavaScriptObject? {
let nativeClassId = ObjectIdentifier(nativeClass)
return getJavaScriptClass(nativeClassId: nativeClassId)
}
// MARK: - Registration
func register(nativeClassId: ObjectIdentifier, javaScriptClass: JavaScriptObject) {
nativeToJS[nativeClassId] = javaScriptClass.createWeak()
}
func register(nativeClass: SharedObject.Type, javaScriptClass: JavaScriptObject) {
let nativeClassId = ObjectIdentifier(nativeClass)
register(nativeClassId: nativeClassId, javaScriptClass: javaScriptClass)
}
internal func clear() {
nativeToJS.removeAll()
}
}

View File

@@ -0,0 +1,277 @@
internal final class Conversions {
/**
Converts an array to tuple. Because of tuples nature, it's not possible to convert an array of any size, so we can support only up to some fixed size.
*/
static func toTuple(_ array: [Any?]) throws -> Any? {
switch array.count {
case 0:
return ()
case 1:
return (array[0])
case 2:
return (array[0], array[1])
case 3:
return (array[0], array[1], array[2])
case 4:
return (array[0], array[1], array[2], array[3])
case 5:
return (array[0], array[1], array[2], array[3], array[4])
case 6:
return (array[0], array[1], array[2], array[3], array[4], array[5])
case 7:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6])
case 8:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7])
case 9:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7], array[8])
case 10:
return (array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7], array[8], array[9])
default:
throw TooManyArgumentsException((count: array.count, limit: 10))
}
}
static func fromNSObject(_ object: Any) -> Any {
switch object {
case let object as NSArray:
return object.map { Conversions.fromNSObject($0) }
case let object as NSDictionary:
let keyValuePairs: [(String, Any)] = object.map { ($0 as! String, Conversions.fromNSObject($1)) }
return Dictionary(uniqueKeysWithValues: keyValuePairs)
case is NSNull:
return Optional<Any>.none as Any
default:
return object
}
}
/**
Picks values under given keys from the dictionary, cast to a specific type. Can throw exceptions when
- The dictionary is missing some of the given keys (`MissingKeysException`)
- Some of the values cannot be cast to specified type (`CastingValuesException`)
*/
static func pickValues<ValueType>(from dict: [String: Any], byKeys keys: [String], as type: ValueType.Type) throws -> [ValueType] {
var result = (
values: [ValueType](),
missingKeys: [String](),
invalidKeys: [String]()
)
for key in keys {
if dict[key] == nil {
result.missingKeys.append(key)
}
if let value = dict[key] as? ValueType {
result.values.append(value)
} else {
result.invalidKeys.append(key)
}
}
if !result.missingKeys.isEmpty {
throw MissingKeysException<ValueType>(result.missingKeys)
}
if !result.invalidKeys.isEmpty {
throw CastingValuesException<ValueType>(result.invalidKeys)
}
return result.values
}
/**
Converts hex string to `UIColor` or throws an exception if the string is corrupted.
*/
static func toColor(hexString hex: String) throws -> UIColor {
var hexStr = hex
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "#", with: "")
// If just RGB, set alpha to maximum
if hexStr.count == 6 { hexStr += "FF" }
if hexStr.count == 3 { hexStr += "F" }
// Expand short form (supported by Web)
if hexStr.count == 4 {
let chars = Array(hexStr)
hexStr = [
String(repeating: chars[0], count: 2),
String(repeating: chars[1], count: 2),
String(repeating: chars[2], count: 2),
String(repeating: chars[3], count: 2)
].joined(separator: "")
}
var rgba: UInt64 = 0
guard hexStr.range(of: #"^[0-9a-fA-F]{8}$"#, options: .regularExpression) != nil,
Scanner(string: hexStr).scanHexInt64(&rgba) else {
throw InvalidHexColorException(hex)
}
return try toColor(rgba: rgba)
}
/**
Converts an integer for ARGB color to `UIColor`. Since the alpha channel is represented by first 8 bits,
it's optional out of the box. React Native converts colors to such format.
*/
static func toColor(argb: UInt64) throws -> UIColor {
guard argb <= UInt32.max else {
throw HexColorOverflowException(argb)
}
let alpha = CGFloat((argb >> 24) & 0xff) / 255.0
let red = CGFloat((argb >> 16) & 0xff) / 255.0
let green = CGFloat((argb >> 8) & 0xff) / 255.0
let blue = CGFloat(argb & 0xff) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
/**
Converts an integer for RGBA color to `UIColor`.
*/
static func toColor(rgba: UInt64) throws -> UIColor {
guard rgba <= UInt32.max else {
throw HexColorOverflowException(rgba)
}
let red = CGFloat((rgba >> 24) & 0xff) / 255.0
let green = CGFloat((rgba >> 16) & 0xff) / 255.0
let blue = CGFloat((rgba >> 8) & 0xff) / 255.0
let alpha = CGFloat(rgba & 0xff) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
/**
Formats an array of keys to the string with keys in apostrophes separated by commas.
*/
static func formatKeys(_ keys: [String]) -> String {
return keys.map { "`\($0)`" }.joined(separator: ", ")
}
static func formatPlural(_ number: Int, _ singular: String, _ plural: String? = nil) -> String {
return String(number) + (number == 1 ? singular : (plural ?? singular + "s"))
}
/**
Converts the function result to the type compatible with JavaScript.
*/
static func convertFunctionResult<ValueType>(
_ value: ValueType?,
appContext: AppContext? = nil,
dynamicType: AnyDynamicType? = nil
) -> Any {
if let value = value as? Record {
return value.toDictionary()
}
if let value = value as? [Record] {
return value.map { $0.toDictionary() }
}
if let appContext {
if let value = value as? JavaScriptObjectBuilder {
return try? value.build(appContext: appContext)
}
// If the returned value is a native shared object, create its JS representation and add the pair to the registry of shared objects.
if let value = value as? SharedObject, let dynamicType = asDynamicSharedObjectType(dynamicType) {
guard let object = try? appContext.newObject(nativeClassId: dynamicType.typeIdentifier) else {
log.warn("Unable to create a JS object for \(dynamicType.description)")
return Optional<Any>.none
}
appContext.sharedObjectRegistry.add(native: value, javaScript: object)
return object
}
}
return value as Any
}
// MARK: - Exceptions
/**
An exception meaning that the number of arguments exceeds the limit.
*/
internal class TooManyArgumentsException: GenericException<(count: Int, limit: Int)> {
override var reason: String {
"Native function expects \(formatPlural(param.limit, "argument")), but received \(param.count)"
}
}
/**
An exception that can be thrown by convertible types, when given value cannot be converted.
*/
internal class ConvertingException<TargetType>: GenericException<Any?> {
override var code: String {
"ERR_CONVERTING_FAILED"
}
override var reason: String {
"Cannot convert '\(String(describing: param))' to \(TargetType.self)"
}
}
/**
An exception that is thrown when given value cannot be cast.
*/
internal class CastingException<TargetType>: GenericException<Any> {
override var code: String {
"ERR_CASTING_FAILED"
}
override var reason: String {
"Cannot cast '\(String(describing: param))' to \(TargetType.self)"
}
}
/**
An exception that can be thrown by convertible types,
when the values in given dictionary cannot be cast to specific type.
*/
internal class CastingValuesException<ValueType>: GenericException<[String]> {
override var code: String {
"ERR_CASTING_VALUES_FAILED"
}
override var reason: String {
"Cannot cast keys \(formatKeys(param)) to \(ValueType.self)"
}
}
/**
An exception that can be thrown by convertible types,
when given dictionary is missing some required keys.
*/
internal class MissingKeysException<ValueType>: GenericException<[String]> {
override var reason: String {
"Missing keys \(formatKeys(param)) to create \(ValueType.self) record"
}
}
/**
An exception that is thrown when null value is tried to be cast to non-optional type.
*/
internal class NullCastException<TargetType>: Exception {
override var reason: String {
"Cannot cast null to non-optional '\(TargetType.self)'"
}
}
/**
An exception used when the hex color string is invalid (e.g. contains non-hex characters).
*/
internal class InvalidHexColorException: GenericException<String> {
override var reason: String {
"Provided hex color '\(param)' is invalid"
}
}
/**
An exception used when the integer value of the color would result in an overflow of `UInt32`.
*/
internal class HexColorOverflowException: GenericException<UInt64> {
override var reason: String {
"Provided hex color '\(param)' would result in an overflow"
}
}
}
/**
Unwraps the dynamic optional type and returns as a dynamic shared object type if possible.
*/
private func asDynamicSharedObjectType(_ dynamicType: AnyDynamicType?) -> DynamicSharedObjectType? {
if let dynamicType = dynamicType as? DynamicOptionalType {
return dynamicType.wrappedType as? DynamicSharedObjectType
}
return dynamicType as? DynamicSharedObjectType
}

View File

@@ -0,0 +1,268 @@
// Copyright 2022-present 650 Industries. All rights reserved.
extension UIColor: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
// swiftlint:disable force_cast
if let value = value as? String {
if let namedColorComponents = namedColors[value] {
return uiColorWithComponents(namedColorComponents.map { $0 / 255 }) as! Self
}
return try Conversions.toColor(hexString: value) as! Self
}
if let components = value as? [Double] {
return uiColorWithComponents(components) as! Self
}
if let value = value as? Int {
return try Conversions.toColor(argb: UInt64(value)) as! Self
}
// Handle `PlatformColor` and `DynamicColorIOS`
if let opaqueValue = value as? [String: Any] {
if let semanticName = opaqueValue["semantic"] as? String,
let color = resolveNamedColor(name: semanticName) {
return color as! Self
}
if let semanticArray = opaqueValue["semantic"] as? [String] {
for semanticName in semanticArray {
if let color = resolveNamedColor(name: semanticName) {
return color as! Self
}
}
}
if let appearances = opaqueValue["dynamic"] as? [String: Any],
let lightColor = try appearances["light"].map({ try UIColor.convert(from: $0, appContext: appContext) }),
let darkColor = try appearances["dark"].map({ try UIColor.convert(from: $0, appContext: appContext) }) {
let highContrastLightColor = try appearances["highContrastLight"].map({ try UIColor.convert(from: $0, appContext: appContext) })
let highContrastDarkColor = try appearances["highContrastDark"].map({ try UIColor.convert(from: $0, appContext: appContext) })
#if os(iOS) || os(tvOS)
let color = UIColor { (traitCollection: UITraitCollection) -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
if traitCollection.accessibilityContrast == .high, let highContrastDarkColor {
return highContrastDarkColor
}
return darkColor
}
if traitCollection.accessibilityContrast == .high, let highContrastLightColor {
return highContrastLightColor
}
return lightColor
}
#elseif os(macOS)
let color = NSColor(name: nil) { (appearance: NSAppearance) -> NSColor in
let isDarkMode = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
let isHighContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
if isDarkMode {
if isHighContrast, let highContrastDarkColor = highContrastDarkColor {
return highContrastDarkColor
}
return darkColor
}
if isHighContrast, let highContrastLightColor = highContrastLightColor {
return highContrastLightColor
}
return lightColor
}
#endif
return color as! Self
}
}
throw Conversions.ConvertingException<UIColor>(value)
// swiftlint:enable force_cast
}
}
extension CGColor: Convertible {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
// swiftlint:disable force_cast
do {
return try UIColor.convert(from: value, appContext: appContext).cgColor as! Self
} catch _ as Conversions.ConvertingException<UIColor> {
// Rethrow `ConvertingError` with proper type
throw Conversions.ConvertingException<CGColor>(value)
}
// swiftlint:enable force_cast
}
}
private func resolveNamedColor(name: String) -> UIColor? {
return UIColor(named: name) ?? uiColorFromSemanticName(name: name)
}
private func uiColorFromSemanticName(name: String) -> UIColor? {
let selector: Selector
if name.hasSuffix("Color") {
selector = Selector(name)
} else {
selector = Selector("\(name)Color")
}
guard UIColor.responds(to: selector) else {
return nil
}
return UIColor.perform(selector).takeUnretainedValue() as? UIColor
}
private func uiColorWithComponents(_ components: [Double]) -> UIColor {
let alpha = components.count > 3 ? components[3] : 1.0
return UIColor(red: components[0], green: components[1], blue: components[2], alpha: alpha)
}
/**
Color components for named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color)
and additionally the transparent color.
*/
private let namedColors: [String: [Double]] = [
"aliceblue": [240, 248, 255, 255],
"antiquewhite": [250, 235, 215, 255],
"aqua": [0, 255, 255, 255],
"aquamarine": [127, 255, 212, 255],
"azure": [240, 255, 255, 255],
"beige": [245, 245, 220, 255],
"bisque": [255, 228, 196, 255],
"black": [0, 0, 0, 255],
"blanchedalmond": [255, 235, 205, 255],
"blue": [0, 0, 255, 255],
"blueviolet": [138, 43, 226, 255],
"brown": [165, 42, 42, 255],
"burlywood": [222, 184, 135, 255],
"cadetblue": [95, 158, 160, 255],
"chartreuse": [127, 255, 0, 255],
"chocolate": [210, 105, 30, 255],
"coral": [255, 127, 80, 255],
"cornflowerblue": [100, 149, 237, 255],
"cornsilk": [255, 248, 220, 255],
"crimson": [220, 20, 60, 255],
"cyan": [0, 255, 255, 255],
"darkblue": [0, 0, 139, 255],
"darkcyan": [0, 139, 139, 255],
"darkgoldenrod": [184, 134, 11, 255],
"darkgray": [169, 169, 169, 255],
"darkgreen": [0, 100, 0, 255],
"darkgrey": [169, 169, 169, 255],
"darkkhaki": [189, 183, 107, 255],
"darkmagenta": [139, 0, 139, 255],
"darkolivegreen": [85, 107, 47, 255],
"darkorange": [255, 140, 0, 255],
"darkorchid": [153, 50, 204, 255],
"darkred": [139, 0, 0, 255],
"darksalmon": [233, 150, 122, 255],
"darkseagreen": [143, 188, 143, 255],
"darkslateblue": [72, 61, 139, 255],
"darkslategray": [47, 79, 79, 255],
"darkslategrey": [47, 79, 79, 255],
"darkturquoise": [0, 206, 209, 255],
"darkviolet": [148, 0, 211, 255],
"deeppink": [255, 20, 147, 255],
"deepskyblue": [0, 191, 255, 255],
"dimgray": [105, 105, 105, 255],
"dimgrey": [105, 105, 105, 255],
"dodgerblue": [30, 144, 255, 255],
"firebrick": [178, 34, 34, 255],
"floralwhite": [255, 250, 240, 255],
"forestgreen": [34, 139, 34, 255],
"fuchsia": [255, 0, 255, 255],
"gainsboro": [220, 220, 220, 255],
"ghostwhite": [248, 248, 255, 255],
"gold": [255, 215, 0, 255],
"goldenrod": [218, 165, 32, 255],
"gray": [128, 128, 128, 255],
"green": [0, 128, 0, 255],
"greenyellow": [173, 255, 47, 255],
"grey": [128, 128, 128, 255],
"honeydew": [240, 255, 240, 255],
"hotpink": [255, 105, 180, 255],
"indianred": [205, 92, 92, 255],
"indigo": [75, 0, 130, 255],
"ivory": [255, 255, 240, 255],
"khaki": [240, 230, 140, 255],
"lavender": [230, 230, 250, 255],
"lavenderblush": [255, 240, 245, 255],
"lawngreen": [124, 252, 0, 255],
"lemonchiffon": [255, 250, 205, 255],
"lightblue": [173, 216, 230, 255],
"lightcoral": [240, 128, 128, 255],
"lightcyan": [224, 255, 255, 255],
"lightgoldenrodyellow": [250, 250, 210, 255],
"lightgray": [211, 211, 211, 255],
"lightgreen": [144, 238, 144, 255],
"lightgrey": [211, 211, 211, 255],
"lightpink": [255, 182, 193, 255],
"lightsalmon": [255, 160, 122, 255],
"lightseagreen": [32, 178, 170, 255],
"lightskyblue": [135, 206, 250, 255],
"lightslategray": [119, 136, 153, 255],
"lightslategrey": [119, 136, 153, 255],
"lightsteelblue": [176, 196, 222, 255],
"lightyellow": [255, 255, 224, 255],
"lime": [0, 255, 0, 255],
"limegreen": [50, 205, 50, 255],
"linen": [250, 240, 230, 255],
"magenta": [255, 0, 255, 255],
"maroon": [128, 0, 0, 255],
"mediumaquamarine": [102, 205, 170, 255],
"mediumblue": [0, 0, 205, 255],
"mediumorchid": [186, 85, 211, 255],
"mediumpurple": [147, 112, 219, 255],
"mediumseagreen": [60, 179, 113, 255],
"mediumslateblue": [123, 104, 238, 255],
"mediumspringgreen": [0, 250, 154, 255],
"mediumturquoise": [72, 209, 204, 255],
"mediumvioletred": [199, 21, 133, 255],
"midnightblue": [25, 25, 112, 255],
"mintcream": [245, 255, 250, 255],
"mistyrose": [255, 228, 225, 255],
"moccasin": [255, 228, 181, 255],
"navajowhite": [255, 222, 173, 255],
"navy": [0, 0, 128, 255],
"oldlace": [253, 245, 230, 255],
"olive": [128, 128, 0, 255],
"olivedrab": [107, 142, 35, 255],
"orange": [255, 165, 0, 255],
"orangered": [255, 69, 0, 255],
"orchid": [218, 112, 214, 255],
"palegoldenrod": [238, 232, 170, 255],
"palegreen": [152, 251, 152, 255],
"paleturquoise": [175, 238, 238, 255],
"palevioletred": [219, 112, 147, 255],
"papayawhip": [255, 239, 213, 255],
"peachpuff": [255, 218, 185, 255],
"peru": [205, 133, 63, 255],
"pink": [255, 192, 203, 255],
"plum": [221, 160, 221, 255],
"powderblue": [176, 224, 230, 255],
"purple": [128, 0, 128, 255],
"rebeccapurple": [102, 51, 153, 255],
"red": [255, 0, 0, 255],
"rosybrown": [188, 143, 143, 255],
"royalblue": [65, 105, 225, 255],
"saddlebrown": [139, 69, 19, 255],
"salmon": [250, 128, 114, 255],
"sandybrown": [244, 164, 96, 255],
"seagreen": [46, 139, 87, 255],
"seashell": [255, 245, 238, 255],
"sienna": [160, 82, 45, 255],
"silver": [192, 192, 192, 255],
"skyblue": [135, 206, 235, 255],
"slateblue": [106, 90, 205, 255],
"slategray": [112, 128, 144, 255],
"slategrey": [112, 128, 144, 255],
"snow": [255, 250, 250, 255],
"springgreen": [0, 255, 127, 255],
"steelblue": [70, 130, 180, 255],
"tan": [210, 180, 140, 255],
"teal": [0, 128, 128, 255],
"thistle": [216, 191, 216, 255],
"tomato": [255, 99, 71, 255],
"transparent": [0, 0, 0, 0],
"turquoise": [64, 224, 208, 255],
"violet": [238, 130, 238, 255],
"wheat": [245, 222, 179, 255],
"white": [255, 255, 255, 255],
"whitesmoke": [245, 245, 245, 255],
"yellow": [255, 255, 0, 255],
"yellowgreen": [154, 205, 50, 255]
]

View File

@@ -0,0 +1,124 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/*
A convertible type wrapper for a value that should be either of two generic types.
*/
open class Either<FirstType, SecondType>: Convertible {
/**
An array of dynamic equivalents for generic either types.
*/
class func dynamicTypes() -> [AnyDynamicType] {
return [~FirstType.self, ~SecondType.self]
}
/**
The underlying type-erased value.
*/
let value: Any?
required public init(_ value: Any?) {
self.value = value
}
/**
Returns a bool whether the value is of the first type.
*/
public func `is`(_ type: FirstType.Type) -> Bool {
return value is FirstType
}
/**
Returns a bool whether the value is of the second type.
*/
public func `is`(_ type: SecondType.Type) -> Bool {
return value is SecondType
}
/**
Returns the value as of the first type or `nil` if it's not of this type.
*/
public func get() -> FirstType? {
return value as? FirstType
}
/**
Returns the value as of the second type or `nil` if it's not of this type.
*/
public func get() -> SecondType? {
return value as? SecondType
}
// MARK: - Convertible
public class func convert(from value: Any?, appContext: AppContext) throws -> Self {
let dynamicTypes = dynamicTypes()
for type in dynamicTypes {
// Initialize the "either" when the current type can cast given value.
if let value = try? type.cast(value, appContext: appContext) {
return Self(value)
}
}
throw NeitherTypeException(dynamicTypes)
}
}
/*
A convertible type wrapper for a value that should be either of three generic types.
*/
open class EitherOfThree<FirstType, SecondType, ThirdType>: Either<FirstType, SecondType> {
override class func dynamicTypes() -> [AnyDynamicType] {
return super.dynamicTypes() + [~ThirdType.self]
}
/**
Returns a bool whether the value is of the third type.
*/
public func `is`(_ type: ThirdType.Type) -> Bool {
return value is ThirdType
}
/**
Returns the value as of the third type or `nil` if it's not of this type.
*/
public func get() -> ThirdType? {
return value as? ThirdType
}
}
/*
A convertible type wrapper for a value that should be either of four generic types.
*/
open class EitherOfFour<FirstType, SecondType, ThirdType, FourthType>: EitherOfThree<FirstType, SecondType, ThirdType> {
override class func dynamicTypes() -> [AnyDynamicType] {
return super.dynamicTypes() + [~FourthType.self]
}
/**
Returns a bool whether the value is of the fourth type.
*/
public func `is`(_ type: FourthType.Type) -> Bool {
return value is FourthType
}
/**
Returns the value as of the fourth type or `nil` if it's not of this type.
*/
public func get() -> FourthType? {
return value as? FourthType
}
}
// MARK: - Exceptions
/**
An exception thrown when the value is of neither type.
*/
internal class NeitherTypeException: GenericException<[AnyDynamicType]> {
override var reason: String {
var typeDescriptions = param.map({ $0.description })
let lastTypeDescription = typeDescriptions.removeLast()
return "Type must be either: \(typeDescriptions.joined(separator: ", ")) or \(lastTypeDescription)"
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A protocol whose intention is to wrap any type
to keep its real signature and not type-erase it by the compiler.
*/
public protocol AnyDynamicType: CustomStringConvertible {
/**
Checks whether the inner type is the same as the given type.
*/
func wraps<InnerType>(_ type: InnerType.Type) -> Bool
/**
Checks whether the dynamic type is equal to another,
that is when the type of the dynamic types are equal and their inner types are equal.
*/
func equals(_ type: AnyDynamicType) -> Bool
/**
Preliminarily casts the given JavaScriptValue to a non-JS value that the other `cast` function can handle.
It **must** be run on the thread used by the JavaScript runtime.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any
/**
Casts the given value to the wrapped type and returns it as `Any`.
NOTE: It may not be just simple type-casting (e.g. when the wrapped type conforms to `Convertible`).
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any
}
extension AnyDynamicType {
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
return jsValue.getRaw()
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return value
}
}
// MARK: - Operators
infix operator ~>
public func ~> <T>(lhs: AnyDynamicType, rhs: T.Type) -> Bool {
return lhs.wraps(rhs)
}
infix operator !~>
public func !~> <T>(lhs: AnyDynamicType, rhs: T.Type) -> Bool {
return !lhs.wraps(rhs)
}
public func == (lhs: AnyDynamicType, rhs: AnyDynamicType) -> Bool {
return lhs.equals(rhs)
}
public func != (lhs: AnyDynamicType, rhs: AnyDynamicType) -> Bool {
return !lhs.equals(rhs)
}

View File

@@ -0,0 +1,56 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type representing array types. Requires the array's element type
for the initialization as it delegates casting to that type for each element in the array.
*/
internal struct DynamicArrayType: AnyDynamicType {
let elementType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let ArrayType = InnerType.self as? AnyArray.Type {
return elementType.equals(ArrayType.getElementDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let arrayType = type as? Self {
return arrayType.elementType.equals(elementType)
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? [Any] {
return try value.map { try elementType.cast($0, appContext: appContext) }
}
// We should probably throw an error if we get here. On the other side, the array type
// requirement can be more loosen so we can try to arrayize values that are not arrays.
return [try elementType.cast(value, appContext: appContext)]
}
var description: String {
"[\(elementType.description)]"
}
}
/**
A type-erased protocol used to recognize if the generic type is an array type.
`Array` is a generic type, so it's impossible to check the inheritance directly.
*/
internal protocol AnyArray {
/**
Exposes the `Element` generic type wrapped by the dynamic type to preserve its metadata.
*/
static func getElementDynamicType() -> AnyDynamicType
}
/**
Extends the `Array` type to expose its generic `Element` as a dynamic type.
*/
extension Array: AnyArray {
static func getElementDynamicType() -> AnyDynamicType {
return ~Element.self
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that wraps any type conforming to `Convertible` protocol.
*/
internal struct DynamicConvertibleType: AnyDynamicType {
let innerType: Convertible.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let convertibleType = type as? Self {
return convertibleType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return try innerType.convert(from: value, appContext: appContext)
}
var description: String {
String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
A dynamic type representing Swift `Data` or Objective-C `NSData` type and backing by JavaScript `Uint8Array`.
*/
internal struct DynamicDataType: AnyDynamicType {
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return InnerType.self == Data.self
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Data.Type
}
/**
Converts JS typed array to its native representation.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let jsTypedArray = jsValue.getTypedArray(), jsTypedArray.kind == TypedArrayKind.Uint8Array else {
throw Conversions.CastingException<Uint8Array>(jsValue)
}
return Data(bytes: jsTypedArray.getUnsafeMutableRawPointer(), count: jsTypedArray.getProperty("byteLength").getInt())
}
var description: String {
return String(describing: Data.self)
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2015-present 650 Industries. All rights reserved.
/**
A dynamic type representing dictionary types. Requires the dictionary's value type
for the initialization as it delegates casting to that type for each element in the dictionary.
*/
internal struct DynamicDictionaryType: AnyDynamicType {
let valueType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let dictionaryType = InnerType.self as? AnyDictionary.Type {
return valueType.equals(dictionaryType.getValueDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let dictionaryType = type as? Self {
return dictionaryType.valueType.equals(valueType)
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if let jsObject = try? jsValue.asObject() {
var result: [AnyHashable: Any] = [:]
for key in jsObject.getPropertyNames() {
result[key] = try valueType.cast(jsValue: jsObject.getProperty(key), appContext: appContext)
}
return result
}
throw Conversions.CastingException<JavaScriptObject>(jsValue)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? [AnyHashable: Any] {
return try value.mapValues { try valueType.cast($0, appContext: appContext) }
}
throw Conversions.CastingException<[AnyHashable: Any]>(value)
}
var description: String {
"[Hashable: \(valueType.description)]"
}
}
/**
A type-erased protocol used to recognize if the generic type is a dictionary type.
`Dictionary` is a generic type, so it's impossible to check the inheritance directly.
*/
internal protocol AnyDictionary {
/**
Exposes the `Value` generic type wrapped by the dynamic type to preserve its metadata.
*/
static func getValueDynamicType() -> AnyDynamicType
}
/**
Extends the `Dictionary` type to expose its generic `Value` as a dynamic type.
*/
extension Dictionary: AnyDictionary {
static func getValueDynamicType() -> AnyDynamicType {
return ~Value.self
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type representing an enum that conforms to `Enumerable`.
*/
internal struct DynamicEnumType: AnyDynamicType {
let innerType: any Enumerable.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let enumType = type as? Self {
return enumType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
return try innerType.create(fromRawValue: value)
}
var description: String {
"Enum<\(innerType)>"
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A dynamic type representing various types of JavaScript values.
*/
internal struct DynamicJavaScriptType: AnyDynamicType {
let innerType: AnyJavaScriptValue.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let providedType = type as? Self {
return providedType.innerType == innerType
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
return try innerType.convert(from: jsValue, appContext: appContext)
}
var description: String {
return String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that represents an optional type, which allows `nil` to be passed when casting.
Requires the optional's wrapped type as it delegates casting to that type for non-nil values.
*/
internal struct DynamicOptionalType: AnyDynamicType {
let wrappedType: AnyDynamicType
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
if let OptionalType = InnerType.self as? AnyOptional.Type {
return wrappedType.equals(OptionalType.getWrappedDynamicType())
}
return false
}
func equals(_ type: AnyDynamicType) -> Bool {
if let optionalType = type as? Self {
return optionalType.wrappedType.equals(wrappedType)
}
return false
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.isUndefined() || jsValue.isNull() {
return Optional<Any>.none as Any
}
return try wrappedType.cast(jsValue: jsValue, appContext: appContext)
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if Optional.isNil(value) || value is NSNull {
return Optional<Any>.none as Any
}
return try wrappedType.cast(value, appContext: appContext)
}
var description: String {
"\(wrappedType)?"
}
}
/**
A type-erased protocol used to recognize if the generic type is an optional type.
`Optional` is a generic enum, so it's impossible to check the inheritance directly.
*/
internal protocol AnyOptional {
/**
Exposes the `Wrapped` generic type wrapped by the dynamic type to preserve its metadata.`
*/
static func getWrappedDynamicType() -> AnyDynamicType
}
/**
Make generic `Optional` implement non-generic `AnyOptional` and add handy check against type-erased `nil`.
*/
extension Optional: AnyOptional {
static func getWrappedDynamicType() -> AnyDynamicType {
return ~Wrapped.self
}
static func isNil(_ object: Wrapped) -> Bool {
switch object as Any {
case Optional<Any>.none:
return true
default:
return false
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
A dynamic type that can wrap any type, but it casts only type-compatible values using `as?` keyword.
The innermost type of the other dynamic types like `ArrayArgumentType` and `OptionalArgumentType`.
*/
internal struct DynamicRawType<InnerType>: AnyDynamicType {
let innerType: InnerType.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return type == innerType
}
func equals(_ type: AnyDynamicType) -> Bool {
return type is Self
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? InnerType {
return value
}
// Raw types are always non-optional, but they may receive `nil` values.
// Let's throw more specific error in this case.
if Optional.isNil(value) {
throw Conversions.NullCastException<InnerType>()
}
throw Conversions.CastingException<InnerType>(value)
}
var description: String {
String(describing: innerType.self)
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A dynamic type representing the `SharedObject` type and its subclasses.
*/
internal struct DynamicSharedObjectType: AnyDynamicType {
let innerType: AnySharedObject.Type
/**
A unique identifier of the wrapped type.
*/
var typeIdentifier: ObjectIdentifier {
return ObjectIdentifier(innerType)
}
init(innerType: AnySharedObject.Type) {
self.innerType = innerType
}
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let sharedObjectType = type as? Self {
return sharedObjectType.innerType == innerType
}
return false
}
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
if let value = value as? SharedObject, type(of: value) == innerType {
// Given value is a shared object already
return value
}
// If the given value is a shared object id, search the registry for its native representation
if let sharedObjectId = value as? SharedObjectId,
let nativeSharedObject = appContext.sharedObjectRegistry.get(sharedObjectId)?.native {
return nativeSharedObject
}
throw NativeSharedObjectNotFoundException()
}
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
if jsValue.kind == .number {
let sharedObjectId = jsValue.getInt() as SharedObjectId
guard let nativeSharedObject = appContext.sharedObjectRegistry.get(sharedObjectId)?.native else {
throw NativeSharedObjectNotFoundException()
}
return nativeSharedObject
}
if let jsObject = try? jsValue.asObject(),
let nativeSharedObject = appContext.sharedObjectRegistry.toNativeObject(jsObject) {
return nativeSharedObject
}
throw NativeSharedObjectNotFoundException()
}
var description: String {
return "SharedObject<\(innerType)>"
}
}
internal final class NativeSharedObjectNotFoundException: Exception {
override var reason: String {
"Unable to find the native shared object associated with given JavaScript object"
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2021-present 650 Industries. All rights reserved.
// Function names should start with a lowercase character, but in this one case
// we want it to be uppercase as we treat it more like a generic class.
// swiftlint:disable identifier_name
/**
Factory creating an instance of the dynamic type wrapper conforming to `AnyDynamicType`.
Depending on the given type, it may return one of `DynamicArrayType`, `DynamicOptionalType`, `DynamicConvertibleType`, etc.
Note that this goes through many type checks, thus it might be a bit more expensive than using generic type constraints,
see the `~` prefix operator below that handles types conforming to `AnyArgument` in a faster way.
*/
private func DynamicType<T>(_ type: T.Type) -> AnyDynamicType {
if let ArrayType = T.self as? AnyArray.Type {
return DynamicArrayType(elementType: ArrayType.getElementDynamicType())
}
if let DictionaryType = T.self as? AnyDictionary.Type {
return DynamicDictionaryType(valueType: DictionaryType.getValueDynamicType())
}
if let OptionalType = T.self as? any AnyOptional.Type {
return DynamicOptionalType(wrappedType: OptionalType.getWrappedDynamicType())
}
if let ConvertibleType = T.self as? Convertible.Type {
return DynamicConvertibleType(innerType: ConvertibleType)
}
if let EnumType = T.self as? any Enumerable.Type {
return DynamicEnumType(innerType: EnumType)
}
if let ViewType = T.self as? UIView.Type {
return DynamicViewType(innerType: ViewType)
}
if let SharedObjectType = T.self as? SharedObject.Type {
return DynamicSharedObjectType(innerType: SharedObjectType)
}
if let TypedArrayType = T.self as? AnyTypedArray.Type {
return DynamicTypedArrayType(innerType: TypedArrayType)
}
if let DataType = T.self as? Data.Type {
return DynamicDataType()
}
if let JavaScriptValueType = T.self as? any AnyJavaScriptValue.Type {
return DynamicJavaScriptType(innerType: JavaScriptValueType)
}
return DynamicRawType(innerType: T.self)
}
/**
Handy prefix operator that makes the dynamic type from the static type.
*/
prefix operator ~
internal prefix func ~ <T>(type: T.Type) -> AnyDynamicType {
return DynamicType(type)
}
internal prefix func ~ <T>(type: T.Type) -> AnyDynamicType where T: AnyArgument {
return T.getDynamicType()
}

View File

@@ -0,0 +1,57 @@
// Copyright 2022-present 650 Industries. All rights reserved.
internal struct DynamicTypedArrayType: AnyDynamicType {
let innerType: AnyTypedArray.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let typedArrayType = type as? Self {
return typedArrayType.innerType == innerType
}
return false
}
/**
Converts JS typed array to its native representation.
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let jsTypedArray = jsValue.getTypedArray() else {
throw NotTypedArrayException(innerType)
}
return TypedArray.create(from: jsTypedArray)
}
/**
Converts the given native `TypedArray` to a concrete typed array class wrapped by the dynamic type.
Throws `ArrayTypeMismatchException` otherwise.
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let typedArray = value as? TypedArray else {
throw NotTypedArrayException(innerType)
}
// Concrete typed arrays must be the same as the inner type.
guard innerType == TypedArray.self || type(of: typedArray) == innerType else {
throw ArrayTypeMismatchException((received: type(of: typedArray), expected: innerType))
}
return typedArray
}
var description: String {
return String(describing: innerType)
}
}
internal final class ArrayTypeMismatchException: GenericException<(received: Any.Type, expected: Any.Type)> {
override var reason: String {
"Received a typed array of type \(param.received), expected \(param.expected)"
}
}
internal final class NotTypedArrayException: GenericException<AnyTypedArray.Type> {
override var reason: String {
"Given argument is not an instance of \(param)"
}
}

View File

@@ -0,0 +1,71 @@
// Copyright 2023-present 650 Industries. All rights reserved.
internal struct DynamicViewType: AnyDynamicType {
let innerType: UIView.Type
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
return innerType == InnerType.self
}
func equals(_ type: AnyDynamicType) -> Bool {
if let viewType = type as? Self {
return viewType.innerType == innerType
}
return false
}
/**
Casts from the React component instance to the view tag (`Int`).
*/
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
guard let viewTag = findViewTag(jsValue) else {
throw InvalidViewTagException()
}
return viewTag
}
/**
Converts a value of type `Int` to a native view with that tag in the given app context.
*/
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
guard let viewTag = value as? Int else {
throw InvalidViewTagException()
}
guard Thread.isMainThread else {
throw NonMainThreadException()
}
guard let view = appContext.findView(withTag: viewTag, ofType: innerType.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: innerType.self))
}
return view
}
var description: String {
return "View<\(innerType)>"
}
}
private func findViewTag(_ value: JavaScriptValue) -> Int? {
if value.isNumber() {
return value.getInt()
}
if value.isObject() {
let nativeTag = value.getObject().getProperty("nativeTag")
if nativeTag.isNumber() {
return nativeTag.getInt()
}
}
return nil
}
internal final class InvalidViewTagException: Exception {
override var reason: String {
"The view tag must be a number"
}
}
internal final class NonMainThreadException: Exception {
override var reason: String {
"All operations on the views must run from the main UI thread"
}
}

View File

@@ -0,0 +1,63 @@
/**
Represents a listener for the specific event.
*/
internal struct EventListener: AnyDefinition {
let name: EventName
let call: (Any?, Any?) throws -> Void
/**
Listener initializer for events without sender and payload.
*/
init(_ name: EventName, _ listener: @escaping () -> Void) {
self.name = name
self.call = { _, _ in listener() }
}
/**
Listener initializer for events with no payload.
*/
init<Sender>(_ name: EventName, _ listener: @escaping (Sender) -> Void) {
self.name = name
self.call = { sender, _ in
guard let sender = sender as? Sender else {
throw InvalidSenderTypeException((eventName: name, senderType: Sender.self))
}
listener(sender)
}
}
/**
Listener initializer for events that specify the payload.
*/
init<Sender, PayloadType>(_ name: EventName, _ listener: @escaping (Sender, PayloadType?) -> Void) {
self.name = name
self.call = { sender, payload in
guard let sender = sender as? Sender else {
throw InvalidSenderTypeException((eventName: name, senderType: Sender.self))
}
listener(sender, payload as? PayloadType)
}
}
}
class InvalidSenderTypeException: GenericException<(eventName: EventName, senderType: Any.Type)> {
override var reason: String {
"Sender for event '\(param.eventName)' must be of type \(param.senderType)"
}
}
public enum EventName: Equatable {
case custom(_ name: String)
// MARK: Module lifecycle
case moduleCreate
case moduleDestroy
case appContextDestroys
// MARK: App (UIApplication) lifecycle
case appEntersForeground
case appBecomesActive
case appEntersBackground
}

View File

@@ -0,0 +1,66 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
An alias for type-erased callback handler.
*/
typealias AnyCallbackHandlerType = @convention(block) ([String: Any]) -> Void
/**
Public type-erased protocol that `Callback` object conforms to.
*/
public protocol AnyCallback {
/**
Initializes an empty callback (no-op).
*/
init()
}
/**
Internal type-erased protocol for `Callback` object.
*/
internal protocol AnyCallbackInternal: AnyCallback {
/**
Sets the callback handler. By default the callback
is not settled which means it has no handler, thus is no-op.
*/
func settle(_ handler: @escaping AnyCallbackHandlerType)
/**
Invalidates the callback, making its handler no-op.
*/
func invalidate()
}
/**
Callable object that represents a JavaScript function.
*/
public class Callback<ArgType>: AnyCallback, AnyCallbackInternal {
/**
The underlying closure to invoke when the callback is called.
*/
private var handler: AnyCallbackHandlerType?
// MARK: AnyCallback
public required init() {}
// MARK: AnyCallbackInternal
internal func settle(_ handler: @escaping AnyCallbackHandlerType) {
self.handler = handler
}
internal func invalidate() {
self.handler = nil
}
// MARK: Calling as function
/**
Allows the callback instance to be called as a function.
*/
public func callAsFunction(_ arg: [String: Any]) {
// TODO: Convert records to dictionaries (@tsapeta)
handler?(arg)
}
}

View File

@@ -0,0 +1,85 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
An object that can dispatch native events.
*/
public final class EventDispatcher {
/**
Type of the event dispatcher handler.
- Note: It must be marked as `@convention(block)` as long as we support Objective-C views and thus need to use `setValue(_:forKey:)`.
*/
internal typealias Handler = @convention(block) ([String: Any]) -> Void
/**
A custom name of the dispatching event. The default name is usually a name of the property holding the dispatcher.
*/
internal var customName: String?
/**
A function that is invoked to dispatch the event, no-op by default.
Something that manages the events should override it.
*/
internal var handler: Handler?
/**
Default initializer of the event dispatcher. Provide a custom name if you want the dispatcher
to refer to an event with different name than the property holding the dispatcher.
*/
public init(_ customName: String? = nil) {
self.customName = customName
}
// MARK: - Calling as function
/**
Dispatches the event with the given dictionary as a payload.
*/
public func callAsFunction(_ payload: [String: Any]) {
handler?(payload)
}
/**
Dispatches the event with the given record as a payload.
*/
public func callAsFunction(_ payload: Record) {
handler?(payload.toDictionary())
}
/**
Dispatches the event with an empty payload.
*/
public func callAsFunction() {
handler?([:])
}
}
/**
Installs convenient event dispatchers for the given event and view.
The provided handler can be specific to Paper or Fabric.
*/
internal func installEventDispatcher<ViewType>(forEvent eventName: String, onView view: ViewType, handler: @escaping EventDispatcher.Handler) {
// Find view's property that is of type `EventDispatcher` and refers to this particular event name.
let child = Mirror(reflecting: view).children.first {
return isEventDispatcherWithName($0, eventName)
}
if var eventDispatcher = child?.value as? EventDispatcher {
eventDispatcher.handler = handler
} else if let view = view as? UIView, view.responds(to: Selector(eventName)) {
// This is to handle events in legacy views written in Objective-C.
// Note that the property should be of type EXDirectEventBlock.
view.setValue(handler, forKey: eventName)
} else {
log.warn("Couldn't find the property for event '\(eventName)' in '\(type(of: view))' class")
}
}
/**
Checks whether the mirror child refers to the event dispatcher with the given event name.
*/
private func isEventDispatcherWithName(_ mirrorChild: Mirror.Child, _ eventName: String) -> Bool {
guard let eventDispatcher = mirrorChild.value as? EventDispatcher else {
return false
}
return eventDispatcher.customName != nil ? eventDispatcher.customName == eventName : mirrorChild.label == eventName
}

View File

@@ -0,0 +1,79 @@
// Copyright 2024-present 650 Industries. All rights reserved.
internal enum EventObservingType: String {
case startObserving
case stopObserving
}
internal protocol AnyEventObservingDefinition: AnyDefinition {
var event: String? { get }
var type: EventObservingType { get }
func call()
}
public final class EventObservingDefinition: AnyEventObservingDefinition {
public typealias ClosureType = () -> Void
let type: EventObservingType
let event: String?
let closure: ClosureType
init(type: EventObservingType, event: String?, _ closure: @escaping ClosureType) {
self.type = type
self.event = event
self.closure = closure
}
func call() {
closure()
}
}
public struct EventObservingDecorator: JavaScriptObjectDecorator {
let definitions: [any AnyEventObservingDefinition]
/**
Decorates the given object with `startObserving` and `stopObserving` functions.
These functions are automatically called by the `EventEmitter` implementation.
*/
func decorate(object: JavaScriptObject, appContext: AppContext) throws {
// We need to keep track the number of observed events
// so we can call observers not attached to any event in the right moment.
var observingEvents: Int = 0
let startObserving = AsyncFunctionDefinition(
EventObservingType.startObserving.rawValue,
firstArgType: String.self,
dynamicArgumentTypes: [~String.self]
) { (event: String) in
observingEvents += 1
for definition in definitions where definition.type == .startObserving {
if definition.event == event || (observingEvents == 1 && definition.event == nil) {
definition.call()
}
}
}
let stopObserving = AsyncFunctionDefinition(
EventObservingType.stopObserving.rawValue,
firstArgType: String.self,
dynamicArgumentTypes: [~String.self]
) { (event: String) in
observingEvents -= 1
for definition in definitions where definition.type == .stopObserving {
if definition.event == event || (observingEvents == 0 && definition.event == nil) {
definition.call()
}
}
}
object.setProperty(startObserving.name, value: try startObserving.build(appContext: appContext))
object.setProperty(stopObserving.name, value: try stopObserving.build(appContext: appContext))
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2024-present 650 Industries. All rights reserved.
public final class LegacyEventEmitterCompat: EXEventEmitterService {
internal weak var appContext: AppContext?
init(appContext: AppContext) {
self.appContext = appContext
}
// Objective-C protocol doesn't specify nullability
// swiftlint:disable:next implicitly_unwrapped_optional
public func sendEvent(withName name: String!, body: Any!) {
guard let appContext, let runtime = try? appContext.runtime else {
log.warn("Unable to send an event '\(name)' because the runtime is not available")
return
}
// Send the event to all modules that declare support for this particular event.
// That's how it works in the device event emitter provided by React Native.
let moduleHoldersWithEvent = appContext.moduleRegistry.filter { holder in
return holder.definition.eventNames.contains(name)
}
runtime.schedule {
for holder in moduleHoldersWithEvent {
if let jsObject = holder.javaScriptObject {
JSIUtils.emitEvent(name, to: jsObject, withArguments: [body], in: runtime)
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
An exception that may have been caused by another error.
*/
public protocol ChainableException: Error, AnyObject {
/**
The direct cause of the exception.
*/
var cause: Error? { get set }
/**
The first error that started the chain of exceptions.
*/
var rootCause: Error? { get }
/**
Sets the direct cause of the exception and returns itself.
*/
func causedBy(_ error: Error?) -> Self
/**
Tells whether any of the cause in chain is of given type.
*/
func isCausedBy<ErrorType: Error>(_ errorType: ErrorType.Type) -> Bool
}
public extension ChainableException {
var rootCause: Error? {
if let cause = cause as? ChainableException {
return cause.rootCause ?? cause
}
return cause
}
@discardableResult
func causedBy(_ error: Error?) -> Self {
cause = error ?? cause
return self
}
func isCausedBy<ErrorType: Error>(_ errorType: ErrorType.Type) -> Bool {
if cause is ErrorType {
return true
}
if let cause = cause as? ChainableException {
return cause.isCausedBy(errorType)
}
return false
}
}

View File

@@ -0,0 +1,55 @@
import Foundation
/**
A protocol for errors specyfing its `code` and providing the `description`.
*/
public protocol CodedError: Error, CustomStringConvertible {
var code: String { get }
var description: String { get }
}
/**
Extends the `CodedError` to make a fallback for `code` and `description`.
*/
public extension CodedError {
/**
The code is inferred from the class name e.g. the code of `ModuleNotFoundError` becomes `ERR_MODULE_NOT_FOUND`.
To obtain the code, the class name is cut off from generics and `Error` suffix, then it's converted to snake case and uppercased.
*/
var code: String {
return errorCodeFromString(String(describing: type(of: self)))
}
/**
The description falls back to object's localized description.
*/
var description: String {
return localizedDescription
}
}
/**
Basic implementation of `CodedError` protocol,
where the code and the description are provided in the initializer.
*/
public struct SimpleCodedError: CodedError {
public var code: String
public var description: String
init(_ code: String, _ description: String) {
self.code = code
self.description = description
}
}
func errorCodeFromString(_ str: String) -> String {
let name = str.replacingOccurrences(of: #"(Error|Exception)?(<.*>)?$"#, with: "", options: .regularExpression)
// The pattern is valid, so it'll never throw
// swiftlint:disable:next force_try
let regex = try! NSRegularExpression(pattern: "(.)([A-Z])", options: [])
let range = NSRange(location: 0, length: name.count)
return "ERR_" + regex
.stringByReplacingMatches(in: name, options: [], range: range, withTemplate: "$1_$2")
.uppercased()
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A group of the most common exceptions that might be necessary for modules.
*/
public struct Exceptions {
/**
The Expo app context is no longer available.
*/
public final class AppContextLost: Exception {
override public var reason: String {
"The app context has been lost"
}
}
/**
The JavaScript runtime is no longer available.
*/
public final class RuntimeLost: Exception {
override public var reason: String {
"The JavaScript runtime has been lost"
}
}
/**
An exception to throw when the operation is not supported on the simulator.
*/
public final class SimulatorNotSupported: Exception {
override public var reason: String {
"This operation is not supported on the simulator"
}
}
/**
An exception to throw when the view with the given tag and class cannot be found.
*/
public final class ViewNotFound<ViewType: UIView>: GenericException<(tag: Int, type: ViewType.Type)> {
override public var reason: String {
"Unable to find the '\(param.type)' view with tag '\(param.tag)'"
}
}
/**
An exception to throw when there is no module implementing the `EXFileSystemInterface` interface.
*/
public final class FileSystemModuleNotFound: Exception {
override public var reason: String {
"FileSystem module not found, make sure 'expo-file-system' is linked correctly"
}
}
/**
An exception to throw when there is no module implementing the `EXPermissionsInterface` interface.
- Note: This should never happen since the module is a part of `expo-modules-core`, but for compatibility reasons
`appContext.permissions` is still an optional value.
*/
public final class PermissionsModuleNotFound: Exception {
override public var reason: String {
"Permissions module not found"
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2022-present 650 Industries. All rights reserved.
open class Exception: CodedError, ChainableException, CustomStringConvertible, CustomDebugStringConvertible {
open lazy var name: String = String(describing: Self.self)
/**
String describing the reason of the exception.
*/
open var reason: String {
"undefined reason"
}
/**
The origin in code where the exception was created.
*/
open var origin: ExceptionOrigin
/**
A custom code of the exception. When unset, the `code` is calculated from the exception name or class name.
*/
let customCode: String?
/**
The default initializer that captures the place in the code where the exception was created.
- Warning: Call it only without arguments!
*/
public init(file: String = #fileID, line: UInt = #line, function: String = #function) {
self.origin = ExceptionOrigin(file: file, line: line, function: function)
self.customCode = nil
}
public init(name: String, description: String, code: String? = nil, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.origin = ExceptionOrigin(file: file, line: line, function: function)
self.customCode = code
self.name = name
self.description = description
}
// MARK: - CodedError
open var code: String {
customCode ?? errorCodeFromString(name)
}
// MARK: - ChainableException
open var cause: Error?
// MARK: - CustomStringConvertible
open lazy var description: String = concatDescription(reason, withCause: cause, debug: false)
// MARK: - CustomDebugStringConvertible
open var debugDescription: String {
let debugDescription = "\(name): \(reason) (at \(origin.file):\(origin.line))"
return concatDescription(debugDescription, withCause: cause, debug: true)
}
}
/**
Concatenates the exception description with its cause description.
*/
private func concatDescription(_ description: String, withCause cause: Error?, debug: Bool = false) -> String {
let causeSeparator = "\n→ Caused by: "
switch cause {
case let cause as Exception:
return description + causeSeparator + (debug ? cause.debugDescription : cause.description)
case let cause as CodedError:
// `CodedError` is deprecated but we need to provide backwards compatibility as some modules already used it.
return description + causeSeparator + cause.description
case let cause?:
return description + causeSeparator + cause.localizedDescription
default:
return description
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Represents the place in code where the exception was created.
*/
public struct ExceptionOrigin: CustomStringConvertible {
/**
The path to the file in which the exception was created.
*/
let file: String
/**
The line number on which the exception was created.
*/
let line: UInt
/**
The name (selector) of the declaration in which the exception was created.
*/
let function: String
/**
Stringified representation of the exception origin.
*/
public var description: String {
"at \(file):\(line) in \(function)"
}
}

View File

@@ -0,0 +1,20 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
The exception that needs some additional parameters to be best described.
*/
open class GenericException<ParamType>: Exception {
/**
The additional parameter passed to the initializer.
*/
public let param: ParamType
/**
The default initializer that takes a param and captures the place in the code where the exception was created.
- Warning: Call it only with one argument! If you need to pass more parameters, use a tuple instead.
*/
public init(_ param: ParamType, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.param = param
super.init(file: file, line: line, function: function)
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Exception wrapper used to handle unexpected internal native errors.
*/
public class UnexpectedException: Exception {
private let errorDescription: String
public init(_ error: Error, file: String = #fileID, line: UInt = #line, function: String = #function) {
self.errorDescription = error.localizedDescription
super.init(file: file, line: line, function: function)
}
public override var reason: String {
return errorDescription
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2024-present 650 Industries. All rights reserved.
#import <React/RCTBridgeModule.h>
#import <ExpoModulesCore/EXNativeModulesProxy.h>
#import <ExpoModulesCore/EXModuleRegistry.h>
@class EXAppContext;
@interface ExpoBridgeModule : NSObject <RCTBridgeModule>
@property (nonatomic, nullable, strong) EXAppContext *appContext;
- (nonnull instancetype)initWithAppContext:(nonnull EXAppContext *) appContext;
- (void)legacyProxyDidSetBridge:(nonnull EXNativeModulesProxy *)moduleProxy
legacyModuleRegistry:(nonnull EXModuleRegistry *)moduleRegistry;
@end

View File

@@ -0,0 +1,88 @@
// Copyright 2024-present 650 Industries. All rights reserved.
#import <ReactCommon/RCTTurboModule.h>
#import <ExpoModulesCore/ExpoBridgeModule.h>
#import <ExpoModulesCore/Swift.h>
// The runtime executor is included as of React Native 0.74 in bridgeless mode.
#if __has_include(<ReactCommon/RCTRuntimeExecutor.h>)
#import <ReactCommon/RCTRuntimeExecutor.h>
#endif // React Native >=0.74
@implementation ExpoBridgeModule
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE(ExpoModulesCore);
- (instancetype)init
{
if (self = [super init]) {
_appContext = [[EXAppContext alloc] init];
}
return self;
}
- (instancetype)initWithAppContext:(EXAppContext *) appContext
{
if (self = [super init]) {
_appContext = appContext;
}
return self;
}
+ (BOOL)requiresMainQueueSetup
{
// We do want to run the initialization (`setBridge`) on the JS thread.
return NO;
}
- (void)setBridge:(RCTBridge *)bridge
{
// As of React Native 0.74 with the New Architecture enabled,
// it's actually an instance of `RCTBridgeProxy` that provides backwards compatibility.
// Also, hold on with initializing the runtime until `setRuntimeExecutor` is called.
_bridge = bridge;
_appContext.reactBridge = bridge;
#if !__has_include(<ReactCommon/RCTRuntimeExecutor.h>)
_appContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:bridge];
#endif // React Native <0.74
}
#if __has_include(<ReactCommon/RCTRuntimeExecutor.h>)
- (void)setRuntimeExecutor:(RCTRuntimeExecutor *)runtimeExecutor
{
_appContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:_bridge withExecutor:runtimeExecutor];
}
#endif // React Native >=0.74
/**
This should be called inside `[EXNativeModulesProxy setBridge:]`.
*/
- (void)legacyProxyDidSetBridge:(nonnull EXNativeModulesProxy *)moduleProxy
legacyModuleRegistry:(nonnull EXModuleRegistry *)moduleRegistry
{
_appContext.legacyModulesProxy = moduleProxy;
_appContext.legacyModuleRegistry = moduleRegistry;
// We need to register all the modules after the legacy module registry is set
// otherwise legacy modules (e.g. permissions) won't be available in OnCreate { }
[_appContext useModulesProvider:@"ExpoModulesProvider"];
}
/**
A synchronous method that is called from JS before requiring
any module to ensure that all necessary bindings are installed.
*/
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(installModules)
{
if (_bridge && !_appContext._runtime) {
// TODO: Keep this condition until we remove the other way of installing modules.
// See `setBridge` method above.
_appContext._runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:_bridge];
}
return nil;
}
@end

View File

@@ -0,0 +1,6 @@
@objc(EXRuntime)
public final class ExpoRuntime: JavaScriptRuntime {
internal func initializeCoreObject(_ coreObject: JavaScriptObject) throws {
global().defineProperty(EXGlobalCoreObjectPropertyName, value: coreObject, options: .enumerable)
}
}

View File

@@ -0,0 +1,89 @@
/**
An alias to `Result<Any, Exception>` which can be passed to the function callback.
*/
public typealias FunctionCallResult = Result<Any, Exception>
/**
A protocol for any type-erased function.
*/
internal protocol AnyFunctionDefinition: AnyDefinition, JavaScriptObjectBuilder {
/**
Name of the function. JavaScript refers to the function by this name.
*/
var name: String { get }
/**
An array of the dynamic types that the function takes. If the last type is `Promise`, it's not included.
*/
var dynamicArgumentTypes: [AnyDynamicType] { get }
/**
A number of arguments the function takes. If the function expects to receive an owner (`this`) as the first argument, it's not counted.
Similarly, if the last argument is of type `Promise`, it is not counted.
*/
var argumentsCount: Int { get }
/**
A minimum number of arguments the functions needs which equals to `argumentsCount` reduced by the number of trailing optional arguments.
*/
var requiredArgumentsCount: Int { get }
/**
Indicates whether the function's arguments starts from the owner that calls this function.
*/
var takesOwner: Bool { get set }
/**
Calls the function with a given owner and arguments and returns a result through the callback block.
- Parameters:
- owner: An object that calls this function. If the `takesOwner` property is true
and type of the first argument matches the owner type, it's being passed as the argument.
- args: An array of arguments to pass to the function. They could be Swift primitives
when invoked through the bridge and in unit tests or `JavaScriptValue`s
when the function is called through the JSI.
- appContext: An app context where the function is being executed.
- callback: A callback that receives a result of the function execution.
*/
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> ())
}
extension AnyFunctionDefinition {
var requiredArgumentsCount: Int {
var trailingOptionalArgumentsCount: Int = 0
for dynamicArgumentType in dynamicArgumentTypes.reversed() {
if dynamicArgumentType is DynamicOptionalType {
trailingOptionalArgumentsCount += 1
} else {
break
}
}
return argumentsCount - trailingOptionalArgumentsCount
}
var argumentsCount: Int {
return dynamicArgumentTypes.count
}
/**
Calls the function just like `call(by:withArguments:appContext:callback:)`, but without an owner
and with an empty callback. Might be useful when you only want to call the function,
but don't care about the result.
*/
func call(withArguments args: [Any], appContext: AppContext) {
call(by: nil, withArguments: args, appContext: appContext, callback: { _ in })
}
}
internal class FunctionCallException: GenericException<String> {
override var reason: String {
"Calling the '\(param)' function has failed"
}
override var code: String {
guard let cause = cause as? Exception else {
return super.code
}
return cause.code
}
}

View File

@@ -0,0 +1,150 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import Dispatch
/**
Type-erased protocol for asynchronous functions.
*/
internal protocol AnyAsyncFunctionDefinition: AnyFunctionDefinition {
/**
Specifies on which queue the function should run.
*/
func runOnQueue(_ queue: DispatchQueue?) -> Self
}
/**
The default queue used for module's async function calls.
*/
private let defaultQueue = DispatchQueue(label: "expo.modules.AsyncFunctionQueue", qos: .userInitiated)
/**
Represents a function that can only be called asynchronously, thus its JavaScript equivalent returns a Promise.
*/
public final class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFunctionDefinition {
typealias ClosureType = (Args) throws -> ReturnType
/**
The underlying closure to run when the function is called.
*/
let body: ClosureType
/**
Bool value indicating whether the function takes promise as the last argument.
*/
let takesPromise: Bool
/**
Dispatch queue on which each function's call is run.
*/
var queue: DispatchQueue?
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
_ body: @escaping ClosureType
) {
self.name = name
self.takesPromise = dynamicArgumentTypes.last?.wraps(Promise.self) ?? false
self.dynamicArgumentTypes = dynamicArgumentTypes
self.body = body
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0) - (takesPromise ? 1 : 0)
}
var takesOwner: Bool = false
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> ()) {
let promise = Promise { value in
callback(.success(Conversions.convertFunctionResult(value)))
} rejecter: { exception in
callback(.failure(exception))
}
var arguments: [Any] = concat(
arguments: args,
withOwner: owner,
withPromise: takesPromise ? promise : nil,
forFunction: self,
appContext: appContext
)
do {
try validateArgumentsNumber(function: self, received: args.count)
// All `JavaScriptValue` args must be preliminarly converted on the JS thread, so before we jump to the function's queue.
arguments = try cast(jsValues: arguments, forFunction: self, appContext: appContext)
} catch let error as Exception {
callback(.failure(error))
return
} catch {
callback(.failure(UnexpectedException(error)))
return
}
let queue = queue ?? defaultQueue
queue.async { [body, name] in
let returnedValue: ReturnType?
do {
// Convert arguments to the types desired by the function.
arguments = try cast(arguments: arguments, forFunction: self, appContext: appContext)
// swiftlint:disable:next force_cast
let argumentsTuple = try Conversions.toTuple(arguments) as! Args
returnedValue = try body(argumentsTuple)
} catch let error as Exception {
promise.reject(FunctionCallException(name).causedBy(error))
return
} catch {
promise.reject(UnexpectedException(error))
return
}
if !self.takesPromise {
promise.resolve(returnedValue)
}
}
}
// MARK: - JavaScriptObjectBuilder
func build(appContext: AppContext) throws -> JavaScriptObject {
// It seems to be safe to capture a strong reference to `self` here. This is needed for detached functions, that are not part of the module definition.
// Module definitions are held in memory anyway, but detached definitions (returned by other functions) are not, so we need to capture them here.
// It will be deallocated when that JS host function is garbage-collected by the JS VM.
return try appContext.runtime.createAsyncFunction(name, argsCount: argumentsCount) { [self] this, args, resolve, reject in
self.call(by: this, withArguments: args, appContext: appContext) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, nil)
case .success(let value):
resolve(value)
}
}
}
}
// MARK: - AnyAsyncFunctionDefinition
public func runOnQueue(_ queue: DispatchQueue?) -> Self {
self.queue = queue
return self
}
}
// MARK: - Exceptions
internal final class NativeFunctionUnavailableException: GenericException<String> {
override var reason: String {
return "Native function '\(param)' is no longer available in memory"
}
}

View File

@@ -0,0 +1,112 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Represents a concurrent function that can only be called asynchronously, thus its JavaScript equivalent returns a Promise.
As opposed to `AsyncFunctionDefinition`, it can leverage the new Swift's concurrency model and take the async/await closure.
*/
public final class ConcurrentFunctionDefinition<Args, FirstArgType, ReturnType>: AnyFunctionDefinition {
typealias ClosureType = (Args) async throws -> ReturnType
let body: ClosureType
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
_ body: @escaping ClosureType
) {
self.name = name
self.body = body
self.dynamicArgumentTypes = dynamicArgumentTypes
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0)
}
var takesOwner: Bool = false
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> Void) {
var arguments: [Any]
do {
try validateArgumentsNumber(function: self, received: args.count)
arguments = concat(
arguments: args,
withOwner: owner,
withPromise: nil,
forFunction: self,
appContext: appContext
)
// All `JavaScriptValue` args must be preliminarly converted on the JS thread, before we jump to the function's queue.
arguments = try cast(jsValues: arguments, forFunction: self, appContext: appContext)
} catch let error as Exception {
callback(.failure(error))
return
} catch {
callback(.failure(UnexpectedException(error)))
return
}
// Switch from the synchronous context to asynchronous
Task { [arguments] in
let result: Result<Any, Exception>
do {
// Convert arguments to the types desired by the function.
let finalArguments = try cast(arguments: arguments, forFunction: self, appContext: appContext)
// TODO: Right now we force cast the tuple in all types of functions, but we should throw another exception here.
// swiftlint:disable force_cast
let argumentsTuple = try Conversions.toTuple(finalArguments) as! Args
let returnValue = try await body(argumentsTuple)
result = .success(returnValue)
} catch let error as Exception {
result = .failure(FunctionCallException(name).causedBy(error))
} catch {
result = .failure(UnexpectedException(error))
}
// Go back to the JS thread to execute the callback
appContext.executeOnJavaScriptThread {
callback(result)
}
}
}
// MARK: - JavaScriptObjectBuilder
func build(appContext: AppContext) throws -> JavaScriptObject {
return try appContext.runtime.createAsyncFunction(name, argsCount: argumentsCount) {
[weak appContext, weak self, name] this, args, resolve, reject in
guard let appContext else {
let exception = Exceptions.AppContextLost()
return reject(exception.code, exception.description, nil)
}
guard let self else {
let exception = NativeFunctionUnavailableException(name)
return reject(exception.code, exception.description, nil)
}
self.call(by: this, withArguments: args, appContext: appContext) { result in
switch result {
case .failure(let error):
reject(error.code, error.description, nil)
case .success(let value):
// Convert some results to primitive types (e.g. records) or JS values (e.g. shared objects)
let convertedResult = Conversions.convertFunctionResult(value, appContext: appContext, dynamicType: ~ReturnType.self)
resolve(convertedResult)
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Type-erased protocol for synchronous functions.
*/
internal protocol AnySyncFunctionDefinition: AnyFunctionDefinition {
/**
Calls the function synchronously with given arguments.
- Parameters:
- owner: An object that calls this function. If the `takesOwner` property is true
and type of the first argument matches the owner type, it's being passed as the argument.
- args: An array of arguments to pass to the function. The arguments must be of the same type as in the underlying closure.
- appContext: An app context where the function is executed.
- Returns: A value returned by the called function when succeeded or an error when it failed.
*/
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext) throws -> Any
}
/**
Represents a function that can only be called synchronously.
*/
public final class SyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnySyncFunctionDefinition {
typealias ClosureType = (Args) throws -> ReturnType
/**
The underlying closure to run when the function is called.
*/
let body: ClosureType
init(
_ name: String,
firstArgType: FirstArgType.Type,
dynamicArgumentTypes: [AnyDynamicType],
_ body: @escaping ClosureType
) {
self.name = name
self.dynamicArgumentTypes = dynamicArgumentTypes
self.body = body
}
// MARK: - AnyFunction
let name: String
let dynamicArgumentTypes: [AnyDynamicType]
var argumentsCount: Int {
return dynamicArgumentTypes.count - (takesOwner ? 1 : 0)
}
var takesOwner: Bool = false
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext, callback: @escaping (FunctionCallResult) -> ()) {
do {
let result = try call(by: owner, withArguments: args, appContext: appContext)
callback(.success(Conversions.convertFunctionResult(result)))
} catch let error as Exception {
callback(.failure(error))
} catch {
callback(.failure(UnexpectedException(error)))
}
}
// MARK: - AnySyncFunctionDefinition
func call(by owner: AnyObject?, withArguments args: [Any], appContext: AppContext) throws -> Any {
do {
try validateArgumentsNumber(function: self, received: args.count)
var arguments = concat(
arguments: args,
withOwner: owner,
withPromise: nil,
forFunction: self,
appContext: appContext
)
// Convert JS values to non-JS native types.
arguments = try cast(jsValues: arguments, forFunction: self, appContext: appContext)
// Convert arguments to the types desired by the function.
arguments = try cast(arguments: arguments, forFunction: self, appContext: appContext)
let argumentsTuple = try Conversions.toTuple(arguments) as! Args
return try body(argumentsTuple)
} catch let error as Exception {
throw FunctionCallException(name).causedBy(error)
} catch {
throw UnexpectedException(error)
}
}
// MARK: - JavaScriptObjectBuilder
func build(appContext: AppContext) throws -> JavaScriptObject {
// We intentionally capture a strong reference to `self`, otherwise the "detached" objects would
// immediately lose the reference to the definition and thus the underlying native function.
// It may potentially cause memory leaks, but at the time of writing this comment,
// the native definition instance deallocates correctly when the JS VM triggers the garbage collector.
return try appContext.runtime.createSyncFunction(name, argsCount: argumentsCount) { [weak appContext, self] this, args in
guard let appContext else {
throw Exceptions.AppContextLost()
}
let result = try self.call(by: this, withArguments: args, appContext: appContext)
return Conversions.convertFunctionResult(result, appContext: appContext, dynamicType: ~ReturnType.self)
}
}
}

View File

@@ -0,0 +1,68 @@
// Copyright 2023-present 650 Industries. All rights reserved.
/**
Represents a JavaScript function that can be called by the native code and that must return the given generic `ReturnType`.
*/
public final class JavaScriptFunction<ReturnType>: AnyArgument, AnyJavaScriptValue {
/**
Raw representation of the JavaScript function that doesn't impose any restrictions on the returned type.
*/
private let rawFunction: RawJavaScriptFunction
/**
Weak reference to the app context that is necessary to convert some arguments associated with the context (e.g. shared objects).
*/
private weak var appContext: AppContext?
init(rawFunction: RawJavaScriptFunction, appContext: AppContext) {
self.rawFunction = rawFunction
self.appContext = appContext
}
// MARK: - Calling
/**
Calls the function with the given `this` object and arguments.
*/
public func call(_ arguments: Any..., usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
return try call(withArguments: arguments, asConstructor: false, usingThis: this)
}
/**
Calls the function as a constructor with the given arguments. It's like calling a function with the `new` keyword.
*/
public func callAsConstructor(_ arguments: Any...) throws -> ReturnType {
return try call(withArguments: arguments, asConstructor: true, usingThis: nil)
}
/**
Universal function that calls the function with given arguments, this object and whether to call it as a constructor.
*/
private func call(withArguments arguments: [Any] = [], asConstructor: Bool = false, usingThis this: JavaScriptObject? = nil) throws -> ReturnType {
guard let appContext else {
throw AppContextLostException()
}
let value = rawFunction.call(withArguments: arguments, thisObject: this, asConstructor: false)
let dynamicType = ~ReturnType.self
guard let result = try dynamicType.cast(jsValue: value, appContext: appContext) as? ReturnType else {
throw UnexpectedReturnType(dynamicType.description)
}
return result
}
// MARK: - AnyJavaScriptValue
internal static func convert(from value: JavaScriptValue, appContext: AppContext) throws -> Self {
guard value.kind == .function else {
throw Conversions.ConvertingException<JavaScriptFunction<ReturnType>>(value)
}
return Self(rawFunction: value.getFunction(), appContext: appContext)
}
}
private final class UnexpectedReturnType: GenericException<String> {
override var reason: String {
return "The function returned a value that cannot be converted to \(param)"
}
}

View File

@@ -0,0 +1,139 @@
// Copyright 2022-present 650 Industries. All rights reserved.
// MARK: - Arguments
/**
Tries to cast a given value to the type that is wrapped by the dynamic type.
- Parameters:
- value: A value to be cast. If it's a ``JavaScriptValue``, it's first unpacked to the raw value.
- type: Something that implements ``AnyDynamicType`` and knows how to cast the argument.
- Returns: A new value converted according to the dynamic type.
- Throws: Rethrows various exceptions that could be thrown by the dynamic types.
*/
internal func cast(_ value: Any, toType type: AnyDynamicType, appContext: AppContext) throws -> Any {
if let dynamicJSType = type as? DynamicJavaScriptType, dynamicJSType.equals(~JavaScriptValue.self) {
return value
}
if !(type is DynamicTypedArrayType), let value = value as? JavaScriptValue {
return try type.cast(value.getRaw(), appContext: appContext)
}
return try type.cast(value, appContext: appContext)
}
/**
Tries to cast the given arguments to the types expected by the function.
- Parameters:
- arguments: An array of arguments to be cast.
- function: A function for which to cast the arguments.
- appContext: A context of the app.
- Returns: An array of arguments after casting. Its size is the same as the input arrays.
- Throws: `InvalidArgsNumberException` when the number of arguments is not equal to the actual number
of function's arguments (without an owner and promise). Rethrows exceptions thrown by `cast(_:toType:)`.
*/
internal func cast(arguments: [Any], forFunction function: AnyFunctionDefinition, appContext: AppContext) throws -> [Any] {
return try arguments.enumerated().map { index, argument in
let argumentType = function.dynamicArgumentTypes[index]
do {
return try cast(argument, toType: argumentType, appContext: appContext)
} catch {
throw ArgumentCastException((index: index, type: argumentType)).causedBy(error)
}
}
}
/**
Casts an array of JavaScript values to non-JavaScript types.
*/
internal func cast(jsValues: [Any], forFunction function: AnyFunctionDefinition, appContext: AppContext) throws -> [Any] {
// TODO: Replace `[Any]` with `[JavaScriptValue]` once we make sure only JS values are passed here
return try jsValues.enumerated().map { index, jsValue in
let type = function.dynamicArgumentTypes[index]
do {
// Temporarily some values might already be cast to primitive types, so make sure we cast only `JavaScriptValue` and leave the others as they are.
if let jsValue = jsValue as? JavaScriptValue {
return try type.cast(jsValue: jsValue, appContext: appContext)
} else {
return jsValue
}
} catch {
throw ArgumentCastException((index: index, type: type)).causedBy(error)
}
}
}
/**
Validates whether the number of received arguments is enough to call the given function.
Throws `InvalidArgsNumberException` otherwise.
*/
internal func validateArgumentsNumber(function: AnyFunctionDefinition, received: Int) throws {
let argumentsCount = function.argumentsCount
let requiredArgumentsCount = function.requiredArgumentsCount
if received < requiredArgumentsCount || received > argumentsCount {
throw InvalidArgsNumberException((
received: received,
expected: argumentsCount,
required: requiredArgumentsCount
))
}
}
/**
Ensures the provided array of arguments matches the number of arguments expected by the function.
- If the function takes the owner, it's added to the beginning.
- If the array is still too small, missing arguments are very likely to be optional so it puts `nil` in their place.
*/
internal func concat(
arguments: [Any],
withOwner owner: AnyObject?,
withPromise promise: Promise?,
forFunction function: AnyFunctionDefinition,
appContext: AppContext
) -> [Any] {
var result = arguments
if function.takesOwner {
result = [owner] + arguments
}
if arguments.count < function.argumentsCount {
result += Array(repeating: Any?.none as Any, count: function.argumentsCount - arguments.count)
}
// Add promise to the array of arguments if necessary.
if let promise {
result += [promise]
}
return result
}
// MARK: - Exceptions
internal class InvalidArgsNumberException: GenericException<(received: Int, expected: Int, required: Int)> {
override var reason: String {
if param.required < param.expected {
return "Received \(param.received) arguments, but \(param.expected) was expected and at least \(param.required) is required"
} else {
return "Received \(param.received) arguments, but \(param.expected) was expected"
}
}
}
internal class ArgumentCastException: GenericException<(index: Int, type: AnyDynamicType)> {
override var reason: String {
"The \(formatOrdinalNumber(param.index + 1)) argument cannot be cast to type \(param.type.description)"
}
func formatOrdinalNumber(_ number: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
formatter.locale = Locale(identifier: "en_US")
return formatter.string(from: NSNumber(value: number)) ?? ""
}
}
private class ModuleUnavailableException: GenericException<String> {
override var reason: String {
"Module '\(param)' is no longer available"
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import os.log
public func createOSLogHandler(category: String) -> LogHandler {
if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
return OSLogHandler(category: category)
}
return PrintLogHandler()
}
public func createPersistentFileLogHandler(category: String) -> LogHandler {
return PersistentFileLogHandler(category: category)
}
/**
The protocol that needs to be implemented by log handlers.
*/
public protocol LogHandler {
func log(type: LogType, _ message: String)
}
/**
The log handler that uses the new `os.Logger` API.
*/
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
internal class OSLogHandler: LogHandler {
private let osLogger: os.Logger
required init(category: String) {
osLogger = os.Logger(subsystem: Logger.EXPO_MODULES_LOG_SUBSYSTEM, category: category)
}
func log(type: LogType, _ message: String) {
osLogger.log(level: type.toOSLogType(), "\(message)")
}
}
/**
Simple log handler that forwards all logs to `print` function.
*/
internal class PrintLogHandler: LogHandler {
func log(type: LogType, _ message: String) {
print(message)
}
}
/**
Log handler that writes all logs to a file using PersistentFileLog
*/
internal class PersistentFileLogHandler: LogHandler {
private let persistentLog: PersistentFileLog
required init(category: String) {
self.persistentLog = PersistentFileLog(category: category)
}
func log(type: LogType, _ message: String) {
persistentLog.appendEntry(entry: message)
}
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import os.log
/**
An enum with available log types.
*/
public enum LogType: Int {
case trace = 0
case timer = 1
case stacktrace = 2
case debug = 3
case info = 4
case warn = 5
case error = 6
case fatal = 7
/**
The string that is used to prefix the messages of this log type.
Logs in Xcode and Console apps are always with the white text,
so we use colored circle emojis to distinguish different types of logs.
*/
var prefix: String {
switch self {
case .trace:
return "⚪️"
case .timer:
return "🟤"
case .stacktrace:
return "🟣"
case .debug:
return "🔵"
case .info:
return "🟢"
case .warn:
return "🟡"
case .error:
return "🟠"
case .fatal:
return "🔴"
}
}
/**
Maps the log types to the log types used by the `os.log` logger.
*/
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public func toOSLogType() -> OSLogType {
switch self {
case .trace, .timer, .stacktrace, .debug:
return .debug
case .info:
return .info
case .warn:
return .default
case .error:
return .error
case .fatal:
return .fault
}
}
}

View File

@@ -0,0 +1,158 @@
// Copyright 2021-present 650 Industries. All rights reserved.
import Dispatch
public let log = Logger(logHandlers: [createOSLogHandler(category: Logger.EXPO_LOG_CATEGORY)])
public typealias LoggerTimerFormatterBlock = (_ duration: Double) -> String
/**
Native iOS logging class for Expo, with options to direct logs
to different destinations.
*/
public class Logger {
public static let EXPO_MODULES_LOG_SUBSYSTEM = "dev.expo.modules"
public static let EXPO_LOG_CATEGORY = "expo"
#if DEBUG || EXPO_CONFIGURATION_DEBUG
private var minLevel: LogType = .trace
#else
private var minLevel: LogType = .info
#endif
private let logHandlers: [LogHandler]
public required init(logHandlers: [LogHandler]) {
self.logHandlers = logHandlers
}
// MARK: - Public logging functions
/**
The most verbose log level that captures all the details about the behavior of the implementation.
It is mostly diagnostic and is more granular and finer than `debug` log level.
These logs should not be committed to the repository and are ignored in the release builds.
*/
public func trace(_ items: Any...) {
log(type: .trace, items)
}
/**
Used to log diagnostically helpful information. As opposed to `trace`,
it is acceptable to commit these logs to the repository. Ignored in the release builds.
*/
public func debug(_ items: Any...) {
log(type: .debug, items)
}
/**
For information that should be logged under normal conditions such as successful initialization
and notable events that are not considered an error but might be useful for debugging purposes in the release builds.
*/
public func info(_ items: Any...) {
log(type: .info, items)
}
/**
Used to log an unwanted state that has not much impact on the process so it can be continued, but could potentially become an error.
*/
public func warn(_ items: Any...) {
log(type: .warn, items)
}
/**
Logs unwanted state that has an impact on the currently running process, but the entire app can continue to run.
*/
public func error(_ items: Any...) {
log(type: .error, items)
}
/**
Logs critical error due to which the entire app cannot continue to run.
*/
public func fatal(_ items: Any...) {
log(type: .fatal, items)
}
/**
Logs the stack of symbols on the current thread.
*/
public func stacktrace(type: LogType = .stacktrace, file: String = #fileID, line: UInt = #line) {
guard type.rawValue >= minLevel.rawValue else {
return
}
let queueName = OperationQueue.current?.underlyingQueue?.label ?? "<unknown>"
// Get the call stack symbols without the first symbol as it points right here.
let symbols = Thread.callStackSymbols.dropFirst()
log(type: type, "The stacktrace from '\(file):\(line)' on queue '\(queueName)':")
symbols.forEach { symbol in
let formattedSymbol = reformatStackSymbol(symbol)
log(type: type, "\(formattedSymbol)")
}
}
/**
Allows the logger instance to be called as a function. The same as `logger.debug(...)`.
*/
public func callAsFunction(_ items: Any...) {
log(type: .debug, items)
}
// MARK: - Timers
/**
Starts a timer that can be used to compute the duration of an operation. Upon calling
`stop` on the returned object, a timer entry will be logged.
*/
public func startTimer(_ formatterBlock: @escaping LoggerTimerFormatterBlock) -> LoggerTimer {
let startTime = DispatchTime.now()
return LoggerTimer {
let endTime = DispatchTime.now()
let diff = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000
self.log(type: .timer, formatterBlock(diff))
}
}
// MARK: - Private logging functions
private func log(type: LogType = .trace, _ items: [Any]) {
guard type.rawValue >= minLevel.rawValue else {
return
}
let messages = items
.map { describe(value: $0) }
.joined(separator: " ")
.split(whereSeparator: \.isNewline)
.map { "\(type.prefix) \($0)" }
logHandlers.forEach { handler in
messages.forEach { message in
handler.log(type: type, message)
}
}
}
private func log(type: LogType = .trace, _ items: Any...) {
log(type: type, items)
}
}
private func reformatStackSymbol(_ symbol: String) -> String {
return symbol.replacingOccurrences(of: #"^\d+\s+"#, with: "", options: .regularExpression)
}
private func describe(value: Any) -> String {
if let value = value as? String {
return value
}
if let value = value as? CustomDebugStringConvertible {
return value.debugDescription
}
if let value = value as? CustomStringConvertible {
return value.description
}
return String(describing: value)
}

View File

@@ -0,0 +1,22 @@
// Copyright 2021-present 650 Industries. All rights reserved.
import Foundation
typealias LoggerTimerStopBlock = () -> Void
/**
An instance of a timer.
*/
public class LoggerTimer {
private let stopBlock: LoggerTimerStopBlock
internal required init(stopBlock: @escaping LoggerTimerStopBlock) {
self.stopBlock = stopBlock
}
/**
End the timer and log a timer entry.
*/
public func stop() {
self.stopBlock()
}
}

View File

@@ -0,0 +1,152 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import Foundation
public typealias PersistentFileLogFilter = (String) -> Bool
public typealias PersistentFileLogCompletionHandler = (Error?) -> Void
/**
* A thread-safe class for reading and writing line-separated strings to a flat file.
* The main use case is for logging specific errors or events, and ensuring that the
* logs persist across application crashes and restarts (for example, OSLogReader can
* only read system logs for the current process, and cannot access anything logged
* before the current process started).
*
* All write access to the file goes through asynchronous public methods managed by a
* serial dispatch queue.
*
* The dispatch queue is global, to ensure that multiple instances accessing the same
* file will have thread-safe access.
*
* The only operations supported are
* - Read the file (synchronous)
* - Append one or more entries to the file
* - Filter the file (only retain entries that pass the filter check)
* - Clear the file (remove all entries)
*
*/
public class PersistentFileLog {
private static let EXPO_MODULES_CORE_LOG_QUEUE_LABEL = "dev.expo.modules.core.logging"
private static let serialQueue = DispatchQueue(label: EXPO_MODULES_CORE_LOG_QUEUE_LABEL)
private let category: String
private let filePath: String
public init(category: String) {
self.category = category
let fileName = "\(PersistentFileLog.EXPO_MODULES_CORE_LOG_QUEUE_LABEL).\(category).txt"
// Execution aborts if no application support directory
self.filePath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName).path
}
/**
Read entries from log file
*/
public func readEntries() -> [String] {
if getFileSize() == 0 {
return []
}
return (try? self.readFileSync()) ?? []
}
/**
Append entry to the log file
Since logging may not require a result handler, the handler parameter is optional
If no error handler provided, print error to the console
*/
public func appendEntry(entry: String, _ completionHandler: PersistentFileLogCompletionHandler? = nil) {
PersistentFileLog.serialQueue.async {
self.ensureFileExists()
self.appendTextToFile(text: entry + "\n")
completionHandler?(nil)
}
}
/**
Filter existing entries and remove ones where filter(entry) == false
*/
public func purgeEntriesNotMatchingFilter(filter: @escaping PersistentFileLogFilter, _ completionHandler: @escaping PersistentFileLogCompletionHandler) {
PersistentFileLog.serialQueue.async {
self.ensureFileExists()
do {
let contents = try self.readFileSync()
let newcontents = contents.filter { entry in filter(entry) }
try self.writeFileSync(newcontents)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
/**
Clean up (remove) the log file
*/
public func clearEntries(_ completionHandler: @escaping PersistentFileLogCompletionHandler) {
PersistentFileLog.serialQueue.async {
do {
try self.deleteFileSync()
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
// MARK: - Private methods
private func ensureFileExists() {
if !FileManager.default.fileExists(atPath: filePath) {
FileManager.default.createFile(atPath: filePath, contents: nil)
}
}
private func getFileSize() -> Int {
// Gets the file size, or returns 0 if the file does not exist
do {
let attrs: [FileAttributeKey: Any?] = try FileManager.default.attributesOfItem(atPath: filePath)
return attrs[FileAttributeKey.size] as? Int ?? 0
} catch {
return 0
}
}
private func appendTextToFile(text: String) {
if let data = text.data(using: .utf8) {
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
}
}
private func readFileSync() throws -> [String] {
return try stringToList(String(contentsOfFile: filePath, encoding: .utf8))
}
private func writeFileSync(_ contents: [String]) throws {
if contents.isEmpty {
try deleteFileSync()
return
}
try contents.joined(separator: "\n").write(toFile: filePath, atomically: true, encoding: .utf8)
}
private func deleteFileSync() throws {
if FileManager.default.fileExists(atPath: filePath) {
try FileManager.default.removeItem(atPath: filePath)
}
}
private func stringToList(_ contents: String?) -> [String] {
// If null contents, or 0 length contents, return empty list
guard let contents = contents, !contents.isEmpty else {
return []
}
return contents
.components(separatedBy: "\n")
.filter {entryString in
!entryString.isEmpty
}
}
}

View File

@@ -0,0 +1,165 @@
import Dispatch
/**
Holds a reference to the module instance and caches its definition.
*/
public final class ModuleHolder {
/**
Instance of the module.
*/
private(set) var module: AnyModule
/**
A weak reference to the app context.
*/
private(set) weak var appContext: AppContext?
/**
JavaScript object that represents the module instance in the runtime.
*/
public internal(set) lazy var javaScriptObject: JavaScriptObject? = createJavaScriptModuleObject()
/**
Caches the definition of the module type.
*/
let definition: ModuleDefinition
/**
Returns `definition.name` if not empty, otherwise falls back to the module type name.
*/
var name: String {
return definition.name.isEmpty ? String(describing: type(of: module)) : definition.name
}
/**
Number of JavaScript listeners attached to the module.
*/
var listenersCount: Int = 0
init(appContext: AppContext, module: AnyModule) {
self.appContext = appContext
self.module = module
self.definition = module.definition()
post(event: .moduleCreate)
}
// MARK: Constants
/**
Merges all `constants` definitions into one dictionary.
*/
func getConstants() -> [String: Any?] {
return definition.getConstants()
}
// MARK: Calling functions
func call(function functionName: String, args: [Any], _ callback: @escaping (FunctionCallResult) -> () = { _ in }) {
guard let appContext else {
callback(.failure(Exceptions.AppContextLost()))
return
}
guard let function = definition.functions[functionName] else {
callback(.failure(FunctionNotFoundException((functionName: functionName, moduleName: self.name))))
return
}
function.call(by: self, withArguments: args, appContext: appContext, callback: callback)
}
@discardableResult
func callSync(function functionName: String, args: [Any]) -> Any? {
guard let appContext, let function = definition.functions[functionName] as? AnySyncFunctionDefinition else {
return nil
}
do {
let arguments = try cast(arguments: args, forFunction: function, appContext: appContext)
let result = try function.call(by: self, withArguments: arguments, appContext: appContext)
if let result = result as? SharedObject {
return appContext.sharedObjectRegistry.ensureSharedJavaScriptObject(runtime: try appContext.runtime, nativeObject: result)
}
return result
} catch {
return error
}
}
// MARK: JavaScript Module Object
/**
Creates the JavaScript object that will be used to communicate with the native module.
The object is prefilled with module's constants and functions.
JavaScript can access it through `global.expo.modules[moduleName]`.
- Note: The object will be `nil` when the runtime is unavailable (e.g. remote debugger is enabled).
*/
private func createJavaScriptModuleObject() -> JavaScriptObject? {
// It might be impossible to create any object at the moment (e.g. remote debugging, app context destroyed)
guard let appContext else {
return nil
}
do {
log.info("Creating JS object for module '\(name)'")
return try definition.build(appContext: appContext)
} catch {
log.error("Building the module object failed: \(error)")
return nil
}
}
// MARK: Listening to native events
func listeners(forEvent event: EventName) -> [EventListener] {
return definition.eventListeners.filter {
$0.name == event
}
}
func post(event: EventName) {
listeners(forEvent: event).forEach {
try? $0.call(module, nil)
}
}
func post<PayloadType>(event: EventName, payload: PayloadType?) {
listeners(forEvent: event).forEach {
try? $0.call(module, payload)
}
}
// MARK: JavaScript events
/**
Modifies module's listeners count and calls `onStartObserving` or `onStopObserving` accordingly.
*/
func modifyListenersCount(_ count: Int) {
guard let appContext else {
return
}
if count > 0 && listenersCount == 0 {
definition.functions["startObserving"]?.call(withArguments: [], appContext: appContext)
} else if count < 0 && listenersCount + count <= 0 {
definition.functions["stopObserving"]?.call(withArguments: [], appContext: appContext)
}
listenersCount = max(0, listenersCount + count)
}
// MARK: Deallocation
deinit {
post(event: .moduleDestroy)
}
// MARK: - Exceptions
internal class ModuleNotFoundException: GenericException<String> {
override var reason: String {
"Module '\(param)' not found"
}
}
internal class FunctionNotFoundException: GenericException<(functionName: String, moduleName: String)> {
override var reason: String {
"Function '\(param.functionName)' not found in module '\(param.moduleName)'"
}
}
}

View File

@@ -0,0 +1,111 @@
public final class ModuleRegistry: Sequence {
public typealias Element = ModuleHolder
private weak var appContext: AppContext?
private var registry: [String: ModuleHolder] = [:]
private var overrideDisallowModules = Set<String>()
init(appContext: AppContext) {
self.appContext = appContext
}
/**
Registers an instance of module holder.
*/
internal func register(holder: ModuleHolder, preventModuleOverriding: Bool = false) {
log.info("Registering module '\(holder.name)'")
// if overriding is disallowed for this module and the module already registered, don't re-register
if overrideDisallowModules.contains(holder.name) && registry[holder.name] != nil {
log.info("Not re-registering module '\(holder.name)' since a previous registration specified preventModuleOverriding: true")
return
}
if preventModuleOverriding {
overrideDisallowModules.insert(holder.name)
}
registry[holder.name] = holder
}
/**
Registers an instance of the module.
*/
public func register(module: AnyModule, preventModuleOverriding: Bool = false) {
guard let appContext else {
log.error("Unable to register a module '\(module)', the app context is unavailable")
return
}
register(holder: ModuleHolder(appContext: appContext, module: module), preventModuleOverriding: preventModuleOverriding)
}
/**
Registers a module by its type.
*/
public func register(moduleType: AnyModule.Type, preventModuleOverriding: Bool = false) {
guard let appContext else {
log.error("Unable to register a module '\(moduleType)', the app context is unavailable")
return
}
register(module: moduleType.init(appContext: appContext), preventModuleOverriding: preventModuleOverriding)
}
/**
Registers modules exported by given modules provider.
*/
public func register(fromProvider provider: ModulesProviderProtocol) {
provider.getModuleClasses().forEach { moduleType in
register(moduleType: moduleType)
}
}
/**
Unregisters given module from the registry.
*/
public func unregister(module: AnyModule) {
if let index = registry.firstIndex(where: { $1.module === module }) {
registry.remove(at: index)
}
}
public func unregister(moduleName: String) {
if registry[moduleName] != nil {
log.info("Unregistering module '\(moduleName)'")
registry[moduleName] = nil
}
}
public func has(moduleWithName moduleName: String) -> Bool {
return registry[moduleName] != nil
}
public func get(moduleHolderForName moduleName: String) -> ModuleHolder? {
return registry[moduleName]
}
public func get(moduleWithName moduleName: String) -> AnyModule? {
return registry[moduleName]?.module
}
public func getModuleNames() -> [String] {
return Array(registry.keys)
}
public func makeIterator() -> IndexingIterator<[ModuleHolder]> {
return registry.map({ $1 }).makeIterator()
}
internal func post(event: EventName) {
log.info("Posting '\(event)' event to registered modules")
forEach { holder in
holder.post(event: event)
}
}
internal func post<PayloadType>(event: EventName, payload: PayloadType? = nil) {
log.info("Posting '\(event)' event to registered modules")
forEach { holder in
holder.post(event: event, payload: payload)
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import React
// The core module that describes the `global.expo` object.
internal final class CoreModule: Module {
internal func definition() -> ModuleDefinition {
// Expose some common classes and maybe even the `modules` host object in the future.
Function("uuidv4") { () -> String in
return UUID().uuidString.lowercased()
}
Function("uuidv5") { (name: String, namespace: String) -> String in
guard let namespaceUuid = UUID(uuidString: namespace) else {
throw InvalidNamespaceException(namespace)
}
return uuidv5(name: name, namespace: namespaceUuid).uuidString.lowercased()
}
Function("getViewConfig") { (viewName: String) -> [String: Any]? in
var validAttributes: [String: Any] = [:]
var directEventTypes: [String: Any] = [:]
let moduleHolder = appContext?.moduleRegistry.get(moduleHolderForName: viewName)
guard let viewDefinition = moduleHolder?.definition.view else {
return nil
}
for prop in viewDefinition.props {
validAttributes[prop.name] = true
}
for eventName in viewDefinition.eventNames {
guard let normalizedEventName = RCTNormalizeInputEventName(eventName) else {
continue
}
directEventTypes[normalizedEventName] = [
"registrationName": eventName
]
}
return [
"validAttributes": validAttributes,
"directEventTypes": directEventTypes
]
}
AsyncFunction("reloadAppAsync") { (reason: String) in
DispatchQueue.main.async {
RCTTriggerReloadCommandListeners(reason)
}
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
`BaseModule` is just a stub class that fulfils `AnyModule` protocol requirement of public default initializer,
but doesn't implement that protocol explicitly, though  it would have to provide a definition which would require
other modules to use `override` keyword in the function returning the definition.
*/
open class BaseModule {
public private(set) weak var appContext: AppContext?
@available(*, unavailable, message: "Module's initializer cannot be overriden, use \"onCreate\" definition component instead.")
public init() {}
required public init(appContext: AppContext) {
self.appContext = appContext
}
/**
Sends an event with given name and body to JavaScript.
*/
public func sendEvent(_ eventName: String, _ body: [String: Any?] = [:]) {
appContext?.eventEmitter?.sendEvent(withName: eventName, body: body)
}
}
/**
An alias for `AnyModule` extended by the `BaseModule` class that provides public default initializer.
*/
public typealias Module = AnyModule & BaseModule

View File

@@ -0,0 +1,111 @@
/**
The definition of the module. It is used to define some parameters
of the module and what it exports to the JavaScript world.
See `ModuleDefinitionBuilder` for more details on how to create it.
*/
public final class ModuleDefinition: ObjectDefinition {
/**
The module's type associated with the definition. It's used to create the module instance.
*/
var type: AnyModule.Type?
/**
Name of the defined module. Falls back to the type name if not provided in the definition.
*/
var name: String
let eventListeners: [EventListener]
let view: AnyViewDefinition?
/**
Names of the events that the module can send to JavaScript.
*/
let eventNames: [String]
let eventObservers: [AnyEventObservingDefinition]
/**
Initializer that is called by the `ModuleDefinitionBuilder` results builder.
*/
override init(definitions: [AnyDefinition]) {
self.name = definitions
.compactMap { $0 as? ModuleNameDefinition }
.last?
.name ?? ""
self.eventListeners = definitions.compactMap { $0 as? EventListener }
self.view = definitions
.compactMap { $0 as? AnyViewDefinition }
.last
self.eventNames = Array(
definitions
.compactMap { ($0 as? EventsDefinition)?.names }
.joined()
)
self.eventObservers = definitions
.compactMap { $0 as? AnyEventObservingDefinition }
super.init(definitions: definitions)
}
/**
Sets the module type that the definition is associated with. We can't pass this in the initializer
as it's called by the results builder that doesn't have access to the type.
*/
func withType(_ type: AnyModule.Type) -> Self {
self.type = type
// Use the type name if the name is not in the definition or was defined empty.
if name.isEmpty {
name = String(describing: type)
}
return self
}
public override func build(appContext: AppContext) throws -> JavaScriptObject {
// Create an instance of `global.expo.NativeModule`
let object = JSIUtils.createNativeModuleObject(try appContext.runtime)
try super.decorate(object: object, appContext: appContext)
if let viewDefinition = view {
let reactComponentPrototype = try viewDefinition.createReactComponentPrototype(appContext: appContext)
object.setProperty("ViewPrototype", value: reactComponentPrototype)
}
if !eventObservers.isEmpty {
try EventObservingDecorator(definitions: eventObservers)
.decorate(object: object, appContext: appContext)
}
// Give the module object a name. It's used for compatibility reasons, see `EventEmitter.ts`.
object.defineProperty("__expo_module_name__", value: name, options: [])
return object
}
}
/**
Module's name definition. Returned by `name()` in module's definition.
*/
internal struct ModuleNameDefinition: AnyDefinition {
let name: String
}
/**
A definition for module's constants. Returned by `constants(() -> SomeType)` in module's definition.
*/
internal struct ConstantsDefinition: AnyDefinition {
let body: () -> [String: Any?]
}
/**
A definition for module's events that can be sent to JavaScript.
*/
public struct EventsDefinition: AnyDefinition {
let names: [String]
}

View File

@@ -0,0 +1,10 @@
/**
A function builder that provides DSL-like syntax. Thanks to this, the function doesn't need to explicitly return an array,
but can just return multiple definitions one after another. This works similarly to SwiftUI's `body` block.
*/
@resultBuilder
public struct ModuleDefinitionBuilder {
public static func buildBlock(_ definitions: AnyDefinition...) -> ModuleDefinition {
return ModuleDefinition(definitions: definitions)
}
}

View File

@@ -0,0 +1,40 @@
import Foundation
/**
Swift protocol defining the requirements for modules providers.
*/
public protocol ModulesProviderProtocol {
func getModuleClasses() -> [AnyModule.Type]
/**
Returns an array of classes that hooks into `ExpoAppDelegate` to receive app delegate events.
*/
func getAppDelegateSubscribers() -> [ExpoAppDelegateSubscriber.Type]
typealias ExpoReactDelegateHandlerTupleType = (packageName: String, handler: ExpoReactDelegateHandler.Type)
/**
Returns an array of `ExpoReactDelegateHandlerTupleType` for `ReactDelegate` to hook React instance creation.
*/
func getReactDelegateHandlers() -> [ExpoReactDelegateHandlerTupleType]
}
/**
The default implementation for modules provider.
The proper implementation is generated by autolinking as part of `pod install` command.
*/
@objc
open class ModulesProvider: NSObject, ModulesProviderProtocol {
public override required init() {}
open func getModuleClasses() -> [AnyModule.Type] {
return []
}
open func getAppDelegateSubscribers() -> [ExpoAppDelegateSubscriber.Type] {
return []
}
open func getReactDelegateHandlers() -> [ExpoReactDelegateHandlerTupleType] {
return []
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A type that can decorate a `JavaScriptObject` with some properties.
*/
internal protocol JavaScriptObjectDecorator {
/**
Decorates an existing `JavaScriptObject`.
*/
func decorate(object: JavaScriptObject, appContext: AppContext) throws
}
/**
A type that can build and decorate a `JavaScriptObject` based on its attributes.
*/
internal protocol JavaScriptObjectBuilder: JavaScriptObjectDecorator {
/**
Creates a decorated `JavaScriptObject` in the given app context.
*/
func build(appContext: AppContext) throws -> JavaScriptObject
}
/**
Provides the default behavior of `JavaScriptObjectBuilder`.
The `build(appContext:)` creates a plain object and uses `decorate(object:appContext:)` for decoration.
*/
extension JavaScriptObjectBuilder {
func build(appContext: AppContext) throws -> JavaScriptObject {
let object = try appContext.runtime.createObject()
try decorate(object: object, appContext: appContext)
return object
}
func decorate(object: JavaScriptObject, appContext: AppContext) throws {
// no-op by default
}
}

View File

@@ -0,0 +1,105 @@
// Copyright 2021-present 650 Industries. All rights reserved.
/**
Base class for other definitions representing an object, such as `ModuleDefinition`.
*/
public class ObjectDefinition: AnyDefinition, JavaScriptObjectBuilder {
/**
A dictionary of functions defined by the object.
*/
let functions: [String: AnyFunctionDefinition]
/**
An array of constants definitions.
*/
let constants: [ConstantsDefinition]
/**
A map of dynamic properties defined by the object.
*/
let properties: [String: AnyPropertyDefinition]
/**
A map of classes defined within the object.
*/
let classes: [String: ClassDefinition]
/**
Default initializer receiving children definitions from the result builder.
*/
init(definitions: [AnyDefinition]) {
self.functions = definitions
.compactMap { $0 as? AnyFunctionDefinition }
.reduce(into: [String: AnyFunctionDefinition]()) { dict, function in
dict[function.name] = function
}
self.constants = definitions
.compactMap { $0 as? ConstantsDefinition }
self.properties = definitions
.compactMap { $0 as? AnyPropertyDefinition }
.reduce(into: [String: AnyPropertyDefinition]()) { dict, property in
dict[property.name] = property
}
self.classes = definitions
.compactMap { $0 as? ClassDefinition }
.reduce(into: [String: ClassDefinition]()) { dict, klass in
dict[klass.name] = klass
}
}
/**
Merges all `constants` definitions into one dictionary.
*/
func getConstants() -> [String: Any?] {
return constants.reduce(into: [String: Any?]()) { dict, definition in
dict.merge(definition.body()) { $1 }
}
}
// MARK: - JavaScriptObjectBuilder
public func build(appContext: AppContext) throws -> JavaScriptObject {
let object = try appContext.runtime.createObject()
try decorate(object: object, appContext: appContext)
return object
}
public func decorate(object: JavaScriptObject, appContext: AppContext) throws {
let runtime = try appContext.runtime
decorateWithConstants(object: object)
try decorateWithFunctions(object: object, appContext: appContext)
try decorateWithProperties(object: object, appContext: appContext)
try decorateWithClasses(object: object, appContext: appContext)
}
// MARK: - Internals
internal func decorateWithConstants(object: JavaScriptObject) {
for (key, value) in getConstants() {
object.setProperty(key, value: value)
}
}
internal func decorateWithFunctions(object: JavaScriptObject, appContext: AppContext) throws {
for fn in functions.values {
object.setProperty(fn.name, value: try fn.build(appContext: appContext))
}
}
internal func decorateWithProperties(object: JavaScriptObject, appContext: AppContext) throws {
for property in properties.values {
let descriptor = try property.buildDescriptor(appContext: appContext)
object.defineProperty(property.name, descriptor: descriptor)
}
}
internal func decorateWithClasses(object: JavaScriptObject, appContext: AppContext) throws {
for klass in classes.values {
object.setProperty(klass.name, value: try klass.build(appContext: appContext))
}
}
}

View File

@@ -0,0 +1,193 @@
// Copyright 2022-present 650 Industries. All rights reserved.
protocol AnyPropertyDefinition {
/**
Name of the property.
*/
var name: String { get }
/**
Creates the JavaScript object representing the property descriptor.
*/
func buildDescriptor(appContext: AppContext) throws -> JavaScriptObject
}
public final class PropertyDefinition<OwnerType>: AnyDefinition, AnyPropertyDefinition {
/**
Name of the property.
*/
let name: String
/**
Synchronous function that is called when the property is being accessed.
*/
var getter: AnySyncFunctionDefinition?
/**
Synchronous function that is called when the property is being set.
*/
var setter: AnySyncFunctionDefinition?
/**
Initializes an unowned PropertyDefinition without getter and setter functions.
*/
init(name: String) {
self.name = name
}
/**
Initializes an unowned PropertyDefinition with a getter without arguments.
*/
init<ReturnType>(name: String, getter: @escaping () -> ReturnType) {
self.name = name
// Set the getter right away
self.get(getter)
}
/**
Initializes an owned PropertyDefinition with a getter that takes the owner as its first argument.
*/
init<ReturnType>(name: String, getter: @escaping (_ this: OwnerType) -> ReturnType) {
self.name = name
// Set the getter right away
self.get(getter)
}
// MARK: - Modifiers
/**
Modifier that sets property getter that has no arguments (the owner is not used).
*/
@discardableResult
public func get<ReturnType>(_ getter: @escaping () -> ReturnType) -> Self {
self.getter = SyncFunctionDefinition(
"get",
firstArgType: Void.self,
dynamicArgumentTypes: [],
getter
)
return self
}
/**
Modifier that sets property getter that receives the owner as an argument.
The owner is an object on which the function is called, like `this` in JavaScript.
*/
@discardableResult
public func get<ReturnType>(_ getter: @escaping (_ this: OwnerType) -> ReturnType) -> Self {
self.getter = SyncFunctionDefinition(
"get",
firstArgType: OwnerType.self,
dynamicArgumentTypes: [~OwnerType.self],
getter
)
self.getter?.takesOwner = true
return self
}
/**
Modifier that sets property setter that receives only the new value as an argument.
*/
@discardableResult
public func set<ValueType>(_ setter: @escaping (_ newValue: ValueType) -> Void) -> Self {
self.setter = SyncFunctionDefinition(
"set",
firstArgType: ValueType.self,
dynamicArgumentTypes: [~ValueType.self],
setter
)
return self
}
/**
Modifier that sets property setter that receives the owner and the new value as arguments.
The owner is an object on which the function is called, like `this` in JavaScript.
*/
@discardableResult
public func set<ValueType>(_ setter: @escaping (_ this: OwnerType, _ newValue: ValueType) -> Void) -> Self {
self.setter = SyncFunctionDefinition(
"set",
firstArgType: OwnerType.self,
dynamicArgumentTypes: [~OwnerType.self, ~ValueType.self],
setter
)
self.setter?.takesOwner = true
return self
}
// MARK: - Internals
internal func getValue<ValueType>(owner: OwnerType? = nil, appContext: AppContext) throws -> ValueType? {
let owner = owner as? AnyObject
let value = try getter?.call(by: owner, withArguments: [], appContext: appContext)
return value as? ValueType
}
internal func setValue(_ value: Any, owner: OwnerType? = nil, appContext: AppContext) {
let owner = owner as? AnyObject
_ = try? setter?.call(by: owner, withArguments: [value], appContext: appContext)
}
/**
Creates the JavaScript function that will be used as a getter of the property.
*/
internal func buildGetter(appContext: AppContext) throws -> JavaScriptObject {
return try appContext.runtime.createSyncFunction(name, argsCount: 0) { [weak appContext, weak self, name] this, args in
guard let appContext else {
throw Exceptions.AppContextLost()
}
guard let self else {
throw NativePropertyUnavailableException(name)
}
guard let getter = self.getter else {
return
}
return try getter.call(by: this, withArguments: args, appContext: appContext)
}
}
/**
Creates the JavaScript function that will be used as a setter of the property.
*/
internal func buildSetter(appContext: AppContext) throws -> JavaScriptObject {
return try appContext.runtime.createSyncFunction(name, argsCount: 1) { [weak appContext, weak self, name] this, args in
guard let appContext else {
throw Exceptions.AppContextLost()
}
guard let self else {
throw NativePropertyUnavailableException(name)
}
guard let setter = self.setter else {
return
}
return try setter.call(by: this, withArguments: args, appContext: appContext)
}
}
/**
Creates the JavaScript object representing the property descriptor.
*/
internal func buildDescriptor(appContext: AppContext) throws -> JavaScriptObject {
let descriptor = try appContext.runtime.createObject()
descriptor.setProperty("enumerable", value: true)
if getter != nil {
descriptor.setProperty("get", value: try buildGetter(appContext: appContext))
}
if setter != nil {
descriptor.setProperty("set", value: try buildSetter(appContext: appContext))
}
return descriptor
}
}
// MARK: - Exceptions
internal final class NativePropertyUnavailableException: GenericException<String> {
override var reason: String {
return "Native property '\(param)' is no longer available in memory"
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2021-present 650 Industries. All rights reserved.
public struct Promise: AnyArgument {
public typealias ResolveClosure = (Any?) -> Void
public typealias RejectClosure = (Exception) -> Void
public var resolver: ResolveClosure
public var rejecter: RejectClosure
/**
The rejecter that is compatible with the legacy `EXPromiseRejectBlock`.
Necessary in some places not converted to Swift, such as `EXPermissionsMethodsDelegate`.
*/
public var legacyRejecter: EXPromiseRejectBlock {
return { code, description, _ in
reject(code ?? "", description ?? "")
}
}
public func resolve(_ value: Any? = nil) {
resolver(value)
}
public func reject(_ error: Error) {
if let exception = error as? Exception {
rejecter(exception)
} else {
rejecter(UnexpectedException(error))
}
}
public func reject(_ error: Exception) {
rejecter(error)
}
public func reject(_ code: String, _ description: String) {
rejecter(Exception(name: code, description: description, code: code))
}
public func settle<ValueType, ExceptionType: Exception>(with result: Result<ValueType, ExceptionType>) {
switch result {
case .success(let value):
resolve(value)
case .failure(let exception):
reject(exception)
}
}
}

View File

@@ -0,0 +1,4 @@
/**
A protocol that must be implemented to be a part of module's definition and the module definition itself.
*/
public protocol AnyDefinition {}

View File

@@ -0,0 +1,7 @@
import React
public protocol AnyExpoView: RCTView {
var appContext: AppContext? { get }
init(appContext: AppContext?)
}

View File

@@ -0,0 +1,17 @@
/**
A protocol for any type-erased module that provides functions used by the core.
*/
public protocol AnyModule: AnyObject, AnyArgument {
/**
The default initializer. Must be public, but the module class does *not* need to
define it as it is implemented in protocol composition, see `BaseModule` class.
*/
init(appContext: AppContext)
/**
A DSL-like function that returns a `ModuleDefinition` which can be built up from module's name, constants or functions.
The `@ModuleDefinitionBuilder` wrapper is *not* required in the implementation it is implicitly inferred from the protocol.
*/
@ModuleDefinitionBuilder
func definition() -> ModuleDefinition
}

View File

@@ -0,0 +1,34 @@
/**
A protocol for any type-erased view definition.
*/
public protocol AnyViewDefinition {
/**
An array of view props supported by the view.
*/
var props: [AnyViewProp] { get }
/**
Names of the events that the view can send to JavaScript.
*/
var eventNames: [String] { get }
/**
Creates an instance of the native view.
*/
func createView(appContext: AppContext) -> UIView?
/**
Returns props definitions as a dictionary where the keys are the prop names.
*/
func propsDict() -> [String: AnyViewProp]
/**
Calls defined lifecycle methods with the given type.
*/
func callLifecycleMethods(withType type: ViewLifecycleMethodType, forView view: UIView)
/**
Creates a JavaScript object that may be used as a React component prototype.
*/
func createReactComponentPrototype(appContext: AppContext) throws -> JavaScriptObject
}

View File

@@ -0,0 +1,23 @@
/**
Protocol for type-erased record fields.
*/
public protocol AnyField {
func get() -> Any
}
/**
Internal-only additions to `AnyField` protocol.
*/
internal protocol AnyFieldInternal: AnyField {
var key: String? { get }
var options: Set<FieldOption> { get set }
/**
Whether the value for this field must be explicitly provided.
The record throws an error when the source dictionary is missing a required value.
Note that it's NOT the opposite to `isOptional`.
*/
var isRequired: Bool { get }
func set(_ newValue: Any?, appContext: AppContext) throws
}

View File

@@ -0,0 +1,104 @@
/**
Property wrapper for `Record`'s data members that takes part in the process of serialization to and deserialization from the dictionary.
*/
@propertyWrapper
public final class Field<Type: AnyArgument>: AnyFieldInternal {
/**
The wrapped value.
*/
public var wrappedValue: Type
private let fieldType: AnyDynamicType = ~Type.self
/**
Field's key in the dictionary, which by default is a label of the wrapped property.
Sadly, property wrappers don't receive properties' label, so we must wait until it's assigned by `Record`.
*/
internal var key: String? {
return options.first { $0.rawValue == FieldOption.keyed("").rawValue }?.key
}
/**
Additional options of the field, such is if providing the value is required (`FieldOption.required`).
*/
internal var options: Set<FieldOption> = Set()
/**
Whether the generic field type accepts `nil` values.
*/
internal var isOptional: Bool {
return fieldType is DynamicOptionalType
}
internal var isRequired: Bool {
options.contains(.required)
}
/**
Initializes the field with given value and customized key.
*/
public init(wrappedValue: Type, _ options: FieldOption...) {
self.wrappedValue = wrappedValue
self.options = Set(options)
}
/**
Alternative default initializer implementation. It's not possible yet to pass an array
as variadic arguments, so we also need to pass an array as a single argument.
*/
public init(wrappedValue: Type, _ options: [FieldOption]) {
self.wrappedValue = wrappedValue
self.options = Set(options)
}
/**
A hacky way to accept optionals without explicit assignment to `nil`. Normally, we would have to do
`@Field var s: String? = nil` but this init with generic constraint allows us to just do `@Field var s: String?`.
*/
public init(wrappedValue: Type = nil) where Type: ExpressibleByNilLiteral {
self.wrappedValue = wrappedValue
}
public init(wrappedValue: Type = nil, _ options: FieldOption...) where Type: ExpressibleByNilLiteral {
self.wrappedValue = wrappedValue
self.options = Set(options)
}
/**
Returns wrapped value as `Any?` to conform to type-erased `AnyField` protocol.
*/
public func get() -> Any {
return wrappedValue
}
/**
Sets the wrapped value with a value of `Any` type.
*/
internal func set(_ newValue: Any?, appContext: AppContext) throws {
if newValue == nil && (!isOptional || isRequired) {
throw FieldRequiredException(key!)
}
do {
if let value = try fieldType.cast(newValue, appContext: appContext) as? Type {
wrappedValue = value
}
} catch {
throw FieldInvalidTypeException((fieldKey: key!, value: newValue, desiredType: Type.self)).causedBy(error)
}
}
}
internal class FieldRequiredException: GenericException<String> {
override var reason: String {
"Value for field '\(param)' is required, got nil"
}
}
internal class FieldInvalidTypeException: GenericException<(fieldKey: String, value: Any?, desiredType: Any.Type)> {
override var reason: String {
let value = String(describing: param.value ?? "null")
let desiredType = String(describing: param.desiredType)
return "Cannot cast '\(value)' for field '\(param.fieldKey)' of type \(desiredType)"
}
}

View File

@@ -0,0 +1,90 @@
/**
Allows declaring non-optional `Int` field without assigning an initial value.
*/
public extension Field where Type == Int {
convenience init(wrappedValue: Type = 0) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = 0, _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional `Double` field without assigning an initial value.
*/
public extension Field where Type == Double {
convenience init(wrappedValue: Type = 0.0) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = 0.0, _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional `Bool` field without assigning an initial value.
*/
public extension Field where Type == Bool {
convenience init(wrappedValue: Type = false) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = false, _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional `String` field without assigning an initial value.
*/
public extension Field where Type == String {
convenience init(wrappedValue: Type = "") {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = "", _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional array field without assigning an initial value.
*/
public extension Field where Type: ExpressibleByArrayLiteral {
convenience init(wrappedValue: Type = []) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = [], _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional dictionary field without assigning an initial value.
*/
public extension Field where Type: ExpressibleByDictionaryLiteral {
convenience init(wrappedValue: Type = [:]) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = [:], _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}
/**
Allows declaring non-optional record field without assigning an initial value.
*/
public extension Field where Type: Record {
convenience init(wrappedValue: Type = Type.init()) {
self.init(wrappedValue: wrappedValue, [])
}
convenience init(wrappedValue: Type = Type.init(), _ options: FieldOption...) {
self.init(wrappedValue: wrappedValue, options)
}
}

View File

@@ -0,0 +1,62 @@
/**
Enum-like struct representing field options that for example can specify if the field
is required and so it must be provided by the dictionary from which the field is created.
*/
public struct FieldOption: Equatable, Hashable, ExpressibleByIntegerLiteral, ExpressibleByStringLiteral {
public let rawValue: Int
public var key: String?
public init(_ rawValue: Int) {
self.rawValue = rawValue
}
// MARK: `Equatable`
/**
Field options are equal when their raw values and parameters are equal.
*/
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.rawValue == rhs.rawValue && lhs.key == rhs.key
}
// MARK: `Hashable`
/**
The options can be stored in `Set` which uses `hashValue` as a unique identifier.
*/
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
// MARK: `ExpressibleByIntegerLiteral`
/**
Initializer that allows to create an instance from int literal.
*/
public init(integerLiteral value: Int) {
self.rawValue = value
}
// MARK: `ExpressibleByStringLiteral`
/**
Initializer that allows to create an instance from string literal.
*/
public init(stringLiteral value: String) {
self.rawValue = 0
self.key = value
}
}
public extension FieldOption {
/**
Field option setting its key to given string. Raw value equals to `0`.
This option can also be initialized with string literal.
*/
static func keyed(_ key: String) -> FieldOption { FieldOption(stringLiteral: key) }
/**
The field must be explicitly provided by the dictionary. Raw value equals to `1`.
*/
static let required: FieldOption = 1
}

View File

@@ -0,0 +1,79 @@
/**
A protocol that allows initializing the object with a dictionary.
*/
public protocol Record: Convertible {
/**
The dictionary type that the record can be created from or converted back.
*/
typealias Dict = [String: Any]
/**
The default initializer. It enforces the structs not to have any uninitialized properties.
*/
init()
/**
Initializes a record from given dictionary. Only members wrapped by `@Field` will be set in the object.
*/
init(from: Dict, appContext: AppContext) throws
/**
Converts the record back to the dictionary. Only members wrapped by `@Field` will be set in the dictionary.
*/
func toDictionary() -> Dict
}
/**
Provides the default implementation of `Record` protocol.
*/
public extension Record {
static func convert(from value: Any?, appContext: AppContext) throws -> Self {
if let value = value as? Dict {
return try Self(from: value, appContext: appContext)
}
throw Conversions.ConvertingException<Self>(value)
}
init(from dict: Dict, appContext: AppContext) throws {
self.init()
let dictKeys = dict.keys
try fieldsOf(self).forEach { field in
guard let key = field.key else {
// This should never happen, but just in case skip fields without the key.
return
}
if dictKeys.contains(key) || field.isRequired {
try field.set(dict[key], appContext: appContext)
}
}
}
func toDictionary() -> Dict {
return fieldsOf(self).reduce(into: Dict()) { result, field in
result[field.key!] = Conversions.convertFunctionResult(field.get())
}
}
}
/**
Returns an array of fields found in record's mirror. If the field is missing the `key`,
it gets assigned to the property label, so after all it's safe to enforce unwrapping it (using `key!`).
*/
private func fieldsOf(_ record: Record) -> [AnyFieldInternal] {
return Mirror(reflecting: record).children.compactMap { (label: String?, value: Any) in
guard var field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else {
return nil
}
field.options.insert(.keyed(key))
return field
}
}
/**
Converts mirror's label to field's key by dropping the "_" prefix from wrapped property label.
*/
private func convertLabelToKey(_ label: String?) -> String? {
return (label != nil && label!.starts(with: "_")) ? String(label!.dropFirst()) : label
}

View File

@@ -0,0 +1,80 @@
// Copyright 2022-present 650 Industries. All rights reserved.
public protocol AnySharedObject: AnyArgument {
var sharedObjectId: SharedObjectId { get }
}
extension AnySharedObject {
public static func getDynamicType() -> AnyDynamicType {
return DynamicSharedObjectType(innerType: Self.self)
}
}
open class SharedObject: AnySharedObject {
/**
An identifier of the native shared object that maps to the JavaScript object.
When the object is not linked with any JavaScript object, its value is 0.
*/
public internal(set) var sharedObjectId: SharedObjectId = 0
/**
An app context for which the shared object was created.
*/
public internal(set) weak var appContext: AppContext?
/**
The default public initializer of the shared object.
*/
public init() {}
/**
Returns the JavaScript shared object associated with the native shared object.
*/
public func getJavaScriptObject() -> JavaScriptObject? {
return appContext?.sharedObjectRegistry.toJavaScriptObject(self)
}
}
// Unfortunately the `emit` function needs to be defined in the extension.
// When put in the class, pack expansion is crashing with `EXC_BAD_ACCESS` code.
// See https://github.com/apple/swift/issues/72381 for more details.
public extension SharedObject { // swiftlint:disable:this no_grouping_extension
// Parameter packs feature requires Swift 5.9 (Xcode 15.0), but some CIs and EAS images may still use older versions.
// As of April 29, all submissions must be made with Xcode 15, so hopefully we can remove this condition soon.
// No one should use <15.0 these days.
#if swift(>=5.9)
/**
Schedules an event with the given name and arguments to be emitted to the associated JavaScript object.
*/
public func emit<each A: AnyArgument>(event: String, arguments: repeat each A) {
guard let appContext, let runtime = try? appContext.runtime else {
log.warn("Trying to send event '\(event)' to \(type(of: self)), but the JS runtime has been lost")
return
}
// Collect arguments and their dynamic types from parameter pack
var argumentPairs: [(AnyArgument, AnyDynamicType)] = []
repeat argumentPairs.append((each arguments, ~(each A).self))
// Schedule the event to be asynchronously emitted from the runtime's thread
runtime.schedule { [weak self, weak appContext] in
guard let appContext, let runtime = try? appContext.runtime, let jsObject = self?.getJavaScriptObject() else {
log.warn("Trying to send event '\(event)' to \(type(of: self)), but the JS object is no longer associated with the native instance")
return
}
// Convert native arguments to JS, just like function results
let arguments = argumentPairs.map { argument, dynamicType in
return Conversions.convertFunctionResult(argument, appContext: appContext, dynamicType: dynamicType)
}
JSIUtils.emitEvent(event, to: jsObject, withArguments: arguments, in: runtime)
}
}
#else // swift(>=5.9)
@available(*, unavailable, message: "Unavailable in Xcode <15.0")
public func emit(event: String, arguments: AnyArgument...) {
fatalError("Emitting events to JS requires at least Xcode 15.0")
}
#endif // swift(<5.9)
}

View File

@@ -0,0 +1,180 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Type of the IDs of shared objects.
*/
public typealias SharedObjectId = Int
/**
A tuple containing a pair of matching native and JS objects.
*/
internal typealias SharedObjectPair = (native: SharedObject, javaScript: JavaScriptWeakObject)
/**
Property name of the JS object where the shared object ID is stored.
*/
let sharedObjectIdPropertyName = "__expo_shared_object_id__"
/**
The registry of all shared objects used in the entire app.
It's been made static for simplicity.
*/
public final class SharedObjectRegistry {
/**
Weak reference to the app context for the registry.
*/
private weak var appContext: AppContext?
/**
The counter of IDs to assign to the shared object pairs.
The next pair added to the registry will be saved using this ID.
*/
internal var nextId: SharedObjectId = 1
/**
A dictionary of shared object pairs.
*/
internal var pairs = [SharedObjectId: SharedObjectPair]()
/**
The lock queue to keep thread safety for internal data structures.
*/
private static let lockQueue = DispatchQueue(label: "expo.modules.core.SharedObjectRegistry")
/**
A number of all pairs stored in the registry.
*/
internal var size: Int {
return Self.lockQueue.sync {
return pairs.count
}
}
/**
Shared object releaser that is common to all instances.
*/
private lazy var objectReleaser: (SharedObjectId) -> Void = { [weak self] objectId in
self?.delete(objectId)
}
/**
The default initializer that takes the app context.
*/
internal init(appContext: AppContext) {
self.appContext = appContext
}
/**
Returns the next shared object ID and increases the counter.
*/
@discardableResult
internal func pullNextId() -> SharedObjectId {
return Self.lockQueue.sync {
let id = nextId
nextId += 1
return id
}
}
/**
Returns a pair of shared objects with given ID or `nil` when there is no such pair in the registry.
*/
internal func get(_ id: SharedObjectId) -> SharedObjectPair? {
return Self.lockQueue.sync {
return pairs[id]
}
}
/**
Adds a pair of native and JS shared object to the registry. Assigns a new shared object ID to these objects.
*/
@discardableResult
internal func add(native nativeObject: SharedObject, javaScript jsObject: JavaScriptObject) -> SharedObjectId {
let id = pullNextId()
// Assign the ID and the app context to the object.
nativeObject.sharedObjectId = id
nativeObject.appContext = appContext
// This property should be deprecated, but it's still used when passing as a view prop.
// It's already defined in the JS base SharedObject class prototype,
// but with the current implementation it's possible to use a raw object for registration.
jsObject.defineProperty(sharedObjectIdPropertyName, value: id, options: [.writable])
// Set the native state in the JS object.
if let runtime = try? appContext?.runtime {
SharedObjectUtils.setNativeState(jsObject, runtime: runtime, objectId: id, releaser: objectReleaser)
}
// Save the pair in the dictionary.
let jsWeakObject = jsObject.createWeak()
Self.lockQueue.async {
self.pairs[id] = (native: nativeObject, javaScript: jsWeakObject)
}
return id
}
/**
Deletes the shared objects pair with a given ID.
*/
internal func delete(_ id: SharedObjectId) {
Self.lockQueue.async {
if let pair = self.pairs[id] {
// Reset an ID on the objects.
pair.native.sharedObjectId = 0
// Delete the pair from the dictionary.
self.pairs[id] = nil
}
}
}
/**
Gets the native shared object that is paired with a given JS object.
*/
internal func toNativeObject(_ jsObject: JavaScriptObject) -> SharedObject? {
if let objectId = try? jsObject.getProperty(sharedObjectIdPropertyName).asInt() {
return Self.lockQueue.sync {
return pairs[objectId]?.native
}
}
return nil
}
/**
Gets the JS shared object that is paired with a given native object.
*/
internal func toJavaScriptObject(_ nativeObject: SharedObject) -> JavaScriptObject? {
let objectId = nativeObject.sharedObjectId
return Self.lockQueue.sync {
return pairs[objectId]?.javaScript.lock()
}
}
/**
Creates a plain JS object and pairs it with a given native object.
*/
internal func createSharedJavaScriptObject(runtime: JavaScriptRuntime, nativeObject: SharedObject) -> JavaScriptObject {
let object = runtime.createObject()
add(native: nativeObject, javaScript: object)
return object
}
/**
Ensures that there is a JS object paired with a given native object. If not, a plain JS object is created.
*/
internal func ensureSharedJavaScriptObject(runtime: JavaScriptRuntime, nativeObject: SharedObject) -> JavaScriptObject {
if let jsObject = toJavaScriptObject(nativeObject) {
// JS object for this native object already exists in the registry, just return it.
return jsObject
}
return createSharedJavaScriptObject(runtime: runtime, nativeObject: nativeObject)
}
internal func clear() {
Self.lockQueue.async {
self.pairs.removeAll()
}
}
}

View File

@@ -0,0 +1,12 @@
/**
Shared object (ref) that holds a pointer to any native object. Allows passing references
to native instances among different independent libraries.
*/
open class SharedRef<PointerType>: SharedObject {
public let pointer: PointerType
public init(_ pointer: PointerType) {
self.pointer = pointer
super.init()
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
A protocol for all typed arrays.
*/
internal protocol AnyTypedArray: AnyArgument {
/**
Initializes a typed array from the given JavaScript representation.
*/
init(_ jsTypedArray: JavaScriptTypedArray)
}
// Extend the protocol to provide custom dynamic type
extension AnyTypedArray {
public static func getDynamicType() -> AnyDynamicType {
return DynamicTypedArrayType(innerType: Self.self)
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Native equivalent of `Int8Array` in JavaScript, an array of two's-complement 8-bit signed integers.
*/
public final class Int8Array: GenericTypedArray<Int8> {}
/**
Native equivalent of `Int16Array` in JavaScript, an array of two's-complement 16-bit signed integers.
*/
public final class Int16Array: GenericTypedArray<Int16> {}
/**
Native equivalent of `Int32Array` in JavaScript, an array of two's-complement 32-bit signed integers.
*/
public final class Int32Array: GenericTypedArray<Int32> {}
/**
Native equivalent of `Uint8Array` in JavaScript, an array of 8-bit unsigned integers.
*/
public final class Uint8Array: GenericTypedArray<UInt8> {}
/**
Native equivalent of `Uint8ClampedArray` in JavaScript, an array of 8-bit unsigned integers clamped to 0-255.
*/
public final class Uint8ClampedArray: GenericTypedArray<UInt8> {}
/**
Native equivalent of `Uint16Array` in JavaScript, an array of 16-bit unsigned integers.
*/
public final class Uint16Array: GenericTypedArray<UInt16> {}
/**
Native equivalent of `Uint32Array` in JavaScript, an array of 32-bit unsigned integers.
*/
public final class Uint32Array: GenericTypedArray<UInt32> {}
/**
Native equivalent of `Float32Array` in JavaScript, an array of 32-bit floating point numbers.
*/
public final class Float32Array: GenericTypedArray<Float32> {}
/**
Native equivalent of `Float64Array` in JavaScript, an array of 64-bit floating point numbers.
*/
public final class Float64Array: GenericTypedArray<Float64> {}
/**
Native equivalent of `BigInt64Array` in JavaScript, an array of 64-bit signed integers.
*/
public final class BigInt64Array: GenericTypedArray<Int64> {}
/**
Native equivalent of `BigUint64Array` in JavaScript, an array of 64-bit unsigned integers.
*/
public final class BigUint64Array: GenericTypedArray<UInt64> {}

View File

@@ -0,0 +1,49 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Generic TypedArray with an associated numeric ContentType (e.g. UInt8, Int16, Double).
*/
public class GenericTypedArray<ContentType: Numeric>: TypedArray {
/**
The unsafe mutable typed buffer that shares the same memory as the underlying JavaScript `ArrayBuffer`.
*/
lazy var buffer = UnsafeMutableBufferPointer<ContentType>(start: pointer, count: length)
/**
The unsafe mutable typed pointer to the start of the array buffer.
*/
lazy var pointer = rawPointer.bindMemory(to: ContentType.self, capacity: length)
public subscript(index: Int) -> ContentType {
get {
return buffer[index]
}
set {
buffer[index] = newValue
}
}
subscript(range: Range<Int>) -> [ContentType] {
get {
return Array(buffer[range])
}
set {
var newValues = newValue
newValues.withUnsafeMutableBufferPointer { newValuesBuffer in
buffer[range] = newValuesBuffer[0..<(range.upperBound - range.lowerBound)]
}
}
}
subscript(range: ClosedRange<Int>) -> [ContentType] {
get {
return Array(buffer[range])
}
set {
var newValues = newValue
newValues.withUnsafeMutableBufferPointer { newValuesBuffer in
buffer[range] = newValuesBuffer[0...(range.upperBound - range.lowerBound)]
}
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
The base class for any type of the typed array.
*/
public class TypedArray: AnyTypedArray {
/**
Creates a concrete TypedArray from the given JavaScriptTypedArray
*/
internal static func create(from jsTypedArray: JavaScriptTypedArray) -> TypedArray {
switch jsTypedArray.kind {
case .Int8Array:
return Int8Array(jsTypedArray)
case .Int16Array:
return Int16Array(jsTypedArray)
case .Int32Array:
return Int32Array(jsTypedArray)
case .Uint8Array:
return Uint8Array(jsTypedArray)
case .Uint8ClampedArray:
return Uint8ClampedArray(jsTypedArray)
case .Uint16Array:
return Uint16Array(jsTypedArray)
case .Uint32Array:
return Uint32Array(jsTypedArray)
case .Float32Array:
return Float32Array(jsTypedArray)
case .Float64Array:
return Float64Array(jsTypedArray)
case .BigInt64Array:
return BigInt64Array(jsTypedArray)
case .BigUint64Array:
return BigUint64Array(jsTypedArray)
@unknown default:
fatalError("Unknown kind of the TypedArray")
}
}
/**
A JavaScript object of the underlying typed array.
*/
let jsTypedArray: JavaScriptTypedArray
/**
The length in bytes from the start of the underlying ArrayBuffer.
Fixed at construction time and thus read-only.
*/
public lazy var byteLength: Int = jsTypedArray.getProperty("byteLength").getInt()
/**
The offset in bytes from the start of the underlying ArrayBuffer.
Fixed at construction time and thus read-only.
*/
public lazy var byteOffset: Int = jsTypedArray.getProperty("byteOffset").getInt()
/**
Returns the number of elements held in the typed array.
Fixed at construction time and thus read only.
*/
public lazy var length: Int = jsTypedArray.getProperty("length").getInt()
/**
The unsafe mutable raw pointer to the start of the array buffer.
*/
public lazy var rawPointer: UnsafeMutableRawPointer = jsTypedArray.getUnsafeMutableRawPointer()
/**
Returns the kind of the typed array, such as `Int8Array` or `Float32Array`.
*/
public var kind: TypedArrayKind {
return jsTypedArray.kind
}
/**
Initializes the typed array with the given JS typed array.
*/
required init(_ jsTypedArray: JavaScriptTypedArray) {
self.jsTypedArray = jsTypedArray
}
}

View File

@@ -0,0 +1,14 @@
/**
Type-erased protocol for view props classes.
*/
public protocol AnyViewProp: AnyViewDefinitionElement {
/**
Name of the view prop that JavaScript refers to.
*/
var name: String { get }
/**
Function that sets the underlying prop value for given view.
*/
func set(value: Any, onView: UIView, appContext: AppContext) throws
}

View File

@@ -0,0 +1,122 @@
// Copyright 2021-present 650 Industries. All rights reserved.
import React
/**
Custom component data extending `RCTComponentData`. Its main purpose is to handle event-based props (callbacks),
but it also simplifies capturing the view config so we can omit some reflections that React Native executes.
*/
@objc(EXComponentData)
public final class ComponentData: RCTComponentDataSwiftAdapter {
/**
Weak pointer to the holder of a module that the component data was created for.
*/
weak var moduleHolder: ModuleHolder?
/**
Initializer that additionally takes the original view module to have access to its definition.
*/
@objc
public init(viewModule: ViewModuleWrapper, managerClass: ViewModuleWrapper.Type, bridge: RCTBridge) {
self.moduleHolder = viewModule.wrappedModuleHolder
super.init(managerClass: managerClass, bridge: bridge, eventDispatcher: bridge.eventDispatcher())
}
// MARK: RCTComponentData
/**
Creates a setter for the specific prop. For non-event props we just let React Native do its job.
Events are handled differently to conveniently use them in Swift.
*/
public override func createPropBlock(_ propName: String, isShadowView: Bool) -> RCTPropBlockAlias {
// Expo Modules Core doesn't support shadow views yet, so fall back to the default implementation.
if isShadowView {
return super.createPropBlock(propName, isShadowView: isShadowView)
}
// If the prop is defined as an event, create our own event setter.
if moduleHolder?.definition.view?.eventNames.contains(propName) == true {
return createEventSetter(eventName: propName, bridge: self.manager?.bridge)
}
// Otherwise also fall back to the default implementation.
return super.createPropBlock(propName, isShadowView: isShadowView)
}
public override func setProps(_ props: [String: Any], forView view: RCTComponent) {
guard let view = view as? UIView else {
log.warn("Given view is not an UIView")
return
}
guard let viewDefinition = moduleHolder?.definition.view else {
log.warn("View manager '\(self.name)' not found")
return
}
guard let appContext = moduleHolder?.appContext else {
log.warn("App context has been lost")
return
}
let propsDict = viewDefinition.propsDict()
var remainingProps = props
for (key, prop) in propsDict {
if props.index(forKey: key) == nil {
continue
}
let newValue = props[key] as Any
// TODO: @tsapeta: Figure out better way to rethrow errors from here.
try? prop.set(value: Conversions.fromNSObject(newValue), onView: view, appContext: appContext)
remainingProps.removeValue(forKey: key)
}
// Let the base class `RCTComponentData` handle all remaining props.
super.setProps(remainingProps, forView: view)
viewDefinition.callLifecycleMethods(withType: .didUpdateProps, forView: view)
}
/**
The base `RCTComponentData` class does some Objective-C dynamic calls in this function, but we don't
need to do these slow operations since the Sweet API gives us necessary details without reflections.
*/
public override func viewConfig() -> [String: Any] {
var propTypes: [String: Any] = [:]
var directEvents: [String] = []
let superClass: AnyClass? = managerClass.superclass()
if let viewDefinition = moduleHolder?.definition.view {
for prop in viewDefinition.props {
// `id` allows every type to be passed in
propTypes[prop.name] = "id"
}
for eventName in viewDefinition.eventNames {
directEvents.append(RCTNormalizeInputEventName(eventName))
propTypes[eventName] = "BOOL"
}
}
return [
"propTypes": propTypes,
"directEvents": directEvents,
"bubblingEvents": [String](),
"baseModuleName": superClass?.moduleName() as Any
]
}
}
/**
Creates a setter for the event prop. Used only by Paper.
*/
private func createEventSetter(eventName: String, bridge: RCTBridge?) -> RCTPropBlockAlias {
return { [weak bridge] (target: RCTComponent, value: Any) in
installEventDispatcher(forEvent: eventName, onView: target) { [weak target] (body: [String: Any]) in
if let target = target {
let componentEvent = RCTComponentEvent(name: eventName, viewTag: target.reactTag, body: body)
bridge?.eventDispatcher()?.send(componentEvent)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More