10 Tips on Developing iOS 14 Widgets

Widgets have been one of the top features of iOS 14 during WWDC 2020, where the biggest change ever of iOS Home Screen has been unveiled. On the user point of view, they represent a new type of interaction, a new entry point for apps; on a technology point of view, they are a manifesto of the way

Widgets have been one of the top features of iOS 14 during WWDC 2020, where the biggest change ever of iOS Home Screen has been unveiled. On the user point of view, they represent a new type of interaction, a new entry point for apps; on a technology point of view, they are a manifesto of the way chosen by Apple, where SwiftUI (the only way to build widget’s views) and an optimized universality (widgets are available on iOS, iPadOS and macOS) are key elements.
After experimenting with the WidgetKit framework on iOS 14 and Xcode 12, I want to share 10 interesting tips that could be useful for many use cases when coding widgets for your apps. Disclaimer: I assume you already know how to create a simple widget and basic related APIs. Let’s start! 👨🏼‍💻

1. UserDefaults Suite

Very likely, you’ll need to use UserDefaults from your widgets, in order to read user preferences or some other small amount of data. To do so, the standard container isn’t the way, because its content isn’t shared between different targets; instead, as for any other app extension, you have to rely (from the app and from widgets) to the shared “suite”, named with the App Group ID, using the dedicated api:

let userDefaults = UserDefaults(suiteName: “your-app-group-id”)

The App Group is a capability available in the “Signin & Capabilities” tab of your targets: remember to add the App Group capability both in the app’s target and in the widget’s target, otherwise it won’t work.

2. Location permission

As a general rule, widgets inherit every permission status from the parent app and it’s not allowed to prompt requests from the widget itself. Location permission represents an exception: when a widget using location is added to the home screen, the user will be automatically requested to grant permission through an alert; the choice can be changed anytime from the Settings, as a new “While Using the App or Widgets” option is available.

To get location services work properly, you have to put the boolean key NSWidgetWantsLocation in the widget extension Info.plist: you can check the status by using CLLocationManager’s authorizedForWidgetUpdates property. In addition, remember that the app must request location authorization from the user anyway, before widget can receive data.

3. Maps with MKMapSnapshotter

Talking about location, another question could come up: what is the best way to present a map view inside a widget? The recommended option is to use a MKMapSnapshotter in place of a MKMapView; in fact, the standard map view doesn't work at all. The snapshot captures the map’s contents in a simple image. To get the desired MKMapSnapshotter.Snapshot:

func getMapSnapshot(completionHandler: @escaping (Image) -> Void) {
    let coordinate = self.locationManager.location?.coordinate ?? self.defaultCoordinate
    let options = MKMapSnapshotter.Options()
    options.region = MKCoordinateRegion(center: coordinate, span: self.span)
    let snapshot = MKMapSnapshotter(options: options)
    snapshot.start { (snapshot, error) in
        let image = Image(uiImage: snapshot?.image ?? UIImage(named: "map-placeholder")!)
        completionHandler(image)
    }
}
The closure is perfect to be used in the getTimeline(in:completion:) function of TimelineProvider protocol: once the image is loaded, you can create your entries and define the timeline. The snapshot is highly customizable and you find the full documentation here.

4. Memory limit

The map shapshot is a great example of how light should be widget’s content. Videos, animations and so on aren’t available in widgets: moreover, UIKit views wrapped in UIViewRepresentable don’t work in widgets. Related to this topic, you should take into consideration the limit on memory allocation: it’s not explicitly stated in docs, but widgets exceeding 30 Mb seems to crash, so keep in mind this.

5. Updates policy

I’m not going through different ways to update your widget and manage the associated timeline (it would request a dedicated article 😁), but an unclear point about this is: how often can a widget be updated? Even in this case the documentation doesn’t point to a precise policy, but thanks to this thread on the Apple Developer Forum, we can say that 15 minutes is probably the minimum time interval to not run out of limited available updates.

6. ContainerRelativeShape

Another interesting as simple UI trick is relative to views’ shape inside widgets. To be consistent with the widget corner radius (which changes across different devices), Apple suggests to use the SwiftUI ContainerRelativeShape api, instead of defining custom radius: it will automatically make shapes concentric with the widget corner radius. Here’s an example:

Image("map-placeholder")
.clipShape(ContainerRelativeShape())

7. Link vs widgetUrl

Once the user interacts (taps) with a widget, the parent app is launched to handle the request: you can specify an url to be passed to the app, in order to do specific actions. There are 2 ways: by using widgetUrl(_:) modifier to define a unique behavior, as it will be applied to all the widget target, or by using the Link control to define different urls for different targets in the view’s hierarchy. As the small widget allows just one single tap target, it’s preferable to use the widgetUrl modifier for it. Let’s see an example:

struct WidgetView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family

    @ViewBuilder
    var body: some View {
        switch family {
        case .systemSmall:
            NewsView(news: entry.newsArray.first)
                .widgetURL(entry.newsArray.first.deeplink)
        default:
            HStack(alignment: .top) {
                ForEach(entry.newsArray, id: \.self) { news in
                    Link(destination: news.deeplink!) {
                        NewsView(news: news)
                    }
                }
            }
        }
    }
}
In the case of medium or large widgets, you can use the two options together: each Link’s url is used for taps in its target view and the widgetUrl for taps anywhere else. The url is handled by the application(_:open:options:) method for apps with classic AppDelegate, or by the onOpenUrl(perform:) api for SwiftUI life cycle apps introduced with iOS 14.

8. Widgets Bundle

Another great feature is the ability to distribute more than a widget for the same app, using the WidgetBundle api:

import WidgetKit
import SwiftUI

@main
struct BundleExample: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        MapWidget()
        NewsWidget()
    }
}

After some experiments, it seems you can bundle up to 5 different widgets, each with the 3 sizes available, but in this thread there’s a way to overcome this limit.

9. WidgetPreviewContext

While coding your widget, it comes really handy to have a preview of what you’re creating in the canvas area of Xcode Previews. You achieve this by specifying a PreviewProvider; the preview is the widget’s view where the PreviewContext is set as a WidgetPreviewContext:

import WidgetKit
import SwiftUI

struct WidgetPreviewSmall_Previews: PreviewProvider {
    static var previews: some View {
        NewsWidgetView(entry: Entry.placeholder)
            .previewContext(WidgetPreviewContext(family: .systemSmall))
            .previewDisplayName("News Widget Small")
            .environment(\.colorScheme, .dark)
    }
}

It’s possible to define more than a single preview, to see widgets in all available family types, as well as modify some environment’s keys: in the example, I previewed a widget in dark mode.

10. Placeholders

Placeholders are used to show a generic representation of the widget view while it loads the first time is added to the homescreen. Through betas, the way to define a placeholder has been simplified and now you just have to implement the required placeholder(in:) function of TimelineProvider, returning a normal TimelineEntry: the redacted(reason: .placeholder) view modifier will be automatically applied. 2 points about this: as now, images aren’t modified by the redacted effect. The second: you can prevent views from being redacted by using the unredacted() view modifier.

Source: https://medium.com/swlh/10-tips-on-developing-ios-14-widgets-f17b865fbdbc



Description

Contact

VNAppMob
Vietnam
+84965235237

Social