๐ŸŒ KO

๐ŸŒค Build Your Own Weather Widget
Challenge

โญ Difficulty: โญโญโญ โฑ๏ธ Est. Time: 2-3h ๐Ÿ“‚ App Frameworks

Following Apple HIG Widget Design Guidelines, we'll build a working interactive weather widget with SwiftUI.

๐Ÿ“… 2025.02 โฑ Estimated 2 Hours ๐Ÿ“ฑ iOS 17+ / Xcode 15+ ๐ŸŽฏ ์ดˆ๊ธ‰ โ†’ ์ค‘๊ธ‰
๐Ÿ”„ ์„ฑ์žฅ๊ณ ๋ฆฌ ์ปค๋ฆฌํ˜๋Ÿผ

Follow the 5-stage growth path to master the complete widget development process.

๐Ÿ“–
Ring 1 ยท ์ดํ•ดํ•˜๊ธฐ
HIG ์œ„์ ฏ guidelines ํ•™์Šต
Glanceable, Relevant, Personalized โ€” The 3 Principles of Widgets
๐Ÿ—๏ธ
Ring 2 ยท ๋ผˆ๋Œ€ ์žก๊ธฐ
Widget Extension Basic Structure
Core architecture: TimelineProvider, Entry, and View
๐ŸŽจ
Ring 3 ยท ๊พธ๋ฏธ๊ธฐ
HIG ์ค€์ˆ˜ UI ๋””์ž์ธ
ํฌ๊ธฐ๋ณ„ ๋ ˆ์ด์•„์›ƒ, ๋‹คํฌ๋ชจ๋“œ, ํ‹ดํŠธ ๋Œ€์‘
โšก
Ring 4 ยท ์—ฐ๊ฒฐํ•˜๊ธฐ
๋ฐ์ดํ„ฐ & ์ธํ„ฐ๋ž™์…˜
๋‚ ์”จ API ์—ฐ๋™, ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ฒ„ํŠผ, ๋”ฅ๋งํฌ
๐Ÿš€
Ring 5 ยท ์™„์„ฑํ•˜๊ธฐ
์„ค์ • & ํผ์Šค๋„๋ผ์ด์ฆˆ
App Intent ์„ค์ •, ์ž ๊ธˆ ํ™”๋ฉด ์œ„์ ฏ

๐Ÿ“– Ring 1 โ€” HIG Widget Guidelines

Apple์€ ์œ„์ ฏ์„ "๋ฏธ๋‹ˆ ์•ฑ"์ด ์•„๋‹ˆ๋ผ ์•ฑ์˜ ํ•ต์‹ฌ ์ •๋ณด๋ฅผ ํ™ˆ ํ™”๋ฉด์— ํˆฌ์˜ํ•˜๋Š” ๊ฒƒ. Users check their home screen over 90 times a day on average, but only stay for a few seconds.

๐ŸŽ HIG Core Principles 3๊ฐ€์ง€

1. Glanceable (ํ•œ๋ˆˆ์— ํŒŒ์•…)
Users should be able to grasp key information within seconds.

2. Relevant (์‹œ์˜์ ์ ˆ)
Show information relevant to the time, place, and user context.

3. Personalized (๊ฐœ์ธํ™”)
Let users customize through widget configuration.

โœ… Do

  • ์ฝ˜ํ…์ธ ๊ฐ€ ์ฃผ์ธ๊ณต โ€” ํฐ ์ˆซ์ž, ๋ช…ํ™•ํ•œ ์•„์ด์ฝ˜
  • ํฌ๊ธฐ๋ณ„๋กœ other ๋ ˆ์ด์•„์›ƒ ์ œ๊ณต
  • ๋‹คํฌ๋ชจ๋“œ/ํ‹ดํŠธ ์ž๋™ ๋Œ€์‘
  • containerBackground๋กœ ๋ฐฐ๊ฒฝ ์ฒ˜๋ฆฌ

