Back to Expo

Building SwiftUI apps with Expo UI

docs/pages/guides/expo-ui-swift-ui/index.mdx

latest19.3 KB
Original Source

import { DocsLogo } from '@expo/styleguide'; import { GithubIcon } from '@expo/styleguide-icons/custom/GithubIcon';

import { BoxLink } from '/ui/components/BoxLink'; import { Collapsible } from '/ui/components/Collapsible'; import { ContentSpotlight } from '/ui/components/ContentSpotlight'; import { FAQ } from '/ui/components/FAQ'; import { Terminal } from '/ui/components/Snippet'; import { Tabs, Tab } from '/ui/components/Tabs'; import { CODE } from '/ui/components/Text'; import { VideoBoxLink } from '/ui/components/VideoBoxLink';

info Available in SDK 54 and later.

Expo UI brings SwiftUI to React Native. You can use modern SwiftUI primitives to build your apps.

This guide covers the basics of using Expo UI to integrate SwiftUI into your Expo apps.

<VideoBoxLink videoId="2wXYLWz3YEQ" title="Expo UI iOS Liquid Glass Tutorial" description="Learn how to build real SwiftUI views in your React Native app with the new Expo UI." />

Features

  • SwiftUI primitives: Expo UI is not another UI library. It brings SwiftUI primitives to Expo.
  • 1-to-1 mapping: The components in Expo UI have a 1-to-1 mapping to SwiftUI views. You can easily explore available views in the SwiftUI ecosystem, such as Explore SwiftUI or the Libraried app, and find the corresponding Expo UI component.
  • Full-app support: Expo UI is designed to be used throughout the entire app. You can write your app entirely in Expo UI, while maintaining flexibility at the same time. The integration works at the component level. You can also mix React Native components, Expo UI components, DOM components, or custom 2D components using react-native-skia.

Installation

You'll need to install the @expo/ui package in your Expo project. Run the following command to install it:

<Terminal cmd={['$ npx expo install @expo/ui']} />

Usage

Expo UI has several SwiftUI components available. You can use them in your app by importing them from @expo/ui/swift-ui. However, to cross the boundary from React Native (UIKit) to SwiftUI, you need to use the Host component. The Host is the container for SwiftUI views. You can think of it like <svg> in the DOM or <Canvas> in react-native-skia. Under the hood, it uses UIHostingController to render SwiftUI views in UIKit.

Basic usage with Host

