오늘의 목표
Lovechive 프로젝트의 위젯을 추가해보자.
iOS 위젯은 앱을 실행하지 않고도 홈 화면이나 잠근 화면에서 중요한 정보를 빠르게 확인할 수 있는 작은 UI 요소다.
예를 들어, 날씨 위젯, 캘린더 일정 위젯, 배터리 상태 위젯 등이 있다.
TimelineProvider 사용)iOS 위젯은 SwiftUI를 기반으로 개발되며, WidgetKit 프레임워크를 사용해 데이터를 관리한다.
여러 제약사항이 많지만, 홈 화면과 잠금 화면에서 유용하게 활용할 수 있다.
Provider란 위젯을 구현할 때 중요한 요소 중 하나로, 위젯의 컨텐츠를 업데이트할 날짜와 시간을 지정하고 위젯을 렌더링하는 데 필요한 데이터를 포함하고 있는 객체이다.
주요 메소드는 아래와 같다.
Date()로 현재 시간을 넣고, 기본 설정값을 적용한다. → 필요에 따라 커스텀// 기본값
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}
// 기본값
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)
}
.atEnd: 마지막 엔트리 이후에 새로운 타임라인을 요청.after(Date): 특정 시간이 지나면 다시 갱신.never: 갱신 없이 한 번만 설정// 기본값
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
return Timeline(entries: entries, policy: .atEnd)
}
// 기본값 → 주석
// func relevances() async -> WidgetRelevances<ConfigurationAppIntent> {
// // Generate a list containing the contexts this widget is relevant in.
// }
위의 메소드들을 보면 SimpleEntry라는 객체를 자주 확인할 수 있는데, 이는 위젯이 표시할 데이터를 저장하는 모델이다.
즉, 위젯이 화면에 나타날 때 필요한 정보(날짜, 텍스트 등)를 담는 데이터 구조체이다.
모든 위젯 엔트리는 TimelineEntry 프로토콜을 준수해야만 한다.
SimpleEntry는 위젯이 표시할 데이터를 담는 모델 객체TimelineEntry 프로토콜을 채택하며, date 속성을 포함해야 함 → date = 업데이트 주기getTimeline()에서 여러 개의 SimpleEntry를 생성해 위젯을 업데이트할 수 있음ConfigurationAppIntent는 iOS 17부터 도입된 위젯 설정을 위한 구조체이다.
사용자가 위젯을 추가할 때 설정할 옵션을 정의하는 역할을 한다.
기존 IntentConfiguration 기반의 SiriKit Intents 방식보다 더 SwiftUI 친화적인 설정 방식이다.
import AppIntents
struct ConfigurationAppIntent: AppIntent {
static var title: LocalizedStringResource = "위젯 설정"
@Parameter(title: "테마 선택")
var theme: WidgetTheme
init() {
self.theme = .light
}
}
AppIntent 프로토콜을 준수하는 구조체title: 위젯 설정 화면에서 표시할 제목@Parameter: 사용자가 선택할 수 있는 옵션| AppIntent(iOS 17~) | IntentConfiguration | |
|---|---|---|
| 사용 방식 | SwiftUI 친화적 | 기존 SiriKit 기반 |
| 성능 | 상대적으로 더 빠름 | 상대적으로 무거움 |
| 코드 가독성 | 간결하고 직관적 | 복잡한 구조 |
| 확장성 | 다양한 설정 추가 가능 | 제한적 |
ConfigurationAppIntent는 사용자가 설정할 옵션을 정의하는 역할@Parameter를 사용해 설정값을 정의Provider에서 사용자의 설정을 받아 SimpleEntry로 전달즉,ConfigurationAppIntent는 위젯의 "설정 화면"을 담당하는 구조체이다.
위젯에 대해 공부를 했으니, 현재 프로젝트에 위젯을 추가해보려고 한다.
Lovechive는 위젯을 통해 D-Day를 표현하려고 한다. 이를 위해 먼저 해줘야 할 작업이 있는데, 바로 AppGroup으로 Lovechive 앱과 위젯이 공용으로 사용할 저장소를 정의하는 것이다.
먼저 알아야할 것은 App과 AppExtension(Today Extension, Widget Extension 등)은 UserDefaults를 공유하지 않는다.
(코어 데이터도 공유하지 않는다.)
그 이유는 AppExtension과 App의 관계를 보면 알 수 있는데, Extension은 App 안에 있지만, 둘은 각각 다른 Container를 가지고 있기 때문에 서로 데이터를 공유하지 않는다.

[출처: App Extension Programming Guide]
때문에 앱에서 설정한 데이터(Lovechive의 경우 D-Day)를 위젯에서 사용하기 위해서는 App과 AppExtension을 연결하고, 함께 사용할 수 있는 공유 저장소를 구축해야 한다.
이번에는 UserDefaults를 사용해서 이를 구현해보려고 한다.
AppGroup을 설정하기 위해서 우선 위젯을 프로젝트에 추가해준다.


