iOS์์ '์์ ฏ'์ด๋ผ๋ ๊ฐ๋
์ ์๊ฐ๋ณด๋ค ์ค๋๋๋ค.
๋ค๋ง ์ฌ์ฉ์์๊ฒ ๋
ธ์ถ๋๋ ๋ชจ์ต๊ณผ ๊ฐ๋ฐ์๊ฐ ๋ค๋ฃจ๋ ๊ธฐ์ ์คํ์ ํฌ๊ฒ ๋ ๋ฒ ๋ฐ๋์๋ค.
์ด๋ฒ ๊ธ์์๋ ์์ ฏ์ ํ๋ฆ์ ๊ฐ๋จํ ์ง๊ณ , ์ ์ง๊ธ์ SwiftUI ๊ธฐ๋ฐ์ WidgetKit์ผ๋ก ๊ฐ๋ฐํด์ผ ํ๋์ง๋ฅผ ์ ๋ฆฌํ ๋ค,
์์ ๋ก D-Day ์์ ฏ์ ๊ตฌํํ๋ ์์๋ฅผ ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด๋ ค๊ณ ํ๋ค.
iOS ์์ ฏ์ ๋ณํ์ WidgetKit์ ๋ฑ์ฅ
1. Today Extension ์์ (iOS 8 ~ iOS 13)
์ด๊ธฐ ์์ ฏ์ Notification Center์ Today View์ ์กด์ฌํ๋ค.
์ด ๋์ ์์ ฏ์
- Today Extension ์ด๋ผ๋ ๋ณ๋์ ํ๊น์ผ๋ก ๊ตฌํ
- UIKit + AutoLayout + Storyboard ๊ธฐ๋ฐ
- ์ฑ๊ณผ์ ์ฐ๋๋ ์ง๊ธ๋ณด๋ค ์ ์ฝ์ด ๋ง๊ณ , UI ์์ ๋๋ ์์์ง๋ง ์์คํ
ํตํฉ ๊ฒฝํ์ ์ ํ์
๋ฌด์๋ณด๋ค, ํ ํ๋ฉด์ ์ฌ๋ ค๋๋ ํํ๊ฐ ์๋๋ผ '์๋ฆผ์ผํฐ ํ ์ผ '์ ์๋ ๋ถ๊ฐ ๊ธฐ๋ฅ ์ ๋์๋ค.
2. WidgetKit๋์
(iOS 14~)
iOS 14๋ถํฐ ์ ํ์ ์์ ฏ์ ํ ํ๋ฉด์ First-class๋ก ๋์ด์ฌ๋ ธ๋ค.
- ํ ํ๋ฉด ์ด๋๋ ๋ฐฐ์น ๊ฐ๋ฅ
- ๋ค์ํ ์ฌ์ด์ฆ(Small / Medium / Large, ์ดํ iOS 15+, 16+์์ ๋ ํ์ฅ)
์ด ์์ ์์ ์์ ฏ ๊ตฌํ ๋ฐฉ์๋ ์์ ํ ๋ฐ๋๊ฒ ๋๋ค.
- UIKit ๊ธฐ๋ฐ Today Extension → SwiftUI ๊ธฐ๋ฐ WidgetKit
- UI๋ ์ ๋ถ SwiftUI View๋ก๋ง ์์ฑ
- ์
๋ฐ์ดํธ๋ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ด ์๋๋ผ Timeline ๊ธฐ๋ฐ์ '์ค๋
์ท ๋ ๋๋ง' ๊ตฌ์กฐ
์ด์ iOS์์ '์์ ฏ์ ๋ง๋ ๋ค'๋ WidgetKit + SwiftUI๋ก ๊ฐ๋ฐํ๋ค๋ ์๋ฏธ๋ค.
WidgetKit์ ์ค๊ณ ์ฒ ํ: ์ SwiftUI๋ง ํ์ฉํ ๊น?
WidgetKit์ ํต์ฌ์ ์๋ ๋ ๊ฐ์ง๋ก ์์ฝ๋๋ค.
1. ์ ์ ์ด๊ณ ์์ธก ๊ฐ๋ฅํ UI
์์ ฏ์ ์ฌ์ฉ์๊ฐ ์ง์ ์ํธ์์ฉํ๋ ํ๋ฉด์ด๋ผ๊ธฐ ๋ณด๋ค,
์์คํ
์ด ์ ํ ๊ฐ๊ฒฉ๊ณผ ๊ท์น์ ๋ฐ๋ผ ๋
ธ์ถ๋๋ '์ ๋ณด ์นด๋'์ ๊ฐ๊น๋ค.
๋ฐ๋ผ์ ์ฑ์ฒ๋ผ ๋ณต์กํ ๋ทฐ ๊ณ์ธต, ์ ๋๋ฉ์ด์
, ์ ์ค์ฒ๊ฐ ์ฃผ์ธ๊ณต์ด ์๋๋ค.
2. Timeline ๊ธฐ๋ฐ ๋ ๋๋ง
WidgetKit์ '์ธ์ , ์ด๋ค ๋ฐ์ดํฐ๋ก ์์ ฏ์ ์
๋ฐ์ดํธํ ์ง'๋ฅผ ๋ฏธ๋ฆฌ ์ ํ ํ์๋ผ์ธ(Timeline)์ ๋๊ฒจ๋ฐ๊ณ ,
๊ทธ ์์ ๋ง๋ค SwiftUI View๋ฅผ ๋ ๋๋งํ์ฌ ํ๋ฉด์ ๊ทธ๋ ค์ค๋ค.
์ด๋ SwiftUI๋
- ์ ์ธํ UI์ด๊ธฐ ๋๋ฌธ์ ํ์ฌ ์ํ → ๋ทฐ๋ก ๋งคํํ๊ธฐ ์ข๊ณ
- ์ฌ๋ ๋๋ง ์ ์์คํ
์ด ๋น์ฉ์ ์์ธกํ๊ธฐ ์ฝ๊ณ
- ์ฌ์ด๋ ์ดํํธ๊ฐ ํต์ ๋๊ธฐ ๋๋ฌธ์
WidgetKit์ '์ค๋
์ท ๊ธฐ๋ฐ ๋ ๋๋ง' ๋ชจ๋ธ๊ณผ ์ ๋ง๋๋ค.
๋ฐ๋๋ก UIKit์ ๋ผ์ดํ์ฌ์ดํด๊ณผ ์ํ๊ฐ ๋ณต์กํ๊ธฐ ๋๋ฌธ์ ์ ๊ตฌ์กฐ์ ๋ง์ง ์์ ์์ ฏ์์๋ ์์ ์ฌ์ฉ์ด ๊ธ์ง๋๋ค.
D-Day ์์ ฏ์ ์์ ๋ก ์ํคํ ์ฒ์ก๊ธฐ
๊ฐ๋จํ๊ฒ ๋ง๋ค์ด ๋ณผ ์์ ฏ์ ๊ธฐ๋ฅ
โ ์ฌ์ฉ์๊ฐ ์ฑ์์ '๊ธฐ๋
์ผ ์ ๋ชฉ + ๋ ์ง'๋ฅผ ์ค์
โก ํ ํ๋ฉด์์
- D-3, D-day, D+2์ ๊ฐ์ด ๋จ์/์ง๋ ๋ ์ง ํ์
- ๊ธฐ๋
์ผ ๋ ์ง, ์ ๋ชฉ ํจ๊ป ๋
ธ์ถ
โข ์์ ์ด ์ง๋ ๋๋ง๋ค ์๋์ผ๋ก D-Day ์ซ์๊ฐ ๋ฐ๋
โฃ ์์ ฏ์ ํญํ๋ฉด ์ฑ์ D-Day ์ค์ ํ๋ฉด์ผ๋ก ์ด๋
๊ฐ๋ฐ ์์
โ ๊ธฐ๋ณธ ํ๋ก์ ํธ + Widget Extension ์์ฑ
โก ๊ณต์ฉ ๋ฐ์ดํฐ ์ ์ฅ์(App Group + UserDefaults) ์ค๊ณ
โข D-Day ๋๋ฉ์ธ ๋ชจ๋ธ ๋ฐ ๋ ์ง ๊ณ์ฐ ๋ก์ง ๊ตฌํ
โฃ TimelineProvider / Entry ๊ตฌํ
โค SwiftUI๋ก ์์ ฏ UI ๊ตฌ์ฑ
โฅ ์ฑ์์ ์ค์ ๋ณ๊ฒฝ ์ ์์ ฏ ๊ฐฑ์ ์ฐ๋(WidgetCenter)
โฆ ์ค๊ธฐ๊ธฐ/์๋ฎฌ๋ ์ดํฐ์์ ์์ ฏ ์ถ๊ฐ ๋ฐ ๋์ ๊ฒ์ฆ
1๋จ๊ณ – Widget Extension ์์ฑ
1. Xcode์์
- File > New > Target…
- iOS ํญ์์ Widget Extension ์ ํ
2. ์ด๋ฆ ์: DDayWidget
3. SwiftUI + WidgetKit ๊ธฐ๋ฐ ํ
ํ๋ฆฟ์ด ์์ฑ๋๋ค.
์์ฑ ํ ํ๋ก์ ํธ ๊ตฌ์กฐ:
- DDayWidget.swift (์์ ฏ ์ ์)
- DDayWidgetExtension ํ๊น
- ๊ธฐ๋ณธ Provider / Entry / View ์ฝ๋
(์ฌ๊ธฐ์ ๊ธฐ์กด ๊ฐ๋ฐํ๋ ํ๋ก์ ํธ๊ฐ UIKit ๊ธฐ๋ฐ์ด๋ผ๋ ์๊ด์์! ํ๊ฒ๋ง ์ถ๊ฐํ๋ฉด ๋จ)