โŒ Don't

  • ์ž‘์€ ํฌ๊ธฐ์— ๋„ˆ๋ฌด ๋งŽ์€ ์ •๋ณด
  • ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ํ‘œ์‹œ
  • ์•ฑ ์•„์ด์ฝ˜/์ด๋ฆ„ ๋ฐ˜๋ณต
  • ์‹ค์‹œ๊ฐ„ ์ดˆ ๋‹จ์œ„ ์—…๋ฐ์ดํŠธ

๐Ÿ—๏ธ Ring 2 โ€” Widget Extension Basic Structure

WidgetKit operates on a 3-layer architecture: TimelineProvider โ†’ TimelineEntry โ†’ View.

Step 2-1: ๋‚ ์”จ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ

WeatherData.swift Swift
import Foundation

struct WeatherData {
    let cityName: String
    let temperature: Int
    let highTemperature: Int
    let lowTemperature: Int
    let condition: WeatherCondition
    let humidity: Int
    let windSpeed: Double
    let hourlyForecast: [HourlyWeather]
}

enum WeatherCondition: String, CaseIterable {
    case sunny = "๋ง‘์Œ"
    case cloudy = "ํ๋ฆผ"
    case rainy = "๋น„"
    case snowy = "๋ˆˆ"
    case stormy = "๋‡Œ์šฐ"
    
    var symbol: String {
        switch self {
        case .sunny:  "sun.max.fill"
        case .cloudy: "cloud.fill"
        case .rainy:  "cloud.rain.fill"
        case .snowy:  "cloud.snow.fill"
        case .stormy: "cloud.bolt.rain.fill"
        }
    }
}

struct HourlyWeather: Identifiable {
    let id = UUID()
    let hour: String
    let temperature: Int
    let condition: WeatherCondition
}

Step 2-2: Implement AppIntentTimelineProvider (iOS 17+)

iOS 17๋ถ€ํ„ฐ AppIntentTimelineProvider integrates with App Intents, letting users customize widgets.

WeatherProvider.swift Swift
import WidgetKit
import SwiftUI
import AppIntents

struct WeatherEntry: TimelineEntry {
    let date: Date
    let weather: WeatherData
    let configuration: SelectCityIntent
}

// iOS 17+ AppIntentTimelineProvider (async/await ๊ธฐ๋ฐ˜)
struct WeatherProvider: AppIntentTimelineProvider {
    
    // ์œ„์ ฏ ๊ฐค๋Ÿฌ๋ฆฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ
    func placeholder(in context: Context) -> WeatherEntry {
        WeatherEntry(date: .now, weather: .preview, configuration: SelectCityIntent())
    }
    
    // ์œ„์ ฏ ์ถ”๊ฐ€ ์‹œ ์Šค๋ƒ…์ƒท (async)
    func snapshot(for configuration: SelectCityIntent, in context: Context) async -> WeatherEntry {
        let weather = await WeatherService.shared.fetchWeather(for: configuration.city)
        return WeatherEntry(date: .now, weather: weather, configuration: configuration)
    }
    
    // ์‹ค์ œ ํƒ€์ž„๋ผ์ธ ์ƒ์„ฑ (async)
    func timeline(for configuration: SelectCityIntent, in context: Context) async -> Timeline<WeatherEntry> {
        let weather = await WeatherService.shared.fetchWeather(for: configuration.city)
        let entry = WeatherEntry(date: .now, weather: weather, configuration: configuration)
        
        // 15๋ถ„ ํ›„ ๋‹ค์Œ ๊ฐฑ์‹ 
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: .now)!
        
        return Timeline(entries: [entry], policy: .after(nextUpdate))
    }
}
โœ…

Ring 2 ์ฒดํฌํฌ์ธํŠธ

The TimelineProvider fetches weather data, creates entries, and sets a 15-minute refresh policy.

๐ŸŽจ Ring 3 โ€” HIG-Compliant UI Design

Implement size-optimized layouts and gradient backgrounds based on weather conditions.

