포스트

WidgetKit (11)

Widget 적용

기본적인 UI도 진행이 되었으니 이젠 Widget 설정을 해본다.

1. Entry

1
2
3
4
struct CalendarEntry: TimelineEntry {
    let date: Date
    let days: [Day]
}

CleanShot 2024-12-10 at 01 59 25

Entry의 이름을 바꾸고 날짜를 담을 days를 만들었는데 에러가 뜬다.

왜냐 Day의 경우 CoreData의 Entity class이기 때문.

해당 class를 사용하기위해선 CoreData파일에서 Target을 체크해야한다.

CleanShot 2024-12-10 at 02 02 32

역시나 없다.

추가해주자.

이젠 적용이 된다. 이때 Entry에 새로운 field가 추가 되었으므로 Missing Error가 발생하지만 지금은 빈배열로 넣어주자.

1
2
// example
CalendarEntry(date: Date(), days: [])

FetchRequest 구현

Provider에서 CalenderView처럼 Environment를 사용해서 context를 사용하고, fetchRequest Wrapper를 사용하면 좋겠지만 그럴수가 없기에

Manual로 진행한다.

1
2
3
4
5
6
7
8
9
10
let viewContext = PersistenceController.shared.container.viewContext

var dayFetchReqeust: NSFetchRequest<Day> {
    let request = Day.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Day.date, ascending: true)]
    request.predicate = NSPredicate(format: "(date >= %@) AND (date <= %@)",
                                Date().startOfCalendarWithPrefixDays as CVarArg,
                                Date().endOfMonth as CVarArg)
    return request
}

이렇게 만들어 준다.

placeholder는 미리보기지만, getSnapshot은 Timeline을 제공하는 녀석이기에 여기서 fetch를 한다.

1
2
3
4
5
6
7
8
9
func getSnapshot(in context: Context, completion: @escaping (CalendarEntry) -> ()) {
    do {
        let days = try viewContext.fetch(dayFetchReqeust)
        let entry = CalendarEntry(date: Date(), days: days)
        completion(entry)
    } catch {
        print("Widget failed to fetch days in snapshot")
    }
}

getTimeline도 비슷하게 해주면 된다.

1
2
3
4
5
6
7
8
9
10
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    do {
        let days = try viewContext.fetch(dayFetchReqeust)
        let entry = CalendarEntry(date: Date(), days: days)
        let timeline = Timeline(entries: [entry], policy: .after(.now.endOfDay))
        completion(timeline)
    } catch {
        print("Widget failed to fetch days in snapshot")
    }
}

이때 하루가 끝날때 즉 00시 이후에 업뎃을 하도록 설정을 해준다.

App에서 Toggle시 Widget과 연동

우선 widgetkit을 import 해주고

CleanShot 2024-12-10 at 03 18 53

WidgetCenter를 사용하여 Timeline을 Reload하는데

여기서 ofKind에 들어가는 값은?

바로

1
2
struct SwiftCalWidget: Widget {
    let kind: String = "SwiftCalWidget"

우리가 Widget을 만들면 생기는 저녀석을 말한다.

reloadTimelines(ofKind:) Docs참고

그리고 기존에 만들어둔 CalendarView의 calculateStreakValue 함수를 widget에 가져와서

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
28
29
30
31
32
33
34
35
36
HStack {
    VStack {
        Text("\(calculateStreakValue())")
            .font(.system(size: 70, design: .rounded))
            .bold()
            .foregroundStyle(.orange)
        
        Text("day streak")
            .font(.caption)
            .foregroundStyle(.orange)
    }
    
    VStack {
        CalendarHeaderView(font: .caption)
        LazyVGrid(columns: columns, spacing: 7) {
            ForEach(entry.days) { day in
                if day.date!.monthInt != Date().monthInt {
                    Text(" ")
                } else {
                    Text(day.date!.formatted(.dateTime.day()))
                        .font(.caption2)
                        .bold()
                        .frame(maxWidth: .infinity)
                        .foregroundStyle(.secondary)
                        .background {
                            Circle()
                                .foregroundStyle(.orange.opacity(day.didStudy ? 0.3 : 0.0))
                                .scaleEffect(1.5)
                        }
                }
            }
        }
    }
    .padding(.leading, 6)
}
.padding()

여기에 적용 해주고

그리고 calendar의 if문을 가져와서 그대로 적용을 해주면 된다.

이제 실행을 해서 테스트를 해야하는데

CleanShot 2024-12-10 at 03 29 20

앱이 없다.

이때는 아래의 New Scheme을 통해 새롭게 만들어 주면 된다.

이후 사진은 패스.

실행해보면 두개가 같이 연동되어있음을 확인할 수 있다.

simulator_screenshot_3565B130-F70A-4728-95EE-F2B263B023D8simulator_screenshot_62284713-1E93-4A2A-ABBB-6785C380AF9A

그리고 앱에서 값을 변경해주어도?

Dec-10-2024 03-43-00

위젯에서도 업뎃이 된걸 확인할 수 있다.

Widget에서 각 View로의 링크 설정

목적은 현재의 SwiftCalWidgetEntryView에서 streak을 담당하는 부분과 calendar를 담당하는 부분을 각각 클릭시 관련된 화면으로 전환을 하려고 함이다.

Link를 사용해서 감싸준다.

1
2
3
4
5
6
7
8
9
10
11
HStack {
    Link(destination: URL(string: "streak")!) {
        VStack { }
    }
    
    Link(destination: URL(string: "calendar")!) {
        VStack { }
    }
    .padding(.leading, 6)
}
.padding()

앱으로 가서 tag를 적용한다.

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
28
29
30
31
32
33
34
35
// deprecated
var body: some Scene {
    WindowGroup {
        TabView(selection: $selectedTab) {
            CalendarView()
                .tabItem { Label("Calendar", systemImage: "calendar") }
                .tag(0)
            StreakView()
                .tabItem { Label("Streak", systemImage: "swift") }
                .tag(1)
        }
        .environment(\.managedObjectContext, persistenceController.container.viewContext)
        .onOpenURL { url in
            selectedTab = url.absoluteString == "calendar" ? 0 : 1
        }
    }
}

// recommended way
var body: some Scene {
    WindowGroup {
        TabView(selection: $selectedTab) {
            Tab("Calendar", systemImage: "calendar", value: 0) {
                CalendarView()
            }
            Tab("Streak", systemImage: "swift", value: 1) {
                StreakView()
            }
        }
        .environment(\.managedObjectContext, persistenceController.container.viewContext)
        .onOpenURL { url in
            selectedTab = url.absoluteString == "calendar" ? 0 : 1
        }
    }
}

위의 코드는 deprecated 될 코드인데 tag를 사용했고, 아래는 앞으로 쓰일 방식인데 tag에서 value로 바뀌었다.

그리고 onOpenURL을 통해 위젯에서 link의 destination을 String Value로 해주었고 그걸 여기서 삼항연산자를 통해 Calendar라면 0으로 아니면 1로 가게 하였다.

실행하면

Dec-10-2024 03-57-28

적용이 잘 되었다.

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