WidgetKit Development
We are currently developing widgets for two of our apps: Pop Out Timer and Milestone Fitness. We’re considering widgets for the rest of our apps as well but we want to see if there is much uptake from users first. In the meantime, it has been a great way to continue to expand our SwiftUI knowledge. Here are a few things we’ve run into.
Getting your widgets in to TestFlight
Starting with Xcode 12 Beta 4 we were finally able to get our widgets to be available in our TestFlight builds. The keys were to set “Dead Code Stripping” to No for your widget target.
The second key was to uncheck “Include bitcode for iOS content” when uploading to the App Store.
Working with Widget Sizes
By default widgets will support the three system sizes: small, medium, and large. It’s going to take a really special app to ever warrant a large widget so we’re focusing on small and medium sizes. You set this by calling the following on your widget configuration.
.supportedFamilies([.systemSmall, .systemMedium])
Something we didn’t realize until we were done building our first small widget is that the small family treats the entire widget as the tap target. You cannot break your widget down into individual buttons that execute different functionality in your app.
Take our Pop Out Timer widgets, for example.
We would love to provide a small widget with 3 recent timers and the start stopwatch button. But it’s impossible with the current implementation of WidgetKit.
To be fair, Apple actually follows their own rules on this one. The best example is the stocks widget.
Tapping a particular stock on the medium widget will take you to its details in the app. You’d think you could do the same on the small widget, after all it has the same vertical tap target. But on the small widget it simply opens the app to the stock list.
Make Tap Targets
Wherever possible we want users to be able to jump directly into relevant content based on the widget they’re operating off. We do this with deep links and a couple of special WidgetKit views/functions.
On a small widget you simply add the following to any View within the hierarchy:
.widgetURL(URL(string: "com.company.app://feature"))
If you’d like to make individual tap targets on a medium widget you use Links instead of Buttons. Like this:
Link("Title)", destination: URL(string: "com.company.app://feature)")!)
Dealing with Failure States
We need to think about failure states. A user might be able to add or configure your widget while still lacking some configuration within your app. In Milestone a user might add a widget to a fitness category they aren’t tracking. We can handle that gracefully and take them to the app’s Settings screen when they tap it.
Widget Bundles
For Milestone we wanted two kinds of small widgets. This is accomplished through a Widget Bundle. Simple enough (once you’ve got your widgets built). Like this:
@main struct Widgets: WidgetBundle { @WidgetBundleBuilder var body: some Widget { CurrentProgressWidget() GoalWidget() } }
Of course, you'll need to remove the @main from the the original widget Xcode created for you.
Don’t judge me for my quarantine step goal.
Updating Your Widget
How you decide to update your widgets is very much dependent on the type of app you’re making. In our apps there is nothing time-based about when we should refresh the widget content. The easy part is the code to reload your timelines using either WidgetCenter.shared.reloadAllTimelines() or WidgetCenter.shared.reloadTimelines(ofKind: "YourWidgetKind”). The harder part is figuring out exactly when and how often those should be called.
For Pop Out Timer we need to refresh our widget whenever the recent timer list changes (which also means clearing the cache). This was pretty easy and these actions are always done while the user is running the app. According to Apple’s documentation you get unlimited widget refreshes while your app is foreground.
Milestone posed a harder problem. There are plenty of actions that a user may take while running the app that should cause a reload (activating or deactivating a data type, reordering data types, setting, changing, or removing a goal, changing the app theme, etc.). What we were more concerned about was updates that should occur while in the background, the most important updates: health data changes. This is what users will want to be able to see without opening the app. We were already monitoring tracked data types to provide goal and record notifications but it’s taken some trial and error to get consistent reloads. For instance, we make sure we don’t try to reload if the values haven’t changed. We also coalesce these updates so that updating 5 categories in quick succession only results in a single call to reloadAllTimelines().
Wrap-Up
Overall I’ve been pretty pleased with WidgetKit. It’s a smart system by Apple to balance functionality and battery life. It’s been a bit frustrating as they’ve changed function signatures with each beta, but it’s neat what you can accomplish with even just a couple hundred lines of code in a single .swift file.












