first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
// Copyright 2024-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/Platform.h>
#ifdef __cplusplus
#import <ReactCommon/RCTHost.h>
#endif
/**
A wrapper around RCTHost. RCTHost isn't directly available in Swift.
*/
NS_SWIFT_NAME(ExpoHostWrapper)
@interface EXHostWrapper : NSObject
#ifdef __cplusplus
- (instancetype _Nonnull)initWithHost:(RCTHost * _Nonnull)host;
#endif
- (nullable UIView *)findViewWithTag:(NSInteger)tag;
- (nullable id)findModuleWithName:(nonnull NSString *)name lazilyLoadIfNecessary:(BOOL)lazilyLoadIfNecessary;
@end

View File

@@ -0,0 +1,34 @@
// Copyright 2024-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/EXHostWrapper.h>
#import <ReactCommon/RCTHost.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTMountingManager.h>
#import <React/RCTComponentViewRegistry.h>
@implementation EXHostWrapper {
__weak RCTHost *_host;
}
- (instancetype)initWithHost:(RCTHost *)host
{
if (self = [super init]) {
_host = host;
}
return self;
}
- (nullable id)findModuleWithName:(nonnull NSString *)name lazilyLoadIfNecessary:(BOOL)lazilyLoadIfNecessary
{
RCTModuleRegistry *moduleRegistry = _host.moduleRegistry;
return [moduleRegistry moduleForName:[name UTF8String] lazilyLoadIfNecessary:lazilyLoadIfNecessary];
}
- (nullable UIView *)findViewWithTag:(NSInteger)tag
{
RCTComponentViewRegistry *componentViewRegistry = _host.surfacePresenter.mountingManager.componentViewRegistry;
return [componentViewRegistry findComponentViewWithTag:tag];
}
@end

View File

