🍎 [Swift] WidgetKit μ΄λž€ ?

TygerΒ·2024λ…„ 6μ›” 23일
1

Swift with iOS

λͺ©λ‘ 보기
1/3
post-thumbnail

🍎 WidgetKit μ΄λž€ ?

WidgetKit | Apple Developer

πŸ”’ μž κΈˆν™”λ©΄ μœ„μ ― λ§Œλ“€κΈ° - WidgetKit
🎸 Live Activity μ‚¬μš©ν•΄ 보기

이번 κΈ€μ—μ„œλŠ” WidgetKit에 λŒ€ν•΄μ„œ 닀뀄볼 μ˜ˆμ •μ΄λ‹€.

주둜 Flutter에 κ΄€ν•œ κΈ€λ§Œ μž‘μ„±ν•˜λŠ” 편인데, λ‹€λ₯Έ μ–Έμ–΄λ‚˜ ν”„λ ˆμž„μ›Œν¬μ— λŒ€ν•΄μ„œ μž‘μ„±ν•΄ 놓지 μ•Šλ‹€λ³΄λ‹ˆ 가끔 μ‚¬μš©ν•˜λ €κ³  ν•  λ•Œλ§ˆλ‹€ μžŠμ–΄λ²„λ €μ„œ μ •λ¦¬ν•˜λŠ” μ°¨μ›μ—μ„œ μž‘μ„±μ„ 해보렀고 ν•œλ‹€.

WidgetKit

WidgetKitμ΄λž€ iOS 14 버전뢀터 λ„μž…λœ ν”„λ ˆμž„μ›Œν¬λ‘œ, μ‚¬μš©μžκ°€ ν™ˆ ν™”λ©΄ 및 μž κΈˆν™”λ©΄μ—μ„œ μ†μ‰½κ²Œ 정보λ₯Ό 확인할 수 μžˆλŠ” μœ„μ ―μ„ κ°œλ°œν•˜λŠ”λ° μ‚¬μš©λ˜λŠ” κΈ°λŠ₯이닀.

iOS 14 μ΄ν•˜λ₯Ό νƒ€κ²Ÿ ν•œλ‹€λ©΄ λ‹Ήμ—°νžˆ μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό μ§„ν–‰ν•΄ μ£Όμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— 이 뢀뢄을 λ†“μΉ˜μ§€ 말아야 ν•œλ‹€. μ—¬κΈ°μ„œλŠ” iOS 14 μ΄μƒλ§Œ νƒ€κ²Ÿν•˜κΈ°μ— κ³ λ €ν•˜μ§€ μ•Šμ„ μ˜ˆμ •μ΄λ‹€.

Static ? Intent ?

WidgetKitμ—μ„œ μœ„μ ―μ„ μ„€μ •ν•˜λŠ” 방법이 두 κ°€μ§€κ°€ μžˆλ‹€.

Static Configurationκ³Ό Intent Configuration이닀.

Static Configuration

λ¨Όμ € Static에 λŒ€ν•΄μ„œ μ‚΄νŽ΄λ³΄μžλ©΄, 의미 κ·ΈλŒ€λ‘œ κ³ μ •λœ μœ„μ ―μ΄λ‹€.

μœ„μ ―μ˜ 데이터가 κ³ μ •λ˜μ–΄μ„œ μ •μ μ΄κ±°λ‚˜ 주기적으둜 μ—…λ°μ΄νŠΈλ§Œ ν•„μš”ν•œ κ²½μš°μ— μ‚¬μš©ν•  수 μžˆλŠ” μœ„μ ― μ’…λ₯˜μ΄λ‹€.

Intent Configuration

κ·Έλ ‡λ‹€λ©΄ IntentλŠ” λ‹Ήμ—°νžˆ λ°˜λŒ€ κ°œλ…μΌ 것이닀.

μœ„μ ―μ˜ 데이터가 고정적이지 μ•Šκ³  λ³€κ²½ν•  수 있으며 μ‚¬μš©μž κ°œμΈν™”κ°€ κ°€λŠ₯ν•œ μœ„μ ―μ„ μ μš©ν•˜κ³  싢을 λ•Œμ— μ‚¬μš©ν•  수 μžˆλŠ” 방법이닀.

κΈ€λ‘œ 보면 이해가 μ•ˆλ  수 μžˆμ§€λ§Œ iOSμ—μ„œλŠ” μœ„μ ―μ˜ νŽΈμ§‘ κΈ°λŠ₯이 μžˆλŠ” κ²½μš°μ™€ μ—†λŠ” 경우 λ”± 두 κ°€μ§€λ§Œ μ‚¬μš©ν•  수 있기 λ•Œλ¬Έμ— κ·Έ 차이라고 보면 λœλ‹€.

