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 and struct 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

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

References

Author

Jigna Patel

iOS Developer

Always curious about learning new things

You may also like

Engineering Awesome Conference 2020

We love engaging with the tech community, and we are big consumers of the awesome work that people share online. So a couple of weeks ago we hosted our first-ever online conference as a way to give back to the community. 6 experts from across our offices in Europe and...

AFK
API First

We’ve been building APIs for a long time, enabling our mobile software solutions to communicate with each other across their respective platforms. For years these APIs have been built as part of a “mobile first”-approach, under the guiding light of an internally defined, living document, our so called “API Manifesto”,...