WidgetKit (7)
이전에는 git address를 enum을 통해 정해진 주소만 사용해서 적용을 했다면 이제는 주소를 추가하여 원하는 Repository를 확인 하도록 만들어 보려고 한다.
ContentView UI Design
Repo Widget의 APP의 ContentView를 디자인 해준다.
이부분은 생략
User Default 추가 및 적용
여기서 App Groups를 추가해준다.
App Group 이란? App Extension Programming Guide도 참고.
App Group은 같은 개발자 계정에 속한 앱들의 데이터 공유를 가능하게 하는 iOS의 기능이다.
- 앱과 위젯은 서로 분리된 프로세스로 실행되므로, 기본적으로 동일한 UserDefaults나 파일 시스템을 공유하지 않는다.
- App Group을 설정하면 공유된 컨테이너를 통해 데이터를 교환할 수 있다.
- 이 컨테이너는 UserDefaults, FileManager 등을 공유할 수 있도록 제공된다.
그리고 Container를 추가를 하면 이렇게 창이뜨는데, 보통은 Bundle Identifier의 형식으로 작성.
이후 새롭게 파일을 만들고 extension을 작성해준다.
1
2
3
4
5
6
extension UserDefaults {
static var shared: UserDefaults {
UserDefaults(suiteName: "group.co.harold.RepoWatcher")!
}
static let repoKey = "repos"
}
이때 suiteName을 위에 적었던것과 일치하게 작성해주자.
그리고 버튼을 눌렀을때 배열과 유저 디폴트에 담을 기능을 구현한다
1
2
3
4
5
6
7
8
9
Button {
if !repos.contains(newRepo) && !newRepo.isEmpty {
repos.append(newRepo)
UserDefaults.shared.set(repos, forKey: UserDefaults.repoKey)
newRepo = ""
} else {
print("repo already exists or name is empty")
}
}
그리고 onAppear에도 적용
1
2
3
4
5
6
7
8
9
10
.onAppear {
guard let retrievedRepos = UserDefaults.shared.value(forKey: UserDefaults.repoKey) as? [String] else {
let defaultValues = ["sallen0400/swift-news"]
UserDefaults.standard.set(defaultValues, forKey: UserDefaults.repoKey)
repos = defaultValues
return
}
repos = retrievedRepos
}
여기선 유저 디폴트에 아무것도 없을 경우 defaultValues를 통해 초기값을 하나 넣어주는 방식으로 적용을 했다.
실행하면 위와 같다.
삭제기능도 추가 하자.
1
2
3
4
5
6
7
8
9
.swipeActions {
Button("Delete") {
if repo.count > 1 {
repos.removeAll { $0 == repo }
UserDefaults.shared.set(repos, forKey: UserDefaults.repoKey)
}
}
.tint(.red)
}
삭제도 아주 잘된다.
Configuration 사용 전 준비
문제점
WidgetKit에서는 위젯의 크기나 상태에 따라 단일 Configuration만 허용되며, 복잡한 상태 관리를 위해 별도의 위젯으로 분리하는 방식이 필요하다.
즉 한개의 위젯에는 하나의 Configuration만 존재해야한다.
현재 CompactRepoWidget은
이렇게 2개의 사이즈가 존재한다. 그리고 문제점은 하나의 위젯인데 repository는 다르게 해야하기에 즉 2개의 configuration이 필요해진다.
왜냐 small 사이즈일때는 repo가 1개 medium 사이즈일때는 repo가 2개 이기 때문.
그래서 다음과 같이 Configuration을 적용하려면
Widget을 고쳐야한다.
ContributorWidget → SingleRepoWidget
ContributorWidget의 경우 레포지토리가 하나만 필요하다.
먼저, Contributor Widget에 관한 이름을 전부 SingleRepo로 바꿔준다.
그리고
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct SingleRepoEntryView : View {
@Environment(\.widgetFamily) var family
var entry: SingleRepoEntry
var body: some View {
switch family {
case .systemMedium:
RepoMediumView(repo: entry.repo)
case .systemLarge:
VStack {
RepoMediumView(repo: entry.repo)
Spacer().frame(height: 40)
ContributorMediumView(repo: entry.repo)
}
.containerBackground(for: .widget) {}
case .systemSmall, .systemExtraLarge, .accessoryCircular, .accessoryRectangular, .accessoryInline:
EmptyView()
@unknown default:
EmptyView()
}
}
}
이렇게 위젯 사이즈에 맞게 뷰를 다르게 적용하게 한다.
그리고 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
28
29
30
31
32
33
34
35
36
37
38
func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline<SingleRepoEntry>) -> Void) {
Task {
let nextUpdate = Date().addingTimeInterval(43200)
do {
//Get Repo
let repoToShow = RepoURL.tpk
var repo = try await NetworkManager.shared.getRepo(atUrl: repoToShow)
let avatarImageData = await NetworkManager.shared.downloadImageData(from: repo.owner.avatarUrl)
repo.avatarData = avatarImageData ?? Data()
if context.family == .systemLarge {
//Get Contributors
let contributors = try await NetworkManager.shared.getContributors(atUrl: repoToShow + "/contributors")
// Filter to just the top 4
var topFour = Array(contributors.prefix(4))
// Download top four avatars
for i in topFour.indices {
let avatarData = await NetworkManager.shared.downloadImageData(from: topFour[i].avatarUrl)
topFour[i].avataData = avatarData ?? Data()
}
repo.contributors = topFour
}
// Create Entry & Timeline
let entry = SingleRepoEntry(date: .now, repo: repo)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) // update every 12hours
completion(timeline)
} catch {
print("❌ Error - \(error.localizedDescription)")
}
}
}
CompactRepoWidget → DoubleRepoWidget
그리고 CompactRepoWidget의 경우는 small사이즈를 아예 없애버리고, medium 사이즈만 두고, 레포지토리를 2개를 받게 한다.
우선, CompactRepo는 Double로 바꿔주고
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
37
38
39
40
41
42
43
44
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task {
let nextUpdate = Date().addingTimeInterval(43200) // 12 hours in seconds
do {
// Get Top Repo
var repo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.google)
let topAvatarImageData = await NetworkManager.shared.downloadImageData(from: repo.owner.avatarUrl)
repo.avatarData = topAvatarImageData ?? Data()
// Get Bottom Repo if in Large Widget
var bottomRepo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.swiftAlgorithms)
let bottomAvatarImageData = await NetworkManager.shared.downloadImageData(from: bottomRepo.owner.avatarUrl)
bottomRepo.avatarData = bottomAvatarImageData ?? Data()
// Create Entry & TimeLine
let entry = DoubleRepoEntry(date: .now, topRepo: repo, bottomRepo: bottomRepo)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) // update every 12hours
completion(timeline)
}
catch {
print("❌ Error - \(error.localizedDescription)")
}
}
}
struct DoubleRepoEntry: TimelineEntry {
let date: Date
let topRepo: Repository
let bottomRepo: Repository
}
struct DoubleRepoEntryView : View {
var entry: DoubleRepoProvider.Entry
var body: some View {
VStack(spacing: 76) {
RepoMediumView(repo: entry.topRepo)
RepoMediumView(repo: entry.bottomRepo)
}
}
}
이렇게 바꿔준다.
옵셔널을 해제하였고, 사이즈에따른 적용도 이제 단일 사이즈이기에 조건을 없애준다.
적용하면 다음과 같이 되는걸 알 수 있다.
전, 후 Widget 비교
비교전 다시 한번 언급을 한다.
- 단일 위젯에 복수 Configuration을 지원할 수 없는 이유:
- iOS WidgetKit은 하나의 위젯당 하나의 Configuration만을 허용한다. 이는 각 위젯의 상태 및 동작을 명확히 정의하기 위한 제약이다.
- 예를 들어, Small 크기의 위젯은 단일 레포지토리를 보여주는 구성이고, Medium 크기의 위젯은 두 개의 레포지토리를 보여주는 구성이라면, 하나의 Configuration에서 이를 동시에 처리할 수 없다.
- 따라서, 위젯 크기에 따라 다르게 구성하기 위해 단일 Configuration을 각 위젯에 맞게 세분화하거나, 아예 별도의 위젯으로 분리해야 했다.
구분 | 변경 전 위젯 이름 | 변경 후 위젯 이름 | 변경 전 지원 크기 | 변경 후 지원 크기 |
---|---|---|---|---|
CompactRepo | CompactRepoWidget | DoubleRepoWidget | .systemMedium, .systemLarge | .systemLarge |
Contributor | ContributorWidget | SingleRepoWidget | .systemLarge | .systemMedium, .systemLarge |
SingleRepoWidget Configuration 설정
이때 빨간색 블럭을 보면 알겠지만
우리는 repository를 추가하거나, 삭제하기에 동적으로 관리가 되므로 해당 부분을 체크해준다.
그리고 Intents Extension을 추가해주는데 이때 왼쪽것과 혼동하지 않게 주의해서 만들도록 하자
그리고 다음과 같이 만들어 준다.
이후 만들게 되면 Activate를 해주자
이렇게 추가가 된걸 확인할 수 있다.
App Group을 통해 유저 디폴트의 값을 공유해야하므로
여기도 체크를 해주자.
IntentHandler 설정
그전에 SelectRepoIntent를 만들었는데, 이녀석이 target이 RepoWatcherIntents까지 같이 설정이 되어있는지 확인을 먼저 해본다.
현재 확인해보니 안되어있다.
Target Membership의 아래에 있는 +를 클릭하여 추가해주자.
역시나 안되어있었다.
추가된걸 확인할 수 있다.
이렇게 기본적인 설정 및 타겟 추가를 하고나서 파일을 확인해보면
RepoWatcherIntents라는 디렉토리가 하나 생겼고, 거기에 IntentHandler.swift 파일이 하나 만들어졌다.
이녀석은 간단하게 말하면
유저가 위젯을 설정할때 그걸 system과 통신을 하게 해주는 일종의 Communication Layer 이다.
위에 타겟을 확인해야하는 이유가
1
2
3
4
5
6
7
8
9
10
11
extension IntentHandler: SelectSingleRepoIntentHandling {
func provideRepoOptionsCollection(for intent: SelectSingleRepoIntent, with completion: @escaping (INObjectCollection<NSString>?, (any Error)?) -> Void) {
}
func provideRepoOptionsCollection(for intent: SelectSingleRepoIntent) async throws -> INObjectCollection<NSString> {
}
}
extension을 통해 추가로 구현하는데 이때 SelectSingleRepoIntentHandling
이 프로토콜을 따라야 하기 때문
해당 프로토콜을 채택하면, 필요 함수를 추가하라고 나오고 fix 누르면 위와 같이 2개의 함수가 추가된다.
같은 내용인데 하나는 콜백함수이고 하나는 비동기 함수이다.
콜백함수를 지우고 비동기 함수만 사용하도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension IntentHandler: SelectSingleRepoIntentHandling {
func provideRepoOptionsCollection(for intent: SelectSingleRepoIntent) async throws -> INObjectCollection<NSString> {
guard let repos = UserDefaults.shared.value(forKey: UserDefaults.repoKey) as? [String] else {
throw UserDefaultsError.retrieval
}
return INObjectCollection(items: repos as [NSString])
}
func defaultRepo(for intent: SelectSingleRepoIntent) -> String? {
return "sallen0400/swift-news"
}
}
provideRepoOptionsCollection
함수에서 UserDefault에서 값을 가져오고 만약 값이 없다면 error를 리턴한다.
그리고 해당 함수의 리턴타입이 INObjectCollection<NSString>
이것이기에 repos as [NSString]
를 통해 타입캐스팅을 해준다.
그리고 defaultRepo
의 경우 선택사항이지만 위젯을 처음 작동했을때 디폴트 값을 하나 정해주는것 이걸 안하면 아마 empty placeholder view가 보여질것이다.
IntentHandler의 역할
- 사용자 입력 처리
- 사용자가 입력한 데이터를 기반으로 적합한 옵션이나 결과값을 생성하여 시스템에 전달한다.
- 예: 사용자가 SelectSingleRepoIntent를 통해 레포지토리를 선택하면, 선택한 레포지토리를 기반으로 데이터를 반환.
- 사용자가 입력한 데이터를 기반으로 적합한 옵션이나 결과값을 생성하여 시스템에 전달한다.
- 데이터 제공
- 시스템이 위젯 업데이트를 위해 데이터 요청 시, 필요한 데이터를 제공한다.
- 예: UserDefaults에서 저장된 레포지토리를 불러와 INObjectCollection 형식으로 반환.
- 시스템이 위젯 업데이트를 위해 데이터 요청 시, 필요한 데이터를 제공한다.
- 기본값 설정
- 사용자가 아무것도 선택하지 않았을 때 시스템이 사용할 기본값을 설정한다.
IntentHandler 동작 과정
- 사용자 요청 처리
- 사용자가 위젯 설정 화면에서 특정 레포지토리를 선택.
- 선택된 값을 IntentHandler로 전달.
- 데이터 로드
- UserDefaults 또는 네트워크에서 데이터를 로드.
- 데이터를 INObjectCollection 형식으로 변환.
- 시스템에 데이터 반환
- 시스템이 해당 데이터를 사용하여 위젯을 업데이트.
- 위젯 업데이트
- 시스템이 TimelineProvider를 호출하여 업데이트된 데이터로 위젯을 렌더링.
SingleRepoWidget Configuration 적용
현재 위젯은
1
2
3
4
5
6
7
8
9
10
11
12
13
struct SingleRepoWidget: Widget {
let kind: String = "SingleRepoWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: SingleRepoProvider()) { entry in
SingleRepoEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Single Repo")
.description("Track a single Repsitory")
.supportedFamilies([.systemMedium, .systemLarge])
}
}
StaticConfiguration
이다.
이걸 이렇게 바꿔주자
1
2
3
4
IntentConfiguration(kind: kind, intent: SelectSingleRepoIntent.self, provider: SingleRepoProvider()) { entry in
SingleRepoEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
그러면 발생하는 에러
struct SingleRepoProvider: TimelineProvider {
현재는 일반 TimelineProvider 프로토콜을 채택하므로, 이걸 바꾸라는것
1
2
3
func getSnapshot(for configuration: SelectSingleRepoIntent, in context: Context, completion: @escaping @Sendable (SingleRepoEntry) -> Void) { }
func getTimeline(for configuration: SelectSingleRepoIntent, in context: Context, completion: @escaping @Sendable (Timeline<SingleRepoEntry>) -> Void) { }
여기만 기존에 있던 내용을 복사해서 붙여넣어주면된다.
placeholder는 수정할 필요 없다.
그리고 getTimeline 함수에서 약간 변경사항이 있다.
1
2
3
4
5
6
7
8
9
10
11
func getTimeline(for configuration: SelectSingleRepoIntent, in context: Context, completion: @escaping @Sendable (Timeline<SingleRepoEntry>) -> Void) {
Task {
let nextUpdate = Date().addingTimeInterval(43200)
do {
//Get Repo
let repoToShow = RepoURL.prefix + configuration.repo! // modified
enum RepoURL {
static let prefix = "https://api.github.com/repos/"
}
prefix라는 상수를 하나 만들어서 길게 적던것을 방지했고, repository 값을 configuration에서 가져오게 한다.
실행하니
위와같은 에러가 발생
여기를 추가해서 되었다고하는데 에러가 그대로여서 info.plist를 직접 수정했다.
이제 실행이 된다.
또한 앱에서 repo를 등록하고
configuration을 통해 적용해도 되는걸 볼 수 있다.
DoubleRepoWidget Configuration 설정
이젠 DoubleRepoWidget에 대한 Configuration을 만들어 본다.
새롭게 Intents를 만들어준다.
그리고 이렇게 Intents를 만들때
Custom Class 명이 제대로 되어있는지도 확인해주면 좋다.
IntentHandler 설정
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
extension IntentHandler: SelectTwoReposIntentHandling {
func provideTopRepoOptionsCollection(for intent: SelectTwoReposIntent) async throws -> INObjectCollection<NSString> {
guard let repos = UserDefaults.shared.value(forKey: UserDefaults.repoKey) as? [String] else {
throw UserDefaultsError.retrieval
}
return INObjectCollection(items: repos as [NSString])
}
func provideBottomRepoOptionsCollection(for intent: SelectTwoReposIntent) async throws -> INObjectCollection<NSString> {
guard let repos = UserDefaults.shared.value(forKey: UserDefaults.repoKey) as? [String] else {
throw UserDefaultsError.retrieval
}
return INObjectCollection(items: repos as [NSString])
}
func defaultTopRepo(for intent: SelectTwoReposIntent) -> String? {
return "apple/swift"
}
func defaultBottomRepo(for intent: SelectTwoReposIntent) -> String? {
return "kudoleh/iOS-Clean-Architecture-MVVM"
}
}
아까 Intents를 추가하면서 만든 Parameter의 갯수에 따라 추가하는 함수의 갯수도 달라진다.
DoubleRepoWidget Configuration 적용
이전과 마찬가지로 StaticConfiguration
이것을 바꿔주자.
그리고 provider의 프로토콜도 바꿔준다.
자세한 내용은 이전에 했으므로 생략.
대신 Timeline 만 repo에 관한 부분만 수정을 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
do {
// Get Top Repo
var topRepo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.prefix + configuration.topRepo!) // modified
let topAvatarImageData = await NetworkManager.shared.downloadImageData(from: topRepo.owner.avatarUrl)
topRepo.avatarData = topAvatarImageData ?? Data()
// Get Bottom Repo if in Large Widget
var bottomRepo = try await NetworkManager.shared.getRepo(atUrl: RepoURL.prefix + configuration.bottomRepo!) // modified
let bottomAvatarImageData = await NetworkManager.shared.downloadImageData(from: bottomRepo.owner.avatarUrl)
bottomRepo.avatarData = bottomAvatarImageData ?? Data()
// Create Entry & TimeLine
let entry = DoubleRepoEntry(date: .now, topRepo: topRepo, bottomRepo: bottomRepo)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) // update every 12hours
completion(timeline)
}
catch {
print("❌ Error - \(error.localizedDescription)")
}
적용 완료.
iOS 17 적용
AppIntent가 새로 생기면서 그에 맞게 적용을 해보도록 한다.
우선 이렇게 개별로 SingleRepo를 달 수 있기때문에 DoubleRepo가 필요없어 졌다.
DoubleRepo와 관련된걸 전부 지워준다.
그리고 이젠 WidgetBundle도 의미없기에 지워준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@main // new
struct SingleRepoWidget: Widget {
let kind: String = "SingleRepoWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: SelectSingleRepoIntent.self, provider: SingleRepoProvider()) { entry in
SingleRepoEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Single Repo")
.description("Track a single Repsitory")
.supportedFamilies([.systemMedium, .systemLarge])
}
}
여기 SingleWidget에 추가를 해준다.
Intent Migration 하기
이전글에서는 Migration을 하는 방법만 소개를 하고 직접 코드를 작성했었다.
이번엔 Migration을 해보자.
그러면 이렇게
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
@available(iOS 17.0, macOS 14.0, watchOS 10.0, *)
struct SelectSingleRepo: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent {
static let intentClassName = "SelectSingleRepoIntent"
static var title: LocalizedStringResource = "Select Single Repo"
static var description = IntentDescription("")
@Parameter(title: "Repo", optionsProvider: StringOptionsProvider())
var repo: String?
struct StringOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
// TODO: Return possible options here.
return []
}
}
static var parameterSummary: some ParameterSummary {
Summary()
}
func perform() async throws -> some IntentResult {
// TODO: Place your refactored intent handler code here.
return .result()
}
}
파일이 만들어진다.
이때 Target을 꼭 확인하자.
parameterSummary, perform은 지금 필요가 없어서 주석을 처리했고,
1
2
3
4
5
6
7
8
9
10
11
12
13
struct RepoOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
// TODO: Return possible options here.
guard let repos = UserDefaults.shared.value(forKey: UserDefaults.repoKey) as? [String] else {
throw UserDefaultsError.retrieval
}
return repos
}
func defaultResult() async -> String? {
"sallen0400/swift-news"
}
}
기존에 IntentHandler의 내용을 복사해주었다.
이제 Intent를 AppIntent로 바꿔준다.
Widget
1
2
3
4
5
6
7
8
9
10
11
12
13
struct SingleRepoWidget: Widget {
let kind: String = "SingleRepoWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: SelectSingleRepo.self, provider: SingleRepoProvider()) { entry in
SingleRepoEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Single Repo")
.description("Track a single Repsitory")
.supportedFamilies([.systemMedium, .systemLarge])
}
}
이렇게 바꿔주고 단지 앞에 App만 붙이는게 아니라 intent 파라미터도 기존에 했던게 있어서 그대로 사용하면 안되고, 새로만든 SelectSingleRepo
를 사용해줘야한다.
Provider
AppIntentTimelineProvider
로 바꿔주고 그러면 또 snapshot, timeline 함수를 설정하라고 한다.
기존과의 차이라면 콜백이아닌 비동기 함수로 바뀐다는것. 즉 completion이 사라진다.
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
37
38
39
40
struct SingleRepoProvider: AppIntentTimelineProvider {
func snapshot(for configuration: SelectSingleRepo, in context: Context) async -> SingleRepoEntry {
return SingleRepoEntry(date: .now, repo: MockData.repoOne)
}
func timeline(for configuration: SelectSingleRepo, in context: Context) async -> Timeline<SingleRepoEntry> {
let nextUpdate = Date().addingTimeInterval(43200)
do {
//Get Repo
let repoToShow = RepoURL.prefix + configuration.repo!
var repo = try await NetworkManager.shared.getRepo(atUrl: repoToShow)
let avatarImageData = await NetworkManager.shared.downloadImageData(from: repo.owner.avatarUrl)
repo.avatarData = avatarImageData ?? Data()
if context.family == .systemLarge {
//Get Contributors
let contributors = try await NetworkManager.shared.getContributors(atUrl: repoToShow + "/contributors")
// Filter to just the top 4
var topFour = Array(contributors.prefix(4))
// Download top four avatars
for i in topFour.indices {
let avatarData = await NetworkManager.shared.downloadImageData(from: topFour[i].avatarUrl)
topFour[i].avataData = avatarData ?? Data()
}
repo.contributors = topFour
}
// Create Entry & Timeline
let entry = SingleRepoEntry(date: .now, repo: repo)
return Timeline(entries: [entry], policy: .after(nextUpdate))
} catch {
return Timeline(entries: [], policy: .after(nextUpdate))
}
}
}
return 부분만 새로 추가 되었다.
물론 timeline에는 Task가 사라지긴 했다. 왜냐 함수자체에 async가 있는 비동기 함수이므로.
그리고 관련된 파일을 전부 날려주었다.
이전에는 Edit Widget을 하면 새롭게 창이 떴는데 그게 바뀌었다.
Intent vs AppIntent
AppIntent와 IntentHandler의 차이 iOS 17에서 AppIntent는 기존 IntentHandler의 많은 역할을 대체하도록 설계되었다. AppIntent는 위젯과 Siri 등 다양한 시스템과의 통합을 간소화하며, 다음과 같은 특징을 가진다.
- 비동기 기반: AppIntent는 비동기적으로 동작하여 성능과 확장성을 향상시킨다.
- 코드 단순화: IntentHandler에서 사용되던 복잡한 프로토콜 대신 간단한 구조체로 Intent를 정의 가능.
- 호환성 개선: 기존 Intent 기반 코드에서 AppIntent로의 마이그레이션이 쉽고, 추가적인 기능 확장이 가능하다.
IntentHandler에서 AppIntent로의 변화
- 기존: IntentHandler를 통해 데이터를 제공하고 위젯 구성.
- 변경 후: AppIntent를 통해 설정 및 데이터 로드를 직접 처리.
- 결론: AppIntent는 기존의 의존성을 줄이고 코드 재사용성을 극대화하는 방향으로 설계되었다.