즉 Static Configuration은 μ• ν”Œ κΈ°λ³Έ μ•± 쀑 날씨, μΊ˜λ¦°λ”, 배터리 μœ„μ ―κ³Ό 같이 정해놓은 λ°μ΄ν„°λ§Œ μ—…λ°μ΄νŠΈν•˜μ—¬ λ³΄μ—¬μ§€κ²Œ 되고 반면 λ―Έλ¦¬μ•Œλ¦Ό, 단좕어, 팟캐슀트 처럼 μœ„μ ―μ—μ„œ λ³΄μ—¬μ§ˆ 데이터λ₯Ό μ‚¬μš©μžκ°€ 직접 μ„ νƒν•˜μ—¬ μœ„μ ―μ„ νŽΈμ§‘ν•  수 μžˆλŠ” 방법이 Intent Configuration인 것이닀.

λ‹Ήμ—°νžˆ Intent Configuration이 ꡬ쑰가 더 λ³΅μž‘ν•˜κ³  κ΅¬ν˜„ν•˜κΈ°κ°€ μƒλŒ€μ μœΌλ‘œ λ³΅μž‘ν•˜κ²Œ λœλ‹€.

Components

WidgetKit을 μ‚¬μš©ν•˜κΈ° μ•žμ„œ 핡심 ꡬ성 μš”μ†Œμ— λŒ€ν•΄μ„œ μš°μ„  μ•Œμ•„λ³΄λ„λ‘ ν•˜κ² λ‹€.

TimelineProvider, Entry, Timeline μ΄λ ‡κ²Œ 3 κ°€μ§€μ˜ 핡심 ꡬ성 μš”μ†Œκ°€ 있고, 데이터λ₯Ό μ œκ³΅ν•˜κ³  κ΄€λ¦¬ν•˜λŠ”λ° 핡심적인 역할을 μˆ˜ν–‰ν•˜λŠ” μ˜μ—­μ΄λ‹€.

TimelineProvider

μœ„μ ―μ— ν•„μš”ν•œ 데이터λ₯Ό μ œκ³΅ν•˜λŠ” ν”„λ‘œν† μ½œλ‘œ, μ–΄λ–€ 데이터λ₯Ό μ–Έμ œ ν‘œμ‹œν• μ§€λ₯Ό μ •μ˜ν•˜λŠ” μš”μ†Œμ΄λ‹€.

placeholder
μœ„μ ―μ˜ 초기 μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λ©° λ„€νŠΈμ›Œν¬ μš”μ²­μ‹œ μš”μ²­μ΄ μ™„λ£Œλ˜κΈ° μ „κΉŒμ§€ 보여쀄 κΈ°λ³Έ 데이터λ₯Ό μ„€μ •ν•œλ‹€.

getSnapshot
미리보기 λ˜λŠ” λΉ λ₯Έ μ—…λ°μ΄νŠΈλ₯Ό μœ„ν•΄ ν˜„μž¬ 데이터λ₯Ό μ œκ³΅ν•˜λ©°, μœ„μ ―μ„ μΆ”κ°€ν•˜κ±°λ‚˜ μ„€μ •μ—μ„œ λ―Έλ¦¬λ³΄κΈ°μ‹œμ— ν˜ΈμΆœλ˜λŠ” λ©”μ„œλ“œμ΄λ‹€.

getTimeline
일정 κ°„κ²©μœΌλ‘œ 데이터λ₯Ό μ—…λ°μ΄νŠΈν•  λ•Œλ₯Ό ν¬ν•¨ν•œ νƒ€μž„λΌμΈμ„ μ œκ³΅ν•˜λ©°, μ—¬λŸ¬ TimelineEntryλ₯Ό ν¬ν•¨ν•œ Timeline 객체λ₯Ό μƒμ„±ν•˜μ—¬ λ°˜ν™˜ν•΄ 주기적으둜 μœ„μ ―μ˜ 데이터λ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 μ‚¬μš©λœλ‹€.

Entry

EntryλŠ” TimelineEntry ν”„λ‘œν† μ½œμ„ μ€€μˆ˜ν•˜λŠ” 객체둜, μœ„μ ―μ— ν‘œμ‹œλ  데이터λ₯Ό λ‚˜νƒ€λ‚΄λ©° μƒμ„±λœ 각 EntryλŠ” νŠΉμ • μ‹œκ°„μ— μ–΄λ–€ 데이터λ₯Ό ν‘œμ‹œν• μ§€λ₯Ό μ •μ˜ν•˜κ²Œ λœλ‹€.

date ν”„λ‘œνΌν‹°λ₯Ό μ‚¬μš©ν•΄ μ‹œκ°„μ„ 생성할 수 있으며, 데이터 ν•„λ“œλ₯Ό μΆ”κ°€ν•˜μ—¬ 보여쀄 데이터λ₯Ό λ‚˜νƒ€λ‚Ό 수 μžˆλ‹€.

Timeline

μ—¬λŸ¬ 개의 Entry둜 κ΅¬μ„±λœ 객체둜, νŠΉμ • μ‹œκ°„μ— μ–΄λ–€ 데이터λ₯Ό ν‘œμ‹œν• μ§€λ₯Ό μ •μ˜ν•˜λŠ”λ° μ‚¬μš©λœλ‹€.

entries
TimelineEntry 객체의 λ°°μ—΄λ‘œ 각 EntryλŠ” νŠΉμ • μ‹œκ°„μ— ν‘œμ‹œλ  데이터와 μ‹œκ°„μ„ ν¬ν•¨ν•œλ‹€.

