[iOS]Widgetย 

iOS WidgetKit ๊ฐœ๋… ์ดํ•ด
Dec 05, 2023
[iOS]Widgetย 

๊ฐœ์š”

  • iOS 8๋ถ€ํ„ฐ ์ง€์›, iOS 13๊นŒ์ง€๋Š” Today Extension์„ ํ†ตํ•ด ์œ„์ ฏ์„ ์ถ”๊ฐ€ ๋ฐ ์‚ฌ์šฉ
  • ๋‹จ, ํ™ˆ ํ™”๋ฉด์ด๋‚˜ ์ž ๊ธˆํ™”๋ฉด์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅ
  • iOS 14 ์ดํ›„๋ถ€ํ„ฐ WidgetKit ๋„์ž…
  • WidgetKit์€ ์˜ค์ง SwiftUI๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ
  • iOS 16 ์ด์ƒ ๋ถ€ํ„ฐ ์ž ๊ธˆ ํ™”๋ฉด์—์„œ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • iOS 17 ์ด์ƒ ๋ถ€ํ„ฐ Mac, iPad ์ž ๊ธˆ ํ™”๋ฉด, StandBy, Watch Smart Stack ์ถ”๊ฐ€
  • ์ •๋ฆฌํ•˜๋ฉด, iOS 17 ๊ธฐ์ค€์œผ๋กœ ์ด 7๊ฐœ์˜ Case์—์„œ ์œ„์ ฏ ๋Œ€์‘ ๊ฐ€๋Šฅ (ํ™ˆ, ์ž ๊ธˆ, ์˜ค๋Š˜ ๋ณด๊ธฐ, Mac, iPad ์ž ๊ธˆ ํ™”๋ฉด, StandBy, Watch Smart Stack)
 

Widget Extension

  • File โ†’ New โ†’ Target โ†’ Widget Extension์„ ํ†ตํ•ด ์ถ”๊ฐ€
  • Activate โ€œWidgetExtensionโ€ Scheme?๋Š” Activate๋กœ ์„ค์ •
 

์œ„์ ฏ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ

  • 4๊ฐ€์ง€ struct๋กœ ๊ตฌ์„ฑ
  • Provider, Entry, EntryView, Widget
  • Provider์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •ํ•œ ์‹œ๊ฐ„์— ๋งž์ถฐ ์œ„์ ฏ์„ ์—…๋ฐ์ดํŠธ
  • Entry๋Š” ์œ„์ ฏ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ œ๊ณต, Model์˜ ์—ญํ• 
  • Entry์—์„œ ์ œ๊ณตํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, EntryView์—์„œ UI ๋‹ด๋‹น
  • EntryView๋ฅผ ํ†ตํ•ด ์ตœ์ข…์ ์œผ๋กœ Widget๋žœ๋”๋ง
    • notion image
 
  1. Provider
  • ์• ํ”Œ์˜ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋”ฐ๋ฅด๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ํ™ˆ ํ™”๋ฉด์— ๋จธ๋ฌด๋ฅด๋Š” ์‹œ๊ฐ„์€ ๋งค์šฐ ์ œํ•œ์ 
  • ๋”ฐ๋ผ์„œ, Widget์„ ๋žœ๋”๋งํ•˜๋Š”๋ฐ ๋งŽ์€ ์‹œ๊ฐ„์„ ์†Œ์š”ํ•˜๋Š” ๊ฒƒ์€ ๊ถŒ์žฅ๋˜์ง€ ์•Š์Œ
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ํŠน์ • ์‹œ๊ฐ„์— Widget์„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก Provider๋ฅผ ์„ค์ •
  • TimelineEntry๋ฅผ ํ†ตํ•ด ํŠน์ • ์‹œ๊ฐ„์— ์œ„์ ฏ์„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
  • Provider์—์„œ๋Š” ์•„๋ž˜ 3๊ฐ€์ง€ ๋ฉ”์„œ๋“œ๋ฅผ ํ•„์ˆ˜์ ์œผ๋กœ ์š”๊ตฌ