<Tabs> <Tab label="Code"> ```tsx SwiftUI loading view import { CircularProgress, Host } from '@expo/ui/swift-ui'; import { View, Text } from 'react-native';
export default function LoadingView() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Host matchContents>
        <CircularProgress />
      </Host>
      <Text>Loading...</Text>
    </View>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Loading view" src="/static/images/expo-ui/swift-ui-guide/loading.png" className="max-w-[480px]" /> </Tab> </Tabs>

Using HStack and VStack

You can also use the HStack and VStack components to build the entire layout in SwiftUI.

<Tabs> <Tab label="Code"> ```tsx SwiftUI loading with HStack and VStack import { CircularProgress, Host, HStack, LinearProgress, VStack } from '@expo/ui/swift-ui';
export default function LoadingView() {
  return (
    <Host style={{ flex: 1, margin: 32 }}>
      <VStack spacing={32}>
        <HStack spacing={32}>
          <CircularProgress />
          <CircularProgress color="orange" />
        </HStack>
        <LinearProgress progress={0.5} />
        <LinearProgress color="orange" progress={0.7} />
      </VStack>
    </Host>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="HStack and VStack" src="/static/images/expo-ui/swift-ui-guide/hstack-vstack.png" className="max-w-[480px]" /> </Tab> </Tabs>

Modifiers

SwiftUI modifier is a powerful way to customize the appearance and behavior of SwiftUI components. Expo UI also provides modifiers for SwiftUI components. You can import modifiers from @expo/ui/swift-ui/modifiers and pass them as an array to the modifiers prop. In the following example, the expo-mesh-gradient and glassEffect modifier are combined to create Liquid Glass text.

<Tabs> <Tab label="Code"> > **Note**: `glassEffect` modifier requires Xcode 26+ and iOS 26+.
```tsx SwiftUI modifiers
import { Host, Text } from '@expo/ui/swift-ui';
import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers';
import { MeshGradientView } from 'expo-mesh-gradient';
import { View } from 'react-native';

export default function Page() {
  return (
    <View style={{ flex: 1 }}>
      <MeshGradientView
        style={{ flex: 1 }}
        columns={3}
        rows={3}
        colors={['red', 'purple', 'indigo', 'orange', 'white', 'blue', 'yellow', 'green', 'cyan']}
        points={[
          [0.0, 0.0],
          [0.5, 0.0],
          [1.0, 0.0],
          [0.0, 0.5],
          [0.5, 0.5],
          [1.0, 0.5],
          [0.0, 1.0],
          [0.5, 1.0],
          [1.0, 1.0],
        ]}
      />
      <Host style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0 }}>
        <Text
          size={32}
          modifiers={[
            padding({
              all: 16,
            }),
            glassEffect({
              glass: {
                variant: 'clear',
              },
            }),
          ]}>
          Glass effect text
        </Text>
      </Host>
    </View>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Liquid Glass effect" src="/static/images/expo-ui/swift-ui-guide/glass-effect.png" className="max-w-[240px]" /> </Tab> </Tabs>

iOS Settings app example

Combining the Expo UI components and modifiers, you can build a UI like iOS Settings app.

<Tabs> <Tab label="Code"> ```tsx SwiftUI Form example to build iOS Settings app import { Button, Form, Host, HStack, Image, Section, Spacer, Toggle, Text, } from '@expo/ui/swift-ui'; import { background, buttonStyle, foregroundStyle, clipShape, frame } from '@expo/ui/swift-ui/modifiers'; import { Link } from 'expo-router'; import { useState } from 'react';
export default function SettingsView() {
  const [isAirplaneMode, setIsAirplaneMode] = useState(true);

  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <HStack spacing={8}>
            <Image
              systemName="airplane"
              color="white"
              size={18}
              modifiers={[
                frame({ width: 28, height: 28 }),
                background('#ffa500'),
                clipShape('roundedRectangle'),
              ]}
            />
            <Text>Airplane Mode</Text>
            <Spacer />
            <Toggle isOn={isAirplaneMode} onIsOnChange={setIsAirplaneMode} />
          </HStack>

          <Link href="/wifi" asChild>
            <Button modifiers={[buttonStyle('plain')]}>
              <HStack spacing={8}>
                <Image
                  systemName="wifi"
                  color="white"
                  size={18}
                  modifiers={[
                    frame({ width: 28, height: 28 }),
                    background('#007aff'),
                    clipShape('roundedRectangle'),
                  ]}
                />
                <Text modifiers={[foregroundStyle({type: 'color', color: 'black'})]}>Wi-Fi</Text>
                <Spacer />
                <Image systemName="chevron.right" size={14} color="secondary" />
              </HStack>
            </Button>
          </Link>
        </Section>
      </Form>
    </Host>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Form example to build iOS Settings app" src="/static/images/expo-ui/swift-ui-guide/form.png" className="max-w-[480px]" /> </Tab> </Tabs>

Secondary text styling

Use foregroundStyle to apply a hierarchical style, which will make text appear lighter and more subtle.

<Tabs> <Tab label="Code"> ```tsx Secondary text styling import { Button, Form, Host, HStack, Image, List, Section, Spacer, Text } from '@expo/ui/swift-ui'; import { buttonStyle, font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';
export default function SecondaryTextExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <List>
            <Button onPress={() => console.log('Navigate')} modifiers={[buttonStyle('plain')]}>
              <HStack>
                <Text>Night Shift</Text>
                <Spacer />
                <Text
                  modifiers={[
                    foregroundStyle({type: 'hierarchical', style: 'secondary'}),
                    padding({ trailing: 8 }),
                  ]}>
                  22:00 to 07:00
                </Text>
                <Image systemName="chevron.right" size={14} color="#C7C7CC" />
              </HStack>
            </Button>
          </List>
          <List>
            <Text modifiers={[foregroundStyle({type: 'hierarchical', style: 'secondary'}), font({ size: 14 })]}>
              Save up to 280.7 MB. This will permanently delete all photos and videos kept in the
              "Recently Deleted" album.
            </Text>
          </List>
        </Section>
      </Form>
    </Host>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Secondary text styling" src="/static/images/expo-ui/swift-ui-guide/secondary-text.png" className="max-w-[480px]" /> </Tab> </Tabs>

Slider with icons

A common pattern for brightness or volume controls is to flank a Slider with icons.

<Tabs> <Tab label="Code"> ```tsx Slider with icons import { useState } from 'react'; import { Form, Host, HStack, Image, List, Section, Slider, Spacer, Text, Toggle, } from '@expo/ui/swift-ui'; import { padding } from '@expo/ui/swift-ui/modifiers';
export default function SliderWithIconsExample() {
  const [brightness, setBrightness] = useState(0.5);
  const [trueToneEnabled, setTrueToneEnabled] = useState(true);

  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section
          header={<Text>Brightness</Text>}
          footer={
            <Text>
              Automatically adapt iPhone display based on ambient lighting
              conditions to make colors appear consistent in different
              environments.
            </Text>
          }
        >
          <List>
            <HStack modifiers={[padding({ vertical: 6 })]}>
              <Image systemName="sun.min.fill" size={22} color="#8E8E93" />
              <Spacer />
              <Slider value={brightness} onValueChange={setBrightness} />
              <Spacer />
              <Image systemName="sun.max.fill" size={22} color="#8E8E93" />
            </HStack>
            <Toggle
              label="True Tone"
              isOn={trueToneEnabled}
              onIsOnChange={setTrueToneEnabled}
            />
          </List>
        </Section>
      </Form>
    </Host>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Slider with icons" src="/static/images/expo-ui/swift-ui-guide/slider-with-icons.png" className="max-w-[480px]" /> </Tab> </Tabs>

Multi-line list items

Use VStack with alignment="leading" for list items with title and subtitle.

<Tabs> <Tab label="Code"> ```tsx Multi-line list item import { Button, Form, Host, HStack, Image, List, Section, Spacer, Text, VStack, } from '@expo/ui/swift-ui'; import { buttonStyle, font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers';
export default function MultiLineListItemExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Form>
        <Section>
          <List>
            <HStack>
              <Image
                systemName="safari"
                size={22}
                modifiers={[padding({ trailing: 6 })]}
              />
              <Spacer />
              <Button
                onPress={() => console.log('Navigate')}
                modifiers={[buttonStyle('plain'), padding({ vertical: 6 })]}
              >
                <VStack spacing={4} alignment="leading">
                  <Text>Chrome</Text>
                  <Text modifiers={[foregroundStyle({type: 'hierarchical', style: 'secondary'}), font({ size: 14 })]}>
                    Last used: Today
                  </Text>
                </VStack>
                <Spacer />
                <Text
                  modifiers={[
                    foregroundStyle({type: 'hierarchical', style: 'secondary'}),
                    font({ size: 16 }),
                  ]}
                >
                  1.57 GB
                </Text>
                <Image systemName="chevron.right" size={14} color="#C7C7CC" />
              </Button>
            </HStack>
          </List>
        </Section>
      </Form>
    </Host>
  );
}
```
</Tab> <Tab label="Preview"> <ContentSpotlight alt="Multi-line list item" src="/static/images/expo-ui/swift-ui-guide/multi-line-list-item.png" className="max-w-[480px]" /> </Tab> </Tabs>

Common questions

<FAQ> <Collapsible summary="Can I use flexbox or other styles in SwiftUI components?">

Flexbox styles can be applied to the Host component itself. Once you're inside the SwiftUI context, however, Yoga is not available — layouts should be defined using <HStack> and <VStack> instead.

</Collapsible>

<Collapsible summary={<>What's the <CODE>Host</CODE> component?</>}>

Host is the container for SwiftUI views. You can think of it like <svg> in the DOM or <Canvas> in react-native-skia. Under the hood, it uses UIHostingController to render SwiftUI views in UIKit.

</Collapsible>

<Collapsible summary={<>How is Expo UI different from libraries like <CODE>react-native-paper</CODE> or <CODE>react-native-elements</CODE>?</>}>

Expo UI is not "yet another" UI library and not an opinionated design kit. Instead, it's a primitives library. It exposes native SwiftUI and Jetpack Compose components directly to JavaScript, rather than re-implementing or simulating UI in JavaScript.

</Collapsible>

<Collapsible summary={<>Can I use <CODE>@expo/ui/swift-ui</CODE> on Android or web?</>}>

The first milestone for Expo UI is achieving a 1-to-1 mapping from SwiftUI to Expo UI. Universal support will come in the next stage of the roadmap. Our priority is to establish strong SwiftUI support first, and then expand to Jetpack Compose on Android and DOM support on the Web.

</Collapsible> <Collapsible summary="Can I use React Native components inside SwiftUI components?">

Yes, you can place React Native components as JSX children of Expo UI components. Expo UI automatically creates a UIViewRepresentable wrapper for you. However, keep in mind that the SwiftUI layout system works differently from UIKit and has some limitations. According to Apple's documentation:

Warning SwiftUI fully controls the layout of the UIKit view's center, bounds, frame, and transform properties. Don't directly set these layout-related properties on the view managed by a UIViewRepresentable instance from your own code because that conflicts with SwiftUI and results in undefined behavior.

Also note that once you render React Native components, you're leaving the SwiftUI context. If you want to add Expo UI components again, you'll need to reintroduce a Host wrapper.

We recommend keeping SwiftUI layouts self-contained. Interop is possible, but it works best when boundaries are clearly defined.

</Collapsible> <Collapsible summary="I'm a SwiftUI developer. Why should I learn Expo UI?">

Because React's promise of "learn once, write anywhere", it now extends to SwiftUI and Jetpack Compose. With Expo UI, you can apply your SwiftUI knowledge to build apps that run in the React Native ecosystem, extend to the Web through DOM components, and even integrate 2D and 3D rendering. The system is flexible enough that different parts of your app can use different approaches — giving you seamless integration at the component level.

</Collapsible> </FAQ>

Additional resources

<BoxLink title="Expo UI reference" Icon={DocsLogo} description="For information on API components, methods, and more, see the Expo UI reference." href="/versions/latest/sdk/ui/" />

<BoxLink title="Expo UI example" description="Our latest Expo UI examples" Icon={GithubIcon} href="https://github.com/expo/expo/tree/main/apps/native-component-list/src/screens/UI" />

<BoxLink title="Hot Chocolate app example" description="An example app replicating the YVR Hot Chocolate Fest app with Expo UI" Icon={GithubIcon} href="https://github.com/expo/hot-chocolate" />

<BoxLink title="Expo UI example replicate to TV" description="TVOS support to Expo UI" Icon={GithubIcon} href="https://github.com/douglowder/ExpoUITV" />