reloadPolicy
λ¦¬λ‘œλ“œ λ˜μ–΄μ•Όν•˜λŠ” μ‹œμ μ„ μ •μ˜ν•˜λ©° .atEnd, .after, .neverλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.

Add WidgetKit

이제 XCode에 ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•œ 뒀에 본격적으둜 WidgetKit을 μ‚¬μš©ν•΄λ³΄λ„λ‘ ν•˜μž.

μ•„λž˜ κ²½λ‘œμ—μ„œ WidgetExtension을 μΆ”κ°€ν•΄ 주도둝 ν•˜μž.

File > Target > ios

μ›ν•˜λŠ” WidgetKit 이름을 μΆ”κ°€ν•΄μ£Όλ©΄ λ˜λŠ”λ°, μ—¬κΈ°μ„œ μ€‘μš”ν•œ 뢀뢄이 λ°”λ‘œ "include Configuration App Intent" 뢀뢄이닀.

μœ„μ—μ„œ μ„€λͺ…ν•œ κ²ƒμ²˜λŸΌ WidgetKit은 StaticConfigurationκ³Ό IntentConfiguration μ΄λ ‡κ²Œ 두 κ°€μ§€ 섀정이 μžˆλ‹€κ³  ν–ˆμ—ˆλ‹€.

이 뢀뢄을 μ²΄ν¬ν•˜κ²Œ 되면 IntentConfiguration으둜 μƒμ„±λ˜λŠ” 것이기 λ•Œλ¬Έμ—, μš°μ„  체크λ₯Ό ν•΄μ œν•˜κ³  StaticConfiguration으둜 생성해 주도둝 ν•˜μž.

이제 κ°„λ‹¨ν•œ μ˜ˆμ œκ°€ μž‘μ„±λœ 파일이 μƒμ„±λœ 것을 확인할 수 μžˆλ‹€.

ν”„λ‘œμ νŠΈλ₯Ό λΉŒλ“œν•œ 후에 μœ„μ ―μ„ 좔가해보면, μœ„μ ―μ„ 선택할 수 μžˆλ„λ‘ λ‚˜νƒ€λ‚΄λŠ” 것을 확인할 수 μžˆλ‹€.

UI

κ°„λ‹¨ν•˜κ²Œ UI 적인 뢀뢄을 κ°€λ³κ²Œ μ‚΄νŽ΄λ³΄λ„λ‘ ν•˜μž.

WidgetFamily

λ¨Όμ € μ‚¬μ΄μ¦ˆμ΄λ‹€. WidgetKit의 μ‚¬μ΄μ¦ˆλŠ” 총 4κ°€μ§€ νƒ€μž…μ„ μ‚¬μš©ν•˜λ„λ‘ λ˜μ–΄ 있으며, μ›ν•˜λŠ” μ‚¬μ΄μ¦ˆλ§Œμ„ μž„μ˜λ‘œ μ§€μ •ν•  μˆ˜λ„ μžˆλ‹€.

.systemSmall
.systemMedium
.systemLarge
.systemExtraLarge (only iPad)

μ›ν•˜λŠ” μ‚¬μ΄μ¦ˆλ§Œμ„ μ§€μ •ν•˜κ³  μ‹Άλ‹€λ©΄ supportedFamilies ν”„λ‘œνΌν‹°λ₯Ό μ‚¬μš©ν•΄ λ°°μ—΄λ‘œ enum νƒ€μž…μ˜ μ‚¬μ΄μ¦ˆλ₯Ό μΆ”κ°€ν•΄ μ£Όλ©΄ λœλ‹€.

struct StaticWidgetKit: Widget {
    let kind: String = "StaticWidgetKit"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            StaticWidgetKitEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemLarge])
    }
}

μ‚¬μ΄μ¦ˆλ³„λ‘œ UIλ₯Ό λΆ„κΈ°ν•˜κ³  μ‹Άλ‹€λ©΄ ν™˜κ²½λ³€μˆ˜λ₯Ό μ‚¬μš©ν•˜λ©΄ λœλ‹€.

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    @Environment(\.widgetFamily) var family

    var body: some View {
        switch family {
        case .systemSmall:
            SmallWidgetView()
        case .systemMedium:
            MediumWidgetView()
        case .systemLarge:
            LargeWidgetView()
        case .systemExtraLarge:
            ExtraLargeWidgetView()
        @unknown default:
            SmallWidgetView()
        }
    }
}

Display

μœ„μ ―μ„ 선택할 λ•Œμ— 상단에 타이틀과 μ„€λͺ…이 λ…ΈμΆœλ˜λŠ” 것을 μ•Œ 수 μžˆλŠ”λ°, 이 뢀뢄을 λ³€κ²½ν•˜κ³  μ‹Άλ‹€λ©΄ 각각 configurationDisplayName, description ν”„λ‘œνΌν‹°μ— ν…μŠ€νŠΈλ₯Ό λ³€κ²½ν•΄μ£Όλ©΄ λœλ‹€.

