DEV Community

Cover image for Apple Watch & iMessage Integration in Flutter
Michael Otieno Olang
Michael Otieno Olang

Posted on

Apple Watch & iMessage Integration in Flutter

A Complete Developer Guide

App Bundle ID: com.example.demoapp
App Group: group.com.example.demoapp
Targets: Runner (iPhone) · DemoWatch Watch App · DemoMessages MessagesExtension


Table of Contents

  1. Architecture Overview
  2. Dependencies Added
  3. Xcode Setup (One-Time)
  4. Apple Watch Integration
  5. iMessage Integration
  6. Data Flow Diagrams
  7. Publishing to the App Store
  8. Build & Archive Checklist
  9. Troubleshooting & Challenges Experienced

1. Architecture Overview

Flutter cannot run its UI engine on Apple Watch or inside iMessage extensions. Instead, we use two communication bridges:

Extension Bridge Technology Direction
Apple Watch WatchConnectivity framework via watch_connectivity Flutter package iPhone ↔ Watch (two-way, real-time)
iMessage App Groups shared UserDefaults container via shared_preference_app_group package Flutter → iMessage (one-way, cached)
┌─────────────────────┐         WatchConnectivity          ┌─────────────────────┐
│   Flutter App        │  ◄──────────────────────────────►  │  DemoWatch App   │
│   (iPhone)           │                                     │  (Apple Watch)      │
└─────────────────────┘                                     └─────────────────────┘
          │
          │  App Groups (UserDefaults)
          │  group.com.example.demoapp
          ▼
┌─────────────────────┐
│  iMessage Extension  │
│  DemoMessages      │
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

2. Dependencies Added

The following packages were added to pubspec.yaml:

dependencies:
  watch_connectivity: ^0.2.8        # Apple Watch two-way communication
  shared_preference_app_group: ^2.1.0  # Shared container for iMessage
Enter fullscreen mode Exit fullscreen mode

Install with:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

3. Xcode Setup (One-Time)

3.1 App Groups Capability

App Groups allow all three targets to share a single data container (group.com.example.demoapp).

You must add this capability to each target in Xcode:

  1. Open ios/Runner.xcworkspace in Xcode.
  2. In the Project Navigator (left panel), select Runner target.
  3. Go to Signing & Capabilities tab.
  4. Click + CapabilityApp Groups.
  5. Add: group.com.example.demoapp
  6. Repeat steps 2–5 for DemoWatch Watch App target.
  7. Repeat steps 2–5 for DemoMessages MessagesExtension target.

✅ Confirmation: Check the entitlements file at
ios/DemoWatch Watch App/DemoWatch Watch App.entitlements — it should contain:

<key>com.apple.security.application-groups</key>
<array>
    <string>group.com.example.demoapp</string>
</array>

3.2 Complication Data Source (Apple Watch only)

To enable watch face complications:

  1. Select the DemoWatch Watch App target in Xcode.
  2. Go to the Info tab.
  3. Add a new row with key: CLKComplicationDataSource
  4. Set value to: ComplicationController

3.3 Watch Target Platform Settings

The Watch target must resolve as a watchOS target during archive. If Xcode resolves it as iphoneos, the Watch asset catalog will be compiled as an iOS asset catalog and the archive will fail with:

Assets.xcassets: The stickers icon set, app icon set, or icon stack named "AppIcon" did not have any applicable content.
Enter fullscreen mode Exit fullscreen mode

For the DemoWatch Watch App target, confirm these build settings in Debug, Release, and Profile:

Setting Required value
SDKROOT watchos
SUPPORTED_PLATFORMS watchos watchsimulator
TARGETED_DEVICE_FAMILY 4
ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME AccentColor

The current project pins these in ios/Runner.xcodeproj/project.pbxproj. This matters because the project-level Flutter/iOS configuration sets SDKROOT = iphoneos and SUPPORTED_PLATFORMS = iphoneos; the Watch target must explicitly override that inherited iOS platform.

