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 widget
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 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)
}
}