Skip to content

Commit 4837a83

Browse files
johankasperikkafar
andauthored
fix(iOS): load header items icons synchronously when feasible (#3355)
## Description When using RCTImageLoader for images in headerRightItems/headerLeftItems the asynchronous `loadImageWithURLRequest` produces som layout bugs as shown under "Screenshots". These are: 1. When setting title and image the title is briefly visible before the image is set. 2. When setting only image the bar button item is briefly shown as a very wide item without any content. ## Changes 1. If the image is a `ImageRequireSource` load the image with `[RCTConvert UIImage]`. I know its deprecated but thats the only synchronous image loader that I know of. 2. If the image is a `ImageURISource` set the bar button title in the completion block of `loadImageWithURLRequest` 3. Updates `BarButtonItems` example to latest types/api from react-navigation/native-stack. ## Screenshots / GIFs ### Before **Title briefly shown** https://github.com/user-attachments/assets/0b964e1a-cfea-4be8-bdf8-a50297ceee9e **Wide bar button item while `loadImageWithURLRequest` completes** https://github.com/user-attachments/assets/0c2fcca0-1860-4cea-b4fe-16d07a5e3a95 ### After https://github.com/user-attachments/assets/653f4c16-c1ff-4aea-bd18-211a1e6eb404 ## Test code and steps to reproduce Use any of the examples with required images in BarButtonItems ## Checklist - [x] Included code example that can be used to test this change - [ ] Updated TS types - [ ] Updated documentation: <!-- For adding new props to native-stack --> - [ ] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [ ] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent b9572f9 commit 4837a83

File tree

5 files changed

+95
-47
lines changed

5 files changed

+95
-47
lines changed

apps/Example.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import Orientation from './src/screens/Orientation';
3030
import SearchBar from './src/screens/SearchBar';
3131
import Events from './src/screens/Events';
3232
import Gestures from './src/screens/Gestures';
33-
// import BarButtonItems from './src/screens/BarButtonItems';
33+
import BarButtonItems from './src/screens/BarButtonItems';
3434

3535
import { GestureDetectorProvider } from 'react-native-screens/gesture-handler';
3636
import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -128,11 +128,11 @@ const SCREENS: Record<
128128
component: Gestures,
129129
type: 'playground',
130130
},
131-
// BarButtonItems: {
132-
// title: 'Bar Button Items',
133-
// component: BarButtonItems,
134-
// type: 'playground',
135-
// },
131+
BarButtonItems: {
132+
title: 'Bar Button Items',
133+
component: BarButtonItems,
134+
type: 'playground',
135+
},
136136
};
137137