You can verify the resolved target settings with:

xcodebuild -showBuildSettings \
  -project ios/Runner.xcodeproj \
  -scheme "DemoWatch Watch App" \
  -configuration Release \
  -destination "generic/platform=watchOS" \
  -derivedDataPath /tmp/travler-derived-data \
  | rg "PLATFORM_NAME|SDKROOT|SUPPORTED_PLATFORMS|TARGETED_DEVICE_FAMILY|BUILT_PRODUCTS_DIR"
Enter fullscreen mode Exit fullscreen mode

Expected output should include:

BUILT_PRODUCTS_DIR = .../Release-watchos
PLATFORM_NAME = watchos
SDKROOT = .../WatchOS.platform/.../WatchOS*.sdk
SUPPORTED_PLATFORMS = watchos watchsimulator
TARGETED_DEVICE_FAMILY = 4
Enter fullscreen mode Exit fullscreen mode

3.4 Watch AppIcon Asset Catalog

The Watch app icon set lives at:

ios/DemoWatch Watch App/Assets.xcassets/AppIcon.appiconset
Enter fullscreen mode Exit fullscreen mode

Contents.json must declare watch-specific icon slots, not only the 1024 marketing icon. The catalog should include these families:

Role Sizes currently covered
notificationCenter 24x24, 27.5x27.5, 33x33
companionSettings 29x29 at 2x and 3x
appLauncher 40x40, 44x44, 46x46, 50x50, 51x51, 54x54
quickLook 86x86, 98x98, 108x108, 117x117, 129x129
watch-marketing 1024x1024

If new icon artwork is generated, preserve the filenames declared in Contents.json or update the JSON and PNGs together. A missing filename, wrong idiom, or iOS-only icon set will break archive.


4. Apple Watch Integration

4.1 Flutter Side — WatchService

File: lib/services/watch_service.dart

This service is the bridge between your Flutter app and the Apple Watch.

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:watch_connectivity/watch_connectivity.dart';

class WatchService {
  final WatchConnectivity _watch = WatchConnectivity();
  StreamSubscription<Map<String, dynamic>>? _messageSubscription;

  // All raw messages from the Watch
  final _messageController = StreamController<Map<String, dynamic>>.broadcast();
  Stream<Map<String, dynamic>> get messageStream => _messageController.stream;

  // Only action commands from the Watch (e.g., quick_book, cancel_booking)
  final _actionController = StreamController<Map<String, dynamic>>.broadcast();
  Stream<Map<String, dynamic>> get actionStream => _actionController.stream;

  WatchService() {
    _messageSubscription = _watch.messageStream.listen((message) {
      _messageController.add(message);
      if (message.containsKey('action')) {
        _actionController.add(message);
      }
    });
  }

  Future<bool> isSupported() async => await _watch.isSupported;
  Future<bool> isReachable() async => await _watch.isReachable;

