Dex (11)
Appgroup
AppGroup 역시 이전에 다뤄봤던 내용이다.
이전글 참고.
App Group 설정 및 권한 부여
Widget도 동일하게 해주자.
이렇게 추가해주면 된다.
효율적인 파일 관리를 위해 새롭게 추가된 Extension.entitlements 파일도 아래로 옮겨주자.
이때 발생하는 에러
해당 문제를 해결하기위해
우리가 눈여겨봐야하는 것은 바로 Signing이다.
지금은 그냥 파일명만 표기되어있다.
하지만 우리는 해당 파일의 위치를 옮겼기에, 해당 파일이 현재 어디에 있는지 명시를 해줘야한다.
그래서 앞에 DexWidget/
을 붙여준다.
이제 빌드가 정상적으로 이뤄진다.
공유 저장소 경로 설정 (CoreData 연동)
Dex 앱과 Widget이 동일한 CoreData 저장소를 사용하도록 AppGroup을 통해 경로를 재설정한다.
이 작업은 Persistence.swift 파일에서 이뤄진다. 핵심은 저장소를 메모리에 둘지, 실제 공유 디스크에 저장할지를 조건에 따라 나누는 것이다.
먼저 아래와 같은 분기문을 확인할 수 있다:
1
2
3
4
5
6
7
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
} else {
container.persistentStoreDescriptions.first!.url = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.HaroldSong.DexGroup")!
.appending(path: "Dex.sqlite")
}
여기서 inMemory
파라미터는 주로 테스트용 Preview 환경에서 CoreData를 임시로 사용할 때 true로 설정된다. 이때는 데이터를 실제 파일에 저장하지 않고 /dev/null
로 연결된 가상 공간에 저장하기 때문에 앱을 재실행하면 모두 초기화된다.
반면 inMemory
가 false일 경우, 실제 AppGroup을 통해 공유 가능한 위치에 .sqlite
파일로 저장하게 되어 Dex 앱과 Dex 위젯 양쪽에서 동일한 데이터베이스를 참조할 수 있게 된다.
이 설정 덕분에 Widget에서도 CoreData를 활용한 데이터 조회가 가능해지는 것이다.
CoreData의 데이터를 Widget에서 사용하기
이제 위젯에서 CoreData에 저장된 포켓몬 데이터를 불러와 표시할 수 있다.
구현 흐름은 다음과 같다:
- CoreData에서 포켓몬 리스트 가져오기
- 랜덤으로 한 마리 선택
- 해당 포켓몬을 기준으로 5초 간격의 타임라인 생성
Provider 구조 내에서 다음과 같은 방식으로 작성된다:
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
// randomPokemon은 CoreData에서 무작위 포켓몬을 추출하는 computed property이다.
var randomPokemon: Pokemon {
var results: [Pokemon] = []
do {
results = try PersistenceController.shared.container.viewContext.fetch(Pokemon.fetchRequest())
} catch {
print("Couldn't fetch: \(error)")
}
return results.randomElement() ?? PersistenceController.previewPokemon
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
for offset in 0..<10 {
let date = currentDate.addingTimeInterval(Double(offset * 5))
let pokemon = randomPokemon
let entry = SimpleEntry(
date: date,
name: pokemon.name!,
types: pokemon.types!,
sprite: pokemon.spriteImage
)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
이제 위젯은 5초 간격으로 포켓몬을 무작위로 바꿔가며 보여주게 된다.
그리고 시뮬레이터를 실행하면 갑자기
CoreData: Declared Objective-C type "[String]" for attribute named types is not valid
이런 문제가 뜨는데 이건 조만간 다뤄볼 예정
상호작용 위젯으로의 확장: AppIntent의 필요성
이번 글에서는 AppGroup을 활용해 Widget에서 CoreData 데이터를 읽어오는 구조를 구현했다.
하지만 이는 어디까지나 읽기(Read-only)에 국한된 형태이다.
예를 들어 위젯에서 포켓몬을 즐겨찾기 추가하거나 삭제하는 식의 사용자 상호작용을 구현하려면, 기존의 TimelineProvider
기반 구성만으로는 한계가 있다.
이러한 동작을 구현하려면 iOS 17부터 도입된 AppIntent
기반의 위젯 상호작용이 필요하다.
이 주제는 이전글에서 실제 구현 사례와 함께 다룬 바 있다.
이번 글에서는 다루지 않지만, 나중에 Dex 앱을 좀 더 발전시켜보고 싶을 때 직접 구현해볼 생각이다.