Step 3-1: Small ์œ„์ ฏ ๋ทฐ

SmallWeatherView.swift Swift
struct SmallWeatherView: View {
    let weather: WeatherData
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            // HIG: ์ฝ˜ํ…์ธ ๊ฐ€ ์ฃผ์ธ๊ณต
            Image(systemName: weather.condition.symbol)
                .font(.title2)
                .symbolRenderingMode(.multicolor)
            
            Spacer()
            
            // HIG: ํ•œ๋ˆˆ์— ํŒŒ์•… โ€” ํฐ ๊ธฐ์˜จ ์ˆซ์ž
            Text("\(weather.temperature)ยฐ")
                .font(.system(size: 36, weight: .light))
                .contentTransition(.numericText())
            
            VStack(alignment: .leading, spacing: 0) {
                Text(weather.cityName)
                    .font(.caption)
                    .fontWeight(.semibold)
                
                Text("H:\(weather.highTemperature)ยฐ L:\(weather.lowTemperature)ยฐ")
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
    }
}

Step 3-2: Medium ์œ„์ ฏ โ€” ์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด

MediumWeatherView.swift Swift
struct MediumWeatherView: View {
    let weather: WeatherData
    
    var body: some View {
        HStack(spacing: 16) {
            // ์™ผ์ชฝ: ํ˜„์žฌ ๋‚ ์”จ
            VStack(alignment: .leading, spacing: 4) {
                Text(weather.cityName)
                    .font(.subheadline)
                    .fontWeight(.semibold)
                
                Text("\(weather.temperature)ยฐ")
                    .font(.system(size: 44, weight: .light))
                
                Text(weather.condition.rawValue)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            
            Divider()
            
            // ์˜ค๋ฅธ์ชฝ: ์‹œ๊ฐ„๋ณ„ ์˜ˆ๋ณด
            HStack(spacing: 12) {
                ForEach(weather.hourlyForecast.prefix(5)) { hourly in
                    VStack(spacing: 6) {
                        Text(hourly.hour)
                            .font(.caption2)
                        Image(systemName: hourly.condition.symbol)
                            .symbolRenderingMode(.multicolor)
                        Text("\(hourly.temperature)ยฐ")
                            .font(.caption)
                    }
                }
            }
        }
    }
}

Step 3-3: ๋‚ ์”จ ๊ธฐ๋ฐ˜ ๊ทธ๋ž˜๋””์–ธํŠธ ๋ฐฐ๊ฒฝ

WeatherGradient.swift Swift
extension WeatherCondition {
    var gradient: LinearGradient {
        switch self {
        case .sunny:
            LinearGradient(
                colors: [.orange, .yellow],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        case .cloudy:
            LinearGradient(
                colors: [Color("CloudTop"), Color("CloudBottom")],
                startPoint: .top,
                endPoint: .bottom
            )
        case .rainy:
            LinearGradient(
                colors: [.gray, .blue.opacity(0.6)],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        case .snowy:
            LinearGradient(
                colors: [.white, Color("IceBlue")],
                startPoint: .top,
                endPoint: .bottom
            )
        case .stormy:
            LinearGradient(
                colors: [Color("StormDark"), Color("StormPurple")],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        }
    }
}
โœ…

Ring 3 ์ฒดํฌํฌ์ธํŠธ

Completed optimal layouts for Small/Medium sizes and gradient backgrounds by weather condition.

โšก Ring 4 โ€” Data & Interaction

From iOS 17, you can add Button and Toggle to widgets for interactivity.

Step 4-1: ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ

RefreshWeatherIntent.swift Swift
import AppIntents
import WidgetKit

struct RefreshWeatherIntent: AppIntent {
    static var title: LocalizedStringResource = "๋‚ ์”จ ์ƒˆ๋กœ๊ณ ์นจ"
    
    func perform() async throws -> some IntentResult {
        WidgetCenter.shared.reloadAllTimelines()
        return .result()
    }
}

// ์œ„์ ฏ ๋ทฐ์—์„œ ์‚ฌ์šฉ:
Button(intent: RefreshWeatherIntent()) {
    Image(systemName: "arrow.clockwise")
        .font(.caption)
}
.buttonStyle(.plain)
โœ…

Ring 4 ์ฒดํฌํฌ์ธํŠธ

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ๊นŒ์ง€ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค!

๐Ÿš€ Ring 5 โ€” Settings & Personalization

With App Intent Configuration, let users choose a city and support Lock Screen widgets.

Step 5-1: City Selection Configuration

SelectCityIntent.swift Swift
import AppIntents
import WidgetKit

struct SelectCityIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "๋„์‹œ ์„ ํƒ"
    static var description: IntentDescription = "๋‚ ์”จ๋ฅผ ํ™•์ธํ•  ๋„์‹œSelectdo it."
    
    @Parameter(title: "๋„์‹œ", default: .seoul)
    var city: CityOption
}

enum CityOption: String, AppEnum {
    case seoul   = "์„œ์šธ"
    case busan   = "๋ถ€์‚ฐ"
    case jeju    = "์ œ์ฃผ"
    case daejeon = "๋Œ€์ „"
    case gwangju = "๊ด‘์ฃผ"
    
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "๋„์‹œ")
    static var caseDisplayRepresentations: [CityOption: DisplayRepresentation] = [
        .seoul:   "์„œ์šธ",
        .busan:   "๋ถ€์‚ฐ",
        .jeju:    "์ œ์ฃผ",
        .daejeon: "๋Œ€์ „",
        .gwangju: "๊ด‘์ฃผ",
    ]
}

Step 5-2: ์ตœ์ข… ์œ„์ ฏ ๊ตฌ์„ฑ

WeatherWidget.swift Swift
import WidgetKit
import SwiftUI

struct WeatherWidget: Widget {
    let kind = "WeatherWidget"
    
