๊ฐ์Widget Extension์์ ฏ์ ๊ธฐ๋ณธ ๊ตฌ์กฐEntryViewWidgetAppGroup์ฃผ์ ์ฌํญreloadAllTimelines()
๊ฐ์
- 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
๋๋๋ง

- 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 ์์ฑ์ ๊ฐ์ง๊ณ ์์ด, ํด๋น ์ํธ๋ฆฌ๊ฐ ์ธ์ ํ์ฑํ๋ ์ง๋ฅผ ๊ฒฐ์ .
- 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์ ํตํด ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํด์ผํ๋ค.
- AppGroup ์์ฑ

- App Group Identifier ๋ฑ๋ก ํ UserDefaults ์์ฑ
extension UserDefaults {
static var groupShared: UserDefaults {
//๋ฐ์ดํฐ ๊ณต์ ๋ฅผ ์ํ ๊ณต์ ์ ์ฅ์ ์์ฑ
let groupID = "group.lyoodong.ExWidget" //์ฑ์ ๋ฒ๋ค ID์ ์ค๋ณต๋ ๊ฒฝ์ฐ, ํฌ๋์ ๋ฐ์
return UserDefaults(suiteName: groupID)!
}
}
- ๊ณต์ ํ๊ณ ์ถ์ ๊ฐ์ groupShared ์ ์ฅ์์ ์ ์ฅ ๋ฐ ํ์ฉ
//๊ฐ ์ ์ฅ
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๋ฅผ ์ ํํ๊ฒ ์ ๋ ฅํ์ง ์์ ์ ํฌ๋์ ๋ฐ์ - UserDefaults์ GroupID ์ด์
- APPGroup์ ID์ ๋์ผํด์ผํจ
Not updating lastKnownShmemState in CFPrefsPlistSource<0x60000300a6d0> (Domain: group.lyoodong.ExWidget, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: Yes): 90 -> 92
- Widget Extension๊ณผ ์ฑ์ด ๊ณต์ ํ๋ ํ์ผ(ex. groupShared, APIManager, Model ๋ฑ)์ ๋ชจ๋ ํ๊ฒ์ ๋ ๊ณณ ๋ชจ๋ ์ค์ ํด์ค์ผ ํ๋ค.

reloadAllTimelines()
- ํน์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด, ์ฆ์ ์์ ฏ์ ์ ๋ฐ์ดํธ ์ํฌ ์ ์๋ค.
- iOS 14 ์ด์ ์ ์ฉ.
//๋ฒํผ ํด๋ฆญ ์ ์์ ฏ ์
๋ฐ์ดํธ
Button("๋ฒํผ") {
//ํน์ Kind(์๋ณ์)์ ๋ํ ์
๋ฐ์ดํธ
WidgetCenter.shared.reloadTimelines(ofKind: "Kind")
//์์๋ ๋ชจ๋ ์์ ฏ ์
๋ฐ์ดํธ
WidgetCenter.shared.reloadAllTimelines()
}
Share article