Introduction
Starting with iOS 14, the new WidgetKit framework was introduced. With the new WidgetKit framework, you can build widgets that can be added to your home screen to let you see important information at a glance. Widgets can be presented in multiple sizes and placed on the iOS Home screen, Today View, or macOS Notification Center.
Widgets created before iOS 14 can't be placed on the home screen, but they are available on the Today View.
Widget design comes in three types: system small, system medium and system large. You can support all three of them or stick with just one size by defining this option in your code.
A widget contains following blocks
New widgets support in your app starts with Widget Extension. Add a new target to the Xcode project. After the widget extension target is added, Xcode will generate some entities required for widget implementation.
Let's do it now, follow these steps:
Widget. In the code listing below, you can see a structure that conforms to the Widget
protocol. It will be the main entry point for a newly created target, and this code is required to initialise and configure the widget.
import SwiftUI import WidgetKit @main struct WordWidgetExtension: Widget { let kind: String = "WordWidgetExtension" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in WordEntryView(entry: entry) } .configurationDisplayName("Word Widget") .description("This is a word widget.") .supportedFamilies([.systemMedium]) } }
The only requirement of the Widget protocol is body
property that should return an instance of WidgetConfiguration
. SwiftUI provides us two structs that conform to WidgetConfiguration
: StaticConfiguration
and IntentConfiguration
.
StaticConfiguration
has three parameters to configure
kind
. A unique string identifier for the widget.provider
. A timeline provider object that supplies an array of timeline entries to WidgetKit to display.content
. A closure that takes a timeline entry and returns a SwiftUI view to display the widget.Timeline Entry. A timeline entry is an object conforming to the TimelineEntry
protocol. There two main parts a data and a date and time for when the entry is valid.
struct WordModel { let word: String let translation: String let lang: String let example: String static func getPlaceholder() -> WordModel { return getData().first! } static func getData() -> [WordModel] { return [ WordModel(word: "comprare", translation: "buy", lang: "it", example: "Io compro una macchina"), WordModel(word: "kaufen", translation: "buy", lang: "ge", example: "Ich kaufe ein Auto") ] } } struct SimpleEntry: TimelineEntry { let date: Date let word: WordModel }
Timeline Provider. The TimelineProvider
protocol provides some data over time with the getTimeline(in:completion:)
method. In this method, you can define a refresh policy by making a new timeline for the widget request so that it can show refreshed data. Also, in this method, you can provide an array of timeline entries, one for the present time, and others for future times based on your widget’s update interval.
There are three methods to build data for timeline entries:
placeholder()
method provides the initial view and give the user a general idea of widget's appearance.getSnapshot()
method provides the widget with a entry to display when the widget needs to be shown in transitory situations.getTimeline()
method provides the widget with an array of values to display over time. You call the completion
handler with the array of timeline entries and the required refresh policy.So, timeline is represented by one or more entries specified in getTimeline
method. There are few ways to tell system to trigger your timeline update, and it depends on the selected reload policy
atEnd
reloads timeline after the last dateafter(date)
reloads timeline after the specified datenever
WidgetKit will never request a new timeline from the widgetstruct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), word: WordModel.getPlaceholder()) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), word: WordModel.getPlaceholder()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] let results = WordModel.getData() let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: 5+hourOffset, to: currentDate)! let randomIndex = Int.random(in: 0..<results.count) let item = results[randomIndex] let entry = SimpleEntry(date: entryDate, word: item) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
View.
struct WordEntryView : View { var entry: Provider.Entry var body: some View { ZStack{ Color.bg VStack(spacing: 10) { HStack { Text(entry.word.word).font(.headline).foregroundColor(.primaryText) Text(entry.word.lang).font(.footnote).foregroundColor(.primaryText).baselineOffset(6.0) } Text(entry.word.translation).foregroundColor(.primaryText).font(.body) Text(entry.word.example).foregroundColor(.primaryText).font(.caption) } } } } import SwiftUI extension Color { static let bg = Color("ColorBg") // #EEE6DB static let primaryText = Color("ColorPrimaryText") // #232220 }
Requesting a reload
You can request a new timeline using WidgetCenter
from any place in your app
WidgetCenter.shared.reloadTimelines(ofKind: "WordWidgetExtension")
Sharing data with a Widget
Let's use CoreData from main app as source for our widget.
First of all, we should add PersistenceController
to WordWidgetExtension
(check WordWidgetExtension at File inspector in Target Membership)
Second, update PersistenceController
and set up app group. App group containers allow apps and targets to share resources.
import Foundation import CoreData class SharedPersistentContainer: NSPersistentContainer { override open class func defaultDirectoryURL() -> URL { var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.me.proft.TestWidget2") storeURL = storeURL?.appendingPathComponent("Words.sqlite") return storeURL! } } struct PersistenceController { static let shared = PersistenceController() let container: NSPersistentContainer init(inMemory: Bool = false) { container = SharedPersistentContainer(name: "Words") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } container.loadPersistentStores(completionHandler: {(storeDescription, error)in if let error = error as NSError?{ fatalError("Unresolved error \(error), \(error.userInfo)") } }) } func save () { guard container.viewContext.hasChanges else { return } do { try container.viewContext.save() } catch { print(error) } } }
Third, add new Add Groups capability for main app and WordWidgetExtension
targets.
Last, update source of items in getTimeline
function in Provider
class.
struct Provider: TimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), word: WordModel.getPlaceholder()) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), word: WordModel.getPlaceholder()) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] let managedObjectContext = PersistenceController.shared.container.viewContext let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Word") var results = [Word]() do { results = try managedObjectContext.fetch(request) as! [Word] } catch let error as NSError { print("Could not fetch \(error), \(error.userInfo)") } let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: 5+hourOffset, to: currentDate)! let randomIndex = Int.random(in: 0..<results.count) let item = results[randomIndex] let word = WordModel(word: item.word ?? "", translation: item.translation ?? "", lang: item.lang ?? "", example: item.example ?? "") let entry = SimpleEntry(date: entryDate, word: word) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }