read

1. Create widget extension

Create a new target and select Widget Extension. An extension is like a mini-app, with it’s own “application identifier”.

In our example, we name it “MyWidget”. A corresponding “MyWidgetExtension.appex” will be automatically embedded in your app target.

2. fastlane match

If you’re using fastlane, then you don’t want Xcode to manage the signing. In that case, there is an extra step to create a new set of application identifier for the extension.

fastlane produce -a com.just2us.myapp.mywidget --skip_itc

Then run fastlane match for the new identifier to set up and download the provisioning profiles. Then in Xcode, disable “Automatically manage signing” and select the provisioning profile.

3. Configure widget

When you create a widget, the initial “Hello World” code will all be in 1 file. I will immediately break it into 3 files, because there are multiple components at work.

First of all, the Widget protocol let us describe the widget. It is the extension entry point.

@main
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            ShortcutsView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("A shortcut widget for the app.")
        .supportedFamilies([.systemSmall])
    }
}

Our example is a “shortcut” for the app, and supports only the small (square) size.

We use the simple StaticConfiguration. We will not dive into the dynamic intent-based configuration in this post.

The widget also states 2 of our custom classes (which you’ll see later):

  1. Provider - our timeline provider
  2. ShortcutsView - our SwiftUI view to render

4. Timeline Provider

This is how a widget works: iOS will ask your widget for a timeline so that iOS can update your widget at the right time.

To do that, you have to provide your implementations for TimelineProvider, and your TimelineEntry struct.

A minimal TimelineEntry requires a date field. Obviously, the date tells iOS the datetime to update the widget.

struct SimpleEntry: TimelineEntry {
    let date: Date
}

TimelineProvider ask for 3 things:

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
        let timeline = Timeline(entries: [SimpleEntry(date: Date())], policy: .atEnd)
        completion(timeline)
    }
}

5. The SwiftUI View

This is where your widget creates the view.

struct ShortcutsView: View {
    @Environment(\.widgetFamily) var size
    var entry: Provider.Entry

    var body: some View {
        switch size {
        case .systemSmall: ...
        case .systemMedium: ...
        case .systemLarge: ...
        }
    }
}

struct ShortcutsView_Previews: PreviewProvider {
    static var previews: some View {
        ShortcutsView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

You will likely want to use the widgetFamily environment key to create the view according to the size.

That’s all for the basics of creating a widget.


When user taps on a widget, the host app will open. You can also specify a link to open. There are 2 ways to provide the link (be it deeplink or universal link):

  1. widgetURL modifier
  2. Link view

The difference is that widgetURL is for the whole widget view; therefore only 1 for a widget.

So most likely you will use the SwiftUI Link for different views in a widget eg. news widget.

Handle the URL in AppDelegate (even if you have already handled in your SceneDelegate).

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // Handle the url
    ...
}

For the small squarish widget, you cannot have multiple links in it. In fact you cannot even have 1 link. Instead, you have to use widgetURL.

This seems to be a design enforcement.

UI Guide & radius

The Human Interface Guideline details how we should design good glanceable widgets.

A new API to help with corner radius is ContainerRelativeShape. It is a shape, like RoundedRectangle, but the corner radius adapt to it’s parent. With that, you need not hard code any corner radius in the widget.

The standard margin is 16pt.

Multiple Widgets

You can have different types of widgets in an app. If your app support more than 1 widget, then you would use the WidgetBundle instead.

@main
struct MultipleWidgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        Widget1()
        Widget2()
    }
}

PITFALL: Conflicting provisioning settings

XyzWidgetExtension has conflicting provisioning settings. XyzWidgetExtension is automatically signed for development, but a conflicting code signing identity iPhone Distribution has been manually specified…

If signing has the above error, it could likely just be Xcode generating improperly. Uncheck “Automatically manage signing”, then check it back, and select a Team.

Another possible error.

Embedded binary is not signed with the same certificate as the parent app. Verify the embedded binary target’s code sign settings match the parent app’s.

For that, make sure the app and extension use the same signing cert. If using fastlane match, read the section above to set up signing.

PITFALL: App review rejection

A rejection story. When I release a widget for Torchlight app, it was rejected.

Specifically, your app’s widget only provides users shortcut to your app’s features, which is not appropriate.

I had a call with an Apple app reviewer, and was told that if I appeal, it will “100% be rejected”. I continue to appeal, quoting other apps that were shortcuts too, and that my torchlight widget is similar to iOS torch shortcut (which is in lockscreen)!

A week later, it was approved! So looks like nothing is 100% when it comes to app review.

PITFALL: Timeline entry not refreshing

While it is up to app to specify how frequent to update the widget, and when to request a new timeline (eg. .atEnd), it is totally up to iOS to refresh the widget or not.

In my personal experience, iOS refresh my widget only a handful of times a day, even when I asked to refresh every hour.

Apparently, the system has a finite number of refresh a day for each app, and it depends on how frequent the user see/interact with the widget, etc There is no clarification on how it really works, yet.


Image

@samwize

¯\_(ツ)_/¯

Back to Home