Search code examples
iosswiftxcodewatchoswidgetkit

How to use the #Preview macro in a watchOS 10 and iOS 17 Widget Extension that shares the same code (Xcode 15 Beta 4)


Please note, this question is regarding Xcode 15 beta 4, iOS 17 beta 3, and watchOS 10 beta 3

I have a watchOS 10 App with iOS 17 companion app with two Widget (WidgetKit) Extension Targets. Both targets share the same codebase where each Swift file has both targets set in the Target Membership settings.

Some parts of my code are behind #if os(watchOS) compiler directives, for example when setting the supported widget families:

#if os(watchOS)
    .supportedFamilies([.accessoryCircular, .accessoryRectangular])
#else
    .supportedFamilies([.accessoryCircular, .systemSmall, .systemMedium])
#endif

I got multiple Widgets that are made available through a WidgetBundle. However, the following issues also exist when there is only one widget.

This is my current WidgetBundle and preview code:

@main
struct WidgetExtensionBundle: WidgetBundle {
    var body: some Widget {

        StaticSampleWidget()

        if #available(iOSApplicationExtension 17.0, *) {
            AppIntentSampleWidget()
        }
    }
}


#if os(watchOS)
#Preview(as: .accessoryRectangular) {
    StaticSampleWidget()
} timeline: {
    StaticSampleWidget_Entry(date: .now, emoji: "😀")
    StaticSampleWidget_Entry(date: .now, emoji: "🤩")
}
#else
#Preview(as: .systemSmall) {
    StaticSampleWidget()
} timeline: {
    StaticSampleWidget_Entry(date: .now, emoji: "😀")
    StaticSampleWidget_Entry(date: .now, emoji: "🤩")
}
#endif

When I try to use the preview in Xcode, it cannot build the app and show no previews. By trying different settings and combinations of selected targets, I sometimes get at least the iOS preview running, but never the watchOS preview. Also, the first #Preview behind the #if os(watchOS) is grayed out. On the Canvas it always shows two Preview tabs, even though it should only be one depending on the selected target. I sometimes get the following errors, and sometimes it doesn't tell me at all what is wrong:

Sample.watchkitapp.WidgetExtensionWatch.appex with id xxx.xxx.Watch- Sample.watchkitapp.WidgetExtensionWatch does not define either an NSExtensionMainStoryboard or NSExtensionPrincipalClass key with a string value

Failed to build WidgetExtensionBundle.swift

How do I correctly setup my targets, and preview code to be able to show the relevant previews for iOS and watchOS?


Solution

  • After several trials and errors, I figured out the issues I had. I'll explain step by step what I've done to get it running in my case. Please note, there might be certain settings that could still prevent the preview from working, but this worked for me. Also, the iOS/watchOS and Xcode versions are still in Beta. Things might change and not work the same after a future release.

    First I confirmed the following settings:

    • The watchOS Widget Target is added as a dependency in the watchOS target The iOS Widget Target is added as a dependency in the iOS app

    • The watchOS Widget Target is set to support only watchOS as a
      destination The iOS Widget Target is set to support iOS and iPad as a destination

    • And in the Build Settings, the supported Platform match those too.

    • In the watchOS Widget target Build Settings I also set App is Available Only on Apple Watch to Yes

    • Two Info.plist and Info-Watch.plist files are set individually in each target's Build Settings under Packaging > Info.plist File

    • There are four build schemes, one for each target.

    • Under Manage Run Destinations > Simulators I ensured the Apple Watch was also set to always show as run destination.

    Most importantly I changed the preview code to the following:

    #if os(watchOS)
    let previewWidgetFamily: WidgetFamily = .accessoryRectangular
    #else
    let previewWidgetFamily: WidgetFamily = .systemSmall
    #endif
    
    #Preview(as: previewWidgetFamily) {
        StaticSampleWidget()
    } timeline: {
        StaticSampleWidget_Entry(date: .now, emoji: "😀")
        StaticSampleWidget_Entry(date: .now, emoji: "🤩")
    }
    

    I think this might be a bug with the new Swift macros. But by putting macros and the compiler directive separate, nothing was grayed out anymore and only one preview tab was shown.

    • Next, I did run each target in the simulator (iOS App, watchOS App, iOS Widget, watchOS Widget) and made sure everything did build for the simulator.

    • Note, when running and debugging the extensions, only one Widget can be inside the WidgetBundle, all the others need to be commented out while running the extension in the simulator

    • I selected either the watchOS or iOS Widget extension scheme.

    • I made sure the preview canvas has also the correct device type selected(!)

    And finally, the preview was working with the correct device.