Project / Widget 추가하기

Lovechive 프로젝트

목록 보기
5/5

오늘의 목표

Lovechive 프로젝트의 위젯을 추가해보자.

1. Widget이란?

iOS 위젯은 앱을 실행하지 않고도 홈 화면이나 잠근 화면에서 중요한 정보를 빠르게 확인할 수 있는 작은 UI 요소다.
예를 들어, 날씨 위젯, 캘린더 일정 위젯, 배터리 상태 위젯 등이 있다.

✅ 위젯의 주요 특징

  • 앱을 실행하지 않고도 정보 제공
  • 홈 화면 & 잠금 화면에서 배치 가능(iOS 16부터 잠금 화면 위젯 지원)
  • 여러 크기 지원 → (systemSmall, systemMedium, systemLarge 등)
  • SwiftUI 기반으로 개발
  • 데이터 업데이트가 제한적 → 실시간 업데이트 불가, 일정 주기로 갱신됨

📌 위젯의 종류

  • 홈 화면 위젯: 홈 화면에서 사용되는 기본적인 위젯
  • 잠금 화면 위젯: iOS 16부터 지원, 잠금 화면에서 사용
  • 스마트 스택: 여러 개의 위젯을 자동으로 변경하며 표시

🚨 위젯 개발 시 주의할 점

  • 실시간 업데이트 불가 → 일정 시간마다 업데이트 가능(TimelineProvider 사용)
  • 버튼 & 인터랙션 제한 → 사용자가 탭하여 앱을 실행하는 정도의 기능만 가능
  • 백그라운드 실행 제한 → 앱과 다르게 실행이 제한적이므로 많은 작업을 할 수 없음

iOS 위젯은 SwiftUI를 기반으로 개발되며, WidgetKit 프레임워크를 사용해 데이터를 관리한다.
여러 제약사항이 많지만, 홈 화면과 잠금 화면에서 유용하게 활용할 수 있다.


2. Provider

Provider란 위젯을 구현할 때 중요한 요소 중 하나로, 위젯의 컨텐츠를 업데이트할 날짜와 시간을 지정하고 위젯을 렌더링하는 데 필요한 데이터를 포함하고 있는 객체이다.

주요 메소드는 아래와 같다.

✨ Provider에서 설정할 수 있는 것들

1) placeholder

  • 위젯의 미리보기 상태를 설정할 수 있다.
  • 사용자가 위젯을 추가하기 전에 기본적으로 보이는 UI 상태를 제공한다.
  • 보통은 Date()로 현재 시간을 넣고, 기본 설정값을 적용한다. → 필요에 따라 커스텀
// 기본값

func placeholder(in context: Context) -> SimpleEntry {
	SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}

2) snapshot

  • 위젯 갤러리에서 스냅샷을 생성할 때 사용된다.
  • 보통 timeline()과 유사하지만, 빠르게 표시할 수 있는 기본 데이터를 제공한다는 특징을 가진다.
  • 앱에서 즉시 보여줄 수 있는 데이터를 반환하는 역할이다.
// 기본값

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
	SimpleEntry(date: Date(), configuration: configuration)
}

3) timeline

  • 위젯이 주기적으로 업데이트할 데이터를 제공한다.
  • entries 배열을 생성해서, 시간대별로 다른 데이터를 보여줄 수 있다.
  • policy 설정을 통해 언제 데이터를 갱신할지 결정 가능:
    • .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)
}

4) relevances

  • 위젯의 중요도를 설정할 수 있다.
  • iOS는 중요도가 높은 위젯을 더 자주 업데이트하거나, 우선적으로 표시할 수 있다.
  • 사용자가 앱을 사용할 때 어떤 컨텍스트에서 위젯을 강조할지 설정 가능하다.
  • 기본적으로 주석 처리가 되어있다.
// 기본값 → 주석

//    func relevances() async -> WidgetRelevances<ConfigurationAppIntent> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }

5) SimpleEntry?

위의 메소드들을 보면 SimpleEntry라는 객체를 자주 확인할 수 있는데, 이는 위젯이 표시할 데이터를 저장하는 모델이다.
즉, 위젯이 화면에 나타날 때 필요한 정보(날짜, 텍스트 등)를 담는 데이터 구조체이다.
모든 위젯 엔트리는 TimelineEntry 프로토콜을 준수해야만 한다.

  • SimpleEntry는 위젯이 표시할 데이터를 담는 모델 객체
  • TimelineEntry 프로토콜을 채택하며, date 속성을 포함해야 함 → date = 업데이트 주기
  • getTimeline()에서 여러 개의 SimpleEntry를 생성해 위젯을 업데이트할 수 있음

