From 06ef96a98844e2e9f6e8ef9a5add265010a7aeb4 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Wed, 10 Jun 2026 00:17:48 +0200 Subject: [PATCH 1/3] Add External Image Repository draft --- .../RNSExternalImageRepository+Internal.h | 11 +++ .../RNSExternalImageRepository.h | 35 ++++++++ .../RNSExternalImageRepository.mm | 88 +++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 ios/integrations/image-repository/RNSExternalImageRepository+Internal.h create mode 100644 ios/integrations/image-repository/RNSExternalImageRepository.h create mode 100644 ios/integrations/image-repository/RNSExternalImageRepository.mm diff --git a/ios/integrations/image-repository/RNSExternalImageRepository+Internal.h b/ios/integrations/image-repository/RNSExternalImageRepository+Internal.h new file mode 100644 index 0000000000..869b1443d7 --- /dev/null +++ b/ios/integrations/image-repository/RNSExternalImageRepository+Internal.h @@ -0,0 +1,11 @@ +#pragma once + +#import "RNSExternalImageRepository.h" + +typedef void (^ImageCallback)(NSString *, UIImage *); + +@interface RNSExternalImageRepository () + +- (nullable UIImage *)imageForKey:(nonnull NSString *)key withInsertionCallback:(ImageCallback)callback; + +@end diff --git a/ios/integrations/image-repository/RNSExternalImageRepository.h b/ios/integrations/image-repository/RNSExternalImageRepository.h new file mode 100644 index 0000000000..53c6052172 --- /dev/null +++ b/ios/integrations/image-repository/RNSExternalImageRepository.h @@ -0,0 +1,35 @@ +#pragma once + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * External image repository. + * + */ +@interface RNSExternalImageRepository : NSObject + ++ (instancetype)sharedInstance; + +/** + * This method will trigger an assertion error in debug mode if the key is nullish. + * In release it'll return `NO`. + */ +- (BOOL)insertImage:(nullable UIImage *)image forKey:(nonnull NSString *)key; + +/** + * This method will trigger an assertion error in debug mode if the key is nullish. + * In release it'll return `nil`. + */ +- (nullable UIImage *)imageForKey:(nonnull NSString *)key; + +/** + * This method will trigger an assertion error in debug mode if the key is nullish. + * In release it'll return `NO`. + */ +- (BOOL)removeImageForKey:(nonnull NSString *)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/integrations/image-repository/RNSExternalImageRepository.mm b/ios/integrations/image-repository/RNSExternalImageRepository.mm new file mode 100644 index 0000000000..1a7eec4089 --- /dev/null +++ b/ios/integrations/image-repository/RNSExternalImageRepository.mm @@ -0,0 +1,88 @@ +#import "RNSExternalImageRepository.h" +#import +#import "RNSExternalImageRepository+Internal.h" + +@implementation RNSExternalImageRepository { + NSMutableDictionary *_imageRegistry; + NSMutableDictionary *_callbackRegistry; +} + ++ (instancetype)sharedInstance +{ + static RNSExternalImageRepository *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [RNSExternalImageRepository new]; + }); + + return instance; +} + +- (BOOL)insertImage:(nullable UIImage *)image forKey:(nonnull NSString *)key +{ + RCTAssert(key != nil, @"[RNScreens] Nullish key on attempt to insert image: %@ to the registry", image); + + if ([[self requireStorage] objectForKey:key]) { + return NO; + } + + [[self requireStorage] setObject:image forKey:key]; + _Nullable ImageCallback imageCallback = [[self requireCallbackRegistry] valueForKey:key]; + if (imageCallback != nil) { + imageCallback(key, image); + } + return YES; +} + +- (nullable UIImage *)imageForKey:(nonnull NSString *)key +{ + return [[self requireStorage] objectForKey:key]; +} + +- (BOOL)removeImageForKey:(nonnull NSString *)key +{ + if ([[self requireStorage] objectForKey:key]) { + [[self requireStorage] removeObjectForKey:key]; + return YES; + } + return NO; +} + +- (nullable UIImage *)imageForKey:(nonnull NSString *)key withInsertionCallback:(ImageCallback)callback +{ + UIImage *image = [self imageForKey:key]; + + if (image != nil) { + return image; + } + + if (callback == nil) { + return nil; + } + + if ([[self requireCallbackRegistry] objectForKey:key]) { + RCTAssert(NO, @"[RNScreens] A callback is already registered for image with key: %@", key); + return nil; + } + + [[self requireCallbackRegistry] setValue:callback forKey:key]; + return nil; +} + +- (NSMutableDictionary *)requireStorage +{ + if (_imageRegistry == nil) { + _imageRegistry = [NSMutableDictionary new]; + } + return _imageRegistry; +} + +- (NSMutableDictionary *)requireCallbackRegistry +{ + if (_callbackRegistry == nil) { + _callbackRegistry = [NSMutableDictionary new]; + } + return _callbackRegistry; +} + +@end From a4b06a1acc5497b476b6d2abc7f8849829c52c3c Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Wed, 10 Jun 2026 11:55:37 +0200 Subject: [PATCH 2/3] Treat all header files explicitly marked as internal as project header files --- RNScreens.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNScreens.podspec b/RNScreens.podspec index eb93ca8a9b..20d8dfa989 100644 --- a/RNScreens.podspec +++ b/RNScreens.podspec @@ -50,7 +50,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_supported_ios_version, :tvos => min_supported_tvos_version, :visionos => min_supported_visionos_version } s.source = { :git => "https://github.com/software-mansion/react-native-screens.git", :tag => "#{s.version}" } s.source_files = source_files - s.project_header_files = "ios/bridging/Swift-Bridging.h" + s.project_header_files = "ios/bridging/Swift-Bridging.h", "ios/**/*Internal.h" s.requires_arc = true if !gamma_project_enabled From 50bacd91f354462b100760079eb24a1a24dbb140 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Thu, 11 Jun 2026 10:32:50 +0200 Subject: [PATCH 3/3] Push work with image repository --- ios/helpers/image/RNSImageLoadingHelper.h | 58 +++++++ ios/helpers/image/RNSImageLoadingHelper.mm | 149 ++++++++++++++++++ .../RNSTabsScreenComponentView+Internal.h | 11 ++ ios/tabs/screen/RNSTabsScreenComponentView.mm | 7 + 4 files changed, 225 insertions(+) create mode 100644 ios/tabs/screen/RNSTabsScreenComponentView+Internal.h diff --git a/ios/helpers/image/RNSImageLoadingHelper.h b/ios/helpers/image/RNSImageLoadingHelper.h index b0ef5b0ba4..741438e650 100644 --- a/ios/helpers/image/RNSImageLoadingHelper.h +++ b/ios/helpers/image/RNSImageLoadingHelper.h @@ -3,8 +3,66 @@ #import #import +typedef NS_ENUM(NSInteger, RNSImageType) { + RNSImageTypeImage, + RNSImageTypeTemplate, + RNSImageTypeSfSymbol, + RNSImageTypeXcasset, +}; + +typedef NS_ENUM(NSInteger, RNSImageSourceType) { + RNSImageSourceTypeResourceName, + RNSImageSourceTypeReactImageSource, +}; + +@interface RNSImageSource : NSObject + +@property (nonatomic, readonly) RNSImageSourceType imageSourceType; + ++ (nullable RNSImageSource *)imageSourceDescriptorWithResourceName:(nullable NSString *)resourceName; + ++ (nullable RNSImageSource *)xcassetSourceDescriptorWithResourceName:(nullable NSString *)resourceName; + ++ (nullable RNSImageSource *)sfSymbolSourceDescriptorWithResourceName:(nullable NSString *)resourceName; + ++ (nullable RNSImageSource *)reactImageSourceDescriptorWithImageSource:(nullable RCTImageSource *)imageSource; + +@end + +@interface RNSResourceNameSourceDescriptor : RNSImageSource + +- (nullable instancetype)initWithResourceName:(nullable NSString *)resourceName; + +@property (nonatomic, readonly, nullable) NSString *resourceName; + +@end + +@interface RNSReactImageSourceSourceDescriptor : RNSImageSource + +- (nullable instancetype)initWithReactImageSource:(nullable RCTImageSource *)imageSource; + +@property (nonatomic, readonly, nullable) RCTImageSource *imageSource; + +@end + +/** + * Information necessary to load an image with image loading helper. + */ +@interface RNSImageDescriptor : NSObject + +@property (nonatomic, readonly) RNSImageType imageType; + +@property (nonatomic, readonly, nonnull) RNSImageSource *imageSource; + +@end + @interface RNSImageLoadingHelper : NSObject ++ (void)loadImageFromDescriptor:(nonnull RNSImageDescriptor *)imageDescriptor + withImageLoader:(nonnull RCTImageLoader *)reactImageLoader + asTemplate:(BOOL)isTemplate + completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock; + /** * Should be called from UI thread only. * If done so, the method **tries** to load the image synchronously from image source represented in JSON via diff --git a/ios/helpers/image/RNSImageLoadingHelper.mm b/ios/helpers/image/RNSImageLoadingHelper.mm index 64beb499a6..fea0f9929f 100644 --- a/ios/helpers/image/RNSImageLoadingHelper.mm +++ b/ios/helpers/image/RNSImageLoadingHelper.mm @@ -3,6 +3,36 @@ @implementation RNSImageLoadingHelper ++ (void)loadImageFromDescriptor:(nonnull RNSImageDescriptor *)imageDescriptor + withImageLoader:(nonnull RCTImageLoader *)reactImageLoader + asTemplate:(BOOL)isTemplate + completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock +{ + RCTAssert(RCTIsMainQueue(), @"[RNScreens] Expected to run on the main queue"); + RCTAssert(imageDescriptor != nil, @"[RNScreens] Expected non-null image descriptor"); + RCTAssert(reactImageLoader != nil, @"[RNScreens] Expected non-null image loader"); + + auto completionBlock = ^(UIImage *image) { + imageLoadingCompletionBlock([self handleRenderingModeForImage:image isTemplate:isTemplate]); + }; + + if (imageDescriptor == nil || reactImageLoader == nil) { + return; + } + + switch (imageDescriptor.imageType) { + case RNSImageTypeImage: + break; + case RNSImageTypeTemplate: + break; + case RNSImageTypeSfSymbol: + [self loadSfSymbolFromSource:imageDescriptor.imageSource completionBlock:completionBlock]; + break; + case RNSImageTypeXcasset: + break; + } +} + + (void)loadImageSyncIfPossibleFromJsonSource:(nonnull NSDictionary *)jsonImageSource withImageLoader:(nonnull RCTImageLoader *)imageLoader asTemplate:(BOOL)isTemplate @@ -68,4 +98,123 @@ + (nullable UIImage *)handleRenderingModeForImage:(nullable UIImage *)image isTe } } ++ (void)loadSfSymbolFromSource:(nonnull RNSImageSource *)sourceDescriptor + completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock +{ + if (sourceDescriptor.imageSourceType != RNSImageSourceTypeResourceName) { + imageLoadingCompletionBlock(nil); + return; + } + + const auto *resourceNameSource = static_cast(sourceDescriptor); + UIImage *_Nullable loadedImage = [UIImage systemImageNamed:resourceNameSource.resourceName]; + imageLoadingCompletionBlock(loadedImage); +} + ++ (void)loadTemplateFromSource:(nonnull RNSImageSource *)sourceDescriptor + withImageLoader:(nonnull RCTImageLoader *)imageLoader + completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock +{ + if (sourceDescriptor.imageSourceType != RNSImageSourceTypeReactImageSource) { + imageLoadingCompletionBlock(nil); + return; + } + + const auto *reactImageSource = static_cast(sourceDescriptor); + [self loadImageFromSource:reactImageSource.imageSource + withImageLoader:imageLoader + asTemplate:YES + completionBlock:imageLoadingCompletionBlock]; +} + ++ (void)loadXcassetFromSource:(nonnull RNSImageSource *)imageSource + completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock +{ + if (imageSource.imageSourceType != RNSImageSourceTypeResourceName) { + imageLoadingCompletionBlock(nil); + return; + } + const auto *resourceNameSource = static_cast(imageSource); + UIImage *_Nullable loadedImage = [UIImage imageNamed:resourceNameSource.resourceName]; + imageLoadingCompletionBlock(loadedImage); +} + +@end + +@interface RNSImageSource () + +@property (nonatomic) RNSImageSourceType imageSourceType; + +@end + +@implementation RNSImageSource + ++ (nullable RNSImageSource *)imageSourceDescriptorWithResourceName:(nullable NSString *)resourceName +{ + if (resourceName == nil) { + return nil; + } + + return [[RNSResourceNameSourceDescriptor alloc] initWithResourceName:resourceName]; +} + ++ (nullable RNSImageSource *)xcassetSourceDescriptorWithResourceName:(nullable NSString *)resourceName +{ + if (resourceName == nil) { + return nil; + } + + return [[RNSResourceNameSourceDescriptor alloc] initWithResourceName:resourceName]; +} + ++ (nullable RNSImageSource *)sfSymbolSourceDescriptorWithResourceName:(nullable NSString *)resourceName +{ + if (resourceName == nil) { + return nil; + } + + return [[RNSResourceNameSourceDescriptor alloc] initWithResourceName:resourceName]; +} + ++ (nullable RNSImageSource *)reactImageSourceDescriptorWithImageSource:(nullable RCTImageSource *)imageSource +{ + if (imageSource == nil) { + return nil; + } + return [[RNSReactImageSourceSourceDescriptor alloc] initWithReactImageSource:imageSource]; +} + +@end + +@implementation RNSResourceNameSourceDescriptor + +- (nullable instancetype)initWithResourceName:(nullable NSString *)resourceName +{ + if (self = [super init]) { + self.imageSourceType = RNSImageSourceTypeResourceName; + _resourceName = resourceName; + } + return self; +} + +@end + +@implementation RNSReactImageSourceSourceDescriptor + +- (nullable instancetype)initWithReactImageSource:(nullable RCTImageSource *)imageSource +{ + if (self = [super init]) { + self.imageSourceType = RNSImageSourceTypeReactImageSource; + _imageSource = imageSource; + } + return self; +} + +@end + +/** + * Information necessary to load an image with image loading helper. + */ +@implementation RNSImageDescriptor + @end diff --git a/ios/tabs/screen/RNSTabsScreenComponentView+Internal.h b/ios/tabs/screen/RNSTabsScreenComponentView+Internal.h new file mode 100644 index 0000000000..45537128e6 --- /dev/null +++ b/ios/tabs/screen/RNSTabsScreenComponentView+Internal.h @@ -0,0 +1,11 @@ +#pragma once + +#import "RNSTabsScreenComponentView.h" + +@class RNSImageSource; + +@interface RNSTabsScreenComponentView () + +- (nullable RNSImageSource *)createIconImageSource; + +@end diff --git a/ios/tabs/screen/RNSTabsScreenComponentView.mm b/ios/tabs/screen/RNSTabsScreenComponentView.mm index 4c3e65a864..8644e72739 100644 --- a/ios/tabs/screen/RNSTabsScreenComponentView.mm +++ b/ios/tabs/screen/RNSTabsScreenComponentView.mm @@ -2,12 +2,14 @@ #import "NSString+RNSUtility.h" #import "RNSConversions.h" #import "RNSDefines.h" +#import "RNSImageLoadingHelper.h" #import "RNSLog.h" #import "RNSSafeAreaViewNotifications.h" #import "RNSScrollViewFinder.h" #import "RNSScrollViewHelper.h" #import "RNSTabBarAppearanceCoordinator.h" #import "RNSTabBarController.h" +#import "RNSTabsScreenComponentView+Internal.h" #import #import @@ -104,6 +106,11 @@ - (void)invalidateImpl }); } +//- (nullable RNSImageSource *)createIconImageSource +//{ +// +//} + #pragma mark - Events - (nonnull RNSTabsScreenEventEmitter *)reactEventEmitter