포스트

WidgetKit (1)

CleanShot 2024-12-03 at 14 31 17

SwiftUI를 공부하기전 파이널프로젝트를 하고 난뒤, 다른 조들의 프로젝트 결과를 보면서 하나 이건 해보고 싶다고 생각했던것이 바로 Widget 사용이었다.

하지만 전제조건은 SwiftUI를 사용해야한다는 것이었다.

그러다보니 그당시엔 UIKit에만 집중을해서 신경을 쓸수가 없었는데 마침 좋은 강의가 있어 그걸 기반으로 정리를 해보려 한다.

Widget관련 Apple 글은 여기서 Interface Guide line 은 여기.

Widget 만들기

프로젝트를 하나 만들고 나서

CleanShot 2024-12-03 at 14 48 17

보통 일반적으로 우리가 View 이런걸 만들때는 File From template을 했는데 여기선 Target으로 한다.

CleanShot 2024-12-03 at 14 48 28

그리고 widget를 만들면 된다.

이름을 정해주고

CleanShot 2024-12-03 at 14 50 42

이렇게 활성화 할거냐고 물으면 활성화를 해주자.

CleanShot 2024-12-03 at 14 52 01

이렇게 새로운 폴더가 생기고

CleanShot 2024-12-03 at 14 51 39

이렇게 위젯이 확인 가능하다.

Widget 기본 구성

우선 여러가지가 있지만

크게 struct로 보게되면

1
2
3
4
5
6
7
8
9
10
11
struct Provider: AppIntentTimelineProvider {
}

struct SimpleEntry: TimelineEntry {
}

struct MonthlyWidgetEntryView : View {
}

struct MonthlyWidget: Widget {
}

provider, entry, view, widget 이렇게 4개로 구성이 된다.

1. provider

위젯에서 표시될 데이터를 제공하고, 타임라인(Timeline)을 생성하는 역할.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Provider: AppIntentTimelineProvider {
    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)
    }
    
    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.
//    }
}

placeholder, snapshot, timeline 이 3개이다.

  1. placeholder
    • 위젯이 데이터를 로드하기 전에 미리보기 상태(Placeholder)를 표시하기 위해 호출된다.
    • DummyData를 통해 보여줄 수 있다.
  2. snapshot
    • Snapshot은 위젯의 현재 상태를 나타낸다. 데이터를 직접 Fetch하여 Snapshot을 생성한다.
  3. timeline
    • 위젯의 타임라인(Timeline)을 생성한다. 즉, 시간이 지남에 따라 표시될 데이터를 정의한다.
    • entry는 기본적으로 데이터를 말한다.
    • Timeline은 위젯이 특정 시간에 업데이트될 데이터를 제공한다

image

Wiget의 간단한 Sequence

이미지 출처 : Docs

2. entry

타임라인 내에서 위젯이 특정 시점에 표시할 데이터를 캡슐화하는 프로토콜

흔히 우리가 아는 데이터 모델링

1
2
3
4
struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}

3. View

SwiftUI 기반으로 위젯의 UI를 그리는 역할을 담당.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MonthlyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)

            Text("Favorite Emoji:")
            Text(entry.configuration.favoriteEmoji)
        }
    }
}

4. Widget

위젯을 앱에 등록하고, 위젯의 구조를 설정하는 역할.

1
2
3
4
5
6
7
8
9
10
struct MonthlyWidget: Widget {
    let kind: String = "MonthlyWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            MonthlyWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}

View 디자인

1
2
3
4
5
6
7
8
9
10
11
12
13
    var body: some View {
        ZStack {
            ContainerRelativeShape()
                .fill(.gray.gradient)
            VStack {
                HStack {
                    Text("☃️")
                    Text(entry.date.weekDayDisplayFormat)
                }
            }
        }
        
    }

이렇게 디자인을 하던 도중 강의에선 배경이 꽉차지만 현재 버전에서는 안되는 이슈가 발생

CleanShot 2024-12-03 at 16 50 04

이렇게 주변이 흰색이아니라 전체가 전부 회색으로 되어야함.

이런 이슈에 대해 참고글을 보고 해결하자

하지만 해보다가 안되어서 혹시나 해서 저부분에 유일하게 containerBackground가 있어서 저기서 변경하니 적용이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MonthlyWidget: Widget {
    let kind: String = "MonthlyWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            MonthlyWidgetEntryView(entry: entry)
                .containerBackground(.gray.gradient, for: .widget) // 여기
        }
        .configurationDisplayName("Monthly Style Widget")
        .description("The theme of the widget changes based on month.")
        .supportedFamilies([.systemSmall])
        .containerBackgroundRemovable(false)
    }
}

