Geek Culture
Published in

Geek Culture

Ultimate Guide to make Widgets in iOS development

In this article, We will make a Widget for an iOS Application. To start with, We need to answer ourself few questions:

  1. What information we are going to display in Widget ?
  2. How frequently we need to update the data in widget ?
  3. Is our widget configurable by user ?

Once you are ready with answers, Start making one with below steps. Even if you are not sure of above questions, Start right away. You can answer by end of this article.

Add new Target to your project

Like any other extensions, Widget is also an extension to our application. It gets the data from main application in same way as other extensions does.

Adding Files and Capabilities to Target

Some of common steps to share data is to select files that are required and add to widget target.

And add necessary capabilities such as App Groups, Cloud Kit containers etc..

Changes to Code

Make necessary changes to Code such that data can be used in extension. For example if your app uses Core Data then we can use App Groups to share data as follows:

class CustomPersistantContainer : NSPersistentContainer {
static let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.maheshsai.******")!
let storeDescription = NSPersistentStoreDescription(url: url)override class func defaultDirectoryURL() -> URL { return url}}

Then we can use CustomPersistentContainer in place of NSPersistentContainer.

container = CustomPersistantContainer(name: "tracker")

Now we can use our data stored with CoreData framework can be used in extension.

Note: Don’t forget to add all necessary files to Widget Target.

The above steps are general for any extension that uses data of main application. But following are specific to Widget.

SwiftUI Views

Widgets use SwiftUI, a declarative framework, to display information to user. Following is a small example that displays some information.

struct viewbox: View { var int: Int var colorForBackground: Color var name: String var extra: String var body: some View {   VStack(alignment: .center) {    HStack(alignment: .center, spacing: 12) {    Image(systemName: name)     .resizable()     .frame(width: 20, height: 20)     .foregroundColor(colorForBackground)    Text("\(int)")     .bold()     .foregroundColor(.black)  }  Text(extra)    .foregroundColor(.black)  } }}

Likewise we need to build some SwiftUI views that shows data.

Configuration And Conformance to Widget

Adding Configuration is a key step in making widget.

Configuration can be Static or Dynamic.

Static Configuration

As name suggests, the widget displays some information without user configurable options. For example a widget displaying total number of pending and critical tasks.

struct trackerWidget: Widget { let kind: String = "com.maheshsai.trackerWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in   trackerWidgetEntryView(entry: entry) } .supportedFamilies([.systemSmall]) .configurationDisplayName("Tracker Widget") .description("Track the count of pending and completed projects") }}

Intent Configuration

Unlike Static configuration, intent configuration provides options to user. For example the widget displaying total number of pending tasks in a particular project.

struct taskWidget: Widget { private let kind: String = "com.maheshsai.TaskWidget" public var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: DynamicProjectsIntent.self,  
provider: taskProvider()) { entry in
TaskWidgetView(entry: entry) } .configurationDisplayName("Task Widget") .description("See some of the tasks from selected project.") .supportedFamilies([.systemMedium]) }}

These options comes from Intent parameter.

Like Siri Shortcut Intents We need to define a intent that provide values to user at runtime. To do so follow the below steps.

  1. Create a intent definition files inside widget target and add intents and responses.

2. Make intent eligible for Widgets.

3. Add parameters and customise them.

4. Add Intent Extension target and confirm the class to created intent.

Intents.sift inside IntentExtension

The above code provides the project options to user. When user edit widget, the above provided options can be selected.

In addition to name, we can provide subtitle and image while creating Type as follows

let imag = UIImage(systemName: $0.image ?? 
"")?.withTintColor(.label)
return Type(identifier: $0.name, display: $0.name ?? "", subtitle:
nil, image: INImage(imageData: imag?.pngData() ?? Data()))

Other than Configurations, Widget also take kind and provider parameters.

kind is a unique identifier to a widget. We will see about entries and Timeline Providers in next section.

Also the body uses modifiers to provide display name and description. The supported families at present has 3 options which can be small, medium and large. Based on supported families option the widget takes space on screen.

Medium and Large Calendar Widgets

TimeLine Provider and Entries