(์ถ๊ฐํ๋ฉด ์ด๋ ๊ฒ ์๋์ผ๋ก ์์ ํ์ผ๊ณผ ํจ๊ป ์์ฑ๋จ)
2๋จ๊ณ - App Group + ๊ณต์ ์ ์ฅ์ ์ค๊ณ
์ฑ๊ณผ ์์ ฏ์ ์๋ก ๋ค๋ฅธ ํ๋ก์ธ์ค์ด๊ธฐ ๋๋ฌธ์, ๋ฐ์ดํฐ๋ฅผ ์ง์ ๊ณต์ ํ ์ ์๋ค.
์ ํ์ ์ด๋ฅผ ์ํด App Groups๋ผ๋ ๊ณต์ ์ปจํ
์ด๋๋ฅผ ์ ๊ณตํ๊ณ ์๋ค.
App Group ์ค์
1. ์ฑ ํ๊ฒ → Signing & Capabilities → + Capability → App Groups ์ถ๊ฐ
2. Widget ํ๊น์๋ ๋์ผํ๊ฒ App Groups ์ถ๊ฐ
3. ex) group.com.yegrcompany.ddaywidget ์์ฑ ํ, ๋ ํ๊น ๋ชจ๋ ์ฒดํฌ
๊ณต์ UserDefaults ๋ํผ
enum SharedStore {
static let suiteName = "group.com.yegrcompany.ddaywidget"
static var defaults: UserDefaults? {
UserDefaults(suiteName: suiteName)
}
}
struct DDayConfig: Codable {
let title: String
let targetDate: Date
}
extension SharedStore {
private static let key = "dday_config"
static func save(config: DDayConfig) {
guard let data = try? JSONEncoder().encode(config) else { return }
defaults?.set(data, forKey: key)
}
static func loadConfig() -> DDayConfig? {
guard
let data = defaults?.data(forKey: key),
let config = try? JSONDecoder().decode(DDayConfig.self, from: data)
else { return nil }
return config
}
}
- ์ฑ์์๋ D-Day ์ค์ ํ๋ฉด์์ save(config:) ํธ์ถ
- ์์ ฏ์์๋ loadConfig()๋ก ์ฝ์ด์ ํ๋ฉด์ ๋ฐ์
3๋จ๊ณ - D-Day ๋ ์ง ๊ณ์ฐ ๋ก์ง
์์ ฏ์ ํต์ฌ ๋๋ฉ์ธ์ ๊ฒฐ๊ตญ '์ค๋๊ณผ ๋ชฉํ์ผ์ ์ผ(day) ๋จ์ ์ฐจ์ด'์ด๋ค.
- ๋ ์ง ์ฐจ์ด ๊ณ์ฐ ๋ก์ง
struct DDayInfo {
let config: DDayConfig
let dayDiff: Int // ์ค๋ ๊ธฐ์ค ๋ ์ง ์ฐจ์ด
}
func calculateDDay(target: Date, now: Date = Date()) -> Int {
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: now)
let startOfTarget = calendar.startOfDay(for: target)
// (์ค๋ → ํ๊ฒ) ๊ธฐ์ค ์ฐจ์ด
let diff = calendar.dateComponents([.day], from: startOfToday, to: startOfTarget).day ?? 0
return diff
}
func formattedDDayString(dayDiff: Int) -> String {
switch dayDiff {
case 0:
return "D-Day"
case let d where d > 0:
return "D-\(d)"
default:
return "D+\(abs(dayDiff))"
}
}
- diff > 0 → ํ๊ฒ์ด ๋ฏธ๋ → D-3
- diff == 0 → ์ค๋์ด ๋ฐ๋ก ๊ทธ ๋ → D-Day
- diff < 0 → ํ๊ฒ์ด ๊ณผ๊ฑฐ → D+2
4๋จ๊ณ - TimelineProvider & Entry ๊ตฌํ
WidgetKit์ ํ์๋ผ์ธ์ ๋ฐ๋ผ ์์ ฏ์ ๊ฐฑ์ ํ๋ค.
์ฌ๊ธฐ์ '์์ ๋ง๋ค ๋ค์ ๊ณ์ฐํด์ ๊ทธ๋ ค๋ฌ๋ผ'๋ ํ์๋ผ์ธ์ ๋๊ฒจ์ฃผ๋ฉด ๋๋ค.
- Entry ์ ์
import WidgetKit
import SwiftUI
struct DDayEntry: TimelineEntry {
let date: Date // ์์ ฏ ์ํ ๊ธฐ์ค ์๊ฐ
let config: DDayConfig? // ์ ์ฅ๋ ์ค์ (์์ ์๋ ์์)
let dayDiff: Int // ๊ณ์ฐ๋ ๋ ์ง ์ฐจ์ด
}
- Provider ๊ตฌํ
struct DDayProvider: TimelineProvider {
func placeholder(in context: Context) -> DDayEntry {
DDayEntry(
date: Date(),
config: DDayConfig(title: "๊ธฐ๋
์ผ", targetDate: Date()),
dayDiff: 0
)
}
func getSnapshot(in context: Context, completion: @escaping (DDayEntry) -> Void) {
let config = SharedStore.loadConfig()
let target = config?.targetDate ?? Date()
let diff = calculateDDay(target: target)
let entry = DDayEntry(date: Date(), config: config, dayDiff: diff)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<DDayEntry>) -> Void) {
let now = Date()
let config = SharedStore.loadConfig()
let target = config?.targetDate ?? now
let diff = calculateDDay(target: target, now: now)
let entry = DDayEntry(date: now, config: config, dayDiff: diff)
// ๋ค์ ์์ ์๊ฐ ๊ณ์ฐ
let calendar = Calendar.current
let nextMidnight = calendar.nextDate(
after: now,
matching: DateComponents(hour: 0, minute: 0, second: 0),
matchingPolicy: .nextTime
)
let timeline: Timeline<DDayEntry>
if let next = nextMidnight {
timeline = Timeline(entries: [entry], policy: .after(next))
}
else {
// ๊ณ์ฐ ์คํจ ์ ์ผ๋จ ๊ณ ์
timeline = Timeline(entries: [entry], policy: .never)
}
completion(timeline)
}
}
์ฌ๊ธฐ์ ํฌ์ธํธ๋
- ์์ ฏ์ด '์ค์๊ฐ'์ผ๋ก ๋์๊ฐ๋ ๊ฒ์ด ์๋๋ผ,
'์ง๊ธ ์ํ๋ฅผ ๋ํ๋ด๋ Entry๋ฅผ ํ๋ ๋๊ฒจ์ฃผ๊ณ , ๋ค์์ ์ธ์ ๋ค์ ๋ถ๋ฌ์ค์ง(timeline policy)๋ฅผ ์๋ ค์ฃผ๋ ๊ตฌ์กฐ'๋ผ๋ ๊ฒ
- D-Day์ ๊ฒฝ์ฐ ๋ ์ง ๋จ์๋ก๋ง ๋ณํ๋ฉด ๋๋ฏ๋ก, ์์ ๋ง๋ค ๋ค์ ํธ์ถ๋๋๋ก ํ์๋ผ์ธ์ ๊ตฌ์ฑํ๋ค๋ ์
5๋จ๊ณ - SwiftUI๋ก ์์ ฏ UI ๊ตฌ์ฑ
์ด์ Entry์ ๋ด๊ธด ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก SwiftUI๋ก UI๋ฅผ ๊ทธ๋ฆฐ๋ค.
struct DDayWidgetView: View {
var entry: DDayProvider.Entry
var body: some View {
ZStack {
LinearGradient(
gradient: Gradient(colors: [.blue.opacity(0.8), .purple]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(alignment: .leading, spacing: 4) {
Text(entry.config?.title ?? "D-Day ๋ฏธ์ค์ ")
.font(.headline)
.foregroundColor(.white)
Text(formattedDDayString(dayDiff: entry.dayDiff))
.font(.system(size: 32, weight: .bold))
.foregroundColor(.white)
if let target = entry.config?.targetDate {
Text(target, style: .date)
.font(.caption)
.foregroundColor(.white.opacity(0.9))
}
}
.padding()
}
}
}
์์ ฏ ์ ์
@main
struct DDayWidget: Widget {
let kind: String = "DDayWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: DDayProvider()) { entry in
DDayWidgetView(entry: entry)
}
.configurationDisplayName("D-Day ์์ ฏ")
.description("์ค์ํ ๋ ์ ํ ํ๋ฉด์์ ๋ฐ๋ก ํ์ธํ์ธ์.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
6๋จ๊ณ - ์ฑ๊ณผ ์์ ฏ์ ์ฐ๋(WidgetCenter)
์ฑ์์ ์ฌ์ฉ์๊ฐ ๊ธฐ๋
์ผ์ ๋ฐ๊ฟจ๋ค๋ฉด, ์์ ฏ๋ ๊ทธ ์ฌ์ค์ ์์์ผ ํ๋ค.
1. ์ฑ์์ ์๋ก์ด D-Day ์ค์ ์ ์ฅ
2. App Group(UserDefaults)์ DDayConfig ์ ์ฅ
3. WidgetCenter๋ฅผ ํตํด ์์ ฏ ํ์๋ผ์ธ ๋ฆฌ๋ก๋ ์์ฒญ
์ฑ์์ ์ฐ๋
import WidgetKit
func updateDDay(title: String, date: Date) {
let config = DDayConfig(title: title, targetDate: date)
SharedStore.save(config: config)
// ์์ ฏ ํ์๋ผ์ธ ์๋ก๊ณ ์นจ ์์ฒญ
WidgetCenter.shared.reloadAllTimelines()
}
์ด๊ฒ ์์ ฏ ์
์ฅ์์๋ ๋ค์ ํ์๋ผ์ธ ์์ฒญ ์์ ์ ์ต์ ์ค์ ์ ์ฝ์ด๊ฐ๊ฒ ๋๋ ๊ตฌ์กฐ๋ค.
7๋จ๊ณ - ์ค์ ๊ธฐ๊ธฐ์์ ์์ ฏ ํ
์คํธ
1. ์ฑ + ์์ ฏ ํ๊น์ด ๋ชจ๋ ํฌํจ๋ ์คํด์ผ๋ก ๋น๋
2. ์ค๊ธฐ๊ธฐ ๋๋ ์๋ฎฌ๋ ์ดํฐ ํ ํ๋ฉด์์:
- ํ ํ๋ฉด ๊ธธ๊ฒ ํฐ์น → '์์ ฏ ์ถ๊ฐ'
- DDayWidget ๊ฒ์ ํ ์ถ๊ฐ
3. ์ฑ์์ D-Day ์ค์ ๋ณ๊ฒฝ → ํ ํ๋ฉด์ ์์ ฏ์ด ์ ์์ ์ผ๋ก ๊ฐฑ์ ๋๋์ง ํ์ธ
์์ ๋ณํ ํ
์คํธ๋ ์๊ฐ์ ๋ฐ๊พธ๊ฑฐ๋, ์์๋ก timeline policy๋ฅผ ์งง๊ฒ ์ค์ ํด์ ๊ฒ์ฆํ๊ณ ,
์ค์ ๋ฐฐํฌ ์ ์๋ ์์ ๊ธฐ์ค์ผ๋ก ๋๋๋ฆฌ๋ ์์ผ๋ก ์งํํ ์ ์๋ค.

์ด๋ฒ ๊ธ์์๋ ์๋ ํ๋ฆ์ผ๋ก iOS ์์ ฏ ๊ฐ๋ฐ์ ์ ๋ฆฌํด๋ดค๋ค.
- ์ ์์ ฏ์ ์ด์ SwiftUI๋ก๋ง ๊ฐ๋ฐ์ด ๋๋๊ฐ?
- WidgetKit ๋์
๋ฐฐ๊ฒฝ๊ณผ Today Extension๊ณผ์ ์ฐจ์ด
- D-Day ์์ ฏ์ ์์ ๋ก ์์ ฏ ๊ตฌ์กฐ ์ค๊ณ
- App Group ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๊ณต์
- ๋ ์ง ๊ณ์ฐ ๋๋ฉ์ธ ๋ก์ง
- TimelineProvider / Entry / SwiftUI View
- ์ฑ๊ณผ ์์ ฏ ์ฐ๋๊น์ง ํฌํจํ end-to-end ๊ฐ๋ฐ ์์
๋ค์์๋
- ์ฌ๋ฌ ๊ฐ์ D-Day๋ฅผ ๊ด๋ฆฌํ๋ ๋ค์ค ์์ ฏ
- Intent ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์๊ฐ ์์ ฏ๋ง๋ค ๋ค๋ฅธ ๊ธฐ๋
์ผ์ ์ง์ ํ๋ ๊ตฌ์กฐ
- ์ ๊ธ ํ๋ฉด ์์ ฏ / StandBy ๋ชจ๋ ๋์
- HealthKit ๊ฑธ์ ์ ์์ ฏ
์ ๊ฐ์ ์ฃผ์ ๋ก ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
'๐' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Swift] iOS WidgetKit์ ๊ฐ๋ฅ ๋ฒ์์ ํ๊ณ: ์ค์๊ฐ์ฑ, ๋ฐ์ดํฐ ๊ฐฑ์ ์ ๋ต (0) | 2025.12.14 |
|---|---|
| [Swift] D-Day ์์ ฏ ๋ง๋ค๊ธฐ (2) (0) | 2025.12.01 |
| [iOS] WKWebView์์์ ์คํฌ๋กคโBounceโSafeArea ๋์ (0) | 2025.11.17 |
| [Swift] Swift Concurrency ์์ ์ ๋ณต (0) | 2025.11.11 |
| iOS ์น๋ทฐ ์๋จ ๋ ธ์น ๋์ํ๊ธฐ (0) | 2025.11.07 |