@@ -0,0 +1,231 @@
// Copyright 2022-present 650 Industries. All rights reserved.
@objc(ExpoFabricView)
open class ExpoFabricView: ExpoFabricViewObjC, AnyExpoView {
/**
A weak reference to the app context associated with this view.
The app context is injected into the class after the context is initialized.
see the `makeClass` static function.
*/
public weak var appContext: AppContext?
/**
The view definition that setup from `ExpoFabricView.create()`.
*/
private var viewDefinition: AnyViewDefinition?
/**
A dictionary of prop objects that contain prop setters.
*/
lazy var viewManagerPropDict: [String: AnyViewProp]? = viewDefinition?.propsDict()
/**
A dictionary to store previous prop values for change detection.
*/
private var previousProps: [String: Any] = [:]
// MARK: - Initializers
// swiftlint:disable unavailable_function
@objc
public init() {
// For derived views, their initializer should be replaced by the 'class_replaceMethod'.
fatalError("Unsupported direct init() call for ExpoFabricView.")
}
// swiftlint:enable unavailable_function
@objc
public override init(frame: CGRect) {
super.init(frame: frame)
}
public func setViewSize(_ size: CGSize) {
super.setShadowNodeSize(Float(size.width), height: Float(size.height))
}
required public init(appContext: AppContext? = nil) {
self.appContext = appContext
super.init(frame: .zero)
}
/**
The view creator expected to be called for derived ExpoFabricView, the `viewDefinition` and event dispatchers will be setup from here.
NOTE: We swizzle the initializers, e.g. `ViewManagerAdapter_ExpoImage.new()` to `ImageView.init(appContext:)`
and we also need viewDefintion (or moduleName) for the `installEventDispatchers()`.
Swizzling ExpoFabricView doesn't give us chance to inject iMethod or iVar of ImageView and pass the moduleName.
Alternatively, we try to add a dedicated `ExpoFabricView.create()` and passing viewDefinition into the class.
That's not a perfect implementation but turns out to be the only way to get the viewDefinition (or moduleName).
The example call flow would be:
`ViewManagerAdapter_ExpoImage.new()` -> `ViewDefinition.createView()` -> `ExpoFabricView.create()` ->
`ImageView.init(appContext:)` -> `ExpoFabricView.init(appContext:)` -> `view.viewDefinition = viewDefinition` here
*/
internal static func create(viewType: ExpoFabricView.Type, viewDefinition: AnyViewDefinition, appContext: AppContext) -> ExpoFabricView {
let view = viewType.init(appContext: appContext)
view.viewDefinition = viewDefinition
assert(appContext == view.appContext)
view.installEventDispatchers()
return view
}
// Mark the required init as unavailable so that subclasses can avoid overriding it.
@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - ExpoFabricViewInterface
@MainActor
public override func updateProps(_ props: [String: Any]) {
guard let context = appContext, let propsDict = viewManagerPropDict else {
return
}
for (key, prop) in propsDict {
let newValue = props[key] as Any
let convertedNewValue = Conversions.fromNSObject(newValue)
let previousValue = previousProps[key]
// only set the prop if the value has changed
if !areValuesEqual(previousValue, convertedNewValue) {
// TODO: @tsapeta: Figure out better way to rethrow errors from here.
// Adding `throws` keyword to the function results in different
// method signature in Objective-C. Maybe just call `RCTLogError`?
try? prop.set(value: convertedNewValue, onView: self, appContext: context)
previousProps[key] = convertedNewValue
}
}
}
/**
Helper function to compare two values for equality using string representation.
*/
private func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
switch (lhs, rhs) {
case (nil, nil):
return true
case let (lhsValue as AnyHashable, rhsValue as AnyHashable):
return lhsValue == rhsValue
case let (lhsValue as NSObjectProtocol, rhsValue as NSObjectProtocol):
return lhsValue.isEqual(rhsValue)
default:
return false
}
}
/**
Calls lifecycle methods registered by `OnViewDidUpdateProps` definition component.
*/
@MainActor
public override func viewDidUpdateProps() {
guard let viewDefinition else {
return
}
guard let view = AppleView.from(self) else {
return
}
viewDefinition.callLifecycleMethods(withType: .didUpdateProps, forView: view)
}
/**
Returns a bool value whether the view supports prop with the given name.
*/
public override func supportsProp(withName name: String) -> Bool {
return viewManagerPropDict?.index(forKey: name) != nil
}
// MARK: - Privates
/**
Installs convenient event dispatchers for declared events, so the view can just invoke the block to dispatch the proper event.
*/
private func installEventDispatchers() {
viewDefinition?.eventNames.forEach { eventName in
installEventDispatcher(forEvent: eventName, onView: self) { [weak self] (body: [String: Any]) in
if let self = self {
self.dispatchEvent(eventName, payload: body)
} else {
log.error("Cannot dispatch an event while the managing ExpoFabricView is deallocated")
}
}
}
}
// MARK: - Statics
/**
Called by React Native to check if the view supports recycling.
*/
@objc
public static func shouldBeRecycled() -> Bool {
// Turn off recycling for Expo views. We don't think there is any benefit of recycling it may lead to more bugs than gains.
// TODO: Make it possible to override this behavior for particular views
return false
}
internal static var viewClassesRegistry = [String: AnyClass]()
/**
Dynamically creates a subclass of the `ExpoFabricView` class with injected app context and name of the associated module.
The new subclass is saved in the registry, so when asked for the next time, it's returned from cache with the updated app context.
- Note: Apple's documentation says that classes created with `objc_allocateClassPair` should then be registered using `objc_registerClassPair`,
but we can't do that as there might be more than one class with the same name (Expo Go) and allocating another one would return `nil`.
*/
@objc
public static func makeViewClass(forAppContext appContext: AppContext, moduleName: String, viewName: String, className: String) -> AnyClass? {
if let viewClass = viewClassesRegistry[className] {
inject(appContext: appContext)
injectInitializer(appContext: appContext, moduleName: moduleName, viewName: viewName, toViewClass: viewClass)
return viewClass
}
guard let viewClass = objc_allocateClassPair(ExpoFabricView.self, className, 0) else {
fatalError("Cannot allocate a Fabric view class for '\(className)'")
}
inject(appContext: appContext)
injectInitializer(appContext: appContext, moduleName: moduleName, viewName: viewName, toViewClass: viewClass)
// Save the allocated view class in the registry for the later use (e.g. when the app is reloaded).
viewClassesRegistry[className] = viewClass
return viewClass
}
internal static func inject(appContext: AppContext) {
// Keep it weak so we don't leak the app context.
weak var weakAppContext = appContext
let appContextBlock: @convention(block) () -> AppContext? = { weakAppContext }
let appContextBlockImp: IMP = imp_implementationWithBlock(appContextBlock)
class_replaceMethod(object_getClass(ExpoFabricView.self), #selector(appContextFromClass), appContextBlockImp, "@@:")
}
internal static func injectInitializer(appContext: AppContext, moduleName: String, viewName: String, toViewClass viewClass: AnyClass) {
// The default initializer for native views. It will be called by Fabric.
let newBlock: @convention(block) () -> Any = {[weak appContext] in
guard let appContext, let moduleHolder = appContext.moduleRegistry.get(moduleHolderForName: moduleName) else {
fatalError(Exceptions.AppContextLost().reason)
}
guard let view = moduleHolder.definition.views[viewName]?.createView(appContext: appContext) else {
fatalError("Cannot create a view '\(viewName)' from module '\(moduleName)'")
}
switch view {
case .uikit(let view):
_ = Unmanaged.passRetained(view) // retain the view given this is an initializer
return view
case .swiftui(let view):
if let viewObject = view as AnyObject? {
_ = Unmanaged.passRetained(viewObject) // retain the view given this is an initializer
}
return view
}
}
let newBlockImp: IMP = imp_implementationWithBlock(newBlock)
class_replaceMethod(object_getClass(viewClass), Selector("new"), newBlockImp, "@@:")
}
// swiftlint:disable unavailable_function
@objc
private dynamic static func appContextFromClass() -> AppContext? {
fatalError("The AppContext must be injected in the 'ExpoFabricView' class")
}
// swiftlint:enable unavailable_function
}

