To-Do App Widget

Next, we will look at how to use CoreData in Widgets. For this, we will use our already completed To-Do app.

Our Widget should, depending on its size, end up looking like this:

Open the corresponding Xcode project and add a Widget Target by selecting File-New-Target in the Xcode toolbar and searching for “Widget Extension”. For example, name the widget “OpenTasksWidget”. Again, make sure you don’t check “Use Configuration Intent” as we will be using a StaticConfiguration for this Widget. When asked, activate the Widget Scheme. 

We will use both the PersistenceController and the CoreData model of our To-Do app for the Widget. Therefore, we add the corresponding files to the Widget Target:

Composing views depending on the Widget family 🎨

We start by designing the OpenTasksEntryView for our Widget. To work with some sample To-Do’s we temporarily add a “sampleTaskTitle” array to our SimpleEntry struct. We will replace this later with a set of respective CoreData instances.

struct SimpleEntry: TimelineEntry {
    let date: Date
    let sampleTaskTitles = ["Learning SwiftUI", "Hitting the gym"]
}

As discussed earlier, a Widget can have three different sizes: .systemSmall, .systemMedium, and .systemLarge. We can simulate these different sizes simultaneously by initializing the OpenTasksWidgetEntryView multiple times in our OpenTasksWidget_Previews using a Group. For each instance we use a different .family:

struct OpenTasksWidget_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date()))
                .previewContext(WidgetPreviewContext(family: .systemSmall))
            OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date()))
                .previewContext(WidgetPreviewContext(family: .systemMedium))
            OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date()))
                .previewContext(WidgetPreviewContext(family: .systemLarge))
        }
    }
}

The Preview simulator should now look like this:

We now have the option to make the OpenTasksWidgetEntryView dependent on the Widget family.

To know which size the user has chosen, we can use the .widgetFamily @Environment property wrapper:

struct OpenTasksWidgetEntryView : View {
     
     @Environment(\.widgetFamily) var family
     
     var entry: Provider.Entry
 
     var body: some View {
        Text(entry.date, style: .time)
    }
}

If we now use an appropriate switch statement, we can easily design the view for our widget depending on its size.

struct OpenTasksWidgetEntryView : View {
    
    @Environment(\.widgetFamily) var family
    
    var entry: Provider.Entry
    
    var body: some View {
        switch family {
        case .systemExtraLarge:
            Text("View for extra large widget")
        case .systemLarge:
            Text("View for large widget")
        case .systemMedium:
            Text("View for medium-sized widget")
        case .systemSmall:
            Text("View for small widget")
        }
    }
}

Let’s start with the .systemLarge family. In this case, we build a view that displays both the number of open tasks and the title of those tasks, where we want to display at most the seven most recent tasks. For example, we can achieve this like this:

case .systemLarge:
    VStack(alignment: .leading) {
        Text("\(entry.sampleTaskTitles.count) open tasks")
            .foregroundColor(.blue)
            .font(.title)
        Divider()
        ForEach(entry.sampleTaskTitles.prefix(7), id: \.self) { item in
            Text(item ?? "")
                .padding(.top, 3)
                .lineLimit(1)
        }
        if entry.sampleTaskTitles.count > 7 {
            Text("More ...")
                .padding(.top, 3)
                .foregroundColor(.blue)
        }
        Spacer()
    }
        .padding()

The corresponding preview should now be updated:

We copy the VStack and paste it to the .systemExtraLarge case to reuse it:

case .systemExtraLarge:
    VStack(alignment: .leading) {
        Text("\(entry.sampleTaskTitles.count) open tasks")
            .foregroundColor(.blue)
            .font(.title)
        Divider()
        ForEach(entry.sampleTaskTitles.prefix(7), id: \.self) { item in
            Text(item)
                .padding(.top, 3)
                .lineLimit(1)
        }
        if entry.sampleTaskTitles.count > 7 {
            Text("More ...")
                .padding(.top, 3)
                .foregroundColor(.blue)
        }
        Spacer()
    }
        .padding()

Since the Widget in the .systemMedium family already has a little less space available, we want to display at most the three most recent open tasks. Also, we change the arrangement a bit, since the widget will be aligned “horizontally” in this case:

case .systemMedium:
    HStack {
        VStack(alignment: .leading) {
            Text("\(entry.sampleTaskTitles.count)")
            Text("open")
            Text("tasks")
    }
       .font(.title)
       .foregroundColor(.blue)
    Divider()
    VStack(alignment: .leading) {
        ForEach(entry.sampleTaskTitles.prefix(3), id: \.self) { item in
            Text(item)
                .padding(.top, 3)
                .lineLimit(1)
        }
        if entry.sampleTaskTitles.count > 3 {
            Text("More ...")
                .padding(.top, 3)
                .foregroundColor(.blue)
        }
        Spacer()
    }
    Spacer()
}
    .padding()

This results in the following preview:

For the .systemSmall family, we use a much scaled-down version of the .systemLarge family and again limit the maximum tasks displayed to three. 

