Introducing iOS 14 WidgetKit With SwiftUI

Let’s learn how to build some widgets for our home screen in a few minutes

WWDC 2020 gave us a lot of enhancements and updates, but the introduction of the WidgetKit framework unarguably stands out.

iOS 14 has introduced a redesigned home screen, with the inclusion of widgets being a huge addition. Widgets are not just an eye-pleasing UI shortcut for our apps. They also aid in providing useful information to the user from time to time.

In some ways, they’re a calmer form of notification that provides you with the latest information from the apps (if the developer has opted to do so) without intruding. Additionally, there’s a new Smart Stack feature in iOS 14 that groups a set of widgets that you can swipe through. Smart Stacks tend to provide the relevant widget at the top by using on-device intelligence that takes into account the time of the day, location, and some other attributes.

WidgetKit is built purely using SwiftUI, which opens endless opportunities for building beautiful widgets. It’s important to note than WidgetKit isn’t meant to build mini-apps.

Besides providing a Link button that lets you set a deep-link URL to navigate to a particular part of your application, you can’t add any animations or other interactions in widgets.

Our Goal

  • Understanding the anatomy of WidgetKit. We’ll explore the framework and see how widgets are created and updated.

  • Build a Joke of the Hour widget in SwiftUI.

  • Bundle our SwiftUI application with two widgets by using WidgetBuilder.

Without further ado, let’s get started.

WidgetKit Framework: Under the Hood

To understand the anatomy of WidgetKit, let’s create a new Xcode 12 SwiftUI project and ensure that the SwiftUI App Lifecycle is chosen.

To create our first widget, Go to File → New → Target and select the Widget Extension template. Make sure you uncheck “Include Configuration Intent,” as we’ll only cover static configurations in this article.

Upon adding the Widget Extension Target, you’ll be greeted with a file containing a bunch of SwiftUI code that might seem new to you. Don’t worry, we’ll walk through all of it.

The following snippet is the starting point of your widget:

These are the inferences drawn from the code above:

  • kind is an identifier used to distinguish the widget from others in the WidgetCenter.

  • Inside WidgetConfiguration, we set a Placeholder view that’s displayed while the widget loads. The contents of the widget are set inside the FirstWidgetEntryView that we’ll see shortly.

  • Provider is a struct of type TimelineProvider that’s the core engine of the widget. It’s responsible for feeding the widget with data and setting intervals for updating the data. Again, we’ll come to this shortly.

  • The view modifiers configurationDisplayName and description display respective information in the Widget Gallery — a place that hosts all the widgets on your device.

There’s another new modifier for the WidgetKit framework (supportedFamilies) inside which we can pass the different sizes of widgets we want for our application. For example, .supportedFamilies([.systemLarge]) allows only large-sized widgets.

WidgetKit Timeline Provider

As the name suggests, the Provider struct looks to provide data for the widget’s content. It conforms to a TimelineProvider protocol that requires the implementation of two methods (snapshot and timeline), as shown in the default example given below:

  • The snapshot function is used to immediately present a widget view, while the Timeline Provider looks to load the data. It’s important that you set the snapshot method with dummy data only, as this view would be displayed in Widget Gallery too.

  • The SimpleEntry struct is what actually holds the data for a single TimelineEntry and is eventually displayed in the widget’s content.

  • The timeline function, on the other hand, is used to create one or more entries. We can set the time interval after which the TimelineEntry needs to be updated.

  • The code above basically adds five timeline entries that would be used to update the widget’s content hourly. The data set in the default example is the date text.

  • The Timeline instance also contains a TimelineReloadPolicy. The system would use this policy to determine when to invoke the timeline function again — for loading the next set of timeline entries. In the code above, the policy is set as atEnd, which means after the fifth SimpleEntry is displayed on the widget, the system would trigger the timeline function for the next batch.

  • Besides atEnd, we also have a after(Date:) property. It is used to set a specific date at which we want the next timeline to be fetched. Do note that system may not fire the timeline function at the exact date. Also, we can set a never as the reload policy to ensure the timeline is not fired again.

Additionally, if you want to trigger a reload of widget entries, you can use the WidgetCenter API. It lets us reload a particular timeline or all timelines.

The TimelineProvider struct is incredibly important, as your dynamic content would be fetched in it to update the widget’s content on specific intervals of your choice.

SwiftUI Widget View

Now that we’re clear on how WidgetKit operates under the hood, let’s look at the SwiftUI view that’ll display the widget on the screen.

The struct above populates the widget’s view with the data set in the Provider.EntryProvider.Entry is the data source for our widget’s view, and if you look back at the TimelineProvider, you’ll see SimpleEntry struct was set as the type alias for Entry.

Now that we’ve seen how WidgetKit operates — from displaying Placeholder to populating the widget views with Timeline entries specified in the TimelineProvider — we’re ready to build our own custom widget: a jokes tracker that updates every hour (you can customize the time interval).

Build a Jokes Widget Tracker in iOS 14

