Watch apps provide a native watchOS experience that runs on Apple Watch. With Expo Apple Targets, you can build watchOS apps using SwiftUI while keeping your source code outside the generated iOS directory.
Watch apps require a paired iOS app and cannot run standalone in this configuration.
Getting Started
Generate a watch app target
This creates targets/watch/ with:
expo-target.config.js — Target configuration
index.swift — App entry point
content.swift — Main view
preview/ — Preview assets for Xcode
Configure the target
targets/watch/expo-target.config.js
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module . exports = {
type: "watch" ,
icon: "../../assets/icon.png" ,
// Share data with the iOS app
entitlements: {
"com.apple.security.application-groups" : [ "group.com.yourapp.data" ],
},
// watchOS deployment target
deploymentTarget: "9.0" ,
};
Run prebuild
npx expo prebuild -p ios --clean
Develop in Xcode
Select the watch app scheme and choose a paired watchOS simulator to run your app.
Watch App Implementation
The generated template provides a basic SwiftUI app:
targets/watch/index.swift
targets/watch/content.swift
import SwiftUI
@main
struct watchEntry : App {
var body: some Scene {
WindowGroup {
ContentView ()
}
}
}
Building a Real Watch App
Here’s an example of a watch app that displays data from your iOS app:
targets/watch/content.swift
import SwiftUI
struct ContentView : View {
@State private var count: Int = 0
var body: some View {
VStack ( spacing : 12 ) {
Text ( "Count" )
. font (. caption )
. foregroundColor (. secondary )
Text ( " \( count ) " )
. font (. system ( size : 48 , weight : . bold , design : . rounded ))
Button ( "Refresh" ) {
loadCount ()
}
. buttonStyle (. borderedProminent )
}
. onAppear {
loadCount ()
}
}
func loadCount () {
let defaults = UserDefaults ( suiteName : "group.com.yourapp.data" )
count = defaults ? . integer ( forKey : "count" ) ?? 0
}
}
Communicating with Your iOS App
Watch apps can share data with your iOS app using App Groups:
Setup App Groups
Add to main app
{
"expo" : {
"ios" : {
"entitlements" : {
"com.apple.security.application-groups" : [ "group.com.yourapp.data" ]
}
}
}
}
Add to watch app
targets/watch/expo-target.config.js
module . exports = ( config ) => ({
type: "watch" ,
entitlements: {
"com.apple.security.application-groups" :
config . ios . entitlements [ "com.apple.security.application-groups" ],
},
});
Write Data from React Native
import { ExtensionStorage } from "@bacons/apple-targets" ;
const storage = new ExtensionStorage ( "group.com.yourapp.data" );
// Update data that the watch can read
storage . set ( "count" , 42 );
storage . set ( "userName" , "Evan" );
storage . set ( "lastUpdate" , new Date (). toISOString ());
Read Data in watchOS
let defaults = UserDefaults ( suiteName : "group.com.yourapp.data" )
let count = defaults ? . integer ( forKey : "count" ) ?? 0
let userName = defaults ? . string ( forKey : "userName" ) ?? "Unknown"
let lastUpdate = defaults ? . string ( forKey : "lastUpdate" ) ?? "Never"
See the Sharing Data guide for more details.
Watch Connectivity
For real-time communication between iOS and watchOS, use the WatchConnectivity framework:
targets/watch/connectivity.swift
import WatchConnectivity
class WatchConnectivityManager : NSObject , ObservableObject , WCSessionDelegate {
@Published var receivedMessage: String = "No message"
override init () {
super . init ()
if WCSession. isSupported () {
let session = WCSession. default
session. delegate = self
session. activate ()
}
}
// Send message to iPhone
func sendMessage ( _ message : [ String : Any ]) {
if WCSession.default.isReachable {
WCSession. default . sendMessage (message, replyHandler : nil )
}
}
// Receive message from iPhone
func session ( _ session : WCSession, didReceiveMessage message : [ String : Any ]) {
DispatchQueue. main . async {
if let text = message[ "text" ] as? String {
self . receivedMessage = text
}
}
}
func session ( _ session : WCSession, activationDidCompleteWith activationState : WCSessionActivationState, error : Error ? ) {
// Handle activation
}
}
Implement the same in your iOS app to enable two-way communication.
The React Native app must be running for WCSession.isReachable to return true. Use App Groups for persistent data.
Watch Complications
To add complications (widgets for watch faces), create a watch widget target:
npx create-target watch-widget
This creates a separate target for watch face complications that follows the same patterns as iOS widgets.
Custom Assets
Add images and colors to your watch app:
targets/watch/expo-target.config.js
module . exports = {
type: "watch" ,
images: {
logo: "../../assets/logo.png" ,
background: "../../assets/background.png" ,
},
colors: {
$accent: "#007AFF" ,
primary: "#FF3B30" ,
},
};
Use them in SwiftUI:
Image ( "logo" )
. resizable ()
. frame ( width : 50 , height : 50 )
Text ( "Hello" )
. foregroundColor ( Color ( "primary" ))
watchOS-Specific SwiftUI
Take advantage of watchOS-specific views:
Digital Crown
struct ContentView : View {
@State private var scrollAmount = 0.0
var body: some View {
VStack {
Text ( " \( scrollAmount, specifier : "%.1f" ) " )
}
. focusable ()
. digitalCrownRotation ($scrollAmount)
}
}
Complications
struct ComplicationView : View {
var body: some View {
Text ( "42" )
. font (. system (. body , design : . rounded ). monospacedDigit ())
}
}
List with Swipe Actions
List {
ForEach (items) { item in
Text (item. name )
. swipeActions {
Button ( "Delete" , role : . destructive ) {
deleteItem (item)
}
}
}
}
Testing on Device
To test on a real Apple Watch:
Pair your Apple Watch with your iPhone
Select your watch in Xcode’s device list
Build and run the watch scheme
The first install on a real watch can take several minutes. Subsequent installs are faster.
Debugging
View Console Logs
In Xcode, open Window > Devices and Simulators, select your watch, and click “Open Console” to see print statements.
SwiftUI Previews
Use SwiftUI previews for rapid iteration:
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView ()
. previewDevice ( "Apple Watch Series 9 - 45mm" )
}
}
Deployment Target
Set the minimum watchOS version:
module . exports = {
type: "watch" ,
deploymentTarget: "9.0" , // watchOS 9.0+
};
Limitations
No React Native support: Watch apps must be built in pure Swift/SwiftUI. React Native does not support watchOS.
Watch apps in this configuration require a companion iOS app and cannot run independently.
Next Steps