Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2023 SwiftUI & UI Frameworks

WWDC23 · 24 min · SwiftUI & UI Frameworks

Build widgets for the Smart Stack on Apple Watch

Follow along as we build a widget for the Smart Stack on watchOS 10 using the latest SwiftUI and WidgetKit APIs. Learn tips, techniques, and best practices for creating widgets that show relevant information on Apple Watch.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 9 snippets

TimelineEntry swift · at 4:15 ↗
struct SimpleEntry: TimelineEntry {
    var date: Date
    var configuration: ConfigurationAppIntent
    var backyard: Backyard
    
    var bird: Bird? {
        return backyard.visitorEventForDate(date: date)?.bird
    }
    
    var waterDuration: Duration {
        return Duration.seconds(abs(self.date.distance(to: self.backyard.waterRefillDate)))
    }

    var foodDuration: Duration {
        return Duration.seconds(abs(self.date.distance(to: self.backyard.foodRefillDate)))
    }

    var relevance: TimelineEntryRelevance? {
        if let visitor = backyard.visitorEventForDate(date: date) {
            return TimelineEntryRelevance(score: 10, duration: visitor.endDate.timeIntervalSince(date))
        }
        return TimelineEntryRelevance(score: 0)
    }
}
placeholder function swift · at 7:50 ↗
func placeholder(in context: Context) -> SimpleEntry {
  return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: Backyard.anyBackyard(modelContext: modelContext))
}
snapshot function swift · at 8:15 ↗
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
  if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) {
    if let event = backyard.visitorEvents.first {
      return SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard)
    } else {
      return SimpleEntry(date: Date(), configuration: configuration, backyard: backyard)
    }
  }

  let yard = Backyard.anyBackyard(modelContext: modelContext)
  return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: yard)
}
Widget Entry View swift · at 10:26 ↗
struct BackyardBirdsWidgetEntryView: View {
    @Environment(\.widgetFamily) private var family
    var entry: SimpleEntry
    
    var body: some View {
        switch family {
        case .accessoryRectangular:
            RectangularBackyardView(entry: entry)
        default:
            Text(entry.date, style: .time)
        }
    }
}
Backyard Rectangular View swift · at 11:23 ↗
struct RectangularBackyardView: View {
    var entry: SimpleEntry
    
    var body: some View {
        HStack {
            if let bird = entry.bird {
                ComposedBird(bird: bird)
                    .scaledToFit()
                    .widgetAccentable()
                    .frame(width: 50, height: 50)
                VStack(alignment: .leading) {
                    Text(bird.speciesName)
                        .font(.headline)
                        .foregroundStyle(bird.colors.wing.color)
                        .widgetAccentable()
                        .minimumScaleFactor(0.75)
                    Text(entry.backyard.name)
                        .minimumScaleFactor(0.75)
                    HStack {
                        Image(systemName: "drop.fill")
                        Text(entry.waterDuration, format: remainingHoursFormatter)
                        Image(systemName: "fork.knife")
                        Text(entry.foodDuration, format: remainingHoursFormatter)
                    }
                    .imageScale(.small)
                    .minimumScaleFactor(0.75)
                    .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            } else {
                Image(.fountainFill)
                    .foregroundStyle(entry.backyard.backgroundColor)
                    .imageScale(.large)
                    .scaledToFit()
                    .widgetAccentable()
                    .frame(width: 50, height: 50)
                VStack(alignment: .leading) {
                    Text(entry.backyard.name)
                        .font(.headline)
                        .foregroundStyle(entry.backyard.backgroundColor)
                        .widgetAccentable()
                        .minimumScaleFactor(0.75)
                    HStack {
                        Image(systemName: "drop.fill")
                        Text(entry.waterDuration, format: remainingHoursFormatter)
                        Image(systemName: "fork.knife")
                        Text(entry.foodDuration, format: remainingHoursFormatter)
                    }
                    .imageScale(.small)
                    .minimumScaleFactor(0.75)
                    Text("\(entry.backyard.historicalEvents.count) visitors")
                        .minimumScaleFactor(0.75)
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .containerBackground(entry.backyard.backgroundColor.gradient, for: .widget)
    }
}
Timeline Function swift · at 16:30 ↗
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
  var entries: [SimpleEntry] = []

  if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) {
    for event in backyard.visitorEvents {
      let entry = SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard)
      entries.append(entry)
      let afterEntry = SimpleEntry(date: event.endDate, configuration: configuration, backyard: backyard)
      entries.append(afterEntry)
    }
  }
  return Timeline(entries: entries, policy: .atEnd)
}
Recommendations Function swift · at 18:35 ↗
func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] {
  var recs = [AppIntentRecommendation<ConfigurationAppIntent>]()

  for backyard in Backyard.allBackyards(modelContext: modelContext) {
    let configIntent = ConfigurationAppIntent()
    configIntent.backyardID = backyard.id.uuidString
    let gardenRecommendation = AppIntentRecommendation(intent: configIntent, description: backyard.name)
    recs.append(gardenRecommendation)
  }

  return recs
}
Relevant Intents Function swift · at 20:47 ↗
func updateBackyardRelevantIntents() async {
    let modelContext = ModelContext(DataGeneration.container)
    var relevantIntents = [RelevantIntent]()
    
    for backyard in Backyard.allBackyards(modelContext: modelContext) {

        let configIntent = ConfigurationAppIntent()
        configIntent.backyardID = backyard.id.uuidString
        let relevantFoodDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .food), to: backyard.expectedEmptyDate(for: .food))
        let relevantFoodIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantFoodDateContext)
        relevantIntents.append(relevantFoodIntent)

        let relevantWaterDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .water), to: backyard.expectedEmptyDate(for: .water))
        let relevantWaterIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantWaterDateContext)
        relevantIntents.append(relevantWaterIntent)
    }

    do {
        try await RelevantIntentManager.shared.updateRelevantIntents(relevantIntents)
    } catch { }
}
Update Relevant Intents swift · at 23:00 ↗
Task {
  await updateBackyardRelevantIntents()
  WidgetCenter.shared.reloadTimelines(ofKind: "BackyardVisitorsWidget")
}

Resources