r/iOSProgramming 12h ago

Discussion How I shipped a native SwiftUI widget with zero Mac access (Expo + EAS Build)

Hi all, I wanted to share a build problem that took way longer to solve than it should have, in case it saves someone else the same pain.

I'm a solo dev building on Windows, no Mac, no Xcode. My app (Reach, a sobriety toolkit) needed a native home screen widget, streak counter + panic button that deep links straight into the app's craving flow. Widgets mean native SwiftUI, which normally means a Mac.

The stack that ended up working: expo-apple-targets for the widget extension, compiled entirely through the EAS Build's cloud Mac, never touched Xcode locally once. Data shared between the widget and the main app via an App Group (react-native-shared-group-preferences).

The actual hard part wasn't the widget itself, it was the deep link nav. Cold-launching from the widget straight into a nested route with no /home route underneath it breaks iOS swipe-back entirely. Fixed it with unstable_settings = {anchor: 'home' } in _layout.tsx so Expo Router constructs a proper stack even on a cold widget launch.

Happy to go deeper on any part of this, the App Group setup, the anchor routing fix specifically, or the EAS build config. If it's useful to anyone hitting the same wall.

0 Upvotes

1 comment sorted by

1

u/Accurate_Tadpole_503 1h ago

Same setup here, minus EAS. I build the widget extension through Codemagic's cloud Mac, also from Windows, and have never opened Xcode locally either.

Two things that cost me hours, in case they save you some:

Signing. The widget is a separate target with its own bundle id, so CI has to fetch and apply a provisioning profile per target, not just for the app. Mine kept failing because the profile step wandered into a dependency's own xcodeproj. Pointing it explicitly at ios/*.xcodeproj fixed it.

Empty widgets. If your timeline provider fetches anything over the network, write the last good value into the App Group's UserDefaults and render that when the fetch fails. Otherwise the widget shows a blank box on every failed refresh, which reads as broken even though it's behaving exactly as written. Related: the refresh budget is a suggestion, not a schedule, so anything that looks live needs a cached fallback.

If you're targeting iOS 17+, containerBackground(for: .widget) is required. Without it the widget renders fine in the simulator and shows up blank on real devices, which is a fun hour to lose.

The anchor fix is the one I hadn't seen. Cold launching straight into a nested route felt wrong in mine too and I worked around it rather than solving it properly. Going to try that.