in the first ever fully-online worldwide developer conference wwdc20, apple announced the introduction of widgets in home screen, with ios 14. this, along with moving apps to app library was an unexpected move since the home screen has not changed a bit since the first version of ios was introduced.
image courtesy – Widget HIG guidelines by Apple
in the meet WidgetKit WWDC20 session, we saw Nahir and Neil from apple walk us through the newly announced widgets in home screen (which are fully built with SwiftUI and interoperable between iOS 14, iPadOS and MacOS Big Sur, btw). the summarised text version of the key points in the keynote is available in my another post over at better programming on medium, a 4 minute read. do give it a read up because this post is going to be purely technical, building up over the concepts i covered in the article linked above.
prerequisites
ensure that you have
1. a mac device running macos catalina 10.15.5 or later
2. xcode 12.0 beta 1 or above installed (though for some reason i was unable to preview widgets in swiftui preview pane in xcode 12.0 beta 1, which was solved once i moved to beta 2)
3. basic knowledge of swiftui. you might find this free course at raywenderlich helpful
today is a great day to build widgets with widgetkit!
open the xcode project you want to add widgets to. if you don’t have one already, would be completely fine to create a new blank one.
the app can be made in UIKit or SwiftUI, since widgets are SwiftUI only, I am proceeding with SwiftUI interface.
once done, add a new Widget Extension by going to File > New > Target > select Widget Extension
you have a checkbox to include Configuration Intent. in this tutorial we are going to cover Static Configuration, since we don’t want to give an option for users to edit something in the widget. uncheck that checkbox and select Finish.
also select Activate on the following screen
navigate to the swift file under the newly created extension, to see the preview of the widget in the preview pane.
you can also preview how the widget looks in your phone by pressing on the play-like icon above the widget in the preview canvas.
understanding the boilerplate
looking at the skeleton of the widget class, you would see a struct Provider of type TimelineEntry having two functions, snapshot and timeline. We will come back to these at a later point of time.
then, there is a struct SimpleEntry
of type TimelineEntry
with a date
property. this is usually the structure for data to be shown in the widget.
then there are two views, PlaceholderView
and Static_WidgetEntryView
. these are the views which are ultimately displayed on the home screen inside the widgets you are going to create. PlaceholderView
is shown when there is no data to display, for eg. when a user has just installed and not opened your app, yet places a widget on the screen.
the other Static_WidgetEntryView
is shown under normal conditions, which is a view designed to look good on the home screen, with data filled in. we will be fully designing the view to be shown in the widget, in the next few minutes.
then there is this important struct
of type Widget
here, the custom widget view is returned at line 7, which needs to be modified based on the view you want to show in the widget’s real estate. the configurationDisplayName
and description
is shown to the user while he adds widget to his home screen, so you might want to modify that as well.
finally, the PreviewProvider
struct
is responsible for showing your widget in the preview canvas.
let's play!
timeline provider:
Timeline Provider is the engine of the widget — the provider class is mainly responsible for providing a bunch of views in a timeline, along with an option to give a snapshot of the widget. for eg. when user wants to place the widget, a preview of the widget is shown to the user which is obtained from snapshot
, whereas after being placed on the screen, timeline
will return the views to be displayed on the home screen.
since widgets load in the home screen, Apple doesn’t want users to look at a bunch of loading widgets. hence, widgets in iOS 14 are just a bunch of views bundled in a timeline.
when the app is opened, it gives the system a bunch of views along with a time label. for example, if your app wants to show countdown to an event in a widget, your app needs to make a bunch of views from that point of time till the event date, and tell the system which view to display at what time. for example, if the event is 4 days away, the app could send in 5 views:
view 1 -> to be shown today > has contents “Event is 4 days away”
view 2-> to be shown tomorrow > has contents “Event is 3 days away”
view 3-> to be shown the day after tomorrow > has contents “Event is 2 days away”
view 4-> to be shown a day before the event > has contents “Event is 1 day away”
view 5-> to be shown on the day of event > has contents “Event has started”
iOS displays the appropriate view based on system time, and hence the widget ends up looking right at any point of time.
also, creating such bunch of views is cheap task, and doesn’t require constant compute time. helping iOS preserve battery as well, adding to the smooth performance.
looking at the code,
on line 14, 5 entries are created, one for each hour. For each entry, a view is created with Static_WidgetEntryView
these views are then provided to the iOS System, so that appropriate views can be shown basis time.
you can try testing this part out by replacing byAdding: .hour
with byAdding: .minute
and test in simulator. After the widget is shown, for the first five minutes, widget view will update once every minute, after which the widget wont change and will keep showing the last view the widget extension sent to iOS.
designing the widget view:
now, let’s make a custom view whose contents will be rendered into the widget.
at this point, we are going to segregate the widget view and provider, since everything is in same swift file. we know that the TimelineProvider
is responsible for building views and showing it onto a timeline (a timeline is nothing but a time labelled series of views, which get shown on the home screen based on current time.
more theory in my previous article, linked at the top of this page) and the main struct Static_Widget
is responsible for defining the widget views in the application, we will be moving the widget view to a new file.
begin by creating a WidgetView swift file by adding a new file > SwiftUI View > Next, name it WidgetView. Ensure it has the widget extension checked under target extension, and click Create
the swift file created would have template code for a SwiftUI View, however we want to design a widget here. In the newly created file, import WidgetKit
as well.
now, let’s look at the widget to be created
a funky static widget showing a static weight, and a relative time which needs to be kept up-to date.
since the widget needs only two variables, we will create a struct to store them. also create an extension of the struct to have a preview data to easily be able to fill in WidgetData, just for demonstration purposes.
now that the data structure is added, let’s prepare the view and preview for the widget. create a WidgetView and a provider for preview as follows
here you will notice that the preview has WidgetView previewed with .previewContext(WidgetPreviewContext(family: .systemSmall))
which renders out the view like a widget. Here the family can be changed to systemMedium
and systemLarge
to preview contents in medium and large formats as well. Adding in previews for other widget families too, the code for WidgetView gets updated to
begin with adding views to the body of the widget on line 17. looking at the design, we need two labels, one for static text “Weight”
and another for weight from data: WidgetData
. so after adding two Text(“”)
views in a VStack
with a Spacer()
in between, lets also add font and foreground colours to make them look good.
to make the contents of widget left aligned, set the alignment
of the VStack to leading
now, we need this VStack to be placed inside another view with a background color of cyan, so let’s put the VStack in a HStack and add the background color as well. also, a little bit of padding might look nice.
here, we have used ContainerRelativeShape().fill()
for the radius of the background. its important to note that acc. to widget design guidelines, its not recommended to use fixed rounded corners, instead use CornerRelativeShape which draws its radius concentric to its superview.
to make the HStack fully occupy the view, let’s add a Spacer(minLength: 0)
along with the VStack in the HStack.
now, we have almost achieved half of what needs to be built
to build the last checked
view, adding a new VStack
to add the two Text
views
here we have used the new Swift syntax for Text to display date relative to current time. for example, Text(“data.date, style: .relative”)
. Using this with a widget ensures that time shown in the widget is relative to current system time and is always updated every second. widget views aren’t meant to update every second, so iOS gives an option to place a dynamic time text, the per second updates are handled efficiently.
now finally placing them in a ZStack to get background color of yellow and with some padding for VStack inside it, let’s extract the two subviews to WeightView
and LastUpdatedView
using Xcode
once that’s done, the final code for WidgetView would be
and the created widgets would look like
by now you might have noticed that we have designed the view for .systemSmall
family and widget isn’t looking good for other sizes. To make the .systemMedium
widget look more appealing, lets add in a relevant icon which is shown only for .systemMedium
widget (Read how to render part of view for widget based on family type)
first, create an environment variable with
@Environment(\.widgetFamily) var widgetFamily
the value of this variable is set as .systemMedium
if the user has placed medium sized widget on his home screen. In that case, you can use
if widgetFamily == .systemMedium {
//Render view here
}
you can add in the image to either side of the current content.
exercise: Design the widget to look good for .systemLarge format too, currently it looks like
for now, we won’t be supporting large size for widget. to support only desired widget size families, head to the main function and add .supportedFamilies([.systemSmall, .systemMedium])
to ensure users can’t place a .systemLarge
widget on their home screen.
putting it all together
now that we have designed the widget view, all that is left is to add this widget view to app’s widget extension. remember the @main
function we talked about earlier? (Scroll to “Understanding the boilerplate” section above). to get the WidgetView as a widget in your app,
add WidgetView(data: .previewData)
as shown replacing the default widget view, and run this on the simulator
and voila! we have your first widget added to your iOS application!
you can download the completed project here on Github — https://github.com/iAkashlal/SwiftUI-Widgets
where to go from here?
congratulations on adding your first widget to your iOS application. what we have discussed till now is just the bare minimum of what you can do with widgets, so thought I’d add a few more things that can be accomplished with WidgetKit.
Timeline Reload Policy
TimelineProvider
provides views of the widget for a series of time. however after the last entry has been provided and shown, TimelineReloadPolicy
can be used to assist WidgetKit with scheduling updates so that widget can be refreshed. TimelineReloadPolicy
is a struct with
struct TimelineReloadPolicy{
atEnd
after(date)
never
}
atEnd
— after last entry is displayed, an update is scheduled. When that update happens, iOS can request for subsequent entries from timeline provider, which can happen in a cycle.
after(date)
— an update is scheduled at the date provided — irrespective of currently existing entries
never
— system will not independently update a widget. we can set when the widget reloads via the WidgetCenter API.
apart from this, the system intelligently schedules updates — with onboard intelligence and based on user behavior, ensuring widgets are always up-to date. You can learn more about this in Widgets — Code Along (Part 2)
relevance
relevance is an optional property on timeline entry, which assists WidgetKit in helping decide which widget to show at the top when multiple widgets are placed in a stack. while calculating entries in a timeline, you can add a float value for relevance for each entry, assisting WidgetKit with the most relevant of all submitted entries for your widget. this is illustrated in part 2 of Widgets — Code Along WWDC20 session.
configuration
you would want configuration in your widgets if you want to give the user an option to edit widget and select from a few available options that affect how the widget is displayed. WidgetKit configuration is driven by SiriKit, with the core technology for configuration being INIntents. more info available in WWDC20 Add Configuration and Intelligence to your Widgets session.
deep linking
.systemSmall
widget’s whole real estate can deeplink into one section of your app using .widgetURL
modifier on the view, while .systemMedium
and .systemLarge
widgets can either use .widgetURL
modifier, or SwiftUI Link API to have tappable zones to link to different pages. This is shown in WWDC20 Widgets — Code Along session (Part 3)
multiple widgets (widget bundles)
if your app wants to have multiple widgets, you can’t keep adding multiple widget extensions. and since one widget extension can have only one main method, you cant use the way we used to add more widget views.
for this purpose, you can use WidgetBundle
and move the main tag there, for example
@main
struct MultipleWidgets: WidgetBundle{
@WidgetBundleBuilder
var body: some Widget{
WidgetView1()
WidgetView2()
}
}
with WidgetView1()
and WidgetView2()
being valid SwiftUI Views.
resources
this piece of content would be impossible without:
understanding WidgetKit in iOS 14
build SwiftUI Views for widgets
widgets — code along (Parts 1, 2 and 3)
How Deep Link “widgetURL” will get in AppDelegate?
to get the whole widget to be one tappable target, you can use the widgetURL modifier on your widget view: Apple documentation for WidgetURL
for medium and large widgets, you can use the new SwiftUI Link view to have different tappable targets with different URLs: https://developer.apple.com/documentation/swiftui/link
Hi Akashlal,
Thanks for sharing this great knowledge about WidgetKit. Do you know if we can add user analytics tool to track events? Like widget is displayed; User click one tappable target…
hey Jerry,
i have never tried adding user analytics. Since WidgetKit is an extension and not a target, i am guessing the only way left is to add some parameter to deeplink url and get the main app to track that tap and add it to say firebase analytics