TimelineProvider

TimelineProvider 객체에 λŒ€ν•΄μ„œ 확인해 보도둝 ν•˜μž.

TimelineEntry λΆ€λΆ„μ˜ emoji λŒ€μ‹  ν…μŠ€νŠΈλ₯Ό λ°›μ•„μ˜€λ„λ‘ μˆ˜μ •ν•˜κ³ , TimelineProvider λ©”μ„œλ“œλ“€μ˜ μƒνƒœλ₯Ό κ°€μ Έμ˜€λ„λ‘ ν•˜μž.

struct SimpleEntry: TimelineEntry {
    let date: Date
    let text: String
}

placeholder, getSnapshot, getTimeline λ©”μ„œλ“œ μ—μ„œ TimelineEntry λ₯Ό ν˜ΈμΆœν•  λ•Œμ— 각 μƒνƒœλ₯Ό ν…μŠ€νŠΈλ‘œ 넣어주도둝 ν•˜μž.

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), text: "placeholder")
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), text: "getSnapshot")
        completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, text: "getTimeline")
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

이제 λ‹€μ‹œ λΉŒλ“œν•œ 후에 μƒνƒœλ₯Ό 확인해보면, μœ„μ ―μ„ μΆ”κ°€ν•˜λ €κ³  ν•  λ•Œμ— getSnapshot λ©”μ„œλ“œμ˜ TimelineEntryλ₯Ό μ‚¬μš©ν•˜κ³ , μœ„μ ―μ΄ μΆ”κ°€λœ 후에 데이터λ₯Ό 뢈러였고 λ‚˜λ©΄ getTimeline λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜λŠ” 것을 확인할 수 μžˆλ‹€.

placeholder λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ§€ μ•ŠλŠ” κ²½μš°κ°€ λ§Žμ€λ°, 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ”λ° μ΄ˆκΈ°μ— λ‘œλ”©μ΄ λ°œμƒν•˜λŠ” κ²½μš°μ—λ§Œ λ‚˜νƒ€λ‚˜κΈ° λ•Œλ¬Έμ— μΌλ°˜μ μœΌλ‘œλŠ” 빈 데이터λ₯Ό λ„£μ–΄μ„œ λ³΄μ—¬μ£ΌλŠ” μš©λ„λ‘œ μ‚¬μš©λœλ‹€.

getTimeline λ©”μ†Œλ“œλ₯Ό μˆ˜μ •ν•΄μ„œ μ‹œκ°„μ— λ”°λ₯Έ μ—…λ°μ΄νŠΈ μ‹œμ μ„ 쑰정해보도둝 ν•˜μž.

