An iOS 17 SwiftUI WidgetKit Tutorial

From the previous chapter, we now understand the elements that make up a widget and the steps involved in creating one. In this, the first of a series of tutorial chapters dedicated to WidgetKit, we will begin creating an app that includes a widget extension. After completing these tutorials, a functioning widget will have been created, including widget design and timelines, support for different size families, deep links, and configuration using intents.

About the WidgetDemo Project

The project created in this tutorial is the early prototype of a weather app designed to teach children about weather storms. The objective is to provide the user with a list of severe weather systems (tropical storms, thunderstorms, etc.) and display a second screen describing the weather system when a storm type is selected.

A second part of the app is intended to provide real-time updates on severe weather occurring in different locations around the world. When a storm is reported, a widget will be updated with information about the type and location of the storm, together with the prevailing temperature. When the user taps the widget, the app will open the screen containing information about that storm category.

Since this app is an early prototype, however, it will only provide weather updates from two cities, and that data will be simulated rather than obtained from a genuine weather service. However, the app will be functional enough to demonstrate how to implement the key features of WidgetKit.

Creating the WidgetDemo Project

Launch Xcode and select the option to create a new Multiplatform App project named WidgetDemo.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Building the App

Before adding the widget extension to the project, the first step is to build the basic structure of the app, consisting of a List view populated with some storm categories, which, when selected, will appear in a detail screen.

The first requirement is a structure to store the current weather type. Within the project navigator panel, right-click on the WidgetDemo folder and select the New File… menu option, followed by the Swift File template. Click the Next button, name the file WeatherType.swift, and click the Create button. Load the new file into the editor and modify it so that it reads as follows:

struct WeatherType: Hashable {
    var name: String
    var icon: String
}Code language: JavaScript (javascript)

The detail screen will be declared in a new SwiftUI View file named WeatherDetailView.swift. Within the project navigator panel, right-click on the WidgetDemo folder and select the New File… menu option. Select the SwiftUI View template option in the resulting dialog and click the Next button. Name the file WeatherDetailView.swift before creating the file.

With the WeatherDetailView.swift file selected, modify the view declaration so that it reads as follows:

import SwiftUI
 
struct WeatherDetailView: View {
    
    var weather: WeatherType
    
    var body: some View {
        VStack {
            Image(systemName: weather.icon)
                .resizable()
                    .scaledToFit()
                    .frame(width: 150.0, height: 150.0)
            Text(weather.name)
                .padding()
                .font(.title)
            Text("If this were a real weather app, a description of \(weather.name) would appear here.")
                .padding()
            Spacer()
        }
    }
}
 
#Preview {
    WeatherDetailView(weather: WeatherType(
              name: "Thunder Storms", icon: "cloud.bolt"))
}Code language: Swift (swift)

When rendered, the above view should appear in the preview canvas, as shown in Figure 55-1 below:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 55-1

Next, select the ContentView.swift file and modify it to add a List view embedded in a NavigationStack as follows:

import SwiftUI
 
struct ContentView: View {
    var body: some View {
        
        NavigationStack {
            List {    
                NavigationLink(value: WeatherType(name: "Hail Storm",
                                                  icon: "cloud.hail")) {
                    Label("Hail Storm", systemImage: "cloud.hail")
                }
 
                NavigationLink(value: WeatherType(name: "Thunder Storm",
                                                  icon: "cloud.bolt.rain")) {
                    Label("Thunder Storm", systemImage: "cloud.bolt.rain")
                }
                
                NavigationLink(value: WeatherType(name: "Tropical Storm",
                                                  icon: "tropicalstorm")) {
                    Label("Tropical Storm", systemImage: "tropicalstorm")
                }
                
            }
            .navigationDestination(for: WeatherType.self) { weather in
                WeatherDetailView(weather: weather)
            }
            .navigationTitle("Severe Weather")
        }
    }
}Code language: Swift (swift)

Once the changes are complete, make sure that the layout matches that shown in Figure 55-2:

Figure 55-2

Using Live Preview, make sure that selecting a weather type displays the detail screen populated with the correct storm name and image.

Adding the Widget Extension

The next step in the project is to add the widget extension by selecting the File -> New -> Target… menu option. From within the target template panel, select the Widget Extension option, as shown in Figure 55-3, before clicking on the Next button:

Figure 55-3

On the subsequent screen, enter WeatherWidget into the product name field. When the widget is completed, the user will be able to select the geographical location for which weather updates are to be displayed. To make this possible, the widget must use the intent configuration type. Before clicking on the Finish button, therefore, make sure that the Include Configuration App Intent option is enabled and the Include Live Activity option is turned off, as shown in Figure 55-4:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 55-4

When prompted, click on the Activate button to activate the extension within the project scheme. This will ensure that the widget is included in the project build process:

Figure 55-5

Once the extension has been added, refer to the project navigator panel, where a new folder containing the widget extension will have been added, as shown in Figure 55-6:

Figure 55-6

Adding the Widget Data

