// Copyright 2022-present 650 Industries. All rights reserved. #import #import #import #import #import #import #import #import 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::Flavor> _componentFlavorsCache; @implementation ExpoFabricViewObjC { ExpoViewShadowNode<>::ConcreteState::Shared _state; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _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(className); } react::ComponentName componentName = react::ComponentName { flavor->c_str() }; react::ComponentHandle componentHandle = reinterpret_cast(componentName); return react::ComponentDescriptorProvider { componentHandle, componentName, flavor, &facebook::react::concreteComponentDescriptorConstructor> }; } - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; if (updateMask & RNComponentViewUpdateMaskProps) { const auto &newProps = static_cast(*_props); NSMutableDictionary *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(*_eventEmitter); eventEmitter.dispatch([normalizeEventName(eventName) UTF8String], [payload](jsi::Runtime &runtime) { return expo::convertObjCObjectToJSIValue(runtime, payload); }); } #pragma mark - Methods to override in Swift - (void)updateProps:(nonnull NSDictionary *)props { // Implemented in `ExpoFabricView.swift` } - (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState { _state = std::static_pointer_cast::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), react::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::quiet_NaN(); float heightValue = height ? [height floatValue] : std::numeric_limits::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), react::EventQueue::UpdateMode::unstable_Immediate); #else _state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue)); #endif } } #pragma mark - Component registration + (void)registerComponent:(nonnull id)viewModule appContext:(nonnull id)appContext { // Cache classes and selectors using dispatch_once to avoid repeated lookups static Class viewModuleWrapperClass; static Class expoFabricViewClass; static SEL createWrapperSelector; static SEL makeViewSelector; static SEL moduleNameSelector; static SEL viewNameSelector; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // EXViewModuleWrapper is defined in Swift - use runtime lookup viewModuleWrapperClass = NSClassFromString(@"EXViewModuleWrapper"); if (!viewModuleWrapperClass) { NSLog(@"[ExpoFabricViewObjC] Warning: Could not find class " @"'EXViewModuleWrapper'"); } // ExpoFabricView is defined in Swift - use runtime lookup expoFabricViewClass = NSClassFromString(@"ExpoFabricView"); if (!expoFabricViewClass) { NSLog(@"[ExpoFabricViewObjC] Warning: Could not find class " @"'ExpoFabricView'"); } // Cache all selectors createWrapperSelector = NSSelectorFromString(@"createViewModuleWrapperClassWithModule:appId:"); makeViewSelector = NSSelectorFromString( @"makeViewClassForAppContext:moduleName:viewName:className:"); moduleNameSelector = NSSelectorFromString(@"moduleName"); viewNameSelector = NSSelectorFromString(@"viewName"); }); if (!viewModuleWrapperClass) { NSLog(@"[ExpoFabricViewObjC] Error: Cannot register component - " @"EXViewModuleWrapper class not found"); return; } if (!expoFabricViewClass) { NSLog(@"[ExpoFabricViewObjC] Error: Cannot register component - " @"ExpoFabricView class not found"); return; } Class wrappedViewModuleClass = nil; NSString *appId = [appContext appIdentifier]; if (![viewModuleWrapperClass respondsToSelector:createWrapperSelector]) { NSLog(@"[ExpoFabricViewObjC] Error: EXViewModuleWrapper does not respond " @"to createViewModuleWrapperClassWithModule:appId:"); return; } NSMethodSignature *sig = [viewModuleWrapperClass methodSignatureForSelector:createWrapperSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setTarget:viewModuleWrapperClass]; [invocation setSelector:createWrapperSelector]; [invocation setArgument:&viewModule atIndex:2]; [invocation setArgument:&appId atIndex:3]; [invocation invoke]; [invocation getReturnValue:&wrappedViewModuleClass]; if (!wrappedViewModuleClass) { NSLog(@"[ExpoFabricViewObjC] Error: Failed to create wrapped view module " @"class for module: %@", viewModule); return; } if (![expoFabricViewClass respondsToSelector:makeViewSelector]) { NSLog(@"[ExpoFabricViewObjC] Error: ExpoFabricView does not respond to " @"makeViewClassForAppContext:moduleName:viewName:className:"); return; } // Get moduleName and viewName from viewModule using cached selectors NSString *moduleName = nil; NSString *viewName = nil; if ([viewModule respondsToSelector:moduleNameSelector]) { moduleName = [viewModule performSelector:moduleNameSelector]; } else { NSLog(@"[ExpoFabricViewObjC] Warning: viewModule does not respond to " @"moduleName selector"); } if ([viewModule respondsToSelector:viewNameSelector]) { viewName = [viewModule performSelector:viewNameSelector]; } else { NSLog(@"[ExpoFabricViewObjC] Warning: viewModule does not respond to " @"viewName selector"); } NSString *className = NSStringFromClass(wrappedViewModuleClass); NSMethodSignature *makeViewSig = [expoFabricViewClass methodSignatureForSelector:makeViewSelector]; NSInvocation *makeViewInvocation = [NSInvocation invocationWithMethodSignature:makeViewSig]; [makeViewInvocation setTarget:expoFabricViewClass]; [makeViewInvocation setSelector:makeViewSelector]; id appContextArg = appContext; [makeViewInvocation setArgument:&appContextArg atIndex:2]; [makeViewInvocation setArgument:&moduleName atIndex:3]; [makeViewInvocation setArgument:&viewName atIndex:4]; [makeViewInvocation setArgument:&className atIndex:5]; [makeViewInvocation invoke]; Class viewClass = nil; [makeViewInvocation getReturnValue:&viewClass]; if (viewClass) { [[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:viewClass]; } else { NSLog(@"[ExpoFabricViewObjC] Error: Failed to create view class for " @"module: %@, view: %@", moduleName, viewName); } } @end