여태 View에서 안됐던 이유가 바로 여기서 색상을 먹고있어서 안되었던것

.containerBackground(.fill.tertiary, for: .widget)

그리고 해당 모디파이어에 대해 여러 위치에서 테스트를 해본결과.

같은 UIcomponent라면 먼저 작성된 background color가 적용 되고

view단위라면 가장 최상위 view가 있는곳의 color가 적용된다.

ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var body: some View {
    ZStack {
        VStack {
            HStack {
                Text("☃️")
                    .font(.title)
                Text(entry.date.weekDayDisplayFormat)
                    .font(.title3)
                    .fontWeight(.bold)
                    .minimumScaleFactor(0.6)
                    .foregroundStyle(.black.opacity(0.6))
                Spacer()
            }
            Text(entry.date.dayDisplayFormat)
                .font(.system(size: 80, weight: .heavy))
                .foregroundStyle(.white.opacity(0.8))
        }
        .containerBackground(.red.gradient, for: .widget)
        .padding(2)
    }
    .containerBackground(.gray.gradient, for: .widget)
}

Zstack이 더 상위므로 해당 색상이 반영.

ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        ZStack {
            VStack {
                HStack {
                    Text("☃️")
                        .font(.title)
                    Text(entry.date.weekDayDisplayFormat)
                        .font(.title3)
                        .fontWeight(.bold)
                        .minimumScaleFactor(0.6)
                        .foregroundStyle(.black.opacity(0.6))
                        .containerBackground(.gray.gradient, for: .widget)
                    Spacer()
                }
                .containerBackground(.red.gradient, for: .widget)
                Text(entry.date.dayDisplayFormat)
                    .font(.system(size: 80, weight: .heavy))
                    .foregroundStyle(.white.opacity(0.8))
            }
            .padding(2)
    }

Text 보다 Hstack이 더 상위므로 Hstack color 반영

ex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ZStack {
    VStack {
        HStack {
            Text("☃️")
                .font(.title)
                .containerBackground(.gray.gradient, for: .widget)
            Text(entry.date.weekDayDisplayFormat)
                .font(.title3)
                .fontWeight(.bold)
                .minimumScaleFactor(0.6)
                .foregroundStyle(.black.opacity(0.6))
                .containerBackground(.red.gradient, for: .widget)
            Spacer()
        }
        Text(entry.date.dayDisplayFormat)
            .font(.system(size: 80, weight: .heavy))
            .foregroundStyle(.white.opacity(0.8))
    }
    .padding(2)
}

첫번째 text가 있는곳이 더 먼저 적용.

Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<DayEntry> {
        var entries: [DayEntry] = []

        // 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 = DayEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }

여기만 수정을 할 예정

현재는 매 5시간 마다 업데이트를 하는데

우리가 사용할 위젯은 날짜, 요일을 보여주기에 매일매일 업데이트를 해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<DayEntry> {
        var entries: [DayEntry] = []

        // Generate a timeline consisting of seven entries an hour apart, starting from the current date.
        let currentDate = Date()
        for dayOffset in 0 ..< 7 {
            let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate)!
            let startOfDate = Calendar.current.startOfDay(for: entryDate)
            let entry = DayEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }

(byAdding: .day여기를 매일매일 하도록 수정.

그리고 시작하는 날짜를 정해준다.

왜냐면 위젯을 추가한 현시점부터 24시간 뒤에 업데이트를 하기 때문 그렇기에 startOfDate를 사용하면 매일 자정(하루의 시작 시점) 기준으로 업데이트되도록 조정할 수 있다.

simulator_screenshot_DDE19A50-1395-4839-BCF5-28D458176C2C

앱을 실행하면 이렇게 위젯을 추가할수있다.

Widget 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MonthlyWidget: Widget {
    let kind: String = "MonthlyWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            MonthlyWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Monthly Style Widget")
        .description("The theme of the widget changes based on month.")
        .supportedFamilies([.systemSmall])
    }
}

supportedFamiles를 통해 어떤디자인만 가능하게할지 설정이 가능.

simulator_screenshot_3AC3BCCF-7AF2-424C-A8B7-012239EB2047

아까와 달리 이젠 하나만 설정이 가능해진다.

이렇게 어떤 크기의 위젯을 할지도 직접 설정이 가능하다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.