View File

@@ -0,0 +1,61 @@
// Copyright 2022-present 650 Industries. All rights reserved.
#import <ExpoModulesCore/Platform.h>
#ifdef __cplusplus
#import <React/RCTViewComponentView.h> // Allows non-umbrella since it's coming from React-RCTFabric
@interface ExpoFabricViewObjC : RCTViewComponentView
@end
#else
#import <React/RCTView.h>
// Interface visible in Swift
@interface ExpoFabricViewObjC : RCTView
@end
#endif // __cplusplus
@class EXAppContext;
@class EXViewModuleWrapper;
// Addition to the interface that is visible in both Swift and Objective-C
@interface ExpoFabricViewObjC (ExpoFabricViewInterface)
- (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload;
- (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props;
- (void)viewDidUpdateProps NS_SWIFT_UI_ACTOR;
- (void)setShadowNodeSize:(float)width height:(float)height;
- (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
- (BOOL)supportsPropWithName:(nonnull NSString *)name;
// MARK: - Derived from RCTComponentViewProtocol
- (void)prepareForRecycle;
/*
* Called for mounting (attaching) a child component view inside `self` component view.
*/
- (void)mountChildComponentView:(nonnull UIView *)childComponentView index:(NSInteger)index;
/*
* Called for unmounting (detaching) a child component view from `self` component view.
*/
- (void)unmountChildComponentView:(nonnull UIView *)childComponentView index:(NSInteger)index;
#pragma mark - Component registration
/**
Registers given view module in the global `RCTComponentViewFactory`.
*/
+ (void)registerComponent:(nonnull EXViewModuleWrapper *)viewModule appContext:(nonnull EXAppContext *)appContext;
@end

View File

@@ -0,0 +1,216 @@
// Copyright 2022-present 650 Industries. All rights reserved.
#import <objc/runtime.h>
#import <string.h>
#import <ExpoModulesCore/ExpoFabricViewObjC.h>
#import <ExpoModulesCore/ExpoViewComponentDescriptor.h>
#import <ExpoModulesCore/Swift.h>
#import <ExpoModulesJSI/EXJSIConversions.h>
#import <React/RCTComponentViewFactory.h>
#import <react/renderer/componentregistry/ComponentDescriptorProvider.h>
using namespace expo;
namespace {
id convertFollyDynamicToId(const folly::dynamic &dyn)
{
// I could imagine an implementation which avoids copies by wrapping the
// dynamic in a derived class of NSDictionary. We can do that if profiling
// implies it will help.
switch (dyn.type()) {
case folly::dynamic::NULLT:
return (id)kCFNull;
case folly::dynamic::BOOL:
return dyn.getBool() ? @YES : @NO;
case folly::dynamic::INT64:
return @(dyn.getInt());
case folly::dynamic::DOUBLE:
return @(dyn.getDouble());
case folly::dynamic::STRING:
return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding];
case folly::dynamic::ARRAY: {
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn) {
id value = convertFollyDynamicToId(elem);
if (value) {
[array addObject:value];
}
}
return array;
}
case folly::dynamic::OBJECT: {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn.items()) {
id key = convertFollyDynamicToId(elem.first);
id value = convertFollyDynamicToId(elem.second);
if (key && value) {
dict[key] = value;
}
}
return dict;
}
}
}
} // namespace
/**
React Native doesn't use the "on" prefix internally. Instead, it uses "top" but it's on the roadmap to get rid of it too.
We're still using "on" in a few places, so let's make sure we normalize that.
*/
static NSString *normalizeEventName(NSString *eventName)
{
if ([eventName hasPrefix:@"on"]) {
NSString *firstLetter = [[eventName substringWithRange:NSMakeRange(2, 1)] lowercaseString];
return [firstLetter stringByAppendingString:[eventName substringFromIndex:3]];
}
return eventName;
}
/**
Cache for component flavors, where the key is a view class name and value is the flavor.
Flavors must be cached in order to keep using the same component handle after app reloads.
*/
static std::unordered_map<std::string, ExpoViewComponentDescriptor::Flavor> _componentFlavorsCache;
@implementation ExpoFabricViewObjC {
ExpoViewShadowNode::ConcreteState::Shared _state;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const expo::ExpoViewProps>();
_props = defaultProps;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider
{
std::string className([NSStringFromClass([self class]) UTF8String]);
// We're caching the flavor pointer so that the component handle stay the same for the same class name.
// Otherwise, the component handle would change after reload which may cause memory leaks and unexpected view recycling behavior.
ExpoViewComponentDescriptor::Flavor flavor = _componentFlavorsCache[className];
if (flavor == nullptr) {
flavor = _componentFlavorsCache[className] = std::make_shared<std::string const>(className);
}
ComponentName componentName = ComponentName { flavor->c_str() };
ComponentHandle componentHandle = reinterpret_cast<ComponentHandle>(componentName);
return ComponentDescriptorProvider {
componentHandle,
componentName,
flavor,
&facebook::react::concreteComponentDescriptorConstructor<expo::ExpoViewComponentDescriptor>
};
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
if (updateMask & RNComponentViewUpdateMaskProps) {
const auto &newProps = static_cast<const ExpoViewProps &>(*_props);
NSMutableDictionary<NSString *, id> *propsMap = [[NSMutableDictionary alloc] init];
for (const auto &item : newProps.propsMap) {
NSString *propName = [NSString stringWithUTF8String:item.first.c_str()];
// Ignore props inherited from the base view and Yoga.
if ([self supportsPropWithName:propName]) {
propsMap[propName] = convertFollyDynamicToId(item.second);
}
}
[self updateProps:propsMap];
[self viewDidUpdateProps];
}
}
#pragma mark - Events
- (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload
{
if (!_eventEmitter) {
return;
}
const auto &eventEmitter = static_cast<const ExpoViewEventEmitter &>(*_eventEmitter);
eventEmitter.dispatch([normalizeEventName(eventName) UTF8String], [payload](jsi::Runtime &runtime) {
return jsi::Value(runtime, expo::convertObjCObjectToJSIValue(runtime, payload));
});
}
#pragma mark - Methods to override in Swift
- (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props
{
// Implemented in `ExpoFabricView.swift`
}
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
{
_state = std::static_pointer_cast<const ExpoViewShadowNode::ConcreteState>(state);
}
- (void)viewDidUpdateProps
{
// Implemented in `ExpoFabricView.swift`
}
- (void)setShadowNodeSize:(float)width height:(float)height
{
if (_state) {
#if REACT_NATIVE_TARGET_VERSION >= 82
_state->updateState(ExpoViewState(width,height), EventQueue::UpdateMode::unstable_Immediate);
#else
_state->updateState(ExpoViewState(width,height));
#endif
}
}
- (BOOL)supportsPropWithName:(nonnull NSString *)name
{
// Implemented in `ExpoFabricView.swift`
return NO;
}
- (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height
{
if (_state) {
float widthValue = width ? [width floatValue] : std::numeric_limits<float>::quiet_NaN();
float heightValue = height ? [height floatValue] : std::numeric_limits<float>::quiet_NaN();
#if REACT_NATIVE_TARGET_VERSION >= 82
// synchronous update is only available in React Native 0.82 and above
_state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue), EventQueue::UpdateMode::unstable_Immediate);
#else
_state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue));
#endif
}
}
#pragma mark - Component registration
+ (void)registerComponent:(nonnull EXViewModuleWrapper *)viewModule appContext:(nonnull EXAppContext *)appContext
{
Class wrappedViewModuleClass = [EXViewModuleWrapper createViewModuleWrapperClassWithModule:viewModule appId:appContext.appIdentifier];
Class viewClass = [ExpoFabricView makeViewClassForAppContext:appContext
moduleName:[viewModule moduleName]
viewName:[viewModule viewName]
className:NSStringFromClass(wrappedViewModuleClass)];
[[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:viewClass];
}
@end