Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export default function TextFieldScreen() {

const TextFieldComponent = outlined ? OutlinedTextField : TextField;

React.useEffect(() => {
fieldValue.onChange = (newValue) => {
'worklet';
console.log('Value changed to:', newValue);
};
return () => {
fieldValue.onChange = null;
};
}, []);

const sharedProps = {
ref: textRef,
value: fieldValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,23 @@ import {
tag,
buttonStyle,
foregroundStyle,
textFieldStyle,
} from '@expo/ui/swift-ui/modifiers';
import * as React from 'react';

export default function TextFieldScreen() {
const textRef = React.useRef<TextFieldRef>(null);

const username = useNativeState('johndoe');
React.useEffect(() => {
username.onChange = (newUsername) => {
'worklet';
console.log('Username changed to:', newUsername);
};
return () => {
username.onChange = null;
};
}, []);
const imperativeText = useNativeState('Select me!');
const imperativeSelection = useNativeState<TextFieldSelection>({ start: 0, end: 0 });
const [imperativeSelDisplay, setImperativeSelDisplay] = React.useState<TextFieldSelection>({
Expand Down Expand Up @@ -62,7 +72,7 @@ export default function TextFieldScreen() {
<TextField
text={username}
placeholder="Username"
modifiers={[autocorrectionDisabled()]}
modifiers={[autocorrectionDisabled(), textFieldStyle('plain')]}
onTextChange={(v) => console.log('username:', v)}
/>
<TextField
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schemaVersion":"2.0","name":"expo-ui/swift-ui/usenativestate","variant":"project","kind":1,"children":[{"name":"ObservableState","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Observable state shared between JavaScript and native views (Jetpack Compose\non Android and SwiftUI on iOS)."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"type":{"type":"intersection","types":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"name":"SharedObject","package":"expo-modules-core"},{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"value","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current value.\n\nWrites from a UI worklet are synchronous and immediately readable. Writes\nfrom the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been\napplied. Prefer writing from a worklet when you need synchronous updates"}]},"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}]}}]}},{"name":"useNativeState","variant":"declaration","kind":64,"signatures":[{"name":"useNativeState","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates an observable native state that is automatically cleaned up when the\ncomponent unmounts. "},{"kind":"code","text":"`initialValue`"},{"kind":"text","text":" is captured once on the first render"}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"parameters":[{"name":"initialValue","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"ObservableState","package":"@expo/ui"}}]}],"packageName":"@expo/ui"}
{"schemaVersion":"2.0","name":"expo-ui/swift-ui/usenativestate","variant":"project","kind":1,"children":[{"name":"ObservableState","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Observable state shared between JavaScript and native views (Jetpack Compose\non Android and SwiftUI on iOS)."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"type":{"type":"intersection","types":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"name":"SharedObject","package":"expo-modules-core"},{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"onChange","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"A single listener invoked on the native UI runtime whenever the value changes\n(after iOS "},{"kind":"code","text":"`didSet`"},{"kind":"text","text":" and Android's setter). Assigning replaces the previous\nlistener; assign "},{"kind":"code","text":"`null`"},{"kind":"text","text":" to clear. The initial value does not fire "},{"kind":"code","text":"`onChange`"},{"kind":"text","text":".\n\nThe callback must be a worklet so it can run synchronously on the UI thread.\nAttach it inside "},{"kind":"code","text":"`useEffect`"},{"kind":"text","text":" and clear it in the cleanup so the listener\nlifecycle matches the component lifecycle."}],"blockTags":[{"tag":"@example","content":[{"kind":"code","text":"```tsx\nconst state = useNativeState(0);\n\nuseEffect(() => {\n state.onChange = (value) => {\n 'worklet';\n console.log('changed to', value);\n };\n return () => {\n state.onChange = null;\n };\n}, []);\n```"}]}]},"type":{"type":"union","types":[{"type":"indexedAccess","indexType":{"type":"literal","value":"listener"},"objectType":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"listener","variant":"declaration","kind":2048,"signatures":[{"name":"listener","variant":"signature","kind":4096,"parameters":[{"name":"value","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"}}]}]}}},{"type":"literal","value":null}]}},{"name":"value","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current value.\n\nWrites from a UI worklet are synchronous and immediately readable. Writes\nfrom the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been\napplied. Prefer writing from a worklet when you need synchronous updates"}]},"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}]}}]}},{"name":"useNativeState","variant":"declaration","kind":64,"signatures":[{"name":"useNativeState","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates an observable native state that is automatically cleaned up when the\ncomponent unmounts. "},{"kind":"code","text":"`initialValue`"},{"kind":"text","text":" is captured once on the first render"}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"parameters":[{"name":"initialValue","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"ObservableState","package":"@expo/ui"}}]}],"packageName":"@expo/ui"}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schemaVersion":"2.0","name":"expo-ui/swift-ui/usenativestate","variant":"project","kind":1,"children":[{"name":"ObservableState","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Observable state shared between JavaScript and native views (Jetpack Compose\non Android and SwiftUI on iOS)."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"type":{"type":"intersection","types":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"name":"SharedObject","package":"expo-modules-core"},{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"value","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current value.\n\nWrites from a UI worklet are synchronous and immediately readable. Writes\nfrom the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been\napplied. Prefer writing from a worklet when you need synchronous updates"}]},"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}]}}]}},{"name":"useNativeState","variant":"declaration","kind":64,"signatures":[{"name":"useNativeState","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates an observable native state that is automatically cleaned up when the\ncomponent unmounts. "},{"kind":"code","text":"`initialValue`"},{"kind":"text","text":" is captured once on the first render"}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"parameters":[{"name":"initialValue","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"ObservableState","package":"@expo/ui"}}]}],"packageName":"@expo/ui"}
{"schemaVersion":"2.0","name":"expo-ui/swift-ui/usenativestate","variant":"project","kind":1,"children":[{"name":"ObservableState","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Observable state shared between JavaScript and native views (Jetpack Compose\non Android and SwiftUI on iOS)."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"type":{"type":"intersection","types":[{"type":"reference","target":{"packageName":"expo-modules-core","packagePath":"src/SharedObject.ts","qualifiedName":"SharedObject"},"name":"SharedObject","package":"expo-modules-core"},{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"onChange","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"A single listener invoked on the native UI runtime whenever the value changes\n(after iOS "},{"kind":"code","text":"`didSet`"},{"kind":"text","text":" and Android's setter). Assigning replaces the previous\nlistener; assign "},{"kind":"code","text":"`null`"},{"kind":"text","text":" to clear. The initial value does not fire "},{"kind":"code","text":"`onChange`"},{"kind":"text","text":".\n\nThe callback must be a worklet so it can run synchronously on the UI thread.\nAttach it inside "},{"kind":"code","text":"`useEffect`"},{"kind":"text","text":" and clear it in the cleanup so the listener\nlifecycle matches the component lifecycle."}],"blockTags":[{"tag":"@example","content":[{"kind":"code","text":"```tsx\nconst state = useNativeState(0);\n\nuseEffect(() => {\n state.onChange = (value) => {\n 'worklet';\n console.log('changed to', value);\n };\n return () => {\n state.onChange = null;\n };\n}, []);\n```"}]}]},"type":{"type":"union","types":[{"type":"indexedAccess","indexType":{"type":"literal","value":"listener"},"objectType":{"type":"reflection","declaration":{"name":"__type","variant":"declaration","kind":65536,"children":[{"name":"listener","variant":"declaration","kind":2048,"signatures":[{"name":"listener","variant":"signature","kind":4096,"parameters":[{"name":"value","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"intrinsic","name":"void"}}]}]}}},{"type":"literal","value":null}]}},{"name":"value","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The current value.\n\nWrites from a UI worklet are synchronous and immediately readable. Writes\nfrom the JS thread are scheduled to the UI thread asynchronously, the new value is not readable until the update has been\napplied. Prefer writing from a worklet when you need synchronous updates"}]},"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}]}}]}},{"name":"useNativeState","variant":"declaration","kind":64,"signatures":[{"name":"useNativeState","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Creates an observable native state that is automatically cleaned up when the\ncomponent unmounts. "},{"kind":"code","text":"`initialValue`"},{"kind":"text","text":" is captured once on the first render"}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072}],"parameters":[{"name":"initialValue","variant":"param","kind":32768,"type":{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}}],"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"ObservableState","package":"@expo/ui"}}]}],"packageName":"@expo/ui"}
1 change: 1 addition & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Fix containment check in tar extraction to cover parallel folders with same prefix ([#45882](https://github.com/expo/expo/pull/45882) by [@kitten](https://github.com/kitten))
- Forward the request HTTP method to the RSC renderer ([#45905](https://github.com/expo/expo/pull/45905) by [@kitten](https://github.com/kitten))
- Add validation to check `EXPO_PUBLIC_FOLDER` is in project root ([#45866](https://github.com/expo/expo/pull/45866) by [@kitten](https://github.com/kitten))
- Fix launching Android activity when activity name is fully specified ([#45773](https://github.com/expo/expo/pull/45773) by [@sebryu](https://github.com/sebryu))

### 💡 Others

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,26 @@ describe(resolveLaunchPropsAsync, () => {
customAppId: 'dev.expo.test',
});
});

it(`resolves launch properties with fully qualified main activity`, async () => {
vol.fromJSON(
{
...rnFixture,
'android/app/src/main/AndroidManifest.xml': rnFixture[
'android/app/src/main/AndroidManifest.xml'
].replace(
'android:name=".MainActivity"',
'android:name="com.reactnativeproject.MainActivity"'
),
},
'/'
);

expect(await resolveLaunchPropsAsync('/', { appId: 'dev.expo.test' })).toEqual({
launchActivity: 'dev.expo.test/com.reactnativeproject.MainActivity',
mainActivity: 'com.reactnativeproject.MainActivity',
packageName: 'com.bacon.mydevicefamilyproject',
customAppId: 'dev.expo.test',
});
});
});
6 changes: 5 additions & 1 deletion packages/@expo/cli/src/run/android/resolveLaunchProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface LaunchProps {
launchActivity: string;
}

function resolveCustomLaunchActivity(packageName: string, mainActivity: string): string {
return mainActivity.startsWith('.') ? `${packageName}${mainActivity}` : mainActivity;
}

async function getMainActivityAsync(projectRoot: string): Promise<string> {
const filePath = await AndroidConfig.Paths.getAndroidManifestAsync(projectRoot);
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(filePath);
Expand Down Expand Up @@ -55,7 +59,7 @@ export async function resolveLaunchPropsAsync(

const launchActivity =
customAppId && customAppId !== packageName
? `${customAppId}/${packageName}${mainActivity}`
? `${customAppId}/${resolveCustomLaunchActivity(packageName, mainActivity)}`
: `${packageName}/${mainActivity}`;

return {
Expand Down
3 changes: 2 additions & 1 deletion packages/expo-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

### 🎉 New features

- [iOS][android] Added `onChange` listener to `useNativeState`. ([#45961](https://github.com/expo/expo/pull/45961) by [@nishan](https://github.com/intergalacticspacehighway))
- Allow writing to native state from the JS thread. ([#45901](https://github.com/expo/expo/pull/45901) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Added `withAnimation(animation, body)` in `@expo/ui/swift-ui`, mirroring SwiftUI's [`withAnimation(_:_:)`](https://developer.apple.com/documentation/swiftui/withanimation(_:_:)). ([#45893](https://github.com/expo/expo/pull/45893) by [@nishan](https://github.com/intergalacticspacehighway))
- [iOS] Added `withAnimation(animation, body)` in `@expo/ui/swift-ui`, mirroring SwiftUI's [`withAnimation(_:_:)`](<https://developer.apple.com/documentation/swiftui/withanimation(_:_:)>). ([#45893](https://github.com/expo/expo/pull/45893) by [@nishan](https://github.com/intergalacticspacehighway))
- [jetpack-compose] Added `Snackbar` component. ([#45667](https://github.com/expo/expo/pull/45667) by [@nishan](https://github.com/intergalacticspacehighway))
- [android] Added `LoadingIndicator` and `ContainedLoadingIndicator` components. ([#41169](https://github.com/expo/expo/pull/41169) by [@suveshmoza](https://github.com/suveshmoza))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class ExpoUIModule : Module() {
}
}
}

Function("setOnChange") { state: ObservableState, callback: WorkletCallback? ->
state.onChange = callback
}
}

//region Views use expo-modules-core DSL for uncommon features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ import expo.modules.kotlin.sharedobjects.SharedObject
*/
class ObservableState(initialValue: Any? = null) : SharedObject() {
private val _state: MutableState<Any?> = mutableStateOf(initialValue)
internal var onChange: WorkletCallback? = null
private var isNotifying = false

var value: Any?
get() = _state.value
set(v) {
_state.value = v
// Skip re-invoking onChange if state.value was written from inside onChange.
if (isNotifying) return
isNotifying = true
try {
onChange?.invoke(v)
} finally {
isNotifying = false
}
}

@Suppress("UNCHECKED_CAST")
Expand Down
27 changes: 27 additions & 0 deletions packages/expo-ui/build/State/useNativeState.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-ui/build/State/useNativeState.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/expo-ui/ios/ExpoUIModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public final class ExpoUIModule: Module {
}
}
}

Function("setOnChange") { (state: ObservableState, callback: WorkletCallback?) in
state.onChange = callback
}
}

// MARK: - Module Functions
Expand Down
Loading
Loading