Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
159: Building a Watch Complication with WidgetKit
Think of a Watch complication not as a live app, but as a series of scheduled postcards. If you wanted to tell a friend how your day was going, you wouldn't stand next to their mailbox and whisper updates every few seconds—that's exhausting and wasteful. Instead, you'd write five postcards in advance: "9 AM: Just had coffee," "12 PM: Lunch was great," "3 PM: Almost done with work," and so on. You give the stack to the mailman and tell him exactly when to deliver each one. The friend just glances at the mailbox and sees the current postcard.
In WidgetKit, your complication works exactly like those postcards. You don't "run" code in real-time on the watch face; instead, you provide a Timeline of entries. The system holds onto these and swaps them out at the exact moment you specified. This is the only way Apple allows complications to work because if every app had a live process running on the watch face, the battery would be dead by noon.
Scheduling the Delivery with TimelineProvider
To make this work, we need a TimelineProvider. This is our "mailman." Its job is to look at the current state of the app and decide what the next few hours of "postcards" should look like. Let's imagine we're building a Water Intake Tracker. We want the complication to show how close the user is to their daily goal.
struct WaterEntry: TimelineEntry {
let date: Date
let ouncesDrunk: Int
let goal: Int
}
struct WaterProvider: TimelineProvider {
func placeholder(in context: Context) -> WaterEntry {
WaterEntry(date: Date(), ouncesDrunk: 32, goal: 64)
}
func getSnapshot(in context: Context, completion: @escaping (WaterEntry) -> Void) {
completion(WaterEntry(date: Date(), ouncesDrunk: 32, goal: 64))
}
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
// I'm creating a timeline that refreshes every hour for the next 5 hours
var entries: [WaterEntry] = []
let currentDate = Date()
for hourOffset in 0..<5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
// In a real app, you'd fetch actual data here
entries.append(WaterEntry(date: entryDate, ouncesDrunk: 32 + (hourOffset * 8), goal: 64))
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
Notice the policy: .atEnd. I'm telling watchOS, "Keep showing these postcards until the last one is delivered, then come back to me for a new stack."
Designing the Glance
Now we need the actual postcard design. Because complications come in so many shapes—circles, rectangles, corners—you have to be flexible. You aren't building a full screen; you're building a tiny fragment of a screen. I usually recommend keeping the UI dead simple: an icon and a percentage or a number.
struct WaterWidgetView : View {
var entry: WaterProvider.Entry
var body: some View {
VStack {
Text("💧")
Text("\(entry.ouncesDrunk)/\(entry.goal)")
.font(.system(size: 10, weight: .bold))
}
}
}
If you try to put too much text in here, watchOS will simply truncate it or, in some cases, refuse to render it. I've spent way too many hours fighting with a font size that was just 2 points too large for a Circular complication. Keep it lean.
Defining the Widget Configuration
Finally, we tie the provider and the view together using a Widget configuration. This is where you tell the system that this specific piece of code is intended to be a complication. By using WidgetFamily, the system knows how to scale your view to fit the specific watch face the user has chosen.
@main
struct WaterWidget: Widget {
let kind: String = "WaterIntakeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WaterProvider()) { entry in
WaterWidgetView(entry: entry)
}
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryCorner])
}
}
One thing to keep in mind: since the view is rendered by the system, not your app, you can't use interactive elements like buttons or text fields inside the complication. The only "interaction" is a tap, which simply launches your main app.
📋 Practical Task
Exercise: The Pomodoro Focus Glance
Build a Watch complication for a Pomodoro timer. Your goal is to create a timeline that alternates between a "Focus" state and a "Break" state.
- Create a
PomodoroEntrythat contains aBooleancalledisFocusSessionand aDate. - Implement a
TimelineProviderthat generates a sequence of entries: 25 minutes of "Focus" followed by 5 minutes of "Break," repeating for at least 3 cycles. - Design a
PomodoroWidgetViewthat displays a red circle icon 🔴 whenisFocusSessionis true, and a green circle icon 🟢 when it is false. - Ensure the widget supports the
.accessoryCircularfamily.
There are no comments for now.