  void dispose() {
    _messageSubscription?.cancel();
    _messageController.close();
    _actionController.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

4.2 Syncing Data to the Watch

Call syncTravelData() whenever the user's data changes — after login, booking, or wallet top-up. The data is sent using Application Context, which means the Watch receives the latest state even if it was not connected at the time.

final watchService = WatchService();

await watchService.syncTravelData(
  walletBalance: 125.50,
  walletCurrency: 'Ksh',
  nextTripDestination: 'Nairobi → Mombasa',
  nextTripDate: 'Aug 12, 2026',
  nextTripTime: '08:30 AM',
  nextTripSeat: '14B',
  qrCodeData: 'TICKET-XYZ-12345',  // the raw ticket/QR string
  activeBookingsCount: 2,
  unreadNotifications: 3,
);
Enter fullscreen mode Exit fullscreen mode

Best places to call this:

  • After a successful booking API response
  • After the user tops up their wallet
  • In your home screen initState() when the app opens

4.3 Listening for Watch Actions in Flutter

When the user presses Quick Book or Cancel on the Apple Watch, the Watch sends a message to Flutter via actionStream. Listen to this in your BLoC, provider, or main widget tree:

final watchService = WatchService();

watchService.actionStream.listen((actionData) {
  final action = actionData['action'] as String?;

  switch (action) {
    case 'quick_book':
      // Trigger your booking flow / open the booking screen
      NavigationKeys.mainNav.currentState?.pushNamed('/booking');
      break;

    case 'cancel_booking':
      final destination = actionData['destination'] as String?;
      // Call your cancel API
      context.read<BookingBloc>().add(CancelBookingEvent(destination: destination));
      break;
  }
});
Enter fullscreen mode Exit fullscreen mode

4.4 Native Watch UI (SwiftUI)

File: ios/DemoWatch Watch App/ContentView.swift

The Watch app has a 4-tab swipeable interface. Each tab is a SwiftUI view powered by WatchViewModel:

Tab View Content
1 TripDashboardView Next trip destination, date, time, seat + Quick Book & Cancel buttons
2 TicketQRView Auto-generated QR code scannable at the boarding point
3 WalletView Wallet balance + active bookings count
4 SupportNotificationsView Call Support button + unread notification count

File: ios/DemoWatch Watch App/WatchViewModel.swift

The WatchViewModel is the native Swift equivalent of WatchService. It implements WCSessionDelegate to receive data from Flutter and manages all published state for the SwiftUI views.

// How the Watch receives data from Flutter:
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
    DispatchQueue.main.async {
        self.parseData(applicationContext)
    }
}

// How the Watch sends actions back to Flutter:
func sendActionToFlutter(action: String, payload: [String: Any] = [:]) {
    var message = payload
    message["action"] = action
    if session.isReachable {
        session.sendMessage(message, replyHandler: nil, errorHandler: nil)
    }
}
Enter fullscreen mode Exit fullscreen mode

4.5 Watch Face Complications

File: ios/DemoWatch Watch App/ComplicationController.swift

Complications display DemoTravel info right on the Apple Watch clock face — without the user needing to open the app.

Supported complication families:

Family Watch Face What's Shown
graphicCorner Infograph 🚌 Mombasa + departure time
graphicCircular Infograph Modular Bus icon + time
graphicBezel Infograph Circle with full destination curved around it
modularLarge Modular 3-row table: Trip / Time / Seat
utilitarianLarge Utility 🚌 Nairobi → Mombasa · 08:30
utilitarianSmall Utility 🚌 08:30
modularSmall Modular Bus + time
circularSmall Color / Chronograph Bus + time

How complications stay up to date:

When WatchViewModel.parseData() receives new data from Flutter, it automatically:

  1. Writes the data to UserDefaults(suiteName: "group.com.example.demoapp")
  2. Calls CLKComplicationServer.sharedInstance().reloadTimeline(for:) on all active complications

This means the watch face updates instantly every time you call syncTravelData() from Flutter.

The ComplicationController reads from UserDefaults:

let defaults = UserDefaults(suiteName: "group.com.example.demoapp")
let destination = defaults?.string(forKey: "nextTripDestination") ?? "No Trip"
let time        = defaults?.string(forKey: "nextTripTime") ?? "--"
let seat        = defaults?.string(forKey: "nextTripSeat") ?? "--"
Enter fullscreen mode Exit fullscreen mode

5. iMessage Integration

5.1 Flutter Side — IMessageSyncService

File: lib/services/imessage_sync_service.dart

This service writes trip and wallet data into the shared App Group container so the iMessage extension can read them in Swift.

import 'dart:convert';
import 'package:shared_preference_app_group/shared_preference_app_group.dart';

const String _kAppGroup = 'group.com.example.demoapp';

class DemoTrip {
  final String id;
  final String destination;
  final String origin;
  final String date;
  final String time;
  final String seat;
  final String qrCodeData;

