Skip to content

Commit 2bc382a

Browse files
committed
feat(api): update TypeScript api
1 parent d9ce444 commit 2bc382a

File tree

6 files changed

+123
-53
lines changed

6 files changed

+123
-53
lines changed

src/SwiftUI.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropsWithChildren, ReactElement, ReactNode, useEffect } from "react";
1+
import { PropsWithChildren, ReactElement, ReactNode, useCallback, useEffect, useId } from "react";
22
import { StyleProp, ViewStyle } from "react-native";
33
import {
44
Button,
@@ -8,6 +8,7 @@ import {
88
HStack,
99
Image,
1010
LazyVGrid,
11+
NumberField,
1112
Picker,
1213
Rectangle,
1314
Section,
@@ -30,30 +31,39 @@ import {
3031
import { buildViewTree } from "./utils/viewTree";
3132

3233
export type SwiftUIProps = {
34+
id?: string;
3335
onEvent?: (event: { nativeEvent: NativeSwiftUIEvent }) => void;
3436
style?: StyleProp<ViewStyle>;
3537
};
3638

3739
export const SwiftUIRoot = ({
40+
id: rootId,
3841
children,
3942
style,
4043
onEvent: rootOnEvent,
4144
}: PropsWithChildren<SwiftUIProps>): ReactNode => {
4245
const { nativeRef, getEventHandler, nodesKey, getNodes, renderSequence } = useSwiftUIContext();
4346

47+
const log = useCallback(
48+
(message: string, ...args: unknown[]) => {
49+
console.log(`SwiftUIRoot(${rootId}) ${message}`, ...args);
50+
},
51+
[rootId],
52+
);
53+
4454
const nodes = getNodes();
45-
console.log(`SwiftUIRoot rendering with ${nodes.size} nodes`);
55+
log(`rendering with ${nodes.size} nodes`);
4656
renderSequence.current = []; // Reset render sequence
4757
useEffect(() => {
4858
const viewTree = buildViewTree(nodes, renderSequence.current);
49-
console.log("SwiftUIRoot updating view tree", viewTree);
59+
log(`updating view tree`, viewTree);
5060
nativeRef.current?.setNativeProps({ viewTree: JSON.stringify(viewTree) });
5161
// eslint-disable-next-line react-hooks/exhaustive-deps
5262
}, [nativeRef, nodesKey]);
5363

5464
const handleEvent: SwiftUIProps["onEvent"] = (event) => {
5565
const { id, name, value } = event.nativeEvent;
56-
console.log(`SwiftUIRoot received event "${name}" for id=${id}, value=${value}`);
66+
log(`received event "${name}" for id=${id}, value=${value}`);
5767
const handler = getEventHandler(id, name);
5868
if (handler) {
5969
handler(name === "change" ? value : ""); // Pass value only for change
@@ -69,10 +79,14 @@ export const SwiftUIRoot = ({
6979
};
7080

7181
export const SwiftUI = ({ children, ...props }: PropsWithChildren<SwiftUIProps>): ReactElement => {
82+
// eslint-disable-next-line react-hooks/rules-of-hooks, @typescript-eslint/prefer-nullish-coalescing
83+
const id = props.id || `root:${useId()}`;
7284
return (
73-
<SwiftUIProvider>
85+
<SwiftUIProvider id={id}>
7486
<SwiftUIParentIdProvider id="__root">
75-
<SwiftUIRoot {...props}>{children}</SwiftUIRoot>
87+
<SwiftUIRoot id={id} {...props}>
88+
{children}
89+
</SwiftUIRoot>
7690
</SwiftUIParentIdProvider>
7791
</SwiftUIProvider>
7892
);
@@ -95,6 +109,7 @@ SwiftUI.Spacer = Spacer;
95109
SwiftUI.Stepper = Stepper;
96110
SwiftUI.Text = Text;
97111
SwiftUI.TextField = TextField;
112+
SwiftUI.NumberField = NumberField;
98113
SwiftUI.Toggle = Toggle;
99114
SwiftUI.VStack = VStack;
100115
SwiftUI.ZStack = ZStack;

src/components/Picker.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReactNode, useMemo } from "react";
2+
import { StyleProp, StyleSheet } from "react-native";
23
import { useSwiftUINode } from "../hooks";
34
import type { NativeTextStyle } from "../types";
45

@@ -7,21 +8,23 @@ import type { NativeTextStyle } from "../types";
78
export type NativePickerStyle = "default" | "inline" | "menu" | "segmented" | "wheel";
89

910
export type NativePickerProps<T extends string> = {
10-
options: T[];
11-
selection?: string;
11+
options: readonly { value: T; label: string }[] | readonly T[];
12+
selection?: T;
1213
label?: string;
1314
pickerStyle?: NativePickerStyle;
1415
disabled?: boolean;
15-
style?: NativeTextStyle;
16+
style?: StyleProp<NativeTextStyle>;
1617
onChange?: (value: T) => void;
1718
onFocus?: () => void;
1819
onBlur?: () => void;
1920
};
2021

2122
export const Picker = <T extends string>({
23+
options,
2224
onChange: onChangeProp,
2325
onFocus,
2426
onBlur,
27+
style,
2528
...otherProps
2629
}: NativePickerProps<T>): ReactNode => {
2730
const onChange = useMemo(
@@ -34,11 +37,25 @@ export const Picker = <T extends string>({
3437
[onChangeProp],
3538
);
3639

37-
useSwiftUINode("Picker", otherProps, {
38-
change: onChange,
39-
focus: onFocus,
40-
blur: onBlur,
41-
});
40+
const normalizedOptions = useMemo(
41+
() =>
42+
options.length > 0 && typeof options[0] === "string"
43+
? options.map((value) => ({ value, label: value }))
44+
: options,
45+
[options],
46+
);
47+
48+
const normalizedStyles = useMemo(() => (Array.isArray(style) ? StyleSheet.flatten(style) : style), [style]);
49+
50+
useSwiftUINode(
51+
"Picker",
52+
{ options: normalizedOptions, style: normalizedStyles, ...otherProps },
53+
{
54+
change: onChange,
55+
focus: onFocus,
56+
blur: onBlur,
57+
},
58+
);
4259
return null;
4360
};
4461
Picker.displayName = "Picker";

src/components/Section.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { type PropsWithChildren } from "react";
22
import { SwiftUIParentIdProvider } from "../contexts";
33
import { useSwiftUINode } from "../hooks";
4-
import type { FunctionComponentWithId } from "../types";
4+
import type { FunctionComponentWithId, NativeViewStyle } from "../types";
55

66
// https://developer.apple.com/documentation/swiftui/section
77

88
export type NativeSectionProps = {
99
header?: string;
1010
footer?: string;
1111
isCollapsed?: boolean;
12+
style?: NativeViewStyle;
1213
};
1314

1415
export const Section: FunctionComponentWithId<PropsWithChildren<NativeSectionProps>> = ({

src/components/TextField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export type NativeTextContentType = "username" | "password" | "emailAddress";
66
export type NativeReturnKeyType = "default" | "done" | "next" | "search";
77
export type NativeAutocapitalizationType = "none" | "words" | "sentences" | "allCharacters";
88

9-
export type NativeTextFieldProps = {
10-
text?: string;
9+
export type NativeTextFieldProps<T = string> = {
10+
text?: T;
1111
label?: string;
1212
placeholder?: string;
1313
keyboardType?: NativeKeyboardType;
@@ -19,7 +19,7 @@ export type NativeTextFieldProps = {
1919
multiline?: boolean;
2020
disabled?: boolean;
2121
style?: NativeTextStyle;
22-
onChange?: (value: string) => void;
22+
onChange?: (value: T) => void;
2323
onFocus?: () => void;
2424
onBlur?: () => void;
2525
};

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from "./Group";
55
export * from "./HStack";
66
export * from "./Image";
77
export * from "./LazyVGrid";
8+
export * from "./NumberField";
89
export * from "./Picker";
910
export * from "./Rectangle";
1011
export * from "./Section";

src/contexts/SwiftUIContext.tsx

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// src/contexts/SwiftUIContext.tsx
2-
import React, { createContext, RefObject, useCallback, useContext, useMemo, useRef, useState } from "react";
2+
import React, {
3+
createContext,
4+
FunctionComponent,
5+
PropsWithChildren,
6+
RefObject,
7+
useCallback,
8+
useContext,
9+
useMemo,
10+
useRef,
11+
useState,
12+
} from "react";
313
import type { ViewTreeNode } from "src/types";
414
import {
515
Commands as NativeSwiftUIRootCommands,
@@ -26,13 +36,27 @@ export type SwiftUIContextValue = {
2636

2737
const SwiftUIContext = createContext<SwiftUIContextValue | undefined>(undefined);
2838

29-
export const SwiftUIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
39+
export type SwiftUIProviderProps = {
40+
id: string;
41+
};
42+
43+
export const SwiftUIProvider: FunctionComponent<PropsWithChildren<SwiftUIProviderProps>> = ({
44+
id: rootId,
45+
children,
46+
}) => {
3047
const eventRegistry = useRef<EventRegistry>(new Map());
3148
const nodeRegistry = useRef<NodeRegistry>(new Map());
3249
const [nodeRegistryVersion, setNodeRegistryVersion] = useState(0);
3350
const renderSequence = useRef<string[]>([]);
3451
const nativeRef = useRef<React.ComponentRef<typeof SwiftUIRootNativeComponent> | null>(null);
3552

53+
const log = useCallback(
54+
(message: string, ...args: unknown[]) => {
55+
console.log(`SwiftUIContext(${rootId}) ${message}`, ...args);
56+
},
57+
[rootId],
58+
);
59+
3660
const nodesKey = useMemo(() => {
3761
const keys = Array.from(nodeRegistry.current.keys());
3862
return JSON.stringify(keys);
@@ -52,13 +76,16 @@ export const SwiftUIProvider: React.FC<{ children: React.ReactNode }> = ({ child
5276
eventRegistry.current.set(id, newHandlersForId);
5377
return newHandlersForId;
5478
};
55-
const registerEvent = useCallback((id: string, name: string, handler: EventHandler) => {
56-
const handlersForId = getEventHandlersForId(id);
57-
if (handlersForId.has(name)) {
58-
console.log(`Overwriting existing event handler for ${id}:${name}`);
59-
}
60-
handlersForId.set(name, handler);
61-
}, []);
79+
const registerEvent = useCallback(
80+
(id: string, name: string, handler: EventHandler) => {
81+
const handlersForId = getEventHandlersForId(id);
82+
if (handlersForId.has(name)) {
83+
log(`overwriting existing event handler for ${id}:${name}`);
84+
}
85+
handlersForId.set(name, handler);
86+
},
87+
[log],
88+
);
6289

6390
const registerEvents = useCallback((id: string, events: Record<string, EventHandler | undefined>) => {
6491
const handlersForId = getEventHandlersForId(id);
@@ -74,18 +101,24 @@ export const SwiftUIProvider: React.FC<{ children: React.ReactNode }> = ({ child
74101
});
75102
}, []);
76103

77-
const registerNode = useCallback((node: ViewTreeNode, parentId?: string) => {
78-
console.log(`SwiftUIContext registering node with id=${node.id}`, { node, parentId });
79-
nodeRegistry.current.set(node.id, { node, parentId });
80-
setNodeRegistryVersion((prev) => prev + 1);
81-
}, []);
82-
83-
const unregisterNode = useCallback((id: string) => {
84-
console.log(`SwiftUIContext unregistering node with id=${id}`);
85-
nodeRegistry.current.delete(id);
86-
eventRegistry.current.delete(id);
87-
setNodeRegistryVersion((prev) => prev + 1);
88-
}, []);
104+
const registerNode = useCallback(
105+
(node: ViewTreeNode, parentId?: string) => {
106+
log(`registering node with id=${node.id}`, { node, parentId });
107+
nodeRegistry.current.set(node.id, { node, parentId });
108+
setNodeRegistryVersion((prev) => prev + 1);
109+
},
110+
[log],
111+
);
112+
113+
const unregisterNode = useCallback(
114+
(id: string) => {
115+
log(`unregistering node with id=${id}`);
116+
nodeRegistry.current.delete(id);
117+
eventRegistry.current.delete(id);
118+
setNodeRegistryVersion((prev) => prev + 1);
119+
},
120+
[log],
121+
);
89122

90123
const getNodes = useCallback(() => nodeRegistry.current, []);
91124

@@ -95,20 +128,23 @@ export const SwiftUIProvider: React.FC<{ children: React.ReactNode }> = ({ child
95128
}
96129
}, []);
97130

98-
const updateNodeProps = useCallback((id: string, props: Record<string, unknown>) => {
99-
if (!nativeRef.current) {
100-
console.warn("Native ref not available");
101-
return;
102-
}
103-
const node = nodeRegistry.current.get(id);
104-
if (!node) {
105-
console.warn(`Node with id=${id} not found`);
106-
return;
107-
}
108-
console.log(`SwiftUIContext updating node with id=${id}`, { props });
109-
node.node.props = { ...node.node.props, ...props };
110-
NativeSwiftUIRootCommands.updateChildProps(nativeRef.current, id, JSON.stringify(props));
111-
}, []);
131+
const updateNodeProps = useCallback(
132+
(id: string, props: Record<string, unknown>) => {
133+
if (!nativeRef.current) {
134+
log("[warn] native ref not available");
135+
return;
136+
}
137+
const node = nodeRegistry.current.get(id);
138+
if (!node) {
139+
log(`[warn] node with id=${id} not found`);
140+
return;
141+
}
142+
log(`updating node with id=${id}`, { props });
143+
node.node.props = { ...node.node.props, ...props };
144+
NativeSwiftUIRootCommands.updateChildProps(nativeRef.current, id, JSON.stringify(props));
145+
},
146+
[log],
147+
);
112148

113149
const context = {
114150
getEventHandler,

0 commit comments

Comments
 (0)