Skip to content

Commit 96c6839

Browse files
committed
feat(react-native): support JS-defined UIKit views
1 parent 3851614 commit 96c6839

12 files changed

Lines changed: 749 additions & 2 deletions

packages/react-native/NativeScriptNativeApi.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require "json"
22

33
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4+
fabric_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1"
45

56
folly_compiler_flags = "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1 -Wno-comma -Wno-shorten-64-to-32"
67

@@ -19,6 +20,7 @@ Pod::Spec.new do |s|
1920
"ios/**/*.{h,mm}",
2021
"native-api-jsi/**/*.{h,mm}"
2122
]
23+
s.exclude_files = "ios/Fabric/**/*" unless fabric_enabled
2224
s.public_header_files = "ios/**/*.h"
2325
s.resource_bundles = {
2426
"NativeScriptNativeApi" => ["metadata/*.nsmd"]

packages/react-native/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,66 @@ await NativeScript.runOnUI(() => {
2727
});
2828
```
2929

30+
## Defining native UIKit views in JS
31+
32+
Use `defineUIKitView()` to turn a NativeScript-created `UIView` tree into a
33+
normal React Native component. The package owns the RN host view; your
34+
definition owns the UIKit subtree. `create`, `update`, `mounted`, and `dispose`
35+
run through the NativeScript UI dispatcher, so UIKit calls are safe and use the
36+
same globals and iOS SDK types as NativeScript.
37+
38+
```tsx
39+
import NativeScript, {defineUIKitView} from "@nativescript/react-native";
40+
import type {UIKitViewRef} from "@nativescript/react-native";
41+
42+
NativeScript.init();
43+
44+
type BadgeProps = {
45+
title: string;
46+
tone?: "blue" | "green";
47+
};
48+
49+
export const NativeBadge = defineUIKitView<BadgeProps, UIView>({
50+
displayName: "NativeBadge",
51+
create() {
52+
const view = UIView.alloc().initWithFrame(CGRectZero);
53+
const label = UILabel.alloc().initWithFrame(CGRectZero);
54+
label.tag = 1;
55+
label.textAlignment = NSTextAlignment.Center;
56+
label.textColor = UIColor.whiteColor;
57+
label.autoresizingMask =
58+
UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
59+
view.addSubview(label);
60+
return view;
61+
},
62+
update(view, props) {
63+
view.backgroundColor =
64+
props.tone === "green" ? UIColor.systemGreenColor : UIColor.systemBlueColor;
65+
view.layer.cornerRadius = 12;
66+
view.clipsToBounds = true;
67+
const label = view.viewWithTag(1) as UILabel;
68+
label.text = props.title;
69+
},
70+
});
71+
72+
<NativeBadge title="UIKit from JS" tone="blue" style={{height: 48}} />;
73+
```
74+
75+
Forward a ref when you need imperative access:
76+
77+
```tsx
78+
const badgeRef = useRef<UIKitViewRef<UIView>>(null);
79+
80+
await badgeRef.current?.runOnUI((view) => {
81+
view.alpha = 0.8;
82+
});
83+
```
84+
85+
React Native view props such as `style`, `testID`, accessibility props, responder
86+
props, and `pointerEvents` go to the host component. Your own props go to the
87+
UIKit definition; use `nativeProps(props)` when a plugin prop should also affect
88+
the RN host.
89+
3090
The published package includes generated NativeScript metadata, the libffi
3191
xcframework, and generated iOS SDK TypeScript declarations. Build it from the
3292
repository root with:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#import <React/RCTViewComponentView.h>
2+
3+
@interface NativeScriptUIViewComponentView : RCTViewComponentView
4+
@end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#import "NativeScriptUIViewComponentView.h"
2+
3+
#import <React/RCTFabricComponentsPlugins.h>
4+
#import <react/renderer/components/NativeScriptNativeApiSpec/ComponentDescriptors.h>
5+
#import <react/renderer/components/NativeScriptNativeApiSpec/Props.h>
6+
7+
#import "NativeScriptUIView.h"
8+
9+
using namespace facebook::react;
10+
11+
@implementation NativeScriptUIViewComponentView {
12+
NativeScriptUIView* _containerView;
13+
}
14+
15+
+ (void)load {
16+
[super load];
17+
}
18+
19+
- (instancetype)initWithFrame:(CGRect)frame {
20+
if (self = [super initWithFrame:frame]) {
21+
static const auto defaultProps = std::make_shared<const NativeScriptUIViewProps>();
22+
_props = defaultProps;
23+
24+
_containerView = [[NativeScriptUIView alloc] initWithFrame:self.bounds];
25+
_containerView.autoresizingMask =
26+
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
27+
self.contentView = _containerView;
28+
}
29+
30+
return self;
31+
}
32+
33+
- (void)dealloc {
34+
[_containerView release];
35+
[super dealloc];
36+
}
37+
38+
- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps {
39+
const auto oldViewProps = std::static_pointer_cast<const NativeScriptUIViewProps>(_props);
40+
const auto newViewProps = std::static_pointer_cast<const NativeScriptUIViewProps>(props);
41+
const std::string oldNativeViewHandle = oldViewProps->nativeViewHandle;
42+
const std::string newNativeViewHandle = newViewProps->nativeViewHandle;
43+
44+
[super updateProps:props oldProps:oldProps];
45+
46+
if (oldNativeViewHandle != newNativeViewHandle) {
47+
NSString* nativeViewHandle = newNativeViewHandle.empty()
48+
? nil
49+
: [NSString stringWithUTF8String:newNativeViewHandle.c_str()];
50+
_containerView.nativeViewHandle = nativeViewHandle;
51+
}
52+
}
53+
54+
- (void)prepareForRecycle {
55+
[super prepareForRecycle];
56+
_containerView.nativeViewHandle = nil;
57+
}
58+
59+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
60+
return concreteComponentDescriptorProvider<NativeScriptUIViewComponentDescriptor>();
61+
}
62+
63+
@end
64+
65+
Class<RCTComponentViewProtocol> NativeScriptUIViewCls(void) {
66+
return NativeScriptUIViewComponentView.class;
67+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <UIKit/UIKit.h>
2+
3+
@interface NativeScriptUIView : UIView
4+
5+
@property(nonatomic, copy) NSString* nativeViewHandle;
6+
7+
@end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#import "NativeScriptUIView.h"
2+
3+
static UIView* NativeScriptUIViewFromHandle(NSString* handle) {
4+
if (handle == nil || handle.length == 0) {
5+
return nil;
6+
}
7+
8+
const char* text = handle.UTF8String;
9+
if (text == nullptr || text[0] == '\0') {
10+
return nil;
11+
}
12+
13+
char* end = nullptr;
14+
unsigned long long address = strtoull(text, &end, 0);
15+
if (address == 0 || end == text) {
16+
return nil;
17+
}
18+
19+
id object = reinterpret_cast<id>(static_cast<uintptr_t>(address));
20+
if (object == nil || ![object isKindOfClass:UIView.class]) {
21+
return nil;
22+
}
23+
24+
return static_cast<UIView*>(object);
25+
}
26+
27+
@implementation NativeScriptUIView {
28+
UIView* _nativeView;
29+
}
30+
31+
- (void)dealloc {
32+
[_nativeView removeFromSuperview];
33+
[_nativeView release];
34+
[_nativeViewHandle release];
35+
[super dealloc];
36+
}
37+
38+
- (void)setNativeViewHandle:(NSString*)nativeViewHandle {
39+
if ((_nativeViewHandle == nativeViewHandle) ||
40+
[_nativeViewHandle isEqualToString:nativeViewHandle]) {
41+
return;
42+
}
43+
44+
[_nativeViewHandle release];
45+
_nativeViewHandle = [nativeViewHandle copy];
46+
[self setNativeView:NativeScriptUIViewFromHandle(_nativeViewHandle)];
47+
}
48+
49+
- (void)setNativeView:(UIView*)nativeView {
50+
if (_nativeView == nativeView) {
51+
return;
52+
}
53+
54+
[_nativeView removeFromSuperview];
55+
[_nativeView release];
56+
_nativeView = nil;
57+
58+
if (nativeView == nil) {
59+
return;
60+
}
61+
62+
_nativeView = [nativeView retain];
63+
[_nativeView removeFromSuperview];
64+
_nativeView.frame = self.bounds;
65+
_nativeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
66+
[self addSubview:_nativeView];
67+
[self setNeedsLayout];
68+
}
69+
70+
- (void)layoutSubviews {
71+
[super layoutSubviews];
72+
_nativeView.frame = self.bounds;
73+
}
74+
75+
@end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#import <React/RCTViewManager.h>
2+
3+
#import "NativeScriptUIView.h"
4+
5+
@interface NativeScriptUIViewManager : RCTViewManager
6+
@end
7+
8+
@implementation NativeScriptUIViewManager
9+
10+
RCT_EXPORT_MODULE(NativeScriptUIView)
11+
12+
- (UIView*)view {
13+
return [[[NativeScriptUIView alloc] initWithFrame:CGRectZero] autorelease];
14+
}
15+
16+
RCT_EXPORT_VIEW_PROPERTY(nativeViewHandle, NSString)
17+
18+
@end

packages/react-native/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@
3838
},
3939
"codegenConfig": {
4040
"name": "NativeScriptNativeApiSpec",
41-
"type": "modules",
41+
"type": "all",
4242
"jsSrcsDir": "src",
4343
"android": {
4444
"javaPackageName": "org.nativescript.nativeapi"
4545
},
4646
"ios": {
47+
"componentProvider": {
48+
"NativeScriptUIView": "NativeScriptUIViewComponentView"
49+
},
4750
"modulesProvider": {
4851
"NativeScriptNativeApi": "NativeScriptNativeApiModuleProvider"
4952
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type {HostComponent, ViewProps} from 'react-native';
2+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
3+
4+
export interface NativeProps extends ViewProps {
5+
nativeViewHandle?: string;
6+
}
7+
8+
export default codegenNativeComponent<NativeProps>(
9+
'NativeScriptUIView',
10+
) as HostComponent<NativeProps>;

packages/react-native/src/index.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
/// <reference path="../types/ios/index.d.ts" />
22

3+
import type {
4+
ForwardRefExoticComponent,
5+
PropsWithoutRef,
6+
RefAttributes,
7+
} from 'react';
8+
import type {ViewProps} from 'react-native';
9+
310
export type NativeApiHost = {
411
runtime?: string;
512
backend?: string;
@@ -30,20 +37,49 @@ export type InstallOptions = {
3037
globals?: boolean;
3138
};
3239

40+
export type UIKitViewDefinition<Props extends object, NativeView = unknown> = {
41+
displayName?: string;
42+
create: (props: Readonly<Props & ViewProps>) => NativeView;
43+
update?: (
44+
view: NativeView,
45+
props: Readonly<Props & ViewProps>,
46+
previousProps?: Readonly<Props & ViewProps>,
47+
) => void;
48+
mounted?: (view: NativeView, props: Readonly<Props & ViewProps>) => void;
49+
dispose?: (view: NativeView, props: Readonly<Props & ViewProps>) => void;
50+
nativeProps?: (
51+
props: Readonly<Props & ViewProps>,
52+
) => Partial<ViewProps> | undefined;
53+
};
54+
55+
export type UIKitViewRef<NativeView = unknown> = {
56+
readonly nativeView: NativeView | null;
57+
runOnUI: (callback: (view: NativeView) => void) => Promise<void>;
58+
};
59+
60+
export type UIKitViewComponent<Props extends object, NativeView = unknown> =
61+
ForwardRefExoticComponent<
62+
PropsWithoutRef<Props & ViewProps> & RefAttributes<UIKitViewRef<NativeView>>
63+
>;
64+
3365
export function init(metadataPath?: string, options?: InstallOptions): boolean;
3466
export const install: typeof init;
3567
export function installGlobals(): boolean;
3668
export function isInstalled(): boolean;
3769
export function defaultMetadataPath(): string;
3870
export function getRuntimeBackend(): string;
3971
export function runOnUI(callback?: () => void): Promise<void>;
72+
export function defineUIKitView<Props extends object, NativeView = unknown>(
73+
definition: UIKitViewDefinition<Props, NativeView>,
74+
): UIKitViewComponent<Props, NativeView>;
4075

4176
declare const NativeScript: {
4277
init: typeof init;
4378
install: typeof install;
4479
installGlobals: typeof installGlobals;
4580
isInstalled: typeof isInstalled;
4681
defaultMetadataPath: typeof defaultMetadataPath;
82+
defineUIKitView: typeof defineUIKitView;
4783
getRuntimeBackend: typeof getRuntimeBackend;
4884
runOnUI: typeof runOnUI;
4985
};

0 commit comments

Comments
 (0)