Now that the widget extension has been added to the project, the next step is adding some data and data structures to provide the basis for the widget timeline. Begin by right-clicking on the WidgetDemo folder in the project navigator and selecting the New File… menu option.

From the template selection panel, select the Swift File entry, click on the Next button, and name the file WeatherData.swift. Before clicking on the Create button, make sure that the WeatherWidgetExtension entry is enabled in the Targets section of the panel, as shown in Figure 55-7, so that the file will be accessible to the extension:

Figure 55-7

As outlined in the previous chapter, each point in the widget timeline is represented by a widget timeline entry instance. Instances of this structure contain the date and time that the entry is to be presented by the widget, together with the data to be displayed. Within the WeatherData.swift file, add a TimelineEntry structure as follows (noting that the WidgetKit framework also needs to be imported):

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

import Foundation
import WidgetKit
 
struct WeatherEntry: TimelineEntry {
    var date: Date
    let city: String
    let temperature: Int
    let description: String
    let icon: String
    let image: String
}Code language: Swift (swift)

Creating Sample Timelines

Since this prototype app does not have access to live weather data, the timelines used to drive the widget content will contain sample weather entries for two cities. Remaining within the WeatherData.swift file, add these timeline declarations as follows:

.
.
let londonTimeline = [
    WeatherEntry(date: Date(), city: "London", temperature: 87, 
          description: "Hail Storm", icon: "cloud.hail", 
                image: "hail"),
    WeatherEntry(date: Date(), city: "London", temperature: 92, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder"),
    WeatherEntry(date: Date(), city: "London", temperature: 95,   
          description: "Hail Storm", icon: "cloud.hail", 
                image: "hail")
]
 
let miamiTimeline = [
    WeatherEntry(date: Date(), city: "Miami", temperature: 81, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder"),
    WeatherEntry(date: Date(), city: "Miami", temperature: 74,
          description: "Tropical Storm", icon: "tropicalstorm", 
                image: "tropical"),
    WeatherEntry(date: Date(), city: "Miami", temperature: 72, 
          description: "Thunder Storm", icon: "cloud.bolt.rain", 
                image: "thunder")
]Code language: Swift (swift)

Note that the timeline entries are populated with the current date and time via a call to the Swift Date() method. The provider will replace these values with more appropriate ones when WidgetKit requests the timeline.

Adding Image and Color Assets

Before moving to the next step of the tutorial, some image and color assets need to be added to the asset catalog of the widget extension.

Begin by selecting the Assets item located in the WeatherWidget folder in the project navigator panel, as highlighted in Figure 55-8:

Figure 55-8

Add a new entry to the catalog by clicking on the button indicated by the arrow in Figure 55-8 above. In the resulting menu, select the Color Set option. Click on the new Color entry and change the name to weatherBackgroundColor. With this new entry selected, click on the Any Appearance block in the main panel, as shown in Figure 55-9:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 55-9

Referring to the Color section of the attributes inspector panel, set Content to Display P3, Input Method to 8-bit Hexadecimal and the Hex field to #4C5057:

Figure 55-10

Select the Dark Appearance and make the same attribute changes, this time setting the Hex value to #3A4150.

Next, add a second Color Set asset, name it weatherInsetColor, and use #4E7194 for the Any Appearance color value and #7E848F for the Dark Appearance.

The images used by this project can be found in the weather_images folder of the sample code download available from the following URL:

https://www.ebookfrenzy.com/retail/ios17_web/

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Once the source archive has been downloaded and unpacked, open a Finder window, navigate to the weather_ images folder, and select, drag and drop the images onto the left-hand panel of the Xcode asset catalog screen, as shown in Figure 55-11:

Figure 55-11

Designing the Widget View

Now that the widget entry has been created and used as the basis for some sample timeline data, the widget view needs to be designed. When the widget extension was added to the project, a template widget entry view was included in the WeatherWidget.swift file, which reads as follows:

struct WeatherWidgetEntryView : View {
    var entry: Provider.Entry
 
    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)
 
            Text("Favorite Emoji:")
            Text(entry.configuration.favoriteEmoji)
        }
    }
}Code language: Swift (swift)

As currently implemented, the view is passed a widget entry from which a date value and emoji are extracted and displayed on Text views.

Modify the view structure so that it reads as follows, keeping in mind that it will result in syntax errors appearing in the editor. These will be resolved later in the tutorial:

struct WeatherWidgetEntryView: View {
    var entry: Provider.Entry
        
    var body: some View {
        ZStack {
            Color("weatherBackgroundColor")
            WeatherSubView(entry: entry)
        }
    }
}
 
struct WeatherSubView: View {
    
    var entry: WeatherEntry
    
    var body: some View {
        
        VStack {
            VStack {
                Text("\(entry.city)")
                    .font(.title)
                Image(systemName: entry.icon)
                    .font(.largeTitle)
                Text("\(entry.description)")
                    .frame(minWidth: 125, minHeight: nil)
            }
            .padding(.bottom, 2)
            .background(ContainerRelativeShape()
                       .fill(Color("weatherInsetColor")))
            Label("\(entry.temperature)°F", systemImage: "thermometer")
        }
        .foregroundColor(.white)
        .padding()
    }
}Code language: Swift (swift)