case .systemSmall:
    VStack(alignment: .leading) {
         Text("\(entry.sampleTaskTitles.count) open tasks")
             .foregroundColor(.blue)
             .font(.headline)
             .padding(.top, 2)
         Divider()
         ForEach(entry.sampleTaskTitles.prefix(3), id: \.self) { item in
             Text(item ?? "")
                 .font(.caption)
                 .padding(.bottom, 2)
                 .lineLimit(1)
         }
         if entry.sampleTaskTitles.count > 3 {
             Text("More ...")
                 .font(.caption)
                 .padding(.top, 2)
                 .foregroundColor(.blue)
         }
         Spacer()
     }
         .padding()

The corresponding Widget will look like this:

Great, we are now done designing our OpenTasksWidgetEntryView!

Accessing CoreData using an App Group 🀝

However, the Widget cannot yet use the CoreData database that is created and managed by the To-Do app. In order for extensions like our OpenTasksWidget to get access to it, we need to create a so-called App Group.

To do this, go to the Apple Developer Account (the one you also use for Xcode Project Signing) and click on “Certificates, Identifies & Profiles”.  

Then click on the plus button under “Identifiers”. Next, select “App Groups” and click “Continue”. 

Now you can pick any description you like. But as the identifier, you should use the following structure “group.*YOUR DEVELOPER NAME*.*YOUR PROJECT NAME“. For example:

After you have registered the identifier, quit Xcode by hitting CMD-Q, and reopen your project. To add the corresponding App Group to the project, you have to go to the Signing & Capabilities tab of your app. Drag and drop the “App Group” capability from the capability library below the “Signing” section. After some time, the identifier you just created should appear. Then set a checkmark to register the App Group. Otherwise, you can just click on the plus button to enter the identifier manually.

You should end up with an entitlements file added to your project. This is a plist file containing the app group identifier you just created:

To share this App Group with the Widget, we repeat the process again for the OpenTasksWidget target itself. After that, you should end up with two entitlement files in total.

Okay, we are almost finished. As a final preparation, we just need to update the Persistence.swift file so that the CoreData database is also captured by the App Group.

To do this, we need to modify the init method of the PersistenceController. So, except for setting the container, delete the current content of this method:

init(inMemory: Bool = false) {
     
}

Next, we define the path where the database should be stored. Make sure you use your App Group identifier for this!

init(inMemory: Bool = false) {
     container = NSPersistentContainer(name: "Shared")
     
     //USE YOUR APP GROUP IDENTIFIER FOR THE "forSecurityApplicationGroupIdentifier" parameter
     guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.BLCKBIRDS.To-Do") else {
         fatalError("Shared file container could not be created.")
     }
     
     let storageURL = fileContainer.appendingPathComponent("ToDoAppDatabase.sqlite")
     
     
 }

Next, we set the description for the database using our storageURL. After this, we load the persistent storage.

init(inMemory: Bool = false) {
     //...
     
     
     let description = NSPersistentStoreDescription(url: storageURL)
     container.persistentStoreDescriptions = [description]
     
     container.loadPersistentStores(completionHandler: { (storeDescription, error) in
         if let error = error as NSError? {
             fatalError("Unresolved error \(error), \(error.userInfo)")
         }
     })
 }

Finally, we are ready to access the CoreData database of our To-Do app through the Widget!

Updating our Provider πŸ”„

Next, we will update our OpenTasksWidget.swift file. The first thing we need to do is to import the CoreData framework.

import CoreData

We can now replace the sampleTaskTitles property of our SimpleEntry model with an array of ToDoItem instances.

struct SimpleEntry: TimelineEntry {
    let date: Date
    let toDoItems: [ToDoItem]
}

Accordingly, we now need to update our OpenTasksWidgetEntry view …

struct OpenTasksWidgetEntryView : View {
     
     @Environment(\.widgetFamily) var family
     
     var entry: Provider.Entry
 

     var body: some View {
         switch family {
         case .systemLarge:
             VStack(alignment: .leading) {
                 Text("\(entry.toDoItems.count) open tasks")
                     //...
                 Divider()
                 ForEach(entry.toDoItems.prefix(7), id: \.self) { item in
                     Text(item.taskTitle ?? "")
                         //...
                 }
                 if entry.toDoItems.count > 7 {
                     //...
                 }
                 Spacer()
             }
                 .padding()
         case .systemMedium:
             HStack {
                 VStack(alignment: .leading) {
                     Text("\(entry.toDoItems.count)")
                     //...
                 }
                     //...
                 Divider()
                 VStack(alignment: .leading) {
                     ForEach(entry.toDoItems.prefix(3), id: \.self) { item in
                         Text(item.taskTitle ?? "")
                             //...
                     }
                     if entry.toDoItems.count > 3 {
                         Text("More ...")
                             //...
                     }
                     Spacer()
                 }
                 Spacer()
             }
                 .padding()
         case .systemSmall:
             VStack(alignment: .leading) {
                 Text("\(entry.toDoItems.count) open tasks")
                     //...
                 Divider()
                 ForEach(entry.toDoItems.prefix(3), id: \.self) { item in
                     Text(item.taskTitle ?? "")
                         //...
                 }
                 if entry.toDoItems.count > 3 {
                     //...
                 }
                 Spacer()
             }
                 .padding()
         }
     }
 }
 