λ°˜λ³΅λ¬Έμ„ μ‚¬μš©ν•΄μ„œ 5개의 Entry 객체λ₯Ό 3초 κ°„κ²©μœΌλ‘œ μ—…λ°μ΄νŠΈ ν•˜λ„λ‘ λ³€κ²½ν•˜λŠ” μ½”λ“œμ΄λ‹€.

    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
        var entries: [SimpleEntry] = []

        let currentDate = Date()
        for i in 0..<5 {
            let entryDate = Calendar.current.date(byAdding: .second, value: i * 3, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, text: "getTimeline")
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
3초 λ§ˆλ‹€ 2초 λ§ˆλ‹€

getTimeline λ©”μ„œλ“œμ•ˆμ—μ„œ Entry 객체의 date ν”„λ‘œνΌν‹°μ˜ 섀정에 따라 μ‹œκ°„μ„ μ›ν•˜λŠ” 주기둜 생성해 쀄 수 μžˆλ‹€.

policy

policy에 λŒ€ν•œ 뢀뢄을 μ‚΄νŽ΄λ³΄λ„λ‘ ν•˜κ² λ‹€.

λ¦¬λ‘œλ“œ μ‹œκΈ°λ₯Ό μ„€μ •ν•˜λŠ” 뢀뢄인데, .atEnd, .after(Date), .never 3κ°€μ§€μ˜ 기쀀을 μ‚¬μš©ν•  수 μžˆλ‹€.

νƒ€μž„λΌμΈμ— λŒ€ν•œ 기쀀을 λ‚˜νƒ€λ‚΄λŠ” ν”Œλ‘œμš°μ΄λ‹€.

atEnd
atEndλŠ” λ§ˆμ§€λ§‰ νƒ€μž„λΌμΈ 이 후에 μƒˆλ‘œμš΄ νƒ€μž„λΌμΈμ„ μš”μ²­ν•˜κ²Œ λœλ‹€.
λ§Œμ•½μ— [now, 1hr, 2hr] μ΄λ ‡κ²Œ νƒ€μž„λΌμΈμ„ 생성 ν•˜μ˜€λ‹€λ©΄, μƒˆλ‘œμš΄ νƒ€μž„λΌμΈμ„ μš”μ²­ν•˜μ—¬ [2hr, 3hr, 4hr]의 νƒ€μž„λΌμΈμ„ λ‹€μ‹œ μƒμ„±ν•˜μ—¬ 주기적으둜 μ—…λ°μ΄νŠΈ ν•  수 있게 λœλ‹€.

after(Date)
afterλŠ” νŠΉμ • μ‹œμ  이 후에 μƒˆλ‘œμš΄ νƒ€μž„λΌμΈμ„ μš”μ²­ν•˜κ²Œ λ˜λŠ” 방식이닀.
[now, 1hr, 2hr] νƒ€μž„λΌμΈμ„ μƒμ„±ν•˜κ³  afterλ₯Ό 1μ‹œκ°„ ν›„λ‘œ μ„€μ •ν•˜κ²Œ 되면, νƒ€μž„λΌμΈμ„ μ§€κΈˆμœΌλ‘œ λΆ€ν„° 1μ‹œκ°„ 뒀에 λ‹€μ‹œ μƒμ„±ν•œλ‹€λŠ” μ˜λ―Έμ΄λ‹€.

never
neverλŠ” 더 이상 μƒˆλ‘œμš΄ νƒ€μž„λΌμΈμ„ μš”μ²­ν•˜κ³  μ‹Άμ§€ μ•Šμ„ λ•Œ μ‚¬μš©ν•˜λ©΄ 되며, never μ‚¬μš©μ‹œ 더 이상 데이터λ₯Ό μ—…λ°μ΄νŠΈ ν•˜μ§€ μ•ŠλŠ”λ‹€.

policy 기쀀은 λ§Œλ“€κ³ μž ν•˜λŠ” κΈ°λŠ₯에 맞게 기쀀을 ν…ŒμŠ€νŠΈ ν•΄λ³΄λ©΄μ„œ μ λ‹Ήν•œ νƒ€μž…μ„ μ‚¬μš©ν•΄μ„œ μƒμ„±λ˜μ–΄μ•Ό ν•˜κ³  WidgetKit은 μƒμ„±λœ νƒ€μž„λΌμΈμ— μ˜ν•΄ μ •ν™•νžˆ μ—…λ°μ΄νŠΈκ°€ λ˜μ§€ μ•Šμ„ 수 있기 λ•Œλ¬Έμ— 이 뢀뢄도 λ°˜λ“œμ‹œ κ³ λ €ν•˜κ³  μžˆμ–΄μ•Ό ν•œλ‹€.

μœ„μ ―μ€ μ—¬λŸ¬ μ„œλΉ„μŠ€μ˜ 앱듀이 μ§€μ›ν•˜μ—¬ μ‚¬μš©λ˜λ‹€ λ³΄λ‹ˆ λͺ¨λ“  μœ„μ ―μ˜ μ—…λ°μ΄νŠΈ μ‹œκΈ°κ°€ 자주 ν˜ΈμΆœλœλ‹€λ©΄ λ””λ°”μ΄μŠ€κ°€ κ³ΌλΆ€ν™” 걸릴 μˆ˜λ„ 있기 λ•Œλ¬Έμ— μ• ν”Œμ€ 배터리 수λͺ…을 μ΅œμ ν™”ν•˜κΈ° μœ„ν•΄ λŒ€λž΅ 15λΆ„ λ‹¨μœ„λ‘œ ꢌμž₯ν•˜κ³  있고 μ ˆλŒ€μ μΈ κ·œμΉ™μ€ μ•„λ‹ˆλΌκ³  ν•œλ‹€.

μ§€κΈˆμ€ ν…ŒμŠ€νŠΈλ₯Ό ν•˜κΈ° μœ„ν•΄μ„œ μ—…λ°μ΄νŠΈλ₯Ό μ΄ˆλ‹¨μœ„λ‘œ 해도 λ¬Έμ œκ°€ μ—†μ§€λ§Œ μ‹€μ œ μš΄μ˜λ˜λŠ” 앱은 μ—…λ°μ΄νŠΈ μ‹œμ μ„ 곡격적으둜 μ‚¬μš©ν•˜μ§€ μ•Šλ„λ‘ κ³ λ €ν•΄μ„œ 섀계해야 λ¬Έμ œκ°€ μ—†λ‹€.

Intent Configuration

μ΄μ–΄μ„œ IntentConfiguration μœ„μ ―μ„ μ‚¬μš©ν•  λ•Œμ— μΆ”κ°€λœ 뢀뢄을 μ‚΄νŽ΄λ³΄λ„λ‘ ν•˜μž.

WidetKit μΆ”κ°€μ‹œ "include Configuration App Intent" λ₯Ό μ²΄ν¬ν•΄μ„œ 생성해주면 λœλ‹€.

μ½”λ“œλ₯Ό μ‚΄νŽ΄λ³΄λ©΄ TimelineEntry 객체에 ConfigurationAppIntent 객체λ₯Ό ν•„μˆ˜λ‘œ λ°›μ•„μ˜€λ„λ‘ ν•˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

이 뢀뢄이 StaticConfiguration 과의 λ‹€λ₯Έ 점이닀.

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}

ConfigurationAppIntent μ½”λ“œλ„ μž‘μ„±λ˜μ–΄ μžˆλŠ” 것을 확인할 수 μžˆλ‹€.