    var body: some WidgetConfiguration {
        AppIntentConfiguration(
            kind: kind,
            intent: SelectCityIntent.self,
            provider: WeatherProvider()
        ) { entry in
            WeatherWidgetEntryView(entry: entry)
                .containerBackground(
                    entry.weather.condition.gradient,
                    for: .widget
                )
        }
        .configurationDisplayName("๋‚ ์”จ")
        .description("ํ˜„์žฌ ๋‚ ์”จ์™€ ์˜ˆ๋ณด๋ฅผ ํ™•์ธdo it.")
        .supportedFamilies([
            .systemSmall, .systemMedium, .systemLarge,
            .accessoryCircular, .accessoryRectangular, .accessoryInline
        ])
    }
}

struct WeatherWidgetEntryView: View {
    @Environment(\.widgetFamily) var family
    let entry: WeatherEntry
    
    var body: some View {
        switch family {
        case .systemSmall:
            SmallWeatherView(weather: entry.weather)
        case .systemMedium:
            MediumWeatherView(weather: entry.weather)
        case .systemLarge:
            LargeWeatherView(weather: entry.weather)
        case .accessoryCircular:
            CircularWeatherView(weather: entry.weather)
        case .accessoryRectangular:
            RectangularWeatherView(weather: entry.weather)
        case .accessoryInline:
            Text("\(entry.weather.condition.rawValue) \(entry.weather.temperature)ยฐ")
        default:
            SmallWeatherView(weather: entry.weather)
        }
    }
}
๐ŸŽ‰

Challenge Complete!

You've completed all 5 growth stages. A fully HIG-compliant weather widget is finished!

๐Ÿ† Bonus Challenge

๐Ÿ”ฅ ๋” ๋„์ „ํ•ด๋ณด์„ธ์š”!

Completed the basic challenge? Level up with these bonus tasks.

๐Ÿ“ฆ Learning Resources

๐Ÿ“š
DocC Tutorial
Try it in Xcode
๐Ÿ’ป
GitHub Project
Complete Source Code
๐ŸŽ
Apple HIG Docs
Widgets guidelines

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions