
iOS 14 WidgetKit | Building COVID-19 API Stats Widget | Static Configuration | SwiftUI

TimeLineProvider를 통한 위젯 리프레시import WidgetKit
import SwiftUI
@main
struct Covid19StatsWidget: Widget {
    let kind: String = "Covid19StatsWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: GlobalTotalStatsTimelineProvider()) { entry in
            StatsWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Covid19-stats")
        .description("Show latest global lifetime stats")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
WidgetConfiguration을 준수하는 해당 바디는 StaticConfiguration이 받아들이는 타임 라인 프로바이더를 통해 위젯 뷰를 렌더링entry는 현재 API로 네트워크 패칭해오는 데이터struct TotalCaseEntry: TimelineEntry {
    var date: Date
    let totalCount: TotalCaseCount
    var isPlaceholder = false
}
import SwiftUI
import WidgetKit
struct StatsWidgetEntryView: View {
    let entry: TotalCaseEntry
    @Environment(\.widgetFamily) var family
    var body: some View {
        switch family {
        case .systemSmall:
            StatsWidgetSmall(entry: entry)
        case .systemLarge:
            StatsWidgetLarge(entry: entry)
        default:
            StatsWidgetMedium(entry: entry)
        }
    }
}
Environment를 통해 위젯 크기를 읽어온 뒤 다이나믹하게 뷰를 렌더링import Foundation
import WidgetKit
struct GlobalTotalStatsTimelineProvider: TimelineProvider {
    typealias Entry = TotalCaseEntry
    let service = Covid19APIService.shared
    
    func placeholder(in context: Context) -> TotalCaseEntry {
        TotalCaseEntry.placeholder
    }
    
    func getSnapshot(in context: Context, completion: @escaping (TotalCaseEntry) -> Void) {
        if context.isPreview {
            completion(TotalCaseEntry.placeholder)
        } else {
            fetchTotalGlobalCaseStats { result in
                switch result {
                case .success(let entry): completion(entry)
                case .failure(_): completion(TotalCaseEntry.placeholder)
                }
            }
        }
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<TotalCaseEntry>) -> Void) {
        fetchTotalGlobalCaseStats { result in
            switch result {
            case .success(let entry):
                let timeline = Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(60 * 10)))
                completion(timeline)
            case .failure(_):
                let timeline = Timeline(entries: [TotalCaseEntry.placeholder], policy: .after(Date().addingTimeInterval(60 * 2)))
                completion(timeline)
            }
        }
    }
    
    private func fetchTotalGlobalCaseStats(completion: @escaping (Result<TotalCaseEntry, Error>) -> Void) {
        service.getGlobalTotalCount { result in
            switch result {
            case .failure(let error): completion(.failure(error))
            case .success(let stats):
                let entry = TotalCaseEntry(date: Date(), totalCount: .init(title: "🌎", confirmed: stats.totalConfirmed, death: stats.totalDeaths, recovered: stats.totalRecovered))
                completion(.success(entry))
            }
        }
    }
}
TotalCaseEntry를 typealias를 통해 현재 TimelineProvider에 어떤 타입을 엔트리로 사용할 것인지 알려주기
위젯 뷰가 구현되는 방식은 프로토콜을 준수하는 식대로 그 흐름이 연결된다. 해당 위젯 뷰를 어느 주기로 리프레시할 것이며, 어떤 데이터로 UI를 그릴지, 또한 플레이스홀더 등 특정 상황에 따른 뷰 핸들링을 어떻게 할지 등 결정 사항을 미리 정할 수 있고, 해당 가이드라인을 제공해주고 있다는 게 포인트.