In WWDC20, Apple introduced App Widgets to the home screen for multiple platforms(iOS, iPadOS, macOS). With WidgetKit, you can see the most important information from your app by putting widgets on your Home screen or Notification Center.
Supported Families
There are three different size styles available for widgets, Small, Medium and Large. By default, all size styles are enabled. You can configure the ones that works best for your app’s content.
Configuration
There are two types of configuration, StaticConfiguration and IntentConfiguration.
StaticConfiguration:
It is used for the widgets that do not provide user-configurable options. For example, a news widget that shows trending headlines.
IntentConfiguration:
It is used for the widgets that want to provide user-configurable options. For example, a stock quotes widget, which allows you to select a specific stock to show in the widget. Click here for more information.
We will focus on StaticConfiguration in this article.
Requirements
- Xcode 12.0.1(or later)
- macOS Catalina 10.15.5 or later
- Basic knowledge of SwiftUI
Create a simple app
Let's first create a simple app that lists quotes along with the author name and on tap show the detailView.
Open Xcode and create iOS App project with name QuotesWidgetDemo. Make sure SwiftUI is selected in Interface
option and SwiftUI App is selected in Life cycle
option.
- Create Quote.swift file and add model
public struct Quote {
public let quoteId: Int
public let author: String
public let quote: String
init(quoteId: Int, author: String, quote: String) {
self.quoteId = quoteId
self.author = author
self.quote = quote
}
}
extension Quote: Identifiable {
public var id: Int {
quoteId
}
}
- Create QuoteProvider.swift file and add data
public struct QuoteProvider {
static func all() -> [Quote] {
return [
Quote(quoteId: 1, author: "Steve Jobs", quote: "Have the courage to follow your heart and intuition. They already know what you truly want to become."),
Quote(quoteId: 2, author: "Albert Einstein", quote: "Everybody is a genius. But if you judge a fish by its ability to climb a tree , it will live its whole life believing that it is stupid."),
Quote(quoteId: 3, author: "J.K. Rowling", quote: "Imagination is the foundation of all invention and innovation."),
Quote(quoteId: 4, author: "Abdul Kalam", quote: "Don't take rest after your first victory because if you fail in second, more lips are waiting to say that your first victory was just luck."),
Quote(quoteId: 5, author: "Sandeep Maheshwari", quote: "Positive Thinking is not about expecting the best to happen. It's about accepting that whatever happens, happens for the best.")
]
}
}
- Rename existing file
ContentView.swift
andstruct ContentView
with QuoteView.swift and struct QuoteView and add the below code for quote listing and detailView
import SwiftUI
struct QuoteView: View {
let quotes: [Quote] = QuoteProvider.all()
@State private var showQuote: Quote?
var body: some View {
NavigationView {
List {
ForEach(quotes) { quote in
Button(action: {
showQuote = quote
}, label: {
QuoteItemView(author: quote.author, quote: quote.quote)
})
.sheet(item: $showQuote) { quote in
QuoteDetailView(quote: quote)
}
}
}
.foregroundColor(.black)
.listStyle(PlainListStyle())
.navigationBarTitle("Quotes")
}
}
}
struct QuoteItemView: View {
let author: String
let quote: String
var body: some View {
VStack {
VStack(alignment: .leading) {
HStack {
Text("\(quote)")
.font(.subheadline)
.bold()
.lineLimit(2)
}
.padding([.leading, .trailing])
Spacer().frame(height: 10.0)
Text("- \(author)")
.padding([.leading, .trailing, .bottom])
.font(.footnote)
}
}
.foregroundColor(.black)
}
}
struct QuoteDetailView: View {
var quote: Quote
var body: some View {
ZStack {
Color(UIColor.systemBlue).edgesIgnoringSafeArea(.all)
VStack {
VStack(alignment: .leading) {
HStack {
Text("\(quote.quote)")
.font(.title)
.bold()
}
.padding()
Text("- \(quote.author)")
.padding([.leading, .trailing, .bottom])
.font(.title3)
}
}
.foregroundColor(.white)
}
}
}
struct QuoteView_Previews: PreviewProvider {
static var previews: some View {
QuoteView()
}
}
We are done with the simple app design. If you run the app, it will look like this
Widget Extension
- To build a widget, you need to add a widget extension to your app. To add Widgit extension, go to File -> New -> Target -> select Widget extension(under iOS tab) -> click on Next -> set Product name to QuoteWidget and make sure Include Configuration Intent option is not selected -> once finish button tapped it will show Dialog, press Activate.
Run the extension, by default it will look like this
-
Enable Target QuoteWidgetExtention for our model Quote.swift and data provider QuoteProvider.swift file.
-
Add random() function to QuoteProvider.swift file to get quote randomly
public struct QuoteProvider { static func all() -> [Quote] { // ... } static func random() -> Quote { let allQuotes = QuoteProvider.all() let randomIndex = Int.random(in: 0..<allQuotes.count) return allQuotes[randomIndex] } }
-
Lets design our custom widgetView. Add a swiftUI View file with name QuoteWidgetView. Make sure it is part of QuoteWidgetExtention target.
-
Update the above added file(QuoteWidgetView.swift) with this code
struct QuoteWidgetView: View { let quote: Quote var body: some View { ZStack { Color(UIColor.systemBlue) VStack { Text(quote.quote) .font(.subheadline) .lineLimit(2) .padding(.top, 5) .padding([.leading, .trailing]) .foregroundColor(.white) Spacer().frame(height: 10.0) Text("- \(quote.author)") .font(.footnote) } } } } struct QuoteWidgetView_Previews: PreviewProvider { static var previews: some View { QuoteWidgetView(quote: QuoteProvider.random()) } }
Note: You can define different views for different style size as per your requirement. I've designed the above view for all supported families(Medium, Large).
-
Update QuoteWidget.swift file
import WidgetKit import SwiftUI struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), quote: QuoteProvider.random()) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), quote: QuoteProvider.random()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, quote: QuoteProvider.random()) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let quote: Quote } struct QuoteWidgetEntryView : View { var entry: Provider.Entry var body: some View { QuoteWidgetView(quote: entry.quote) } } @main struct QuoteWidget: Widget { // string that identifies the widget let kind: String = "QuoteWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in QuoteWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") .supportedFamilies([.systemMedium, .systemLarge]) } } struct QuoteWidget_Previews: PreviewProvider { static var previews: some View { QuoteWidgetEntryView(entry: SimpleEntry(date: Date(), quote: QuoteProvider.random())) .previewContext(WidgetPreviewContext(family: .systemSmall)) } }
Let's understand the above code
@main attribute:
It indicates that the QuoteWidget is the entry point for the widget extension.
Content closure:
It contains SwiftUI views like QuoteWidgetEntryView in the above case. WidgetKit invokes this to render the widget’s content.
TimelineEntry:
"A type that specifies the date to display a widget, and, optionally, indicates the current relevance of the widget’s content." - Apple
-
SimpleEntry conforms to TimelineEntry protocol. It holds date for the TimelineProvider to provide to WidgetKit to update the widget. You can specify additional properties like we have added quote in the above code.
-
Inside QuoteWidgetEntryView, replaced default body
Text(entry.date, style: .time)
with our custom QuoteWidgetView
TimelineProvider:
"A type that advises WidgetKit when to update a widget’s display." - Apple
Provider conforms to TimelineProvider protocol and it provides the following methods
-
Placeholder:
It allows you to display a placeholder view to the user. It tells the WidgetKit what to render while the widget is loading. -
getSnapshot:
WidgetKit makes the snapshot request when displaying the widget in transient situations, such as when you are adding a widget. -
getTimeline:
It allows you to fetch data and declare the next refresh moment of your widget. The above code uses .atEnd update policy, which tells WidgetKit to ask for a new timeline once the last date has passed.
The above output is showing two size styles, Medium and Large because we specified .systemMedium and .systemLarge inside supportedFamilies
Deep Links
When we tap on the widget, it opens a quote listing(QuoteView). To open QuoteDetailView on widget tap, we need to implement deep link in our app.
- Update model file Quote.swift and include unique url property.
public struct Quote {
public let author: String
public let quote: String
public let url: URL?
init(author: String, quote: String) {
// ...
self.url = URL(string: "QuotesWidgetDemo://\(self.quoteId)")
}
}
extension Quote: Identifiable {
// ...
}
In the above code I've created url in the form appname://quoteId i.e for quoteId 1, url will be QuotesWidgetDemo://1. Make sure url does not contain whitespace, For example, if I create url in the form appname://author, then for author Steve Jobs, url will be QuotesWidgetDemo://SteveJobs
- Add onOpenURL modifier to AppView i.e. update QuoteView.swift file
struct QuoteView: View {
let quotes: [Quote] = QuoteProvider.all()
@State private var showQuote: Quote?
var body: some View {
NavigationView {
List {
ForEach(quotes) { quote in
Button(action: {
showQuote = quote
}, label: {
QuoteItemView(author: quote.author, quote: quote.quote)
})
.sheet(item: $showQuote) { quote in
QuoteDetailView(quote: quote)
}
.onOpenURL { url in
guard let quote = quotes.first(where: { $0.url == url }) else { return }
showQuote = quote
}
}
}
.foregroundColor(.black)
.listStyle(PlainListStyle())
.navigationBarTitle("Quotes")
}
}
}
struct QuoteItemView: View {
// ...
}
struct QuoteDetailView: View {
// ...
}
struct QuoteView_Previews: PreviewProvider {
// ...
}
- Add widgetURL modifier to WidgetView i.e update QuoteWidgetView.swift file
struct QuoteWidgetView: View {
let quote: Quote
var body: some View {
ZStack {
// ...
}
.widgetURL(quote.url)
}
}
struct QuoteWidgetView_Previews: PreviewProvider {
// ...
}
Let's run the app (make sure QuoteWidgetDemo scheme is selected) and see the output