Getting Started With Widgets

In this tutorial, you’ll add a widget to a large SwiftUI app, reusing its views to show entries from the app’s repository. By Audrey Tam.

3.2 (38) · 4 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Defining Your Widget

It makes sense to make your widget display some of the information the app shows for each tutorial.

Card view in the Emitron app.

Card view in the Emitron app.

Card view in the Emitron app.

This view is defined in UI/Shared/Content List/CardView.swift. My first idea was to just add the widget target to this file. But that required adding more and more and more files, to accommodate all the intricate connections in Emitron.

All you really need are the Text views. The images are cute, but you’d need to include the persistence infrastructure to keep them from disappearing.

You’re going to copy the layout of the relevant Text views. These use several utility extensions, so find these files and add the EmitronWidgetExtension target to them:

Add the widget target to these files.

Add the widget target to these files.

Add the widget target to these files.

Note: Be sure you notice Assets at the top of the image.

CardView displays properties of a ContentListDisplayable object. This is a protocol defined in Displayable/ContentDisplayable.swift:

protocol ContentListDisplayable: Ownable {
  var id: Int { get }
  var name: String { get }
  var cardViewSubtitle: String { get }
  var descriptionPlainText: String { get }
  var releasedAt: Date { get }
  var duration: Int { get }
  var releasedAtDateTimeString: String { get }
  var parentName: String? { get }
  var contentType: ContentType { get }
  var cardArtworkUrl: URL? { get }
  var ordinal: Int? { get }
  var technologyTripleString: String { get }
  var contentSummaryMetadataString: String { get }
  var contributorString: String { get }
  // Probably only populated for screencasts
  var videoIdentifier: Int? { get }
}

Your widget only needs name, cardViewSubtitle, descriptionPlainText and releasedAtDateTimeString. So you’ll create a struct for these properties.

Creating a TimelineEntry

Create a new Swift file named WidgetContent.swift and make sure its targets are emitron and EmitronWidgetExtension:

Create WidgetContent with targets emitron and widget.

Create WidgetContent with targets emitron and widget.

Create WidgetContent with targets emitron and widget.

It should be in the EmitronWidget group.

Now add this code to your new file:

import WidgetKit

struct WidgetContent: TimelineEntry {
  var date = Date()
  let name: String
  let cardViewSubtitle: String
  let descriptionPlainText: String
  let releasedAtDateTimeString: String
}

To use WidgetContent in a widget, it must conform to TimelineEntry. The only required property is date, which you initialize to the current date.

Creating an Entry View

Next, create a view to display the four String properties. Create a new SwiftUI View file and name it EntryView.swift. Make sure its target is only EmitronWidgetExtension, and it should also be in the EmitronWidget group:

Create EntryView with only the widget as target.

Create EntryView with only the widget as target.

Create EntryView with only the widget as target.

Now replace the contents of struct EntryView with this code:

let model: WidgetContent

var body: some View {
  VStack(alignment: .leading) {
    Text(model.name)
      .font(.uiTitle4)
      .lineLimit(2)
      .fixedSize(horizontal: false, vertical: true)
      .padding([.trailing], 15)
      .foregroundColor(.titleText)
    
    Text(model.cardViewSubtitle)
      .font(.uiCaption)
      .lineLimit(nil)
      .foregroundColor(.contentText)
    
    Text(model.descriptionPlainText)
      .font(.uiCaption)
      .fixedSize(horizontal: false, vertical: true)
      .lineLimit(2)
      .lineSpacing(3)
      .foregroundColor(.contentText)
    
    Text(model.releasedAtDateTimeString)
      .font(.uiCaption)
      .lineLimit(1)
      .foregroundColor(.contentText)
  }
  .background(Color.cardBackground)
  .padding()
  .cornerRadius(6)
}

You’re essentially copying the Text views from CardView and adding padding.

Delete EntryView_Previews entirely.

Creating Your Widget

Now start defining your widget. Open EmitronWidget.swift and double-click SimpleEntry in the line:

struct SimpleEntry: TimelineEntry {

Choose Editor ▸ Edit All in Scope and change the name to WidgetContent. This will cause several errors, which you’ll fix in the next few steps. First delete the declaration:

struct WidgetContent: TimelineEntry {
  let date: Date
}

This declaration is now redundant and conflicts with the one in WidgetContent.swift.

One of the provider’s methods provides a snapshot entry to display in the widget gallery. You’ll use a specific WidgetContent object for this.

Just below the import statements, add this global object:

let snapshotEntry = WidgetContent(
  name: "iOS Concurrency with GCD and Operations",
  cardViewSubtitle: "iOS & Swift",
  descriptionPlainText: """
    Learn how to add concurrency to your apps! \
    Keep your app's UI responsive to give your \
    users a great user experience.
    """,
  releasedAtDateTimeString: "Jun 23 2020 • Video Course (3 hrs, 21 mins)")

This is the update to our concurrency video course, which was published on WWDC day 2.

Now replace the line in placeholder(in:) with this:

snapshotEntry

To display your widget for the first time, WidgetKit renders this entry in the widget’s view, using the modifier redacted(reason: .placeholder). This displays text and images in the correct layout, but masks their contents. This method is synchronous, so don’t do any network downloads or complex calculations here.

Also replace the first line of getSnapshot(in:completion:) with this:

let entry = snapshotEntry

WidgetKit displays this entry whenever the widget is in a transient state, waiting for data or appearing in the widget gallery.

A widget needs a TimelineProvider to feed it entries of type TimelineEntry. It displays each entry at the time specified by the entry’s date property.

The most important provider method is getTimeline(in:completion:). It already has some code to construct a timeline, but you don’t have enough entries yet. So comment out all but the last two lines, and add this line above those two lines:

let entries = [snapshotEntry]

You’re creating an entries array that contains just your snapshotEntry.

Finally, you can put all these parts together.

First, delete EmitronWidgetEntryView. You’ll use your EntryView instead.

Now replace the internals of struct EmitronWidget with the following:

private let kind: String = "EmitronWidget"

public var body: some WidgetConfiguration {
  StaticConfiguration(
    kind: kind, 
    provider: Provider()
  ) { entry in
    EntryView(model: entry)
  }
  .configurationDisplayName("RW Tutorials")
  .description("See the latest video tutorials.")
}

The three strings are whatever you want: kind describes your widget, and the last two strings appear above each widget size in the gallery.

Build and run on your device, sign in, then close the app to see your widget.
If it’s still displaying the time, delete it and add it again.

Widget gallery with snapshot entry.

Widget gallery with snapshot entry.

Widget gallery with snapshot entry.

And here’s what the medium size widget looks like now:

The medium size widget on the home screen.

The medium size widget on the home screen.

The medium size widget on the home screen.

Only the medium size widget looks OK, so modify your widget to provide only that size. Add this modifier below .description:

.supportedFamilies([.systemMedium])

Next, you’ll provide real entries for your timeline, directly from the app’s repository!