Creating a watchOS App
This guide walks you through creating a watchOS companion app from scratch, including project setup in Xcode, integrating the CapgoWatchSDK, and building a functional watch app with SwiftUI.
Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
- Xcode 15 or later (download from Mac App Store)
- macOS Sonoma or later (for latest watchOS SDK)
- An existing Capacitor iOS project (run
npx cap add iosif you haven’t) - Apple Developer account (free account works for development)
Project Structure Overview
Section titled “Project Structure Overview”After completing this guide, your project will have this structure:
Directoryios/
DirectoryApp/
DirectoryApp/ (your main iOS app)
- …
- App.xcodeproj
- App.xcworkspace (use this to open the project)
- Podfile
DirectoryMyWatch/ (new watch app)
DirectoryMyWatch/ (watch app source)
- MyWatchApp.swift
- ContentView.swift
DirectoryAssets.xcassets/
- …
- MyWatch.xcodeproj
Step 1: Open Your iOS Project in Xcode
Section titled “Step 1: Open Your iOS Project in Xcode”- Navigate to your Capacitor project’s
ios/Appfolder - Open
App.xcworkspace(not.xcodeproj) by double-clicking it - Wait for Xcode to index the project
Step 2: Add a watchOS Target
Section titled “Step 2: Add a watchOS Target”-
In Xcode, go to File → New → Target…
-
In the template chooser:
- Select watchOS tab at the top
- Choose App
- Click Next
-
Configure your watch app:
- Product Name:
MyWatch(or your preferred name) - Team: Select your Apple Developer team
- Organization Identifier: Should match your iOS app (e.g.,
app.capgo) - Bundle Identifier: Will be auto-generated (e.g.,
app.capgo.myapp.watchkitapp) - Language: Swift
- User Interface: SwiftUI
- Watch App Type: App (not App for Existing iOS App)
- Uncheck Include Notification Scene (unless you need it)
- Uncheck Include Complication (unless you need it)
- Product Name:
-
Click Finish
-
When prompted “Activate ‘MyWatch’ scheme?”, click Activate
Step 3: Configure Watch App Settings
Section titled “Step 3: Configure Watch App Settings”-
In the Project Navigator (left sidebar), select your project (the blue icon at the top)
-
Select your watch target (e.g., “MyWatch”) from the targets list
-
Go to the General tab:
- Display Name: The name shown under the app icon (e.g., “My App”)
- Bundle Identifier: Should end with
.watchkitapp - Version: Match your iOS app version
- Build: Match your iOS app build number
-
Go to Signing & Capabilities tab:
- Enable Automatically manage signing
- Select your Team
- Xcode will create provisioning profiles automatically
-
Set Deployment Info:
- Minimum Deployments: watchOS 9.0 or later
Step 4: Add CapgoWatchSDK via Swift Package Manager
Section titled “Step 4: Add CapgoWatchSDK via Swift Package Manager”The CapgoWatchSDK provides a ready-to-use WatchConnector class for communication.
-
In Xcode, go to File → Add Package Dependencies…
-
In the search field, enter:
https://github.com/Cap-go/capacitor-watch.git -
Press Enter and wait for Xcode to fetch the package
-
Configure the package:
- Dependency Rule: “Up to Next Major Version” with “8.0.0”
- Click Add Package
-
Choose which products to add:
- IMPORTANT: Only select
CapgoWatchSDK - Make sure it’s added to your watch target (e.g., “MyWatch”), not the iOS app
- Click Add Package
- IMPORTANT: Only select
Step 5: Implement the Watch App
Section titled “Step 5: Implement the Watch App”Now let’s create the watch app code. Replace the auto-generated files with the following:
5.1 Create the App Entry Point
Section titled “5.1 Create the App Entry Point”Edit MyWatch/MyWatchApp.swift:
import SwiftUIimport CapgoWatchSDK
@mainstruct MyWatchApp: App { init() { // Activate WatchConnectivity when app launches WatchConnector.shared.activate() }
var body: some Scene { WindowGroup { ContentView() } }}5.2 Create the Main View
Section titled “5.2 Create the Main View”Edit MyWatch/ContentView.swift:
import SwiftUIimport CapgoWatchSDK
struct ContentView: View { // Observe the WatchConnector for automatic UI updates @ObservedObject var connector = WatchConnector.shared
// Local state @State private var messageText = "" @State private var statusMessage = "Ready"
var body: some View { ScrollView { VStack(spacing: 16) { // Connection Status ConnectionStatusView(connector: connector)
Divider()
// Message Input TextField("Message", text: $messageText) .textFieldStyle(.roundedBorder)
// Send Buttons HStack { Button("Send") { sendMessage() } .disabled(!connector.isReachable || messageText.isEmpty)
Button("Request") { sendWithReply() } .disabled(!connector.isReachable || messageText.isEmpty) }
Divider()
// Status Text(statusMessage) .font(.caption) .foregroundColor(.secondary)
// Last Received Message if !connector.lastMessage.isEmpty { VStack(alignment: .leading) { Text("Last Message:") .font(.caption) .foregroundColor(.secondary) Text(formatMessage(connector.lastMessage)) .font(.caption2) } .frame(maxWidth: .infinity, alignment: .leading) } } .padding() } }
private func sendMessage() { connector.sendMessage(["text": messageText, "timestamp": Date().timeIntervalSince1970]) statusMessage = "Message sent" messageText = "" }
private func sendWithReply() { connector.sendMessage(["text": messageText, "needsReply": true]) { reply in DispatchQueue.main.async { statusMessage = "Reply: \(formatMessage(reply))" } } messageText = "" }
private func formatMessage(_ message: [String: Any]) -> String { message.map { "\($0.key): \($0.value)" }.joined(separator: ", ") }}
// Separate view for connection statusstruct ConnectionStatusView: View { @ObservedObject var connector: WatchConnector
var body: some View { HStack { Circle() .fill(connector.isReachable ? Color.green : Color.red) .frame(width: 12, height: 12)
Text(connector.isReachable ? "Connected" : "Disconnected") .font(.headline)
Spacer()
if connector.isActivated { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) } } }}
#Preview { ContentView()}Step 6: Configure iOS App for WatchConnectivity
Section titled “Step 6: Configure iOS App for WatchConnectivity”Your iOS app also needs WatchConnectivity capability.
-
In the Project Navigator, select your project
-
Select your iOS App target (not the watch target)
-
Go to Signing & Capabilities tab
-
Click + Capability
-
Search for and add WatchConnectivity (if available) or it may be added automatically
-
The Capacitor plugin handles the iOS side automatically, but ensure your Info.plist has:
<key>WKCompanionAppBundleIdentifier</key><string>app.capgo.myapp.watchkitapp</string>
Step 7: Build and Run
Section titled “Step 7: Build and Run”Run on Simulator
Section titled “Run on Simulator”-
Select your watch scheme from the scheme selector (top of Xcode window)
-
Choose a watch simulator:
- Click on the device selector next to the scheme
- Select an Apple Watch simulator (e.g., “Apple Watch Series 9 (45mm)”)
-
Click the Run button (▶️) or press
Cmd + R -
The iOS Simulator will launch with both iPhone and Apple Watch
Run on Physical Device
Section titled “Run on Physical Device”-
Connect your iPhone via USB
-
Ensure your Apple Watch is paired with that iPhone
-
Select your watch scheme
-
Select your physical Apple Watch from the device list
-
Click Run
-
First time: You may need to trust your computer on both devices
Step 8: Test Communication
Section titled “Step 8: Test Communication”From iPhone (Capacitor) to Watch
Section titled “From iPhone (Capacitor) to Watch”In your Capacitor app:
import { CapgoWatch } from '@capgo/capacitor-watch';
// Check connectionconst info = await CapgoWatch.getInfo();console.log('Watch reachable:', info.isReachable);
// Send a messageif (info.isReachable) { await CapgoWatch.sendMessage({ data: { action: 'update', value: 'Hello from iPhone!' } });}From Watch to iPhone
Section titled “From Watch to iPhone”The watch app uses WatchConnector:
// Send message (fire and forget)WatchConnector.shared.sendMessage(["action": "buttonTapped"])
// Send message with replyWatchConnector.shared.sendMessage(["request": "getData"]) { reply in print("Got reply: \(reply)")}Handle Messages on iPhone
Section titled “Handle Messages on iPhone”// Listen for messages from watchawait CapgoWatch.addListener('messageReceived', (event) => { console.log('Message from watch:', event.message); // { action: 'buttonTapped' }});
// Handle messages that need a replyawait CapgoWatch.addListener('messageReceivedWithReply', async (event) => { console.log('Request from watch:', event.message);
// Send reply back await CapgoWatch.replyToMessage({ callbackId: event.callbackId, data: { status: 'success', items: ['item1', 'item2'] } });});Advanced: Custom Delegate for More Control
Section titled “Advanced: Custom Delegate for More Control”If you need more control, implement WatchConnectorDelegate:
import SwiftUIimport CapgoWatchSDK
class WatchHandler: WatchConnectorDelegate { func didReceiveMessage(_ message: [String: Any]) { print("Received: \(message)") // Handle incoming message }
func didReceiveMessageWithReply(_ message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { print("Received request: \(message)") // Process and send reply replyHandler(["status": "processed"]) }
func didReceiveApplicationContext(_ context: [String: Any]) { print("Context updated: \(context)") }
func didReceiveUserInfo(_ userInfo: [String: Any]) { print("User info received: \(userInfo)") }
func reachabilityDidChange(_ isReachable: Bool) { print("Reachability changed: \(isReachable)") }
func activationDidComplete(with state: WCSessionActivationState) { print("Activation completed: \(state.rawValue)") }}
// In your app setup:let handler = WatchHandler()WatchConnector.shared.delegate = handlerWatchConnector.shared.activate()Troubleshooting
Section titled “Troubleshooting”Watch App Not Appearing on Watch
Section titled “Watch App Not Appearing on Watch”- Ensure bundle IDs are correctly related (watch app bundle ID should be iOS app bundle ID +
.watchkitapp) - Check that both apps are signed with the same team
- On physical device: Open Watch app on iPhone → My Watch → scroll to find your app → toggle ON
Messages Not Being Received
Section titled “Messages Not Being Received”- Verify both apps have WCSession activated
- Check
isReachablebefore sending messages - For guaranteed delivery, use
transferUserInfoinstead ofsendMessage - Ensure listeners are registered before the other device sends messages
”Session Not Activated” Error
Section titled “”Session Not Activated” Error”- Call
WatchConnector.shared.activate()early in app lifecycle - On iOS, the plugin activates automatically - ensure plugin is imported
- Check that WatchConnectivity capability is added to iOS target
Build Errors with CapgoWatchSDK
Section titled “Build Errors with CapgoWatchSDK”- Ensure the package is added to the watch target, not iOS target
- Clean build folder: Product → Clean Build Folder (Cmd + Shift + K)
- Reset package caches: File → Packages → Reset Package Caches
Simulator Issues
Section titled “Simulator Issues”- Reset the simulators: Device → Erase All Content and Settings
- Ensure iOS and watchOS simulators are compatible pairs
- Both simulators need to be running for communication to work
Next Steps
Section titled “Next Steps”- API Reference - Complete API documentation
- Communication Patterns - When to use each method
- Example App - Full working example