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를 그릴지, 또한 플레이스홀더 등 특정 상황에 따른 뷰 핸들링을 어떻게 할지 등 결정 사항을 미리 정할 수 있고, 해당 가이드라인을 제공해주고 있다는 게 포인트.