138138
if (isTestSectionEnabled()) {

ios/RNSBarButtonItem.mm

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#import <React/RCTConvert.h>
33
#import <React/RCTFont.h>
44
#import <React/RCTImageSource.h>
5-
#import <objc/runtime.h>
5+
#import "RCTImageSource+AccessHiddenMembers.h"
66
#import "RNSDefines.h"
77

88
@implementation RNSBarButtonItem {
@@ -20,48 +20,36 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict
2020
return self;
2121
}
2222

23-
self.title = dict[@"title"];
24-
23+
NSString *title = dict[@"title"];
2524
NSDictionary *imageSourceObj = dict[@"imageSource"];
2625
NSDictionary *templateSourceObj = dict[@"templateSource"];
27-
28-
RCTImageSource *imageSource = nil;
29-
if (imageSourceObj) {
30-
imageSource = [RCTConvert RCTImageSource:imageSourceObj];
31-
} else if (templateSourceObj) {
32-
imageSource = [RCTConvert RCTImageSource:templateSourceObj];
33-
}
34-
35-
if (imageSource) {
36-
[imageLoader loadImageWithURLRequest:imageSource.request
37-
size:imageSource.size
38-
scale:imageSource.scale
39-
clipped:true
40-
resizeMode:RCTResizeModeContain
41-
progressBlock:^(int64_t progress, int64_t total) {
42-
}
43-
partialLoadBlock:^(UIImage *_Nonnull image) {
44-
}
45-
completionBlock:^(NSError *_Nullable error, UIImage *_Nullable image) {
46-
dispatch_async(dispatch_get_main_queue(), ^{
47-
UIImage *imageWithRenderingMode = nil;
48-
if (imageSourceObj) {
49-
imageWithRenderingMode = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
50-
} else if (templateSourceObj) {
51-
imageWithRenderingMode = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
52-
}
53-
self.image = imageWithRenderingMode;
54-
});
55-
}];
56-
}
5726
NSString *sfSymbolName = dict[@"sfSymbolName"];
58-
if (sfSymbolName) {
27+
28+
if (imageSourceObj != nil) {
29+
void (^completionAction)(NSError *_Nullable, UIImage *_Nullable) =
30+
^(NSError *_Nullable error, UIImage *_Nullable image) {
31+
self.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
32+
};
33+
[self loadImageSyncFromImageSourceJson:imageSourceObj withImageLoader:imageLoader completionBlock:completionAction];
34+
} else if (templateSourceObj != nil) {
35+
void (^completionAction)(NSError *_Nullable, UIImage *_Nullable) =
36+
^(NSError *_Nullable error, UIImage *_Nullable image) {
37+
self.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
38+
};
39+
[self loadImageSyncFromImageSourceJson:templateSourceObj
40+
withImageLoader:imageLoader
41+
completionBlock:completionAction];
42+
} else if (sfSymbolName != nil) {
5943
self.image = [UIImage systemImageNamed:sfSymbolName];
6044
}
6145

62-
NSDictionary *titleStyle = dict[@"titleStyle"];
63-
if (titleStyle) {
64-
[self setTitleStyleFromConfig:titleStyle];
46+
if (title != nil) {
47+
self.title = title;
48+
49+
NSDictionary *titleStyle = dict[@"titleStyle"];
50+
if (titleStyle != nil) {
51+
[self setTitleStyleFromConfig:titleStyle];
52+
}
6553
}
6654

6755
id tintColorObj = dict[@"tintColor"];
@@ -339,4 +327,55 @@ - (void)setBadgeFromConfig:(NSDictionary *)badgeObj
339327
}
340328
#endif
341329

330+
/**
331+
* Should be called from UI thread only. If done so, the method **tries** to load the image synchronously.
332+
* There is no guarantee, because in release mode we rely on `RCTImageLoader` implementation details.
333+
* No matter how the image is loaded, `completionBlock` is executed on main queue.
334+
*/
335+
- (void)loadImageSyncFromImageSourceJson:(nonnull NSDictionary *)imageSourceJson
336+
withImageLoader:(nullable RCTImageLoader *)imageLoader
337+
completionBlock:
338+
(void (^_Nonnull)(NSError *_Nullable error, UIImage *_Nullable image))completion
339+
{
340+
RCTAssert(RCTIsMainQueue(), @"[RNScreens] Expected to run on main queue");
341+
342+
RCTImageSource *imageSource = [RCTConvert RCTImageSource:imageSourceJson];
343+
RCTAssert(imageSource != nil, @"[RNScreens] Expected nonnill image source");
344+
345+
// We use `+ [RCTConvert UIImage:]` only in debug mode, because it is deprecated, however
346+
// I haven't found different way to load image synchronously in debug other than
347+
// writing the code manually.
348+
349+
#if !defined(NDEBUG) // We're in debug mode here
350+
if (!imageSource.packagerAsset) {
351+
// This is rather unexpected. In debug mode local asset should be sourced from packager.
352+
RCTLogWarn(@"[RNScreens] Unexpected case during image load: loading not a packager asset");
353+
}
354+
355+
// Try to load anyway.
356+
UIImage *loadedImage = [RCTConvert UIImage:imageSourceJson];
357+
completion(nil, loadedImage);
358+
return;
359+
#else // We're in release mode here
360+
[imageLoader loadImageWithURLRequest:imageSource.request
361+
size:imageSource.size
362+
scale:imageSource.scale
363+
clipped:true
364+
resizeMode:RCTResizeModeContain
365+
progressBlock:^(int64_t progress, int64_t total) {
366+
}
367+
partialLoadBlock:^(UIImage *_Nonnull image) {
368+
}
369+
completionBlock:^(NSError *_Nullable error, UIImage *_Nullable image) {
370+
if (RCTIsMainQueue()) {
371+
completion(error, image);
372+
} else {
373+
dispatch_async(dispatch_get_main_queue(), ^{
374+
completion(error, image);
375+
});
376+
}
377+
}];
378+
#endif
379+
}
380+
342381
@end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
// This field should exist in extension in `RCTImageSource.m`
4+
5+
@interface RCTImageSource (AccessHiddenMembers)
6+
7+
@property (nonatomic, assign) BOOL packagerAsset;
8+
9+
@end

react-navigation

Submodule react-navigation updated 295 files

src/gesture-handler/fabricUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
import { View } from "react-native";
3+
import { View } from 'react-native';
44

55
/* eslint-disable */
66

@@ -24,11 +24,11 @@ export function getShadowNodeWrapperAndTagFromRef(ref: View | null): {
2424
return {
2525
shadowNodeWrapper: null,
2626
tag: -1,
27-
}
27+
};
2828
}
2929
const internalRef = ref as unknown as HostInstance;
3030
return {
3131
shadowNodeWrapper: internalRef.__internalInstanceHandle.stateNode.node,
3232
tag: internalRef.__nativeTag,
33-
}
33+
};
3434
}

0 commit comments

Comments
 (0)