  DemoTrip({
    required this.id,
    required this.destination,
    required this.origin,
    required this.date,
    required this.time,
    required this.seat,
    required this.qrCodeData,
  });

  Map<String, dynamic> toJson() => {
    'id': id,
    'destination': destination,
    'origin': origin,
    'date': date,
    'time': time,
    'seat': seat,
    'qrCodeData': qrCodeData,
  };
}

class IMessageSyncService {
  static const String _tripsKey  = 'demo_imessage_trips';
  static const String _walletKey = 'demo_imessage_wallet';

  // Call once at app startup (e.g. in main.dart)
  Future<void> init() async {
    await SharedPreferenceAppGroup.setAppGroup(_kAppGroup);
  }

  Future<void> syncAll({
    required List<DemoTrip> trips,
    required double walletBalance,
    required String walletCurrency,
  }) async {
    await syncTrips(trips);
    await syncWalletBalance(walletBalance, walletCurrency);
  }

  Future<void> syncTrips(List<DemoTrip> trips) async {
    final encoded = jsonEncode(trips.map((t) => t.toJson()).toList());
    await SharedPreferenceAppGroup.setString(_tripsKey, encoded);
  }

  Future<void> syncWalletBalance(double balance, String currency) async {
    await SharedPreferenceAppGroup.setString(
      _walletKey,
      jsonEncode({'balance': balance, 'currency': currency}),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

5.2 Syncing Trip Data for iMessage

Step 1 — Initialise once at app startup (e.g., in main.dart before runApp):

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // ... existing initialisation ...

  // Initialise iMessage sync
  final iMessageSync = IMessageSyncService();
  await iMessageSync.init();

  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Sync whenever data changes:

final iMessageSync = IMessageSyncService();

await iMessageSync.syncAll(
  walletBalance: 125.50,
  walletCurrency: 'Ksh',
  trips: [
    DemoTrip(
      id: 'booking-001',
      origin: 'Nairobi',
      destination: 'Mombasa',
      date: 'Aug 12, 2026',
      time: '08:30 AM',
      seat: '14B',
      qrCodeData: 'TICKET-XYZ-12345',
    ),
    DemoTrip(
      id: 'booking-002',
      origin: 'Mombasa',
      destination: 'Nairobi',
      date: 'Aug 15, 2026',
      time: '02:00 PM',
      seat: '7A',
      qrCodeData: 'TICKET-ABC-67890',
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

5.3 Native iMessage UI (UIKit/Swift)

File: ios/DemoMessages MessagesExtension/MessagesViewController.swift

The iMessage extension is built with UIKit. It:

  1. Reads trip and wallet data from UserDefaults(suiteName: "group.com.example.demoapp")
  2. Displays a scrollable list of upcoming trips
  3. When a trip is tapped, inserts a rich MSMessage bubble into the chat

Reading from the shared container in Swift:

let defaults = UserDefaults(suiteName: "group.com.example.demoapp")

// Read trips
if let tripsString = defaults?.string(forKey: "demo_imessage_trips"),
   let data = tripsString.data(using: .utf8),
   let rawList = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
    trips = rawList.compactMap { DemoTrip(dict: $0) }
}

// Read wallet
if let walletString = defaults?.string(forKey: "demo_imessage_wallet"),
   let data = walletString.data(using: .utf8),
   let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
    walletBalance  = dict["balance"]  as? Double ?? 0
    walletCurrency = dict["currency"] as? String ?? "Ksh"
}
Enter fullscreen mode Exit fullscreen mode

Sending a rich iMessage bubble:

func sendTrip(_ trip: DemoTrip) {
    guard let conversation = currentConversation else { return }

    let message = MSMessage(session: conversation.selectedMessage?.session ?? MSSession())
    let layout  = MSMessageTemplateLayout()

    layout.caption             = "🚌 \(trip.origin)\(trip.destination)"
    layout.subcaption          = "\(trip.date) at \(trip.time)"
    layout.trailingCaption     = "Seat \(trip.seat)"
    layout.trailingSubcaption  = "Trip Booking"
    layout.image               = generateQRImage(from: trip.qrCodeData)  // QR code

    message.layout = layout
    message.url    = URL(string: "https://example.com")!

    conversation.insert(message, completionHandler: nil)
}
Enter fullscreen mode Exit fullscreen mode

Important: do not name your stored conversation property activeConversation. MSMessagesAppViewController already exposes an inherited activeConversation property. Use a separate name such as:

private var currentConversation: MSConversation?

override func willBecomeActive(with conversation: MSConversation) {
    currentConversation = conversation
    loadData()
}
Enter fullscreen mode Exit fullscreen mode

What the recipient sees:

The recipient sees a custom rich bubble in their chat containing:

  • 🚌 Nairobi → Mombasa (caption)
  • Aug 12, 2026 at 08:30 AM (subcaption)
  • Seat 14B (trailing)
  • QR code image embedded in the bubble

6. Data Flow Diagrams

Apple Watch — Data Flow

Flutter (Dart)                        Apple Watch (Swift)
─────────────────                     ────────────────────
WatchService.syncTravelData()
        │
        │  WatchConnectivity
        │  updateApplicationContext()
        │ ─────────────────────────►  WatchViewModel.didReceiveApplicationContext()
        │                                      │
        │                                      ├──► Updates @Published UI state
        │                                      ├──► Persists to UserDefaults (App Group)
        │                                      └──► Reloads watch face complications
        │
        │  WCSession.sendMessage()
        │ ◄─────────────────────────  WatchViewModel.sendActionToFlutter()
        │           (user taps Quick Book / Cancel on Watch)
        │
WatchService.actionStream emits
        │
        └──► Your BLoC / Provider handles the action
Enter fullscreen mode Exit fullscreen mode

iMessage — Data Flow

Flutter (Dart)                        iMessage Extension (Swift)
─────────────────                     ──────────────────────────
IMessageSyncService.syncAll()
        │
        │  UserDefaults
        │  (suiteName: group.com.example.demoapp)
        │ ─────────────────────────►  MessagesViewController.loadData()
        │                                      │
        │                                      ├──► Renders trip list
        │                                      └──► User taps trip → sends MSMessage bubble
        │
        │  (No return path — iMessage is one-way)
Enter fullscreen mode Exit fullscreen mode

7. Publishing to the App Store

Good news: The Apple Watch and iMessage extensions are bundled inside your main iOS app. You do not submit them separately. When users download DemoTravel from the App Store, the Watch app automatically installs on their paired Apple Watch.

Step-by-step publishing process:

Step 1 — Archive the app:

flutter build ipa
Enter fullscreen mode Exit fullscreen mode

Or in Xcode: Product → Archive (with "Any iOS Device" selected as the destination).

Step 2 — Open Xcode Organizer:
After archiving, Xcode Organizer opens automatically. Select your archive and click Distribute App.

Step 3 — Upload to App Store Connect:
Choose App Store Connect → Upload. Follow the prompts. The Watch and iMessage extensions are included automatically.

Step 4 — Add Apple Watch Screenshots:
In App Store Connect, navigate to your app listing. You will now see a section for Apple Watch Screenshots. Take screenshots from the Watch Simulator in Xcode and upload them here. Apple requires these before the app can be reviewed.

Required Watch screenshot sizes:

  • 40mm: 394 × 324 px
  • 41mm: 352 × 430 px (Series 7/8/9)
  • 44mm: 368 × 448 px
  • 45mm: 396 × 484 px

Step 5 — Submit for Review:
Once screenshots are uploaded and everything looks good, click Submit for Review. Apple reviews the Watch and iMessage extensions as part of the same review process.


8. Build & Archive Checklist

Use this checklist before creating an App Store or TestFlight archive that includes Apple Watch and iMessage targets.

8.1 Before archiving

  • Open ios/Runner.xcworkspace, not ios/Runner.xcodeproj, when using Xcode manually.
  • Confirm the Runner scheme includes dependencies on:
    • DemoWatch Watch App
    • DemoMessages MessagesExtension
  • Confirm DemoWatch Watch App resolves as watchOS:
    • PLATFORM_NAME = watchos
    • BUILT_PRODUCTS_DIR contains Release-watchos
    • SUPPORTED_PLATFORMS = watchos watchsimulator
  • Confirm DemoMessages MessagesExtension resolves as iPhoneOS:
    • PLATFORM_NAME = iphoneos
    • PRODUCT_TYPE = com.apple.product-type.app-extension.messages
  • Confirm all targets share the App Group:
    • Runner
    • DemoWatch Watch App
    • DemoMessages MessagesExtension

8.2 Useful build-only verification

This command checks the Watch and iMessage target build path without running tests or static analysis:

xcodebuild build \
  -project ios/Runner.xcodeproj \
  -scheme "DemoWatch Watch App" \
  -configuration Release \
  -destination "generic/platform=watchOS" \
  -derivedDataPath /tmp/travler-derived-data \
  CODE_SIGNING_ALLOWED=NO
Enter fullscreen mode Exit fullscreen mode

Successful Watch asset catalog output looks like:

CompileAssetCatalogVariant ... DemoWatch Watch App/Assets.xcassets
... --app-icon AppIcon --target-device watch --platform watchos
/* com.apple.actool.compilation-results */
.../Assets.car
LinkAssetCatalog ... DemoWatch Watch App/Assets.xcassets
note: Emplaced .../DemoWatch Watch App.app/Assets.car
Enter fullscreen mode Exit fullscreen mode

8.3 What not to run for this workflow

For fast archive debugging, skip:

flutter test
flutter analyze
dart analyze
Enter fullscreen mode Exit fullscreen mode

The archive blockers here are Xcode target configuration, asset catalogs, and native Swift extension compilation. Unit/widget tests and Dart static analysis do not diagnose those issues.


9. Troubleshooting & Challenges Experienced

Watch data not updating?

  • Make sure syncTravelData() is being called after a booking or login.
  • Confirm the Watch app is installed on a paired Watch.
  • On a physical device, both the iPhone and Watch must be connected.
  • Check that the App Group group.com.example.demoapp is listed in both the Runner and Watch targets in Xcode.

iMessage extension shows "No upcoming trips"?

  • Make sure IMessageSyncService.init() is called at app startup before any sync calls.
  • Confirm syncAll() has been called at least once.
  • Verify the App Group identifier group.com.example.demoapp is set on both Runner and DemoMessages targets.
  • Double check the key names in Swift (demo_imessage_trips, demo_imessage_wallet) match the constants in Dart.

Watch face complications not appearing?

  • Confirm CLKComplicationDataSource = ComplicationController is set in the Watch target's Info.plist in Xcode.
  • Long-press your watch face → Edit → browse Complications for "DemoTravel".
  • After calling syncTravelData() in Flutter, the complication should update within seconds.

Build error: No such module 'CoreImage.CIFilterBuiltins'

  • This module does not exist on watchOS. Use plain import CoreImage instead.
  • Use CIFilter(name: "CIQRCodeGenerator") instead of CIFilter.qrCodeGenerator().
  • Use Image(cgImage:, scale:, label:) in SwiftUI instead of Image(uiImage:).

pod install fails with xcodeproj compatibility error?

  • This is a known CocoaPods issue with Xcode 26+. Run flutter run from the command line instead of pod install directly — Flutter's tooling handles the pod installation correctly.

Archive error: AppIcon did not have any applicable content

Symptom:

/ios/DemoWatch Watch App/Assets.xcassets:
The stickers icon set, app icon set, or icon stack named "AppIcon" did not have any applicable content.
Enter fullscreen mode Exit fullscreen mode

Root causes seen in this project:

  1. The Watch target was resolving as iphoneos, so Xcode was trying to compile watch-only icon slots as iOS icons.
  2. The Watch AppIcon.appiconset/Contents.json only exposed watch-marketing content or incomplete watch slots.

Fix:

  • In ios/Runner.xcodeproj/project.pbxproj, ensure the DemoWatch Watch App target has:
SDKROOT = watchos;
SUPPORTED_PLATFORMS = "watchos watchsimulator";
TARGETED_DEVICE_FAMILY = 4;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
Enter fullscreen mode Exit fullscreen mode
  • In ios/DemoWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json, declare actual idiom: watch entries for notificationCenter, companionSettings, appLauncher, and quickLook, plus watch-marketing.
  • Confirm every filename referenced by Contents.json exists and has the expected pixel dimensions.

Verification:

jq empty "ios/DemoWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json"
sips -g pixelWidth -g pixelHeight "ios/DemoWatch Watch App/Assets.xcassets/AppIcon.appiconset/"*.png
Enter fullscreen mode Exit fullscreen mode

Then confirm the target is not resolving as iPhoneOS:

xcodebuild -showBuildSettings \
  -project ios/Runner.xcodeproj \
  -scheme "DemoWatch Watch App" \
  -configuration Release \
  -destination "generic/platform=watchOS" \
  -derivedDataPath /tmp/travler-derived-data \
  | rg "PLATFORM_NAME|SDKROOT|SUPPORTED_PLATFORMS|BUILT_PRODUCTS_DIR"
Enter fullscreen mode Exit fullscreen mode

Build error: No available simulator runtimes for platform watchsimulator

This can happen when actool runs in a restricted environment or Xcode cannot access simulator runtime services.

Symptoms:

No available simulator runtimes for platform watchsimulator.
SimServiceContext supportedRuntimes=[]
Enter fullscreen mode Exit fullscreen mode

Fix:

  • Run the build from normal Terminal or Xcode, not a restricted sandbox.
  • Open Xcode once and let it finish installing components.
  • Check Xcode → Settings → Platforms and install the required watchOS simulator runtime if missing.
  • Use a device/generic watchOS destination for archive:
xcodebuild build \
  -project ios/Runner.xcodeproj \
  -scheme "DemoWatch Watch App" \
  -configuration Release \
  -destination "generic/platform=watchOS" \
  -derivedDataPath /tmp/travler-derived-data \
  CODE_SIGNING_ALLOWED=NO
Enter fullscreen mode Exit fullscreen mode

Build error: activeConversation override in iMessage extension

Symptom:

overriding property must be as accessible as its enclosing type
cannot override with a stored property 'activeConversation'
ambiguous use of 'activeConversation'
Enter fullscreen mode Exit fullscreen mode

Cause:

MSMessagesAppViewController already has an inherited activeConversation property. A stored property with the same name in MessagesViewController conflicts with it.

Fix:

Rename the custom property:

private var currentConversation: MSConversation?

override func willBecomeActive(with conversation: MSConversation) {
    currentConversation = conversation
    loadData()
}

private func sendTrip(_ trip: DemoTrip) {
    guard let conversation = currentConversation else { return }
    // insert MSMessage...
}
Enter fullscreen mode Exit fullscreen mode

Build reaches Run Script and appears stuck

After Watch assets and iMessage compile issues are fixed, the build can spend a long time in Flutter/Xcode script phases, especially Run Script, Thin Binary, or Flutter embed steps.

What to check:

  • Confirm previous failures are gone before interrupting. In the log, Watch asset success looks like Emplaced .../DemoWatch Watch App.app/Assets.car.
  • Run from a normal Terminal when possible so script phases can access the full user environment.
  • If the script is silent for several minutes, rerun with Xcode's report navigator open to see the exact active phase.

Documentation generated for DemoTravel v3.5.4 — Apple Watch & iMessage Integration
This sample project is provided for educational purposes.
Bundle ID: com.example.demoapp · App Group: group.com.example.demoapp

Top comments (0)