extension ConfigurationAppIntent {
    fileprivate static var smiley: ConfigurationAppIntent {
        let intent = ConfigurationAppIntent()
        intent.favoriteEmoji = "πŸ˜€"
        return intent
    }
    
    fileprivate static var starEyes: ConfigurationAppIntent {
        let intent = ConfigurationAppIntent()
        intent.favoriteEmoji = "🀩"
        return intent
    }
}

AppIntent λΌλŠ” νŒŒμΌμ•ˆμ— ConfigurationAppIntent에 λŒ€ν•œ μ½”λ“œκ°€ μ—¬κΈ°μ—μ„œ μž‘μ„±λ˜μ–΄ μžˆλ‹€.


import WidgetKit
import AppIntents

struct ConfigurationAppIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Configuration"
    static var description = IntentDescription("This is an example widget.")

    // An example configurable parameter.
    @Parameter(title: "Favorite Emoji", default: "πŸ˜ƒ")
    var favoriteEmoji: String
}
Intent Static

κ²°κ΅­ WidgetConfigurationIntent 객체λ₯Ό μ‚¬μš©ν•˜μ—¬ μœ„μ ― νŽΈμ§‘μ„ λ§Œλ“€μ–΄ 쀄 수 μžˆλ‹€.

선택창을 λ§Œλ“€μ–΄ μœ„μ ―μ˜ λ°±κ·ΈλΌμš΄λ“œ 컬러λ₯Ό λ³€κ²½ν•  수 μžˆλ„λ‘ ν•΄λ³΄μž.

λ¨Όμ € WidgetConfigurationIntentλ₯Ό μ‚¬μš©ν•΄μ„œ νŽΈμ§‘μ°½μ„ 선택 κ°€λŠ₯ν•˜λ„λ‘ μˆ˜μ •ν•΄ 주자.

struct SelectColorAppIntent : WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Select Color"
    
    @Parameter(title: "color",default: .redType)
    var type : SelectColorType
}

SelectColorType을 생성해 주도둝 ν•˜μž.

enum SelectColorType: String, AppEnum {
    case redType, blueType, orangeType

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Color Type"
    static var caseDisplayRepresentations: [SelectColorType : DisplayRepresentation] = [
        .redType: "Red",
        .blueType:"Blue",
        .orangeType: "Oragnge",
    ]
}

μƒμ„±ν•œ WidgetConfigurationIntent 객체λ₯Ό ν™•μž₯ν•΄ μ£Όκ³ , ν•΄λ‹Ήν•˜λŠ” νƒ€μž…μ˜ 컬러둜 λ°±κ·ΈλΌμš΄λ“œ 컬러λ₯Ό λ§Œλ“€μ–΄μ£Όμž.

struct IntentWidgetKit: Widget {
    let kind: String = "IntentWidgetKit"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: SelectColorAppIntent.self, provider: Provider()) { entry in
            IntentWidgetKitEntryView(entry: entry)
                .containerBackground(entry.configuration.type == .redType ? .red : entry.configuration.type  == .blueType ? .blue : .orange, for: .widget)
        }
    }
}

extension SelectColorAppIntent {
    fileprivate static var redType : SelectColorAppIntent {
        let intent = SelectColorAppIntent()
        intent.type = .redType
        return intent
    }
    fileprivate static var blueType : SelectColorAppIntent {
        let intent = SelectColorAppIntent()
        intent.type = .blueType
        return intent
    }
    fileprivate static var orangeType : SelectColorAppIntent {
        let intent = SelectColorAppIntent()
        intent.type = .orangeType
        return intent
    }
}

κ°„λ‹¨ν•˜κ²Œ ꡬ성해 λ΄€λŠ”λ°, 선택 μœ„μ ― 외에도 ν…μŠ€νŠΈ μž…λ ₯, μŠ€μœ„μΉ˜ λ“±μ˜ λ‹€μ–‘ν•œ ꡬ성을 νŽΈμ§‘κΈ°λŠ₯으둜 μ‚¬μš©ν•  수 μžˆμœΌλ‹ˆ μΆ”κ°€μ μœΌλ‘œ ν•΄λ‹Ή 뢀뢄은 λ³„λ„λ‘œ 글을 μž‘μ„±ν•΄ 보도둝 ν•˜κ² λ‹€.

WidgetURL

μ΄μ–΄μ„œ μœ„μ ―μ„ ν΄λ¦­ν–ˆμ„ λ•Œμ— μ²˜λ¦¬ν•  수 μžˆλŠ” λ”₯링크λ₯Ό μΆ”κ°€ν•΄ 주도둝 ν•˜μž.

λ¨Όμ € WidgetKit의 View κ°μ²΄μ—μ„œ μ›ν•˜λŠ” ν„°μΉ˜ ν¬μΈνŠΈμ— widgetUrl을 μ‚¬μš©ν•΄ μ£Όλ©΄ λœλ‹€.

URL νƒ€μž…μ„ μ‚¬μš©ν•˜μ—¬ μ›ν•˜λŠ” μŠ€ν‚΄μ„ λ§Œλ“€μ–΄ μ£Όλ©΄ λœλ‹€.