3. ConfigurationAppIntent

ConfigurationAppIntent는 iOS 17부터 도입된 위젯 설정을 위한 구조체이다.
사용자가 위젯을 추가할 때 설정할 옵션을 정의하는 역할을 한다.

기존 IntentConfiguration 기반의 SiriKit Intents 방식보다 더 SwiftUI 친화적인 설정 방식이다.

✨ ConfigurationAppIntent의 역할

  • 위젯의 사용자 설정 옵션을 정의
  • 위젯을 추가할 때 선택할 수 있는 값 제공
  • 사용자가 설정한 값을 위젯 프로바이더(Provider)로 전달

📝 ConfigurationAppIntent 예제 코드

import AppIntents

struct ConfigurationAppIntent: AppIntent {
    static var title: LocalizedStringResource = "위젯 설정"

    @Parameter(title: "테마 선택")
    var theme: WidgetTheme
    
    init() {
        self.theme = .light
    }
}
  • AppIntent 프로토콜을 준수하는 구조체
  • title: 위젯 설정 화면에서 표시할 제목
  • @Parameter: 사용자가 선택할 수 있는 옵션

🔥 AppIntent VS IntentConfiguration

AppIntent(iOS 17~)IntentConfiguration
사용 방식SwiftUI 친화적기존 SiriKit 기반
성능상대적으로 더 빠름상대적으로 무거움
코드 가독성간결하고 직관적복잡한 구조
확장성다양한 설정 추가 가능제한적

✅ 정리

  • ConfigurationAppIntent는 사용자가 설정할 옵션을 정의하는 역할
  • 위젯이 사용자 설정을 받을 수 있도록 도와줌
  • @Parameter를 사용해 설정값을 정의
  • Provider에서 사용자의 설정을 받아 SimpleEntry로 전달
  • 위젯 UI에서 사용자의 설정값을 반영 가능

즉,ConfigurationAppIntent는 위젯의 "설정 화면"을 담당하는 구조체이다.


4. Lovechive에 위젯 추가하기

위젯에 대해 공부를 했으니, 현재 프로젝트에 위젯을 추가해보려고 한다.
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를 사용해서 이를 구현해보려고 한다.

1) Capability 설정하기

AppGroup을 설정하기 위해서 우선 위젯을 프로젝트에 추가해준다.

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

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

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

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

그리고 이 과정을 위젯에서도 동일하게 진행하면 된다.
위젯은 앱 그룹을 추가하지 않아도 이전에 추가한 앱 그룹이 있을 것이기 때문에 체크박스만 설정해주면 된다.

2) UserDefaults 설정하기

이제 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와의 차이점

UserDefaults.standardUserDefaults(suitedName:)
저장 위치현재 앱 전용앱 그룹 공유
사용 가능 범위앱 내부에서만 사용 가능같은 앱 그룹 내 모든 앱에서 접근 가능
사용 예시단일 앱의 설정 저장본 앱과 위젯 간 데이터 공유

이렇게 새로 만든 객체는 아래와 같이 기존 방식대로 사용할 수 있다.

UserDefaults.shared.set("test", forKey: "test")

3) Widget 설정

위젯을 추가하면 아래와 같은 새로운 폴더가 형성되는 것을 볼 수 있다.

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와 연동도 필요하고, 아직 위젯에 대한 이해도와 숙련도가 부족한 탓에 구현하지 못했다.


5. 결론

오늘은 Lovechive 앱에 위젯을 추가하고, 그 과정에서 위젯에 대해 학습을 진행했다.
SwiftUI는 오랜만이라 조금 해맸는데... 사실 SwiftUI보다 Provider라던가 다른 위젯을 설정하는 객체들에 대해 공부하는 것이 더 힘들었다.
위젯이 제공하는 정보나 설정, 업데이트 주기가 생각보다 많은 것을 지원하지 않았기 때문에 약간 아쉬움도 느꼈던 것 같다.


마지막으로 lovechive 프로젝트 글을 작성한게 벌써 3주 전이더라...
뭔가를 쓰고 싶어도 트러블슈팅을 적을 만한 일이 없고, 대부분 UI 구현에 시간을 써서 그런지
블로그에 작성할 만한 내용이 없었다.
그나마 Firebase 로직에 대한 내용은 적을만 하기로 해서... 고려를 해봐야겠다.
profile
이유있는 코드를 쓰자!!

0개의 댓글