Color Mode


    Language

WidgetKit

November 16, 2020

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

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

References

  • Widget Extension
  • Meet WidgetKit
  • Getting Started With Widgets
  • Build Your First iOS Widget
ioswwdc20swiftswiftui

Author

Jigna Patel

Jigna Patel

iOS Developer

Always curious about learning new things

You may also like

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

May 27, 2024

Introducing UCL Max AltPlay, a turn-by-turn real-time Football simulation

At this year's MonstarHacks, our goal was to elevate the sports experience to the next level with cutting-edge AI and machine learning technologies. With that in mind, we designed a unique solution for football fans that will open up new dimensions for wa...

Rayhan NabiRokon UddinArman Morshed

Rayhan Nabi, Rokon Uddin, Arman Morshed

MonstarHacks

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service