The TimelineProvider lets us fetch data from network requests or a database while the PlaceholderView is displayed. The approach to fetching data from an API for widgets is the same as you’d do for the application.

The following code uses a Combine-powered URLSession to decode the API response and pass it through a completion block:

public class DataFetcher : ObservableObject{
var cancellable : Set<AnyCancellable> = Set()
static let shared = DataFetcher()
func getJokes(completion: @escaping ([ChuckValue]?) -> Void){
let urlComponents = URLComponents(string: "http://api.icndb.com/jokes/random/10/")!
URLSession.shared.dataTaskPublisher(for: urlComponents.url!)
.map{$0.data}
.decode(type: ChuckJokes.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
}) { response in
completion(response.value)
}
.store(in: &cancellable)
}
}

Do note that SwiftUI widget views cannot and should not be updated using property wrappers like Published or State.

The response is decoded using the following Codable struct:

struct ChuckJokes : Decodable {
let type : String?
let value : [ChuckValue]?
enum CodingKeys: String, CodingKey {
case type = "type"
case value = "value"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decodeIfPresent(String.self, forKey: .type)
value = try values.decodeIfPresent([ChuckValue].self, forKey: .value)
}
}
struct ChuckValue : Decodable {
let id : Int?
let joke : String?
enum CodingKeys: String, CodingKey {
case id = "id"
case joke = "joke"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey: .id)
joke = try values.decodeIfPresent(String.self, forKey: .joke)
}
}
view raw Jokes-Codable.swift hosted with ❤ by GitHub

Creating Our Data Source

Our widget would display the jokes as a string. So, let's add the field in our SimpleEntry struct as shown below:

struct JokesEntry: TimelineEntry {
public let date: Date
public let joke : String
}

Creating a Timeline Provider

The following timeline provider structure fetches the data from DataFetcher and sets them on an array of TimelineEntries configured at a given time interval:

struct JokeProvider: TimelineProvider {
public func snapshot(with context: Context, completion: @escaping (JokesEntry) -> ()) {
let entry = JokesEntry(date: Date(), joke: "...")
completion(entry)
}
public func timeline(with context: Context,
completion: @escaping (Timeline<Entry>) -> ()) {
DataFetcher.shared.getJokes{
response in
let date = Date()
let calendar = Calendar.current
let entries = response?.enumerated().map { offset, currentJoke in
JokesEntry(date: calendar.date(byAdding: .second, value: offset*2, to: date)!, joke: currentJoke.joke ?? "...")
}
let timeLine = Timeline(entries: entries ?? [], policy: .atEnd)
completion(timeLine)
}
}
}

Note: Setting a time interval of seconds isn’t recommended in production applications. The code snippet above does that for demonstration purposes only.

Creating Widget SwiftUI View

The following piece of code represents our SwiftUI view with the SimpleEntry struct set as its data source:

struct JokesWidgetEntryView : View {
var entry: JokeProvider.Entry
@Environment(\.widgetFamily) var family
@ViewBuilder
var body: some View {
switch family {
case .systemSmall:
Text(entry.joke)
.minimumScaleFactor(0.3)
.padding(.all, 5)
default:
VStack{
Text("Chuck Norris Cracks:")
.font(.system(.headline, design: .rounded))
.padding()
Text(entry.joke)
.minimumScaleFactor(0.3)
.padding(.all, 5)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.pink)
}
}
}

In the code above, by using the SwiftUI @ViewBuilder and using the widgetFamily enum type as an EnvironmentObject, we’ve set different SwiftUI views for various widget shapes.

Finally, let’s set our WidgetConfiguration and launch the application on an iOS 14 device:

struct JokesWidget: Widget {
private let kind: String = "JokesWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: JokeProvider(), placeholder: PlaceholderView()) { entry in
JokesWidgetEntryView(entry: entry)
}
.configurationDisplayName("Jokes Widget")
.description("This is a widget ")
}
}

Here is the output of the widget in action:

As you can see, we have two widgets of different sizes and different layouts. You can further customize them with your own beautiful SwiftUI views.

For demonstration purposes, I’ve set the TimelineProvider interval to one second (though it isn’t ideal in widgets and can eat a lot of battery).

Using Widget Builder for Creating Multiple Widgets

We can create different widgets and bundle them together using a WidgetBundleBuilder:

@main
struct MyWidgetBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
AnotherJokesWidget()
JokesWidget()
}
}

Conclusion

Widgets are handy and pleasing to the eyes. As Apple has introduced a new WidgetKit framework and widgets on the home screen on iOS, a lot of applications will look to leverage it. However, it’ll be interesting to see how much of an impact it has on user privacy. The full source code is available in the GitHub repository.

In the next part, we’ll use intent configurations and links to build some handy widgets.

That’s it for this one. Thanks for reading.

Looking to get started with iOS 14? I recommend Programming iOS 14: Dive Deep into Views, View Controllers, and Frameworks, Eleventh Edition (Grayscale Indian Edition)