struct IntentWidgetKitEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(entry.date, style: .time)
        }.widgetURL(URL(string: "widgetSample://test.com"))
    }
}

μƒμ„±ν•œ μŠ€ν‚΄μ„ μˆ˜μ‹ ν•˜κΈ° μœ„ν•΄ WidowGroup ν•˜μœ„ μœ„μ ―μ— onOpenURL을 μ‚¬μš©ν•΄ μŠ€ν‚΄ 정보λ₯Ό μˆ˜μ‹ λ°›μ•„ μ²˜λ¦¬ν•΄μ£Όλ©΄ λœλ‹€.

import SwiftUI

@main
struct WidgetKitSampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL(perform: { url in
                    print(url)
                })
        }
    }
}

WidgetCenter

μœ„μ ―μ˜ 일뢀 λ˜λŠ” 전체 νƒ€μž„λΌμΈμ„ λ‹€μ‹œ λ‘œλ“œν•˜κ±°λ‚˜ ꡬ성을 κ°€μ Έμ˜€λŠ” 데 μ‚¬μš©λ˜λŠ” κΈ°λŠ₯으둜 μœ„μ ―μ— μžˆλŠ” 데이터λ₯Ό λ™μ μœΌλ‘œ μ—…λ°μ΄νŠΈ λ˜λŠ” μƒνƒœλ₯Ό 확인할 수 μžˆλŠ” κΈ°λŠ₯이닀.

ν•΄λ‹Ή κΈ°λŠ₯도 iOS 14 μ΄μƒμ—μ„œλ§Œ λ™μž‘ν•˜λŠ” WidgetKit의 일뢀 κΈ°λŠ₯이닀.

예λ₯Ό λ“€μ–΄ μœ„μ ―μ— μ‚¬μš©μžμ˜ 주식 수읡λ₯ μ„ 보여주고 μžˆλ‹€κ³  κ°€μ •ν•΄λ³΄μž.
ν•΄λ‹Ή μ‚¬μš©μžμ˜ 인증 여뢀에 따라 μœ„μ ―μ„ μ—…λ°μ΄νŠΈ ν•΄μ£Όμ–΄μ•Ό ν•˜λŠ”λ°, μ‚¬μš©μžμ˜ 계정이 λ‘œκ·Έμ•„μ›ƒ λ˜λ”λΌλ„ νƒ€μž„λΌμΈμ΄ 즉각 μ—…λ°μ΄νŠΈ λœλ‹€λŠ” 보μž₯이 μ—†κΈ° λ•Œλ¬Έμ— λ³„λ„λ‘œ λ‘œκ·Έμ•„μ›ƒ μ²˜λ¦¬μ‹œ WidgetCenterλ₯Ό μ‚¬μš©ν•΄ λ°μ΄ν„°λ‚˜ μƒνƒœλ₯Ό μ¦‰μ‹œ μ—…λ°μ΄νŠΈ μ‹œμΌœμ€„ 수 있게 λœλ‹€.

getCurrentConfigurations

ν˜„μž¬ μœ„μ ―μ˜ μƒνƒœλ₯Ό 확인할 수 μžˆλŠ” κΈ°λŠ₯이닀.

WidgetCenter.shared.getCurrentConfigurations { result in
    switch result {
    case .success(let widgetInfos):
        for widgetInfo in widgetInfos {
            print("Widget kind: \(widgetInfo.kind)")
            print("Widget family: \(widgetInfo.family)")
        }
    case .failure(let error):
        print("Error fetching widget configurations: \(error.localizedDescription)")
    }
}

reloadTimelines

WidgetKitμ—μ„œ μ‚¬μš© 쀑인 νŠΉμ • μœ„μ ―μ˜ νƒ€μž„λΌμΈμ„ λ‹€μ‹œ 생성할 λ•Œμ— μ‚¬μš©ν•  수 μžˆλ‹€.

WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

WidgetKitμ—μ„œ μ‚¬μš© 쀑인 kind와 μΌμΉ˜ν•˜μ—¬μ•Ό ν•˜λŠ”λ°, kindλŠ” WidgetKitμ—μ„œ μ„€μ •ν•  수 μžˆλ‹€.

reloadAllTimelines

νŠΉμ • μœ„μ ―μ΄ μ•„λ‹Œ 전체 WidgetKit의 νƒ€μž„λΌμΈμ„ λ‹€μ‹œ 생성해 쀄 λ•Œμ— μ‚¬μš©ν•  수 μžˆλ‹€.

WidgetCenter.shared.reloadAllTimelines()

AppGroup

μ΄λ²ˆμ—λŠ” 앱에 숫자λ₯Ό μ¦κ°€μ‹œμΌœ UserDefaultsλ₯Ό μ‚¬μš©ν•΄ μ¦κ°€ν•œ 값을 μ €μž₯ν•˜κ³  WidgetKitμ—μ„œ ν•΄λ‹Ή 데이터λ₯Ό 가져와 μ—…λ°μ΄νŠΈ μ‹œμ μ— μ¦κ°€λœ 카운트 값을 보여쀄 수 μžˆλ„λ‘ ν•΄λ³΄μž.

