App Groups ▽
Configuring App Groups | Apple Developer Documentation
Enable communication and data sharing between multiple installed apps created by the same developer.
developer.apple.com
[iOS] Widget - IntentConfiguration (1) 첫 부분에도 컨테이너와 관련하여 간단히 설명했듯
앱과 위젯은 서로 격리되어 있어 직접적인 데이터 공유가 불가능하기에 앱 그룹을 통해 이런 제한을 극복하고,
앱과 위젯이 서로 데이터를 공유할 수 있도록 해야한다. 오늘은 위젯이 앱 그룹을 통해 메인 앱과 데이터를 공유하는 방식과 그 방법(설정)에 대해 챌린더 프로젝트의 방식을 기록하며 정리한다.
App Group
앱 그룹은 여러 앱 또는 앱 확장이 데이터를 공유할 수 있도록 하는 방법
앱 그룹을 사용하면 동일한 앱 그룹 식별자를 가진 앱들이 동일한 컨테이너에 접근하여 데이터 공유를 가능하게 해준다.
App Group을 설정하고,
CoreDataManager file에서 URL Extension을 사용하여 데이터베이스 파일에 대한 URL을 생성했다. 이 URL은 앱 그룹 컨테이너 내부에 저장된 파일에 접근할 수 있게 하고 이를 통해 앱과 위젯(확장 프로그램)이 동일한 데이터베이스 파일을 공유할 수 있도록 해준다.
CoreDataManager 파일에 추가한 URL Extension ▽
extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
- storeURL(for:databaseName:) 메서드
- 앱 그룹 식별자와 데이터베이스 이름을 받아 해당 데이터베이스 파일에 접근할 수 있는 URL을 반환
- FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:) 메서드
- 주어진 앱 그룹 식별자(appGroup)에 해당하는 공유 컨테이너 디렉토리의 URL을 반환
- 반환된 URL에 데이터베이스 이름을 추가하여 SQLite 데이터베이스 파일의 위치를 지정
챌린더의 데이터(데이터 매니저는) 앱 그룹과 CloudKit을 통해 운영된다.
- 데이터 공유: 앱 그룹을 사용하여 여러 앱 또는 앱 확장이 동일한 데이터베이스 파일에 접근할 수 있도록 함.
- 컨테이너 URL 설정: URL.storeURL(for:databaseName:) 메서드를 통해 앱 그룹 컨테이너의 경로를 설정.
- Persistent Container 설정: Core Data와 CloudKit의 통합을 설정할 때 앱 그룹 컨테이너를 사용하여 데이터베이스 파일을 저장 및 로드.
** 데이터매니저 코드 분석하기**
import UIKit
import CoreData
import UserNotifications
extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
// CoreData 함수용 Manager 싱글톤
class CoreDataManager {
static let shared = CoreDataManager()
private init() {
// 요청 알림 권한
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("Notification permission error: \(error)")
}
}
}
// Core Data persistent container 초기화
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Challendar")
let storeURL = URL.storeURL(for: "group.com.seungwon.ChallendarGroup", databaseName: "Challendar")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Failed to initialize persistent Container")
}
// CloudKit 및 CoreData 설정
// description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
// description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
//
// // CloudKit 컨테이너 식별자 설정
// description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.seungwon.Challendar")
//
// container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
// container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
// 원격 변경 알림 구독
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}()
// Core Data context 반환
var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
// 변경 토큰을 저장할 변수
private var lastHistoryToken: NSPersistentHistoryToken? {
get {
guard let data = UserDefaults.standard.data(forKey: "lastHistoryToken") else { return nil }
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data)
}
set {
guard let token = newValue else { return }
let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
UserDefaults.standard.set(data, forKey: "lastHistoryToken")
}
}
// 원격 변경 알림 처리
@objc private func storeRemoteChange(_ notification: Notification) {
fetchHistoryAndUpdateContext()
}
// 앱 시작 시 동기화 트리거
func triggerSync() {
fetchHistoryAndUpdateContext()
}
// 변경 사항 가져와서 컨텍스트 업데이트
private func fetchHistoryAndUpdateContext() {
context.perform {
do {
// 변경 사항 가져오기
let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastHistoryToken)
if let historyResult = try self.context.execute(fetchRequest) as? NSPersistentHistoryResult,
let transactions = historyResult.result as? [NSPersistentHistoryTransaction] {
for transaction in transactions {
self.context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
// 마지막 토큰 업데이트
self.lastHistoryToken = transactions.last?.token
}
try self.context.save()
NotificationCenter.default.post(name: NSNotification.Name("CoreDataChanged"), object: nil, userInfo: nil)
// 로컬 알림 생성
// self.sendLocalNotification()
} catch {
print("Failed to process remote change notification: \(error)")
}
}
}
// 로컬 알림 생성 메서드
private func sendLocalNotification() {
let content = UNMutableNotificationContent()
content.title = "Data Updated"
content.body = "Data has been synchronized with CloudKit."
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
// CRUD Operations
// Create
func createTodo(newTodo: Todo) {
let todo = TodoModel(context: context)
todo.id = UUID()
todo.title = newTodo.title
todo.memo = newTodo.memo
todo.startDate = newTodo.startDate
todo.endDate = newTodo.endDate
todo.completed = newTodo.completed
todo.isChallenge = newTodo.isChallenge
todo.percentage = newTodo.percentage
if let imagesArray = newTodo.images {
let imageData = imagesArray.map { $0.pngData() }
todo.images = try? JSONEncoder().encode(imageData)
}
todo.isCompleted = newTodo.iscompleted
saveContext()
}
// Save context
func saveContext() {
if context.hasChanges {
do {
try context.save()
NotificationCenter.default.post(name: NSNotification.Name("CoreDataChanged"), object: nil, userInfo: nil)
print("NOTIFICATIONSD")
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
// Read
func fetchTodos() -> [Todo] {
let fetchRequest: NSFetchRequest<TodoModel> = TodoModel.fetchRequest()
// 날짜 내림차순 정렬 요청
let dateOrder = NSSortDescriptor(key: "startDate", ascending: false)
fetchRequest.sortDescriptors = [dateOrder]
do {
let todoModels = try context.fetch(fetchRequest)
return todoModels.map { model in
let images: [UIImage]? = {
if let imageData = model.images, let decodedData = try? JSONDecoder().decode([Data].self, from: imageData) {
return decodedData.compactMap { UIImage(data: $0) }
}
return []
}()
return Todo(
id: model.id,
title: model.title,
memo: model.memo,
startDate: model.startDate,
endDate: model.endDate,
completed: model.completed,
isChallenge: model.isChallenge,
percentage: model.percentage,
images: images,
iscompleted: model.isCompleted
)
}
} catch {
print("Failed to fetch todos: \(error)")
return []
}
}
func fetchTodoById(id: UUID) -> TodoModel? {
let fetchRequest: NSFetchRequest<TodoModel> = TodoModel.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", id as CVarArg)
do {
let todos = try context.fetch(fetchRequest)
return todos.first
} catch {
print("Failed to fetch todo by id: \(error)")
return nil
}
}
// Update
func updateTodoById(id: UUID, newTitle: String? = nil, newMemo: String? = nil, newStartDate: Date? = nil, newEndDate: Date? = nil, newCompleted: [Bool]? = nil, newIsChallenge: Bool? = nil, newPercentage: Double? = nil, newImages: [UIImage]? = nil, newIsCompleted: Bool? = nil) {
guard let todoToUpdate = fetchTodoById(id: id) else {
print("Todo not found")
return
}
if let newTitle = newTitle {
todoToUpdate.title = newTitle
}
if let newMemo = newMemo {
todoToUpdate.memo = newMemo
}
if let newStartDate = newStartDate {
todoToUpdate.startDate = newStartDate
}
if let newEndDate = newEndDate {
todoToUpdate.endDate = newEndDate
}
if let newCompleted = newCompleted {
todoToUpdate.completed = newCompleted
todoToUpdate.percentage = Double(newCompleted.filter{$0 == true}.count) / Double(newCompleted.count)
}
if let newIsChallenge = newIsChallenge {
todoToUpdate.isChallenge = newIsChallenge
}
if let newPercentage = newPercentage {
todoToUpdate.percentage = newPercentage
}
if let newImages = newImages {
let imageData = newImages.map { $0.pngData() }
todoToUpdate.images = try? JSONEncoder().encode(imageData)
}
if let newIsCompleted = newIsCompleted {
todoToUpdate.isCompleted = newIsCompleted
}
saveContext()
}
// Delete
func deleteTodoById(id: UUID) {
guard let todoToDelete = fetchTodoById(id: id) else {
print("Todo not found")
return
}
context.delete(todoToDelete)
saveContext()
}
func deleteAllTodos() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = TodoModel.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try context.execute(deleteRequest)
saveContext()
} catch {
print("Failed to delete all todos: \(error)")
}
}
}
'iOS 앱 개발자 프로젝트' 카테고리의 다른 글
[iOS] 챌린더 : 쉽게 쓰고 관리하는 나의 도전 (0) | 2024.07.13 |
---|---|
[iOS] Widget - IntentConfiguration (4) (0) | 2024.06.21 |
[iOS] Widget - IntentConfiguration (3) (0) | 2024.06.19 |
[iOS] Widget - IntentConfiguration (2) (0) | 2024.06.19 |
[iOS] Widget - IntentConfiguration (1) (0) | 2024.06.18 |