포스트

Dex (11)

Appgroup

AppGroup 역시 이전에 다뤄봤던 내용이다.

이전글 참고.

App Group 설정 및 권한 부여

Image

Image

Widget도 동일하게 해주자.

이렇게 추가해주면 된다.

효율적인 파일 관리를 위해 새롭게 추가된 Extension.entitlements 파일도 아래로 옮겨주자.

Image

이때 발생하는 에러

Image

해당 문제를 해결하기위해

Image

우리가 눈여겨봐야하는 것은 바로 Signing이다.

지금은 그냥 파일명만 표기되어있다.

Image

하지만 우리는 해당 파일의 위치를 옮겼기에, 해당 파일이 현재 어디에 있는지 명시를 해줘야한다.

그래서 앞에 DexWidget/을 붙여준다.

Image

이제 빌드가 정상적으로 이뤄진다.

공유 저장소 경로 설정 (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에 저장된 포켓몬 데이터를 불러와 표시할 수 있다.

구현 흐름은 다음과 같다:

  1. CoreData에서 포켓몬 리스트 가져오기
  2. 랜덤으로 한 마리 선택
  3. 해당 포켓몬을 기준으로 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초 간격으로 포켓몬을 무작위로 바꿔가며 보여주게 된다.

Image

그리고 시뮬레이터를 실행하면 갑자기

CoreData: Declared Objective-C type "[String]" for attribute named types is not valid 이런 문제가 뜨는데 이건 조만간 다뤄볼 예정


상호작용 위젯으로의 확장: AppIntent의 필요성

이번 글에서는 AppGroup을 활용해 Widget에서 CoreData 데이터를 읽어오는 구조를 구현했다.
하지만 이는 어디까지나 읽기(Read-only)에 국한된 형태이다.

예를 들어 위젯에서 포켓몬을 즐겨찾기 추가하거나 삭제하는 식의 사용자 상호작용을 구현하려면, 기존의 TimelineProvider 기반 구성만으로는 한계가 있다.

이러한 동작을 구현하려면 iOS 17부터 도입된 AppIntent 기반의 위젯 상호작용이 필요하다.

이 주제는 이전글에서 실제 구현 사례와 함께 다룬 바 있다.

이번 글에서는 다루지 않지만, 나중에 Dex 앱을 좀 더 발전시켜보고 싶을 때 직접 구현해볼 생각이다.

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