We also need to set the container background color of the WeatherWidgetEntryView when it is called in the body of the WeatherWidget declaration:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

struct WeatherWidget: Widget {
    let kind: String = "WeatherWidget"
 
    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, 
          intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            WeatherWidgetEntryView(entry: entry)
                .containerBackground(Color("weatherBackgroundColor"), 
                                for: .widget)
        }
    }
}Code language: Swift (swift)

Since we have changed the view, the preview provider declaration will also need to be changed as follows:

#Preview(as: .systemSmall) {
    WeatherWidget()
} timeline: {
    WeatherEntry(date: Date(),
                        city: "London", temperature: 89,
                 description: "Thunder Storm",
                        icon: "cloud.bolt.rain", image: "thunder")
    WeatherEntry(date: Date(),
                        city: "London", temperature: 89,
                 description: "Hail",
                        icon: "cloud.hail", image: "hail")
Code language: Swift (swift)

Once all necessary changes have eventually been made to the WeatherWidget.swift file, the above preview provider will display a preview canvas configured for the widget’s small family size with two sample timeline entries.

Modifying the Widget Provider

When the widget extension was added to the project, Xcode added a widget provider to the WeatherWidget. swift file. This declaration now needs to be modified to make use of the WeatherEntry structure declared in the WeatherData.swift file. The first step is to modify the snapshot() method to use WeatherEntry and to return an instance populated with sample data:

.
.
struct Provider: AppIntentTimelineProvider {
.
.
    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) 
                                              async -> WeatherEntry {
        WeatherEntry(date: Date(), city: "London",
                            temperature: 89, description: "Thunder Storm",
                                 icon: "cloud.bolt.rain", image: "thunder")
    }
.
.Code language: Swift (swift)

Next, the timeline() method needs to be modified to return an array of timeline entry objects together with a reload policy value. Since user configuration has not yet been added to the widget, the timeline() method will be configured initially to return the timeline for London:

func timeline(for configuration: ConfigurationAppIntent, in context: Context) 
                                           async -> Timeline<WeatherEntry> {
    var entries: [WeatherEntry] = []
    var eventDate = Date()
    let halfMinute: TimeInterval = 30
 
    for var entry in londonTimeline {
        entry.date = eventDate
        eventDate += halfMinute
        entries.append(entry)
    }
            
    return Timeline(entries: entries, policy: .atEnd)
}Code language: Swift (swift)

The above code begins by declaring an array to contain the WeatherEntry instances before creating variables designed to represent the current event time and a 30-second time interval, respectively.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

A loop then iterates through the London timeline declared in the WeatherData.swift file, setting the eventDate value as the date and time at which the event is to be displayed by the widget. A 30-second interval is then added to the eventDate ready for the next event. Finally, the modified event is appended to the entries array. Once all of the events have been added to the array, it is used to create a Timeline instance with a reload policy of never (in other words, WidgetKit will not ask for a new timeline when the first timeline ends). The timeline is then returned to WidgetKit via the completion handler.

This implementation of the timeline() method will result in the widget changing content every 30 seconds until the final entry in the London timeline array is reached.

Configuring the Placeholder View

The Final task before previewing the widget is to make sure that the placeholder view has been implemented. Xcode will have already created a placeholder() method for this purpose within the WeatherWidget.swift file, which reads as follows:

func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}Code language: Swift (swift)

This method now needs to be modified so that it returns a WeatherWidget instance populated with some sample data as follows:

func placeholder(in context: Context) -> WeatherEntry {    
    WeatherEntry(date: Date(), city: "London",
                       temperature: 89, description: "Thunder Storm",
                            icon: "cloud.bolt.rain", image: "thunder")
}Code language: Swift (swift)

Previewing the Widget

Using the preview canvas, verify that the widget appears, as shown in Figure 55-12 below:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 55-12

The preview also includes a panel for stepping through the timeline entries, as shown in Figure 55-13 below:

Figure 55-13

Before we can test the widget on a device or simulator, we must install the WidgetDemo app. In the Xcode toolbar, select WidgetDemo from the scheme menu:

Figure 55-14

If the WidgetDemo target is not listed in the menu, select the Manage Schemes… option to display the dialog shown in Figure 55-15:

Figure 55-15

Click on the + button highlighted above and select WidgetDemo from the resulting dialog before clicking OK and closing the Manage Schemes dialog:

Figure 55-16

Next, test the widget on a device or simulator by changing the active scheme in the Xcode toolbar to the WeatherWidgetExtension scheme before clicking on the run button:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 55-17

After a short delay, the widget will appear on the home screen and cycle through the different weather events at 30-second intervals:

Figure 55-18

Summary

The example project created in this chapter has demonstrated how to use WidgetKit to create a widget extension for an iOS app. This included the addition of the extension to the project, the design of the widget view and entry together with the implementation of a sample timeline. The widget created in this chapter, however, has yet to make use of the different widget size families supported by WidgetKit, a topic that will be covered in the next chapter.


Categories