Timeline Provider depends on configuration of widget. If widget uses dynamic configuration then it takes IntentTimeLine Provider otherwise it takes TimeLine Provider.

Whatever may be the type of TImeLine Provider if we observe the example codes of static and dynamic configurations, both uses entries.

An entry is a structure that confirms to TimelineEntry protocol. This has date(when to display content) and other information used by SwiftUI views.

struct SimpleEntry: TimelineEntry { let date: Date var completed: Int var noncompleted: Int}

This entry or entries are given by TimelineProvider to Configuration which is then used by SwiftUI views to display content.

Let’s see how Timeline Providers provide entries to configuration.

Conformance to TimeLineProvider/IntentTimeLineProvider

we will start by creating a structure that confirms to TimeLineProvider or IntentTimeLineProvider.

For confirmation to protocol, the structure need to have 3 functions which provide a placeholder, snapshot and timeline.

Placeholder function provides entry to generic placeholder view with no content to user.

Snapshot function provides entry to views in transient situations such as Widget in Widget Gallery.

Timeline function provides an array of entries for current and future times to update a widget.

Here in above function it provides five entries that are to be displayed at interval of one hour. Similarly you can provide single entry that corresponds to present date and time.

If we observe that last line, this contains a policy to update timeline.

let timeline = Timeline(entries: entries, policy: .atEnd)

our The policy determines how frequently your widget need to be updatd. The policy can be atEnd, after() or never.

As name says atEnd policy refreshes timeline after the end of existing time line i.e.,creates new timeline after five entries are completed in above example.

after(date: ) refreshes timeline only after specified date and time.

never will not refresh timeline until you explicitly reloads it. We can reload timeline of particular widget or all widgets explicitly from main application using below functions.

WidgetCenter.shared.reloadTimelines(ofKind: "com.maheshsai.TaskWidget")WidgetCenter.shared.reloadAllTimelines()

As previously mentioned kind uniquely identifies each Widget.

Widgets of apps like Notes need to change content only when user mades changes inside app. So they uses never policy and reloads when changes are done inside app.

Note: The intelligence of widgets is in design of Configuration and Timeline.

WidgetURL

If you want to land on specific part of application other than landing page of your application when you tap your widget then you need to add .widgetURL(url) to your SwiftUI view.

struct WidgetDisplayView: View {
var body: some View {
VStack {
...
}.widgetURL(URL("page2"))
}
}

Then got to main application validate and define what to do using onOpenURL function.

struct MainDisplayView: View { ... var body: some View {   VStack {    ...    NavigationLink(destination:..,active: $isActive,label:...)   }.onOpenURL { url in     if url == URL("page2") {       isActive = true     }   }}

New in iOS15

Starting from iOS 15 we can expand the intelligence of our widget by donating intents like any other donation of siri intents and shortcuts.

// Donate INIntent from the app.onAppear {
let intent = ViewRecentIntent()
intent.project = Project(identifier: project.id.uuidString, displayString: project.name)
let interaction = INInteraction(intent: intent, response: nil)
interaction.donate { error in
if let error = error {
print(error.localizedDescription)
}
}
}

Also the other way is to provide relevance

var relevantShortcuts: [INRelevantShortcut] = []if let shortcut = INShortcut(intent: intent) {
let relevantShortcut = INRelevantShortcut(shortcut: shortcut)
relevantShortcut.shortcutRole = .information
relevantShortcut.widgetKind = “com.maheshsai.suggestionsWidget”
let dateProvider = INDateRelevanceProvider(start: Date(),
end:
Date(timeIntervalSinceNow: 1800))
relevantShortcut.relevanceProviders = [dateProvider]
relevantShortcuts.append(relevantShortcut)
}
INRelevantShortcutStore.default.setRelevantShortcuts(relevantShortcuts) { (error) in
if let error = error {
print("Failed to set relevant shortcuts. \(error))")
} else {
print("Relevant shortcuts set.")
}
}

We will provide relevance from start date and time to end date and time. So if this widget is hidden inside a stack then with relevance system brings it to top for that period.

That’s it !!

If you are still having some trouble, Please follow code along of WWDC20

Thanks for Reading:))

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store