struct Provider: TimelineProvider { //์œ„์ ฏ์˜ ์ดˆ๊ธฐ ํ™”๋ฉด func placeholder(in context: Self.Context) -> Self.Entry //์œ„์ ฏ ๊ฐค๋Ÿฌ๋ฆฌ์˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™”๋ฉด(์œ„์ ฏ์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๋ณด์—ฌ์ง€๋Š” ํ™”๋ฉด) func getSnapshot(in context: Self.Context, completion: @escaping (Self.Entry) -> Void) //ํƒ€์ž„๋ผ์ธ ์ƒ์„ฑ, ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ ์„ค์ • //ํƒ€์ž„๋ผ์ธ -> TimelineEntry์˜ ์ปฌ๋ ‰์…˜ //์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ -> ์–ธ์ œ, ์–ด๋–ค ์ฃผ๊ธฐ๋กœ ์œ„์ ฏ์„ ์—…๋ฐ์ดํŠธํ•  ์ง€ ๊ฒฐ์ • func getTimeline(in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) }
Timeline๊ณผ TimelineEntry
  • Timeline: ์ด ๊ฐ์ฒด๋Š” ์—ฌ๋Ÿฌ TimelineEntry ๊ฐ์ฒด์˜ ์ปฌ๋ ‰์…˜์„ ํฌํ•จ. ๊ฐ TimelineEntry๋Š” ํŠน์ • ์‹œ๊ฐ„์— ์œ„์ ฏ์— ํ‘œ์‹œ๋  ๋‚ด์šฉ์„ ์ •์˜.
  • TimelineEntry: ์ด ํ”„๋กœํ† ์ฝœ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฐ์ฒด๋Š” ์œ„์ ฏ์ด ํŠน์ • ์‹œ๊ฐ„์— ํ‘œ์‹œํ•ด์•ผ ํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜. ๊ฐ ์—”ํŠธ๋ฆฌ๋Š” date ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด, ํ•ด๋‹น ์—”ํŠธ๋ฆฌ๊ฐ€ ์–ธ์ œ ํ™œ์„ฑํ™”๋ ์ง€๋ฅผ ๊ฒฐ์ •.
 
  1. Entry
  • Widget์˜ Model ์—ญํ• 
  • ์œ„์ ฏ์˜ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„์„ ์•Œ๋ ค์ฃผ๋Š” date ํ”„๋กœํผํ‹ฐ๋ฅผ ํ•„์ˆ˜ ๊ตฌํ˜„
  • ์Šค๋งˆํŠธ ์Šคํƒ ๋“ฑ์—์„œ ์œ„์ ฏ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์•Œ๋ ค์ฃผ๋Š” relevance๋„ ์กด์žฌ
struct Entry: TimelineEntry { //์—…๋ฐ์ดํŠธํ•  ์‹œ๊ฐ„ let date: Date }
 
TimelineEntry ํ”„๋กœํ† ์ฝœ
  • date: ์œ„์ ฏ์ด ๋‹ค์‹œ ๊ทธ๋ ค์งˆ ์‹œ๊ฐ„์— ๋Œ€ํ•œ ์ •๋ณด
  • relevance: ์Šค๋งˆํŠธ ์Šคํƒ ๋“ฑ์—์„œ ์œ„์ ฏ์˜ ์šฐ์„  ์ˆœ์œ„
 

EntryView

  • Provider๋ฅผ ํ†ตํ•ด Entry๋ฅผ ์ œ๊ณต๋ฐ›์œผ๋ฉด, Entry๋ฅผ ์ด์šฉํ•ด ์œ„์ ฏ์— ๋‚˜ํƒ€๋‚  ๋ทฐ๋ฅผ ๊ทธ๋ ค์ค€๋‹ค.
  • entry ๋ณ€์ˆ˜๋ฅผ ํ™œ์šฉํ•ด ์œ„์ ฏ์— ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
struct MyWidgetEntryView : View { var entry: Provider.Entry var body: some View { VStack { Text("Time:") Text(entry.date, style: .time) Text("Emoji:") Text(entry.emoji) } } }
 

Widget

  • ์ตœ์ข…์ ์œผ๋กœ Widget ํ”„๋กœํ† ์ฝœ์„ ์ค€์ˆ˜ํ•˜๋Š” Widget ๊ตฌ์กฐ์ฒด๋ฅผ ์„ค์ •
  • Widget์˜ ๊ณ ์œ  ๋ฌธ์ž์—ด, ์œ„์ ฏ ๊ฐค๋Ÿฌ๋ฆฌ์— ๋ณด์ผ ์œ„์ ฏ์˜ ์ด๋ฆ„ / ์„ค๋ช…, ์œ„์ ฏ์˜ ํฌ๊ธฐ ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
struct MyWidget: Widget { let kind: String = "MyWidget" //์œ„์ ฏ ๊ณ ์œ  ๋ฌธ์ž์—ด var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in if #available(iOS 17.0, *) { MyWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } else { MyWidgetEntryView(entry: entry) .padding() .background() } } .configurationDisplayName("My Widget") //๊ฐค๋Ÿฌ๋ฆฌ ์ด๋ฆ„ .description("This is an example widget.") //๊ฐค๋Ÿฌ๋ฆฌ ์„ค๋ช… .supportedFamilies([.systemSmall, .systemLarge])//์œ„์ ฏ ํฌ๊ธฐ } }

AppGroup

