com um clique
expo-modules
// Building great Expo native modules for iOS and Android. Views, APIs, Marshalling, Shared Objects, Expo Documentation, Verifying Expo modules.
// Building great Expo native modules for iOS and Android. Views, APIs, Marshalling, Shared Objects, Expo Documentation, Verifying Expo modules.
Seed and verify HealthKit data in running Expo apps using the apple-health CLI
Building Expo DevTools Plugins with CLI Interfaces for interacting with running Expo apps using agents.
Interact with iOS simulators and verify app behavior using xcobra
| name | expo-modules |
| description | Building great Expo native modules for iOS and Android. Views, APIs, Marshalling, Shared Objects, Expo Documentation, Verifying Expo modules. |
std:kv-storage, clipboard.capture(options: { isHighQuality: boolean }), use capture(options: { quality: 'high' | 'medium' | 'low' }).isAvailable functions or constants. eg snapshot.capture?.() instead of snapshot.isAvailable && snapshot.capture().Example of a GREAT Expo module:
import { NativeModule } from "expo";
declare class AppClipModule extends NativeModule<{}> {
prompt(): void;
isAppClip?: boolean;
}
// This call loads the native module object from the JSI.
const AppClipNative =
typeof expo !== "undefined"
? (expo.modules.AppClip as AppClipModule) ?? {}
: {};
if (AppClipNative?.isAppClip) {
navigator.appClip = {
prompt: AppClipNative.prompt,
};
}
// Add types for the global `navigator.appClip` object.
declare global {
interface Navigator {
/**
* Only available in an App Clip context.
* @expo
*/
appClip?: {
/** Open the SKOverlay */
prompt: () => void;
};
}
}
export {};
isAvailable methods.Example of a POOR Expo module:
import { NativeModulesProxy } from "expo-modules-core";
const { ExpoAppClip } = NativeModulesProxy;
export default {
promptAppClip() {
return ExpoAppClip.promptAppClip();
},
isAppClipAvailable() {
return ExpoAppClip.isAppClipAvailable();
},
};
isAvailable(), explain why it exists in the docs. Research cases where it may return false such as in a simulator or particular OS version.import * as MediaLibrary from 'expo-media-library'; instead of import { MediaLibrary } from 'expo/media';Take API inspiration from great web component libraries like BaseUI and Radix.
Consider if you're building a control or a display component. Controls should have more interactive APIs, while display components should be more declarative.
Prefer functions on views instead of useImperativeHandle + findNodeHandle.
AsyncFunction("capture") { (view, options: Options) -> Ref in
return try capture(self.appContext, view)
}
Remember to export views in the module:
import ExpoModulesCore
public class ExpoWebViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView.self) {}
}
}
Consider this example https://github.com/EvanBacon/expo-shared-objects-haptics-example/blob/be90e92f8dba9b0807009502ab25c423c57e640d/modules/my-module/ios/MyModule.swift#L1C1-L178C2
Using @retroactive Convertible and AnyArgument to convert between Swift types and dictionaries enables passing complex data structures across the boundary without writing custom serialization code for each type.
extension CHHapticEventParameter: @retroactive Convertible, AnyArgument {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let dict = value as? [String: Any],
let parameterIDRaw = dict["parameterID"] as? String,
let value = dict["value"] as? Double else {
throw NotADictionaryException()
}
return Self(parameterID: CHHapticEvent.ParameterID(rawValue: parameterIDRaw), value: Float(value))
}
}
extension CHHapticEvent: @retroactive Convertible, AnyArgument {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let dict = value as? [String: Any],
let eventTypeRaw = dict["eventType"] as? String,
let relativeTime = dict["relativeTime"] as? Double else {
throw NotADictionaryException()
}
let eventType = CHHapticEvent.EventType(rawValue: eventTypeRaw)
let parameters = (dict["parameters"] as? [[String: Any]])?.compactMap { paramDict -> CHHapticEventParameter? in
try? CHHapticEventParameter.convert(from: paramDict, appContext: appContext)
} ?? []
return Self(eventType: eventType, parameters: parameters, relativeTime: relativeTime)
}
}
extension CHHapticDynamicParameter: @retroactive Convertible, AnyArgument {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let dict = value as? [String: Any],
let parameterIDRaw = dict["parameterID"] as? String,
let value = dict["value"] as? Double,
let relativeTime = dict["relativeTime"] as? Double else {
throw NotADictionaryException()
}
return Self(parameterID: CHHapticDynamicParameter.ID(rawValue: parameterIDRaw), value: Float(value), relativeTime: relativeTime)
}
}
extension CHHapticPattern: @retroactive Convertible, AnyArgument {
public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
guard let dict = value as? [String: Any],
let eventsArray = dict["events"] as? [[String: Any]] else {
throw NotADictionaryException()
}
let events = try eventsArray.map { eventDict -> CHHapticEvent in
try CHHapticEvent.convert(from: eventDict, appContext: appContext)
}
let parameters = (dict["parameters"] as? [[String: Any]])?.compactMap { paramDict -> CHHapticDynamicParameter? in
return try? CHHapticDynamicParameter.convert(from: paramDict, appContext: appContext)
} ?? []
return try Self(events: events, parameters: parameters)
}
}
internal final class NotAnArrayException: Exception {
override var reason: String {
"Given value is not an array"
}
}
internal final class IncorrectArraySizeException: GenericException<(expected: Int, actual: Int)> {
override var reason: String {
"Given array has unexpected number of elements: \(param.actual), expected: \(param.expected)"
}
}
internal final class NotADictionaryException: Exception {
override var reason: String {
"Given value is not a dictionary"
}
}
Later this can be used to implement methods that accept complex data structures as arguments.
Function("playPattern") { (pattern: CHHapticPattern) in
let player = try hapticEngine.makePlayer(with: pattern)
try player.start(atTime: 0)
}
Use shorthand where possible, especially when the JS value matches the Swift value:
Property("__typename") { $0.__typename }
Shared objects are long-lived native instances that are shared to JS. They can be used to keep heavy state objects, such as a decoded bitmap, alive across React components, rather than spinning up a new native instance every time a component mounts.
To interact with HealthKit, the module may need to respond to app lifecycle events. This can be done by implementing the ExpoAppDelegateSubscriber protocol.
import ExpoModulesCore
public class ExpoHeadAppDelegateSubscriber: ExpoAppDelegateSubscriber {
// Any AppDelegate methods you want to implement
public func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
launchedActivity = userActivity
// ...
return false
}
}
Then add the subscriber to the expo-module.config.json:
{
"platforms": ["apple", "android", "web"],
"apple": {
"modules": ["ExpoHeadModule", ...],
"appDelegateSubscribers": ["ExpoHeadAppDelegateSubscriber"]
}
}
expo-quick-actions which has a expo-quick-actions/router import for automatic deep linking. Other good examples are Expo notifications (open settings, redirect notifications), widgets, siri shortcuts.
yarn expo run:ios --no-bundler in an Expo app to headlessly compile the module and verify there are no compilation errors.