Interactive Widgets With SwiftUI

Discover how iOS 17 takes widgets to the next level by adding interactivity. Use SwiftUI to add interactive widgets to an app called Trask. Explore different types of interactive widgets and best practices for design and development. By Alessandro Di Nepi.

1 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Your Second Widget

Congratulations, you completed your first interactive widget! It’s time to add your second one. :]

Before you dive into this, review some concepts about widget sizes and best practices.

Widgets Design Concepts

WidgetKit supports multiple devices and different sizes for each device.

On iOS, the supported sizes are Small, Medium and Large. Each size provides a different layout and amount of space for the detail, so you should consider what and how to present the data in your widget.

I like to follow this general rule of thumb while also looking at widgets designed by Apple.

      • Small widgets focus on a specific object, such as a task.
      • Bigger sizes offer a broader view, either to show more object details or to present multiple objects.
    • Small widgets focus on a specific object, such as a task.
    • Bigger sizes offer a broader view, either to show more object details or to present multiple objects.
  • Small widgets focus on a specific object, such as a task.
  • Bigger sizes offer a broader view, either to show more object details or to present multiple objects.

Adding a TodoList Widget

With this second widget, you use the extra space of the medium and large widget sizes to provide the user with a broader view, presenting multiple tasks at once.

To streamline the UI, the list includes TODO tasks only, and you use a toggle button to mark the task as done.

Start by adding a new file named TodoListWidget.swift to TraskWidgets target.

Now, add the basic struct describing the widget in the new file. Open TodoListWidget.swift and add the following content.

import WidgetKit
import SwiftUI

struct TodoListWidget: Widget {
  let kind: String = "TodoListWidget"

  var body: some WidgetConfiguration {
    // 1. Static configuration
    StaticConfiguration(
      kind: kind,
      // 2. Timeline provider
      provider: TodoListProvider()
    ) { entry in
      // 3. SwiftUI View
      TodoListWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("Todo List")
    .description("Shows the list of todo tasks")
    // 4. Supported families
    .supportedFamilies([.systemMedium, .systemLarge])
  }
}

Here is a summary of the key points in the widget definition.

      1. You used a StaticConfiguration because the widget will provide a list of tasks to display itself. In the previous widget, you used an AppIntentConfiguration since the user had the choice of selecting a task via the App Intent.
      2. As you did for the status widget, you’ll write a timeline provider that returns the list of TODO tasks to present along with the update policy.
      3. This is the main view representing the widget, you’ll add it soon.
      4. You indicate that the widget supports just the medium and large sizes.
    1. You used a StaticConfiguration because the widget will provide a list of tasks to display itself. In the previous widget, you used an AppIntentConfiguration since the user had the choice of selecting a task via the App Intent.
    2. As you did for the status widget, you’ll write a timeline provider that returns the list of TODO tasks to present along with the update policy.
    3. This is the main view representing the widget, you’ll add it soon.
    4. You indicate that the widget supports just the medium and large sizes.
  1. You used a StaticConfiguration because the widget will provide a list of tasks to display itself. In the previous widget, you used an AppIntentConfiguration since the user had the choice of selecting a task via the App Intent.
  2. As you did for the status widget, you’ll write a timeline provider that returns the list of TODO tasks to present along with the update policy.
  3. This is the main view representing the widget, you’ll add it soon.
  4. You indicate that the widget supports just the medium and large sizes.

TodoList Timeline Provider

As seen for the other widget, the Timeline provider, TodoListProvider, provides the items to present in the widget.

Add the following code to TodoListWidget.swift.

struct TodoListProvider: TimelineProvider {
  // 1. Filter Todo task
  private var storedTodos: [TaskItem] {
    UserDefaultStore()
      .loadTasks()
      .filter(\.isToDo)
  }

  func placeholder(in context: Context) -> TodoListEntry {
    return TodoListEntry(date: Date(), todos: TaskItem.sampleTasks)
  }

  func getSnapshot(in context: Context, completion: @escaping (TodoListEntry) -> Void) {
    completion(TodoListEntry(date: Date(), todos: storedTodos))
  }

  func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
    let entry = TodoListEntry(date: Date(), todos: storedTodos)
    completion(Timeline(entries: [entry], policy: .never))
  }
}

// 2. Timeline entry
struct TodoListEntry: TimelineEntry {
  let date: Date
  let todos: [TaskItem]
}

Two things are worth emphasizing.

      1. TodoListProvider filters the tasks to provide just the TODO items.
      2. TodoListEntry provides a list of TODO tasks that the widget will show.
    1. TodoListProvider filters the tasks to provide just the TODO items.
    2. TodoListEntry provides a list of TODO tasks that the widget will show.
  1. TodoListProvider filters the tasks to provide just the TODO items.
  2. TodoListEntry provides a list of TODO tasks that the widget will show.

TodoList Widget View

Finally, add the SwiftUI view for the widget to TodoListWidget.swift.

struct TodoListWidgetEntryView: View {
  var entry: TodoListProvider.Entry
  // 1. Widget family
  @Environment(\.widgetFamily) var widgetFamily
  private var listLength: Int { widgetFamily == .systemLarge ? 5 : 3 }

  var body: some View {
    // 2. List not allowed in Widgets
    ForEach(entry.todos.prefix(listLength)) { todo in
      HStack {
        Label(todo.name, systemImage: todo.category.systemImage)
          .padding([.vertical])
          .font(.headline)
          .strikethrough(todo.isCompleted)
          .foregroundColor(todo.tint.color)
          .frame(maxWidth: .infinity, alignment: .leading)

        // 3. Toogle using AppIntent
        Toggle(isOn: todo.isCompleted, intent: TaskIntent(taskEntity: TaskEntity(task: todo))) {
          Image(systemName: "checkmark")
        }
      }
    }
    .padding()
    .transition(.push(from: .bottom))
    .containerBackground(.clear, for: .widget)
  }
}
      1. The widgetFamily environment variable reflects the actual size of the widget. In this case, you use this size to present a different number of tasks.
      2. Not all SwiftUI constructs are available in WidgetKit.
      3. Toggle(isOn:,intent:,_:) is new to iOS 17. It allows the app to call an App Intent when the state changes.
    1. The widgetFamily environment variable reflects the actual size of the widget. In this case, you use this size to present a different number of tasks.
    2. Not all SwiftUI constructs are available in WidgetKit.
    3. Toggle(isOn:,intent:,_:) is new to iOS 17. It allows the app to call an App Intent when the state changes.
  1. The widgetFamily environment variable reflects the actual size of the widget. In this case, you use this size to present a different number of tasks.
  2. Not all SwiftUI constructs are available in WidgetKit.
  3. Toggle(isOn:,intent:,_:) is new to iOS 17. It allows the app to call an App Intent when the state changes.