  • App๊ณผ App Extension(์œ„์ ฏ)๋Š” ๋…๋ฆฝ์ ์ธ ํ”„๋กœ์„ธ์Šค๋กœ ์‹คํ–‰๋œ๋‹ค.
  • ๋”ฐ๋ผ์„œ, ๋ฐ์ดํ„ฐ์˜ sync๋ฅผ ๋งž์ถ”๊ธฐ ์œ„ํ•ด์„œ๋Š” AppGroup์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•ด์•ผํ•œ๋‹ค.
  1. AppGroup ์ƒ์„ฑ
    1. notion image
  1. App Group Identifier ๋“ฑ๋ก ํ›„ UserDefaults ์ƒ์„ฑ
    1. extension UserDefaults { static var groupShared: UserDefaults { //๋ฐ์ดํ„ฐ ๊ณต์œ ๋ฅผ ์œ„ํ•œ ๊ณต์œ  ์ €์žฅ์†Œ ์ƒ์„ฑ let groupID = "group.lyoodong.ExWidget" //์•ฑ์˜ ๋ฒˆ๋“ค ID์™€ ์ค‘๋ณต๋  ๊ฒฝ์šฐ, ํฌ๋ž˜์‹œ ๋ฐœ์ƒ return UserDefaults(suiteName: groupID)! } }
  1. ๊ณต์œ ํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์„ groupShared ์ €์žฅ์†Œ์— ์ €์žฅ ๋ฐ ํ™œ์šฉ
    1. //๊ฐ’ ์ €์žฅ var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") Button("๋ฒ„ํŠผ") { print("ํ˜ธ์ถœ ์‹œ์ž‘") startTimer() } } .padding() } func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in APIManager.getTexts { texts in if let firstText = texts.first { //๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ†ตํ•ด, 30์ดˆ ํ•œ๋ฒˆ์”ฉ ๋žœ๋ค ํ…์ŠคํŠธ ์ €์žฅ UserDefaults.groupShared.set(firstText, forKey: "randomText") print("firstText ์„ฑ๊ณต", firstText) } else { print("firstText ์‹คํŒจ") } } } }
      //๊ฐ’ ํ™œ์šฉํ•ด ์œ„์ ฏ์— ํ‘œ๊ธฐ(1๋ถ„ ์ฃผ๊ธฐ) func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) { var entries: [SimpleEntry] = [] let currentDate = Date() let entryDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)! let text = UserDefaults.groupShared.string(forKey: "randomText") ?? "์‹คํŒจ" let entry = SimpleEntry(date: entryDate, image: newjeansImage, texts: text) entries.append(entry) let timeline = Timeline(entries: entries, policy: .after(entryDate)) completion(timeline) }

์ฃผ์˜ ์‚ฌํ•ญ

  • AppGroup, Widget Extension, ์•ฑ ๋ชจ๋‘ ๊ฐœ๋ณ„์ ์ธ ID๋ฅผ ๋“ฑ๋ก. ์ค‘๋ณต ์‹œ ํฌ๋ž˜์‹œ ๋ฐœ์ƒ
  • AppGroup ์ƒ์„ฑ์‹œ .entitlements ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š” ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๋Š”๋ฐ ์ด๋•Œ, AppGroupID๋ฅผ ์ •ํ™•ํ•˜๊ฒŒ ์ž…๋ ฅํ•˜์ง€ ์•Š์„ ์‹œ ํฌ๋ž˜์‹œ ๋ฐœ์ƒ
    • Not updating lastKnownShmemState in CFPrefsPlistSource<0x60000300a6d0> (Domain: group.lyoodong.ExWidget, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: Yes): 90 -> 92
        • UserDefaults์˜ GroupID ์ด์Šˆ
        • APPGroup์˜ ID์™€ ๋™์ผํ•ด์•ผํ•จ
    • Widget Extension๊ณผ ์•ฑ์ด ๊ณต์œ ํ•˜๋Š” ํŒŒ์ผ(ex. groupShared, APIManager, Model ๋“ฑ)์€ ๋ชจ๋‘ ํƒ€๊ฒŸ์„ ๋‘ ๊ณณ ๋ชจ๋‘ ์„ค์ •ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
      • notion image

    reloadAllTimelines()

    • ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ์ฆ‰์‹œ ์œ„์ ฏ์„ ์—…๋ฐ์ดํŠธ ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
    • iOS 14 ์ด์ƒ ์ ์šฉ.
    //๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์œ„์ ฏ ์—…๋ฐ์ดํŠธ Button("๋ฒ„ํŠผ") { //ํŠน์ • Kind(์‹๋ณ„์ž)์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ WidgetCenter.shared.reloadTimelines(ofKind: "Kind") //์†Œ์†๋œ ๋ชจ๋“  ์œ„์ ฏ ์—…๋ฐ์ดํŠธ WidgetCenter.shared.reloadAllTimelines() }
     
    Share article
    RSSPowered by inblog