Exploring WidgetKit in iOS 14

Ratnesh Jain

Oct 16, 2020 | 6 min read

Last time we explored the brand new SwiftUI introduced in WWDC19 by building an analog clock. It was really fun to use SwiftUI. Now this year Apple introduced lots of new technologies. Today we will explore the new WidgetKit technology by extending our previous SwiftUI blog.

According to Apple,

Widgets are used to show relevant, glanceable content from your app on the iOS Home screen or macOS Notification Center.

WidgetKit is available for iOS 14.0+, macOS 11.0+, Mac Catalysts 14.0+.

WidgetKit works in a separate target of our main app. So any app that follows the best practices for the app extension can adopt this feature easily.

Even though our app is supporting lower versions of iOS, our Widgetkit extension target can be deployed to iOS 14.0+ versions.

In this diagram, you can see that WidgetKitExtension is a separate extension in which we are having Widget which is responsible for providing the TimeLine for our widgets to be displayed by the iOS on the home screen (springboard).

WidgetKit works in a sense of the timeline. Our app/extension can provide the timeline with our data to be displayed on by the widgetkit on the home screen.

For example

  • Stocks app can update its widget to reflect the new stock prices at the appropriate time.
  • Clock’s widget can display current time for multiple places.
  • Calendar’s widget can display upcoming events right on the home screen.
  • Weather’s widget can display the weather of your selected region.
  • News’s widget can display the latest news.
    And so on.

As in the diagram, A Widget basically provides a timeline and provides a SwiftUI view for that timeline instance.

To provide the timeline, WidgetKit provides a protocol called TimelineProvider (or it can be IntentTimeLineProvider in case of IntentConfiguration).
By following these protocol requirements we can provide our full timeline and reload policy to WidgetKit.

A reload policy is used by the WidgetKit to reload the timeline once the current timeline ends or at a custom request from our application or extension.

Once we provide the timeline, WidgetKit will call our closure with the timeline entry to collect the new SwiftUI view for the latest entry from the timeline.

Clocky Widget.

Adjustment in earlier code.

Before we start creating the new widget, we first adjust our code we did in the previous article to make easy adoption for our shiny new widget.

Since the Timeline protocol has a date: Date requirement, we are moving the Timer publisher from TickHands to our top ContentView so it only depends on the external date dependency.

And we extracted the assembly of the clock views from the ContentView to new ClockView so we can share it to our widget extension.

And to adapt all the 3 of WidgetFamily which are systemSmall, systemMedium and systemLarge we are creating some variables for the font size and insets.

That’s all the changes we need to do.

Creating the new target.
To create a new widget kit extension called ClockyWidget we need to create a new target within the Xcode project

Create new target.
Clocky Widget

Now activate the target and Xcode will create a new Folder ClockyWidget with the required files and sample code implementation.

Now we modify some basic information for our widget like in below code.

@main
struct ClockyWidget: Widget {
    // 1
    private let kind: String = "ClockyWidget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            ClockyWidgetEntryView(entry: entry)
        }
	// 2
        .configurationDisplayName("Clocky Widget")
	// 3
        .description("Displays the current time")
    }
}
  1. We provide a kind string for our Static Configuration.
  2. We change our Display name to Clocky Widget
  3. We provide the description of our widget.

Here in ClockyWidget.swift file, all the protocol requirements are generated by the Xcode for us. So we only need to provide some of the data for timeline and our swiftui view from our main application target.

To use the source files we need to add those to our widget extension target also.

Add target membership of comman files to Widget extension

All the setup done. Now we can add some timeline entries.

// 4
struct ClockyEntry: TimelineEntry {
    public let date: Date
}

4- We create the struct for the TimelineEntry.
TimelineEntry protocol has one required property which is date.

// 5
struct Provider: TimelineProvider {
    // 6
    public typealias Entry = ClockyEntry
    // 7
    func getSnapshot(in context: Context, completion: @escaping (ClockyEntry) -> Void) {
        let entry = ClockyEntry(date: Date())
        completion(entry)
    }
    // 8
    func getTimeline(in context: Context, completion: @escaping (Timeline<ClockyEntry>) -> Void) {
        var entries: [ClockyEntry] = []
        
        // Generate a timeline consisting of 60 entries a second apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 60 {
            let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
            let entry = ClockyEntry(date: entryDate)
            entries.append(entry)
        }
        
        // 9
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
    
    // 10
    public func placeholder(in: Context) -> ClockyEntry {
        ClockyEntry(date: Date())
    }
}
  1. To create the whole timeline we need to follow TimelineProvider protocol.
  2. In this we declare that our Entry will be of type ClockEntry.
  3. this method is called when our widget is in the widget gallery waiting to be added in the home screen springboard.
    Here we return our ClockEntry in the completion with the current date for the snapshot.
  4. This is the heart of all the widget kit. In this we need to pass the Timeline instance to completion.
    So for our sample project we create the TimeLineEntries for every seconds for 1 min and append to our entries array.
  5. Then we create the Timeline object using our entries array and .atEnd reload policy.
    That means when in the timeline all the 60 entries get completed, we request WidgetKit to reload our timeline again.
  6. Now we pass our timeline to completion.

SwiftUI Magic.

Now we have our full timeline ready with the reload policy, we need to provide the view to be rendered on the home screen for the specific timeline entry.

// 11
struct ClockyWidgetEntryView : View {
    // 12
    var entry: ClockyProvider.Entry

    // 13
    @Environment(\.widgetFamily) var family: WidgetFamily
    
    // 14
    @ViewBuilder
    var body: some View {
        switch family {
        // 15
        case .systemSmall:
            ClockView(
                currentDate: entry.date,
                minuteTickHeight: 8,
                hourTickHeight: 16,
                tickInset: 32,
                font: Font.system(size: 12)
            )

        // 16    
        case .systemMedium:
            HStack {
                ClockView(
                    currentDate: entry.date,
                    minuteTickHeight: 8,
                    hourTickHeight: 16,
                    tickInset: 32,
                    font: Font.system(size: 12)
                )
                ClockView(
                    currentDate: entry.date.convert(to: TimeZone(identifier: "Europe/Berlin")!),
                    minuteTickHeight: 8,
                    hourTickHeight: 16,
                    tickInset: 32,
                    font: Font.system(size: 12)
                )
            }

        // 17
        case .systemLarge:
            ClockView(currentDate: entry.date)
        @unknown default:
            ClockView(currentDate: entry.date)
        }
        
    }
}
  1. SwiftUI View requires a View protocol to be conformed. So we created the ClockyWidgetEntryView.
  2. From the WidgetKit we will get the one entry for every timeline entry we set before. So to accommodate that we are declaring the variable entry: Provider.Entry which is our ClockyEntry.
  3. Since Widgets come in 3 sizes, and we are also supporting all sizes, we declare an Environment variable with \.widgetFamily key.
  4. We need to switch for all the widget families so we need to declare our body as a @ViewBuilder property.
  5. In systemSmall widget family we are only showing one small ClockView.
  6. In systemMedium widget family we are showing two clockView side by side with TimeZone difference.
  7. In systemLarge widget family we are again showing the one big ClockView.

As we had used custom font sizes for our TickText in previous article. We are now adjusting them according to widget family.

And that's all. Now we can run our extension and the result is beautiful.