[SwiftUI] WidgetClone: Static Configuration

Junyoung Park·2022년 12월 7일
0

SwiftUI

목록 보기
123/136
post-thumbnail
post-custom-banner

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

WidgetClone: Static Configuration

구현 목표

  • 위젯 킷 UI 구현

구현 태스크

  • 위젯 킷 타겟 연결
  • TimeLineProvider를 통한 위젯 리프레시
  • 위젯 플레이스홀더 구현
  • API를 통한 위젯 UI 구현
  • 위젯 크기에 따른 커스텀 UI 구현
  • 다이나믹 위젯 매핑

핵심 코드

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))
            }
        }
    }
}
  • TotalCaseEntrytypealias를 통해 현재 TimelineProvider에 어떤 타입을 엔트리로 사용할 것인지 알려주기
  • 플레이스 홀더로 사용할 데이터는 커스텀 데이터
  • 스냅샷은 현 시점의 UI를 그릴 때 사용, API를 통해 데이터를 패치한 뒤 결과값을 해당 뷰에 전달
  • 타입라인을 가져오는 함수는 해당 위젯을 몇 번 주기로 리프레시할 지 결정

구현 화면

위젯 뷰가 구현되는 방식은 프로토콜을 준수하는 식대로 그 흐름이 연결된다. 해당 위젯 뷰를 어느 주기로 리프레시할 것이며, 어떤 데이터로 UI를 그릴지, 또한 플레이스홀더 등 특정 상황에 따른 뷰 핸들링을 어떻게 할지 등 결정 사항을 미리 정할 수 있고, 해당 가이드라인을 제공해주고 있다는 게 포인트.

profile
JUST DO IT
post-custom-banner

0개의 댓글