포스트

WidgetKit (10)

App Group 적용하기

앱과 위젯에 같은 CoreData의 값을 같이 사용하기 위해 App Group을 사용해 준다.

이전글에서 관련 내용을 다뤘으니 한번 다시 보는것도 좋을듯.

그전에 Widget을 만들어 준다.

CleanShot 2024-12-09 at 17 19 49

만드는건 이사진 한장으로 대체

App Group 만드는것도 이전에 언급했으므로 아래 사진으로 대체한다.

첫번째는 앱에서 App Group 생성 CleanShot 2024-12-09 at 17 24 55

두, 세번째는 생성된 App Group을 위젯에도 적용 CleanShot 2024-12-09 at 17 22 39 CleanShot 2024-12-09 at 17 22 58

CoreData의 Container Migration

위의 과정을 통해 Shared Container를 생성했지만, 현재 CoreData를 관리하는 Persistence 파일을 확인해 보면 여전히 App Container를 사용하고 있다. 이를 Shared Container로 변경하려면 Migration 작업이 필요하다.

CleanShot 2024-12-09 at 17 30 01

Shared Container URL 생성

먼저 Shared Container URL을 정의한다. 이 URL은 App Group을 활용하여 CoreData가 Shared Container를 참조하도록 설정한다.

1
2
3
4
var sharedStoreURL: URL {
    let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.HaroldSong.SwiftCal")!
    return (container.appendingPathComponent(databaseName))
    }
  • forSecurityApplicationGroupIdentifier: App Group의 식별자를 지정.
  • appendingPathComponent: CoreData 파일 이름(SwiftCal.sqlite)을 추가.

이렇게 URL주소를 하나 만들어 준다.

⚠️ 참고: appendingPathComponent는 곧 Deprecated될 예정으로, appending(path:directoryHint:)를 사용할 것을 권장한다.

CleanShot 2024-12-09 at 17 44 26

앞으로는 이걸로 사용하면 되긴한다.

1
2
3
4
var sharedStoreURL: URL {
        let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.HaroldSong.SwiftCal")!
        return container.appending(path: databaseName, directoryHint: .notDirectory)
    }

이렇게 혼자서 만들어보긴했는데 우선은 해당내용은 적용하지는 말자.

Migration 문제점 및 대처방안

simulator_screenshot_2BA10C84-BA60-4BB4-B886-6640356F6C06

현재 App Container에 저장된 데이터를 Shared Container로 옮기는 과정에서, 잘못된 설정이나 실수로 인해 데이터가 손실될 위험이 있다. 이를 방지하기 위해 기존 데이터의 URL을 별도로 정의하고 마이그레이션 로직을 추가한다.

1
2
3
4
5
6
let databaseName = "SwiftCal.sqlite"

var oldStoreURL: URL {
    let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
    return directory.appendingPathComponent(databaseName)
}

그리고 if문을

1
2
3
4
5
if inMemory {
    container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
} else {
    container.persistentStoreDescriptions.first!.url = sharedStoreURL
}

여기에 else를 추가해준다.

Migration 함수 생성 및 Migration 적용

강의에선 우선 CoreData 값을 리셋해주기위해 앱을 삭제하고 재설치를 진행했다.

그리고 기존 데이터를 Shared Container로 옮기는 Migration 작업을 처리하는 함수는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func migrateStore(for container: NSPersistentContainer) {
    print("➡️ went into migrateStore")
    let coordinator = container.persistentStoreCoordinator
    
    guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else { return }
    print("🛡️ old store no longer exists")

    do {
        let _ = try coordinator.migratePersistentStore(oldStore, to: sharedStoreURL, type: .sqlite)
        print("🏁 Migration Succesfully Done")
    } catch {
        fatalError("Unable to migrate to shared store.")
    }
    
    do {
        try FileManager.default.removeItem(at: oldStoreURL)
        print("🗑️ Old store deleted")
    } catch {
        print("Unable to delete old store")
    }
}

CoreData 초기화 시 Migration 로직 추가

CoreData 초기화(init) 시 기존 데이터를 확인하고, 필요한 경우 마이그레이션을 수행하기위해 init을 수정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "SwiftCal")
    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    } else if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
        print("🎅🏻old store Doesn't exist. Using new shared URL")
        container.persistentStoreDescriptions.first!.url = sharedStoreURL
    }
    
    print("🕸️ container URL = \(container.persistentStoreDescriptions.first!.url)")
    
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    migrateStore(for: container)
    container.viewContext.automaticallyMergesChangesFromParent = true
}

Migration 진행 과정

그리고 실행전에 console 확인을 위해

1
2
3
//container.persistentStoreDescriptions.first!.url = sharedStoreURL

//migrateStore(for: container)

init 내부의 위와같이 주석을 잡아주었다.

잘못된 Target 설정으로 인한 에러발생