위젯을 추가한 후에는 본격적으로 Capability를 설정해줄 차례인데, 먼저 App에서 설정을 해보겠다.
Capability를 설정하기 위해 target으로 이동한 뒤 Capability를 선택해준다.

Capability를 클릭하면 여러 항목이 표시될텐데, 이 중 "AppGroup"을 선택하면 된다.

AppGroup을 추가하면 Target에 새로운 칸이 생성되며 아래 이미지와 같은 항목이 추가될텐데, 여기서 +를 눌러 새로운 앱 그룹을 형성해 주면 된다.

앱 그룹을 형성할 때 Bundle Identifier를 설정해야 하는데, 나중에 UserDefaults의 suitName을 이 이름과 동일하게 설정해야 하기 때문에 앱과 연관되도록 작성하면 좋다.


그리고 이 과정을 위젯에서도 동일하게 진행하면 된다.
위젯은 앱 그룹을 추가하지 않아도 이전에 추가한 앱 그룹이 있을 것이기 때문에 체크박스만 설정해주면 된다.
이제 AppGroup을 만들었으니 여기에 연결할 UserDefaults 객체를 만들면 된다.
기본적으로 UserDefaults는 standard라는 싱글톤 객체를 지원하지만, 이 객체는 각각의 container에 저장되는 요소이기 때문에 새로운 객체를 생성해줘야 한다.
이를 위해 UserDefaults를 Extension해서 새로운 객체를 만들어주기로 했다.
extension UserDefaults {
static var shared: UserDefaults {
let groupId: String = "group.lovechive"
return UserDefaults(suiteName: groupId)!
}
}
UserDefaults(suiteName:)은 앱 그룹에서 공유 데이터를 저장하는 객체를 생성하는 방법이다.
즉, 같은 앱 그룹에 속한 여러 앱이 데이터를 공유할 때 사용하는 초기화 방법이다.
| UserDefaults.standard | UserDefaults(suitedName:) | |
|---|---|---|
| 저장 위치 | 현재 앱 전용 | 앱 그룹 공유 |
| 사용 가능 범위 | 앱 내부에서만 사용 가능 | 같은 앱 그룹 내 모든 앱에서 접근 가능 |
| 사용 예시 | 단일 앱의 설정 저장 | 본 앱과 위젯 간 데이터 공유 |
이렇게 새로 만든 객체는 아래와 같이 기존 방식대로 사용할 수 있다.
UserDefaults.shared.set("test", forKey: "test")
위젯을 추가하면 아래와 같은 새로운 폴더가 형성되는 것을 볼 수 있다.

Lovechive는 설정을 만들 필요도 없고, 별다른 옵션을 설정할 것 없이 D-Day만 표시하면 되기 때문에 UI만 구현하기로 했다.
이를 위해서 Widget 파일로 이동해서, View 구조체를 수정한다.
// 위젯 UI 설정
struct LovechiveWidgetEntryView : View {
var entry: Provider.Entry
var dDay: Int = UserDefaults.shared.integer(forKey: "dDay")
var body: some View {
ZStack {
VStack(alignment: .center, spacing: 0) {
Image("heartIcon")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
Image("Lovechive")
.resizable()
.scaledToFit()
.frame(width: 100, height: 30)
}
Text("\(dDay)일")
.font(.myoyaFont(32))
.foregroundStyle(Color.white)
.padding(.bottom, 35)
}
}
}
이렇게 설정하면 이제 위젯 UI가 설정되어 시뮬레이터에서 확인할 수 있다.

원래 연애 기념일 등을 설정하는 기능도 추가하고 싶었지만, Firestore와 연동도 필요하고, 아직 위젯에 대한 이해도와 숙련도가 부족한 탓에 구현하지 못했다.
오늘은 Lovechive 앱에 위젯을 추가하고, 그 과정에서 위젯에 대해 학습을 진행했다.
SwiftUI는 오랜만이라 조금 해맸는데... 사실 SwiftUI보다 Provider라던가 다른 위젯을 설정하는 객체들에 대해 공부하는 것이 더 힘들었다.
위젯이 제공하는 정보나 설정, 업데이트 주기가 생각보다 많은 것을 지원하지 않았기 때문에 약간 아쉬움도 느꼈던 것 같다.
마지막으로 lovechive 프로젝트 글을 작성한게 벌써 3주 전이더라...
뭔가를 쓰고 싶어도 트러블슈팅을 적을 만한 일이 없고, 대부분 UI 구현에 시간을 써서 그런지
블로그에 작성할 만한 내용이 없었다.
그나마 Firebase 로직에 대한 내용은 적을만 하기로 해서... 고려를 해봐야겠다.