Extending with SwiftUI
Edit page
Learn how to create custom SwiftUI components and modifiers that integrate with Expo UI.
This guide explains how to create custom SwiftUI components and modifiers that integrate seamlessly with Expo UI.
Prerequisites
Before you begin, make sure you have:
@expo/uiinstalled in your project. See Building SwiftUI apps with Expo UI for more information.- A development build of your app (Expo UI is not available in Expo Go)
- Basic familiarity with Expo Modules API and SwiftUI
-Â npx expo install @expo/uiCreating a custom component
Project setup
1
Create a local Expo module in your project:
-Â npx create-expo-module@latest --local my-ui2
Add ExpoUI as a dependency in your module's podspec file:
Pod::Spec.new do |s| s.name = 'MyUi' s.version = '1.0.0' s.summary = 'Custom UI components extending Expo UI' # ... other config # Add ExpoUI dependency s.dependency 'ExpoUI' # ... other config end
Creating a SwiftUI view
3
Create your SwiftUI view with two parts:
- Props class: Extends
UIBaseViewPropsfromExpoUIto get automatic support for themodifiersprop - View struct: Conforms to
ExpoSwiftUI.Viewprotocol, which requires an@ObservedObjectprops property and abody
import SwiftUI import ExpoModulesCore import ExpoUI final class MyCustomViewProps: UIBaseViewProps { @Field var title: String = "" } struct MyCustomView: ExpoSwiftUI.View { @ObservedObject public var props: MyCustomViewProps var body: some View { VStack { Text(props.title) .font(.headline) Children() // Renders React children } } }
4
Register the view in your module using ExpoUIView. This wraps your SwiftUI view with modifier support and makes it available to JavaScript:
import ExpoModulesCore import ExpoUI public class MyUiModule: Module { public func definition() -> ModuleDefinition { Name("MyUi") ExpoUIView(MyCustomView.self) } }
5
Create a wrapper component that connects modifiers with event handling. The createViewModifierEventListener utility enables event-based modifiers like onTapGesture and onAppear to work with your custom view:
import { requireNativeView } from 'expo'; import { type CommonViewModifierProps } from '@expo/ui/swift-ui'; import { createViewModifierEventListener } from '@expo/ui/swift-ui/modifiers'; export interface MyCustomViewProps extends CommonViewModifierProps { title: string; children?: React.ReactNode; } const NativeMyCustomView = requireNativeView<MyCustomViewProps>('MyUi', 'MyCustomView'); export function MyCustomView({ modifiers, ...restProps }: MyCustomViewProps) { return ( <NativeMyCustomView modifiers={modifiers} {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)} {...restProps} /> ); }
Using your custom component
Your custom component now works with all ExpoUI built-in modifiers:
import { Host, Text } from '@expo/ui/swift-ui'; import { padding, cornerRadius, background } from '@expo/ui/swift-ui/modifiers'; import { MyCustomView } from './modules/my-ui'; export default function App() { return ( <Host style={{ flex: 1 }}> <MyCustomView title="Hello World" modifiers={[padding({ all: 16 }), cornerRadius(12), background('#f0f0f0')]}> <Text>Child content</Text> </MyCustomView> </Host> ); }
Creating custom modifiers
You can also create custom modifiers that work with any Expo UI component.
Modifiers are SwiftUI's way to configure views for styling, layout, behavior, and more. Learn more in Apple's ViewModifier documentation.
Native modifier implementation
1
Create a modifier struct that conforms to ViewModifier and Record:
import SwiftUI import ExpoModulesCore import ExpoUI struct CustomBorderModifier: ViewModifier, Record { @Field var color: Color = .red @Field var width: CGFloat = 2 @Field var cornerRadius: CGFloat = 0 func body(content: Content) -> some View { content .overlay( RoundedRectangle(cornerRadius: cornerRadius) .stroke(color, lineWidth: width) ) } }
2
Register your modifier with ViewModifierRegistry in your module definition. Use OnCreate to register and OnDestroy to unregister to avoid race conditions with the SwiftUI render thread:
import ExpoModulesCore import ExpoUI public class MyUiModule: Module { public func definition() -> ModuleDefinition { Name("MyUi") OnCreate { ViewModifierRegistry.register("customBorder") { params, appContext, _ in return try CustomBorderModifier(from: params, appContext: appContext) } } OnDestroy { ViewModifierRegistry.unregister("customBorder") } ExpoUIView(MyCustomView.self) } }
JavaScript modifier function
3
Create a TypeScript function that generates the modifier config:
import { createModifier } from '@expo/ui/swift-ui/modifiers'; export const customBorder = (params: { color?: string; width?: number; cornerRadius?: number }) => createModifier('customBorder', params);
4
Export the modifier from your module:
export { MyCustomView, type MyCustomViewProps } from './src/MyCustomView'; export { customBorder } from './src/modifiers';
Using custom modifiers
Your custom modifier works with any ExpoUI component:
import { Host, Text, VStack } from '@expo/ui/swift-ui'; import { padding } from '@expo/ui/swift-ui/modifiers'; import { customBorder } from './modules/my-ui'; export default function App() { return ( <Host style={{ flex: 1 }}> <VStack modifiers={[ padding({ all: 20 }), customBorder({ color: '#FF6B35', width: 3, cornerRadius: 8 }), ]}> <Text>This has a custom border!</Text> </VStack> </Host> ); }
Next steps
Congratulations! You've learned how to extend Expo UI with custom SwiftUI components and modifiers. Your custom components now integrate seamlessly with the built-in modifier system.
Here are some ideas for what to build next:
- Use the built-in SwiftUI components that come with Expo UI
- Build custom modifiers for app-specific styling patterns
- Wrap third-party SwiftUI libraries for use in React Native
- Share your components as an npm package for others to use