… our OpenTasksWidget_Previews struct…

struct OpenTasksWidget_Previews: PreviewProvider {
     static var previews: some View {
         Group {
             OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date(), toDoItems: [ToDoItem]()))
                 .previewContext(WidgetPreviewContext(family: .systemSmall))
             OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date(), toDoItems: [ToDoItem]()))
                 .previewContext(WidgetPreviewContext(family: .systemMedium))
             OpenTasksWidgetEntryView(entry: SimpleEntry(date: Date(), toDoItems: [ToDoItem]()))
                 .previewContext(WidgetPreviewContext(family: .systemLarge))
         }
     }
 }

… and the placeholder function of our Provider:

func placeholder(in context: Context) -> SimpleEntry {
     SimpleEntry(date: Date(), toDoItems: [ToDoItem()])
 }

Both our getSnapshot and getTimeline functions require access to the CoreData database. Let’s start with the getSnapshot function. Since we can’t use the @FetchRequest property wrapper in Widgets, we need to implement the corresponding functionality manually.

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
     
     let viewContext = PersistenceController.shared.container.viewContext
     let request = NSFetchRequest<ToDoItem>(entityName: "ToDoItem")
     request.predicate = NSPredicate(format: "taskDone = %d")
     
 }

We can fetch the ToDoItems as usual and generate a corresponding SimpleEntry:

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
     
    let viewContext = PersistenceController.shared.container.viewContext
    let request = NSFetchRequest<ToDoItem>(entityName: "ToDoItem")
    //Filter out tasks marked as done
    request.predicate = NSPredicate(format: "taskDone = %d")
     
    do {
        let fetchedItems = try viewContext.fetch(request)
        let entry = SimpleEntry(date: Date(), toDoItems: fetchedItems)
        completion(entry)
    } catch {
        fatalError("Failed to fetch tasks: \(error)")
    }
}

Also for the timeline, we fetch the ToDoItems from our CoreData database. Then, we create a single SimpleEntry instance from the corresponding array and add it to the timeline.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
     
    let viewContext = PersistenceController.shared.container.viewContext
    let request = NSFetchRequest<ToDoItem>(entityName: "ToDoItem")
    request.predicate = NSPredicate(format: "taskDone = %d")
     
    do {
        let fetchedItems = try viewContext.fetch(request)
        print(fetchedItems)
        var entries: [SimpleEntry] = []
        entries.append(SimpleEntry(date: Date(), toDoItems: fetchedItems))
        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
    } catch {
        fatalError("Failed to fetch tasks: \(error)")
    }
}

If we now run our OpenTasksWidgetExtension in the standard simulator, the widget should successfully display the open To-Do’s (assuming you already added some in the To-Do app before).

Globally Reloading Widgets using the WidgetCenter 🌎

You probably noticed that we had set the refresh policy to .never. However, to refresh the widget, we don’t want to choose a time interval, so neither the .atEnd nor the .after policy is suitable.

Instead, we want to refresh the widget every time a new task is added, deleted or marked as done.

Widgets can easily be refreshed manually, and that too from the app itself! To do this, we simply use the so-called Widget Center.

We add the following statement to the saveTask and markTaskAsDone functions of our TasksView:

func saveTask() {
    //...
     
    do {
        //...
        WidgetCenter.shared.reloadAllTimelines()
    } catch {
        //...
    }
     
    //...
}
 

func markTaskAsDone(at index: Int) {
    //...
    do {
        //...
        WidgetCenter.shared.reloadAllTimelines()
    } catch {
        //...
    }
}

At the same time, we need to make sure that the corresponding file also contains the WidgetKit framework.

import WidgetKit

We repeat the same steps for the removeItems method in our TasksDoneView.

import SwiftUI
import WidgetKit
 

struct TasksDoneView: View {
     
    //...
     
    var body: some View {
        //...
    }
     
    private func removeItems(at offsets: IndexSet) {
        //...
        do {
            //...
            WidgetCenter.shared.reloadAllTimelines()
        } catch {
            //...
        }
    }
}

Now every time we create a new task, mark it as done, or delete it, the getTimeline method of the widget Provider is called manually, and the Widget gets refreshed!

Conclusion 🎊

That’s it! We learned how to use CoreData databases in Widgets using App Groups and how to manually refresh Widgets using the WidgetCenter. You also learned how to adjust the design of a Widget to its size using widget families. 

If you have any questions, feel free to leave a comment below!

One reply on “To-Do App Widget”

Leave a Reply

Your email address will not be published. Required fields are marked *