[SwiftUI] WidgetClone: SiriKit

Junyoung Park·2022년 12월 7일
0

SwiftUI

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

iOS 14 WidgetKit Pt 2 | Building COVID-19 API Stats | SiriKit Intent Configurable Parameter

WidgetClone: SiriKit

구현 목표

  • 위젯에 시리킷을 연동하기

구현 태스크

  • 위젯 컨텍스트 메뉴 구현 → 해당 파라미터를 사용한 새로운 위젯 UI 구현
  • 파라미터에 대한 다이나믹 값을 얻어내기 위한 프로바이더 핸들러 구현
  • intentTimelineProvider 프로토콜 구현
  • intentConfiguration 구현

핵심 코드

  • 시리킷 인덴트
extension CountryParam {
    convenience init(country: Country) {
        self.init(identifier: country.id, display: country.name)
        self.iso = country.iso
    }
    
    static var global: CountryParam {
        CountryParam(country: .init(id: "global", name: "Global", iso: ""))
    }
}
  • 국가별 파라미터
struct TotalStatsIntentTimelineProvider: IntentTimelineProvider {
    
    typealias Entry = TotalCaseEntry
    typealias Intent = SelectCountryIntent
    
    let service = Covid19APIService.shared
    
    func placeholder(in context: Context) -> TotalCaseEntry {
        .placeholder
    }
    
    func getSnapshot(for configuration: SelectCountryIntent, in context: Context, completion: @escaping (TotalCaseEntry) -> Void) {
        if context.isPreview {
            completion(.placeholder)
        } else {
            fetchTotalCaseStats(for: configuration.country ?? CountryParam.global) { result in
                switch result {
                case .success(let entry):
                    completion(entry)
                case .failure(_):
                    completion(.placeholder)
                }
            }
        }
    }
    
    func getTimeline(for configuration: SelectCountryIntent, in context: Context, completion: @escaping (Timeline<TotalCaseEntry>) -> Void) {
        fetchTotalCaseStats(for: configuration.country ?? CountryParam.global) { 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 fetchTotalCaseStats(for param: CountryParam, completion: @escaping (Result<TotalCaseEntry, Error>) -> Void) {
        guard let id = param.identifier else {
            completion(.failure(Covid19APIError.noData))
            return
        }
        
        switch id {
        case CountryParam.global.identifier:
            service.getGlobalTotalCount { result in
                switch result {
                case .success(let stats):
                    let totalCaseEntry = TotalCaseEntry(date: Date(), totalCount: .init(title: "🌎", confirmed: stats.totalConfirmed, death: stats.totalDeaths, recovered: stats.totalRecovered))
                    completion(.success(totalCaseEntry))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        default:
            service.getTotalCount(countryId: id) { result in
                switch result {
                case .success(let totalCase):
                    let totalCaseEntry = TotalCaseEntry(date: Date(), totalCount: .init(title: param.iso?.flag ?? param.displayString, confirmed: totalCase.confirmed, death: totalCase.deaths, recovered: totalCase.recovered))
                    completion(.success(totalCaseEntry))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
    }
}
  • 인덴트 목록을 포함한 위젯 프로바이더
  • 스냅샷과 리프레시 시간을 구성
  • fetchTotalCaseStats는 이전의 글로벌 데이터만을 패치해온 것과 달리 현 시점에서는 국가 데이터 또한 패치
import Intents

class IntentHandler: INExtension, SelectCountryIntentHandling {
    
    func provideCountryOptionsCollection(for intent: SelectCountryIntent, with completion: @escaping (INObjectCollection<CountryParam>?, Error?) -> Void) {
        Covid19APIService.shared.getAllCountries { result in
            switch result {
            case .success(let countries):
                let countryParams = countries.map({ CountryParam(country: $0)})
                completion(INObjectCollection(sections: [
                    INObjectSection(title: "Global", items: [CountryParam.global]),
                    INObjectSection(title: "Countries", items: countryParams)
                ]), nil)
            case .failure(_):
                completion(INObjectCollection(items: [CountryParam.global]), nil)
            }
        }
    }
    
    override func handler(for intent: INIntent) -> Any {
        return self
    }
    
}
  • 위젯 컨텍스트 메뉴를 클릭할 때 사용할 국가 목록을 렌더링하기 위해 API를 통해 데이터를 패치
  • INObjectCollection을 통해 글로벌과 각 국가 목록을 제공
import WidgetKit
import SwiftUI

@main
struct Covid19StatsWidget: Widget {
    let kind: String = "Covid19StatsWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: SelectCountryIntent.self, provider: TotalStatsIntentTimelineProvider(), content: { entry in
            StatsWidgetEntryView(entry: entry)
        })
        .configurationDisplayName("Covid19-stats")
        .description("Show latest lifetime stats")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
  • 인텐트 구성을 포함한 위젯 뷰를 렌더링하기 위한 메인 뷰
  • IntentConfiguration을 통해 SelectCountryIntent를 파라미터로 넘겨준 뒤 TotalStatsIntentTimelineProvider()를 통해 제공

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글