하지만 실행을 하니

1
[S:1] Error received: Connection invalidated.

이런 에러가 발생 아마 CoreData를 수정하면서 문제가 발생했기에 뭔가 놓친게 있는지 확인을 해본다.

CleanShot 2024-12-09 at 19 21 36

코드상 문제가 없어 확인을 하던 도중 WidgetExtension을 만들면서 자연스럽게 이부분이 앱이 아닌 위젯으로 되어있으면서 생긴 에러였다…

Migration 진행

문제를 해결하고 실행하니 다음과 같이 나온다.

  1. 초기실행 (앱을 삭제후 설치)
    • Old store Container 생성.
      1
      2
      
      🎅🏻old store Doesn't exist. Using new shared URL
      🕸️ container URL = Optional(file:///Users/dongik/Library/Developer/CoreSimulator/Devices/ECF12B83-9492-49E4-B3E8-BD8B6338334F/data/Containers/Data/Application/6435F397-CA40-44F6-B5E7-9D977796E3FE/Library/Application%20Support/SwiftCal.sqlite)
      
  2. 재실행 및 값 입력
    • Old store Container가 초기실행으로 인해 생성되었으므로 존재 하지 않는 메세지가 삭제
      1
      
      🕸️ container URL = Optional(file:///Users/dongik/Library/Developer/CoreSimulator/Devices/ECF12B83-9492-49E4-B3E8-BD8B6338334F/data/Containers/Data/Application/7B738302-1C36-4B99-85C3-A9946AE2A17C/Library/Application%20Support/SwiftCal.sqlite)
      
    • 첫번재 줄이 사라졌다.
    • 이제 값을 넣어주도록 하자. simulator_screenshot_2A679F0A-8DA8-4B3D-AA95-4D69E3D288D5
  3. 재실행
    • 데이터 정상적으로 들어온 것 확인.
  4. Migration 진행
    • 주석을 걸었던것을 풀고 Migration을 진행한다.
      1
      2
      3
      4
      5
      
      🕸️ container URL = Optional(file:///Users/dongik/Library/Developer/CoreSimulator/Devices/ECF12B83-9492-49E4-B3E8-BD8B6338334F/data/Containers/Data/Application/3C7F6B6B-C1FA-4AAC-BB0C-7EA1F6F27E2A/Library/Application%20Support/SwiftCal.sqlite)
      ➡️ went into migrateStore
      🛡️ old store no longer exists
      🏁 Migration Succesfully Done
      🗑️ Old store deleted
      
    • container url이 기존 old store 로 나오지만 이후 Migration이 진행이 되었음.
  5. Migration 이후 재실행
    1
    2
    3
    
    🎅🏻old store Doesn't exist. Using new shared URL
    🕸️ container URL = Optional(file:///Users/dongik/Library/Developer/CoreSimulator/Devices/ECF12B83-9492-49E4-B3E8-BD8B6338334F/data/Containers/Shared/AppGroup/F00B2756-3D80-4FD0-AEDA-0F90A8DB58E9/SwiftCal.sqlite)
    ➡️ went into migrateStore
    
    • 기존 old store가 삭제가 됨.
    • 이제는 Container의 주소가 AppGroup으로 바뀌어있는걸 알 수 있다.

Widget UI 디자인

CleanShot 2024-12-09 at 20 44 13

이렇게 디자인을 할 예정이다.

CalendarView 세분화 하기

지금 달력의 경우, Widget에서도 사용하기에 기존에 App에 있던 View를 Widget에서도 사용하기위해 세분화 해준다.

이때 위젯 앱 전부 사용해야하기에 Target 체크를 확실하게 해준다.

CleanShot 2024-12-09 at 20 48 39

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]
var font: Font = .body

var body: some View {
    HStack {
        ForEach(daysOfWeek, id: \.self) { dayOfWeek in
            Text(dayOfWeek)
                .font(font)
                .fontWeight(.black)
                .foregroundStyle(.orange)
                .frame(maxWidth: .infinity)
        }
    }
}

이렇게 위의 요일을 나타내는 Header를 별도로 분리해준다.

Widget Design

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
let columns = Array(repeating: GridItem(.flexible()), count: 7)
    
    var body: some View {
        HStack {
            VStack {
                Text("30")
                    .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(0..<31) { _ in
                        Text("30")
                            .font(.caption2)
                            .bold()
                            .frame(maxWidth: .infinity)
                            .foregroundStyle(.secondary)
                            .background {
                                Circle()
                                    .foregroundStyle(.orange.opacity(0.3))
                                    .scaleEffect(1.5)
                            }
                    }
                }
            }
            .padding(.leading, 6)
        }
        .padding()
    }

이렇게 하면

CleanShot 2024-12-10 at 01 48 23

이런 결과를 얻을 수 있다.

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