WidgetKitμ—μ„œ κ°€μ Έμ˜€λŠ” UserDefaults 값이 κ³΅μœ λ˜μ§€ λͺ»ν•˜κ³  μžˆλŠ” 것을 μ•Œ 수 μžˆλ‹€.

μ΄μœ λŠ” WidgetKit의 ν™˜κ²½μ΄ μ•±κ³ΌλŠ” λ…λ¦½μ μœΌλ‘œ μž‘λ™λ˜κΈ° λ•Œλ¬Έμ— ν™˜κ²½μ΄ μ™„μ „νžˆ λΆ„λ¦¬λ˜μ–΄ μžˆλŠ” μƒνƒœμ΄λ‹€. 즉 동일 ν™˜κ²½μ— μœ„μΉ˜ν•œ 앱이 μ•„λ‹Œ 것이닀.

μ΄λŸ¬ν•œ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” 곡유 μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‚¬μš©ν•΄ 데이터가 곡유될 수 μžˆλ„λ‘ λ©”μ»€λ‹ˆμ¦˜μ„ μ œκ³΅ν•΄ μ£Όμ–΄μ•Ό ν•œλ‹€.

AppGroup을 μΆ”κ°€ν•΄ UserDefaultsν™˜κ²½μ„ κ³΅μœ μ‹œμΌœ 주도둝 ν•˜μž.

TARGETS을 ν΄λ¦­ν•΄μ„œ Capablilityλ₯Ό μΆ”κ°€ν•΄ 주도둝 ν•˜μž.

App Groupsλ₯Ό μΆ”κ°€ν•΄ μ£Όλ©΄ λœλ‹€.

App Groupsκ°€ μΆ”κ°€λœ 것이 λ³΄μ΄λŠ”λ°, Apple Developerμ—μ„œ 동일 κ³„μ •μœΌλ‘œ μ‚¬μš©μ€‘μΈ App Groups λͺ©λ‘μ΄ λͺ¨λ‘ λ³΄μ—¬μ§€λŠ”λ°, μƒˆλ‘­κ²Œ 그룹을 μƒμ„±ν•˜κ³  μ‹Άλ‹€λ©΄ "+"λ₯Ό 클릭해 주도둝 ν•˜μž.

ν•΄λ‹Ή ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•˜κ³  싢은 그룹의 이름을 자유둭게 μ§€μ •ν•΄ μ£Όλ©΄λœλ‹€.

μƒˆλ‘­κ²Œ μΆ”κ°€λœ App Group을 선택해 μ²΄ν¬ν•΄μ£Όμž.

ν™˜κ²½μ„ κ³΅μœ ν•˜κ³  싢은 Widget의 νƒ€κ²Ÿμ„ λ³€κ²½ν•˜μ—¬ λ™μΌν•˜κ²Œ App Groupsλ₯Ό μΆ”κ°€ν•΄ μ€€λ’€, μ•žμ„œ μ²΄ν¬ν•œ 그룹의 λ™μΌν•œ 그룹을 μ—¬κΈ°μ„œλ„ 체크해 주도둝 ν•˜μž.

이제 UserDefaults μΈμŠ€ν„΄μŠ€μ‹œ suiteName에 μΆ”κ°€ν•œ App Group을 μ‚¬μš©ν•˜λ©΄ λœλ‹€.

UserDefaults(suiteName: "group.tyger.widgetSample")

이제 μ •μƒμ μœΌλ‘œ 둜컬 λ°μ΄ν„°λ² μ΄μŠ€μ— μžˆλŠ” 데이터가 곡유된 것을 확인할 수 μžˆλ‹€.

마무리

iOS의 WidgetKit에 λŒ€ν•΄μ„œ 글을 μž‘μ„±ν–ˆλŠ”λ°, ν•œ λ²ˆμ— λ‹€λ£° λ‚΄μš©μ΄ λ§Žμ•„μ„œ λ””ν…ŒμΌν•˜κ²Œ 닀뀄봐야 ν•  λ‚΄μš©μ΄λ‚˜ μ‚¬μš© 방법듀을 μ „λΆ€ μž‘μ„±ν•˜μ§€λŠ” λͺ»ν•˜μ˜€λ‹€.

좔가적인 뢀뢄에 λŒ€ν•΄μ„œλŠ” λ³„λ„λ‘œ 글을 μž‘μ„±ν•  μ˜ˆμ •μ΄κ³ , Androidμ—μ„œ μ‚¬μš©ν•˜λŠ” μœ„μ ―μ— λŒ€ν•œ λ‚΄μš©κ³Ό Flutterμ—μ„œ iOS, Android의 μœ„μ ―μ„ μ—°κ²°ν•΄ μ‚¬μš©ν•  수 μžˆλŠ”μ§€μ— λŒ€ν•΄μ„œλ„ μž‘μ„±ν•΄ 보도둝 ν•˜κ² λ‹€.

κΈ΄ κΈ€ μ½μ–΄μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€ !

profile
Flutter Developer

0개의 λŒ“κΈ€