Home / Blog / Migrating from WatchKit to a native watchOS app: lessons from two apps

Migrating from WatchKit to a native watchOS app: lessons from two apps

watchOS 9 deprecated the old WatchKit extensions. I ran the migration on two apps and a few surprises showed up along the way.

watchOS 9 (2022) was Apple’s big shift for Apple Watch app architecture. The old “WatchKit extension” model was deprecated. The new “watchOS app” target became the standard.

I migrated both my Apple Watch apps (Dentii and Snoozio) to the new architecture. Each one surfaced different surprises. Here’s what the migration actually looked like and what to watch out for.

Old versus new

Old WatchKit Extension approach:
– Three targets in Xcode: iOS app, Watch App, Watch Extension
– Watch App: UI files (storyboard)
– Watch Extension: executable code
– The split made sense for watchOS 1 to 6: code didn’t run on the Watch, an Extension on the iPhone updated the UI

New watchOS App approach:
– Two targets in Xcode: iOS app and Watch App (one target)
– UI and code in a single binary
– A native watchOS target that runs independently

Apple introduced native watchOS apps with watchOS 6+, and kept supporting the old approach alongside it. watchOS 9 deprecated the old approach.

Why migrate

It’s not just Apple’s nudge:

  1. New watchOS features only ship on the native target. Always On display, WidgetKit complications, new SwiftUI APIs.
  2. Build times improve. One target means less overhead.
  3. Debugging is easier. Extension debugging could get painful.
  4. App Store Connect submissions are cleaner. Watch extensions sometimes ended up in a separate review.

Stay on the old architecture and you can’t use new features. Eventually Xcode support goes away too.

Migration steps

The order I followed in Dentii:

Step 1: create the new target in Xcode.

File, New, Target, Watch App. Xcode drops in a target using the new architecture. The old Extension target stays, they coexist during migration.

Step 2: move the UI from the old Watch App.

If the old target used a storyboard, convert to SwiftUI. On watchOS 7+ SwiftUI is enough. Storyboards still work but SwiftUI is the future.

Step 3: move the code.

Copy the Swift files from the old Extension to the new target. Old WatchKit classes like InterfaceController are gone, you use SwiftUI views directly.

Step 4: revisit your WatchConnectivity logic.

WCSession setup and delegates stay the same. Message sending has shifted in some edge cases. Test.

Step 5: migrate complications.

The old CLKComplicationDataSource is deprecated. Rewrite with WidgetKit. This is the hardest part, the UI is fully new.

Step 6: delete the old Extension target.

When everything works, remove the old target. Clean up.

Total time: four days on Dentii, three on Snoozio. Depends on codebase size.

The three surprises I hit

Surprise 1: the Complication API is brand new

Old CLKComplication:
– CLKComplicationDataSource protocol
– Multiple family support (utilitarianSmall, modularSmall, and so on)
– CLKComplicationTemplate subclasses
– getCurrentTimelineEntry callback

New WidgetKit:
– TimelineProvider protocol
– WidgetFamily enum
– SwiftUI view tree
– getTimeline() function

Conceptually similar, but the syntax is all new. I rewrote my complications from scratch. A one day job.

Surprise 2: background refresh behaviour

Old Watch Extensions ran in background with certain constraints. On a new watchOS app:
– Background refresh fires through WKApplicationRefreshBackgroundTask, not BGTaskScheduler
– Apple throttles frequency more aggressively
– Battery impact is accounted for more strictly

Dentii used to plan a background refresh per brushing session. Under the new architecture refresh frequency dropped. The user experience shifted slightly. I accepted the trade-off for battery.

Surprise 3: iOS companion app communication

The WCSession API is the same, but edge case behaviour differs. Specifically:
– sendMessage frequency
– transferUserInfo delays
– updateApplicationContext timing

Some messages started arriving later than I expected. I widened the test coverage. Two or three hours of debugging.

Apple Watch Always On display

Always On is natively supported in the new watchOS app target. The screen stays on at low brightness even with the wrist down:

ContentView()
    .onChange(of: scenePhase) { _, newPhase in
        if newPhase == .inactive {
            // Always On mode
        }
    }

In Dentii the brushing timer screen shows a dim countdown in Always On. The user doesn’t have to lift the wrist constantly. That wasn’t possible on the old architecture.

Designing WidgetKit complications

WidgetKit now supports six widget families:

  • .accessoryCorner, corner of the circular face
  • .accessoryCircular, circular complications
  • .accessoryRectangular, one-line rectangular
  • .accessoryInline, one-line text in the clock bar

Each gets its own SwiftUI view. Support them all:

struct MyComplication: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "MyComplication", provider: Provider()) { entry in
            ComplicationView(entry: entry)
        }
        .supportedFamilies([.accessoryCircular, .accessoryCorner, .accessoryRectangular, .accessoryInline])
    }
}

You handle each family case separately on the UI side.

Post-migration testing

Lots of real-device testing:

  1. App launch time. Compare old and new.
  2. Complication refresh. Stays current on the watch face over hours?
  3. WCSession delivery. Data moves from iPhone to Watch?
  4. Background wake. Refresh events fire while the app is backgrounded?
  5. Memory usage. The Watch CPU is weak, no memory pressure allowed.
  6. Battery impact. After a day, battery usage old vs new?

Once that list passes, you’re ready for App Store submission.

Production gotchas

Things I watched after migration:

  • Old app versions still run the old architecture. Until users update, old binaries keep working. They coexist.
  • Watch app auto-install. When the iOS app installs and the user has an Apple Watch, the Watch app auto-installs. Behaviour is more reliable on the new architecture.
  • App Store Connect config. The Watch app, when defined on the new target, is embedded in the iOS app binary. Not a separate bundle.

Takeaway

Migrating a watchOS app is unavoidable. Apple is slowly burying the old WatchKit architecture. A three to five day investment, but you get Always On, native WidgetKit complications, and better performance on the other side.

If you have an Apple Watch app still on the old architecture, put migration on the 2025 roadmap. Future watchOS versions will only add new features to the native target.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch