iOS 강의를 다 듣고 바로 만들어서 배포해 본 앱이 있는데,, 이번에 다시 보니 정말 배웠던거 그대로 다 넣느라고 이상한게 많아서 수정하면서 정리를 하기로 했다.
앱의 기능 중 맘에 드는 곡을 북마크 하면 따로 애창곡 리스트로 볼 수 있는 기능이 있는데, 당시에 UserDefaults를 알게되고 아! 이게 iOS에서 DB처럼 쓰는거구나! 하면서 그냥 사용했다.
그러다 옛날 블로그 포스팅으로도 있지만 CoreData 존재를 알고나서 보니 멍청한 짓을 한 거 같아서 이것부터 수정하기로 했다.
일단 둘의 차이점을 먼저 알아보자면
UserDefaults는 App Setting 정보 같은 간단한 정보를 저장하기에 적합하고
CoreData는 복잡하고 큰 데이터를 저장하기 적합하다.
CoreData를 데이터베이스 자체로 알고 있었지만, 조금 찾아보니 CoreData는 프레임워크고 영구 데이터 저장, 임시 데이터 캐시, CloudKit을 통해 데이터 동기화 등을 할 수 있는 것으로, 데이터베이스를 사용할 수 있는 기능도 있는것!
[SwiftUI] CoreData + CloudKit으로 데이터 관리하기
CoreData는 UserDefaults와 비슷하다 생각할 수 있지만, UserDefaults는 간단한 데이터 정도의 저장이 적합하고, CoreData는 UserData, 큰 데이터를 저장하기 용이하다. 여기서 CloudKit까지 같이 사용해 준다면, G
justhm.tistory.com
여기서도 정리를 해 두었지만 이번에는 CoreData 사용과 UserDefaults에 있는 데이터를 CoreData로 이전한 내용 정도만 정리 하기로
추가로 CoreData에 만들어둔 Entity들은 SwiftData로도 그대로 쓸 수 있게 마이그레이션을 지원해준다. 나중에 SwiftData도 사용하게 된다면 이 앱에서 마이그레이션 해 기능을 변경해 볼 생각이다.
Zedd님 블로그를 참고해서 공부했었다...
https://zeddios.tistory.com/987
Core Data (1)
안녕하세요 :) Zedd입니다. Core Data를 사용할 일이 생겼는데...제가 옜날에 해봤단 말이죠..!?!?!? 근데 다시 하려니까 생각이 하나도 안나는거에요 그때는 문서 볼 생각도 안했었는데..ㅎㅎㅎㅎ 이
zeddios.tistory.com
1. CoreData 모델 생성 및 사용
프로젝트 생성시 체크하지 않았기 때문에 아무것도 없다. 그렇기에 먼저 모델을 생성해줘야 한다.
왼쪽의 Entity를 생성하고 Attribute를 추가하면 사용할 모델을 만들 수 있다.
기본으로 지원하는 타입 말고도 custom한 enum, struct를 사용할 수 도 있다.
다 만들고 나면 Inspector에서 Class>Module의 설정을 Current Product Module로 바꿔야 한다.
이것때문에 초반에 문제가 있었는데, 기본으로 Model, Entity를 만들면 Class가 구현되어 접근을 할 수 있는데 안되길래 보니 이 설정이 필요했다.
SwiftUI에서 프로젝트 생성시에는 문제없이 됐던걸로 기억하는데 프로젝트 생성시 설정을 안한 프로젝트여서 그런 것 같다.
추가로 Inspector에 Codegen 속성도 있는데, Entity를 확장해서 쓸 때 저 속성에서 Category/Extension 속성을 선택하고
Editor>Create NSManagedObject Subclass를 누르면 파일들이 프로젝트에 생성되는데, 거기서 확장을 하면 된다.
실제로는 Category/Extension 속성을 활용하는 경우가 더 많다고 하는데 내 프로젝트는 간단해서 그럴 필요가 없었다.
2. CoreData 사용할 클래스 생성
보통 프로젝트 생성시 선택했다면 기본적으로 만들어진 걸 활용하면 되지만, 없으니 만들었다.
기본적으로 두 가지를 먼저 해야하는데, Container와 Context를 생성해야한다.
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "SongModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
var context: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
PersistentContainer 뭔지 알아보려면 CoreDataStack을 알아야 한다. [[이거 추가해야함]]
먼저 데이터를 저장하는 코드
func addFavoriteSong(song: Song) -> Bool {
let entity = NSEntityDescription.entity(forEntityName: "FavoriteSong", in: self.context)
if let entity {
let managedObject = NSManagedObject(entity: entity, insertInto: self.context)
managedObject.setValue(song.id, forKey: "id")
managedObject.setValue(song.brand.name, forKey: "brand")
managedObject.setValue(song.title, forKey: "title")
managedObject.setValue(song.no, forKey: "number")
managedObject.setValue(song.singer, forKey: "singer")
managedObject.setValue(song.composer, forKey: "composer")
managedObject.setValue(song.lyricist, forKey: "lyricist")
do {
try self.context.save()
return true
} catch {
//ERROR
return false
}
}
else {
//ERROR
return false
}
}
NSEntityDescription.entity 메서드를 이용해 먼저 정의했던 Entity를 가져온다.
그 후 Entity의 Attribute에 맞게 데이터를 넣어주면 된다. CoreData의 Entity 데이터 형식은 NSManagedObject이니 다음과 같이 생성해주고 각 Attribute의 값을 주입하면 된다.
CodeGen에서 클래스 확장을 선택해 간편하게 저장할 데이터를 넣을 수 있다는 거
그리고 꼭! 해야할 작업이 save()다.
데이터 저장, 삭제시 save()를 하지 않으면 반영되지 않으니 절대 잊지 말기.
추가, 수정, 삭제 등 전체 코드로 나중에 보기로 하고 먼저 좀 헤맸던 부분을 정리해보자면
노래를 저장할 때 브랜드이름+곡번호 로 저장을 하고 검색도 그 ID를 사용하려 했는데 CoreData에서는 어떻게 검색을 하는지 의문이었다.
그냥 특정키, 값을 파라미터로 하는 검색 메서드가 있는 줄 알았지만, Entity 요청에 처음 본 NSPredicate 클래스를 사용해 검색을 할 수 있었다.
func fetchByBrand(brand: String) -> [FavoriteSong] {
let context = persistentContainer.viewContext
let request: NSFetchRequest<FavoriteSong> = FavoriteSong.fetchRequest()
request.predicate = NSPredicate(format: "brand == %@", brand)
do {
let song = try context.fetch(request)
return song
} catch {
return []
}
}
NSFetchRequest는 Persistent store에서 데이터를 검색하는 데 사용되는 검색 기준에 대한 설명으로 Document에 나와있는데
검색을 하고싶은 Entity를 타겟으로 요청을 생성하는 것이다.
가만보면 URLSessionDataTask였나 그 친구도 그렇고 request를 한다면 그것에 대한 정의를 하고 요청을 하는게 정석인거 같다.
NSPredicate는 메모리 내에 어떤 값을 가져올 때 Filter에 대한 조건을 정의하는 것으로 주로 NSArray, CoreData, 정규식에서 사용된다고 한다.
사용하는 검색용 문법 정도로 이해가 된다..
Predicate문법이 정말 생소했는데 아래 블로그를 보며 금방 감을 잡았다.
복잡하게 검색을 할 것도 아니였기 때문에 코드를 보면 brand Attribute 중 해당 브랜드와 같은 브랜드의 데이터를 필터링 하는 것을 간단하게 만들 수 있었다.
https://onelife2live.tistory.com/35
[iOS] NSPredicate 문법 정리
NSArray를 필터링 할 때, CoreData를 사용할 때 Predicate 문법을 사용하여 필터링 하곤 합니다. 이 때 주로 사용하는 문법들을 정리해보겠습니다. let request: NSFetchRequest = Entity.fetchRequest() let predicate = NSPr
onelife2live.tistory.com
Singleton으로 만든 Persistence Manager 전체코드
import CoreData
class PersistenceManager {
static var shared: PersistenceManager = PersistenceManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "SongModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
var context: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
func fetchData() -> [FavoriteSong] {
do {
let fetchRequest = FavoriteSong.fetchRequest()
let data = try self.context.fetch(fetchRequest)
return data
} catch {
//ERROR
print("\(error.localizedDescription)")
return []
}
}
@discardableResult
func addFavoriteSong(song: Song) -> Bool {
let entity = NSEntityDescription.entity(forEntityName: "FavoriteSong", in: self.context)
if let entity {
let managedObject = NSManagedObject(entity: entity, insertInto: self.context)
managedObject.setValue(song.id, forKey: "id")
managedObject.setValue(song.brand.name, forKey: "brand")
managedObject.setValue(song.title, forKey: "title")
managedObject.setValue(song.no, forKey: "number")
managedObject.setValue(song.singer, forKey: "singer")
managedObject.setValue(song.composer, forKey: "composer")
managedObject.setValue(song.lyricist, forKey: "lyricist")
do {
try self.context.save()
return true
} catch {
//ERROR
return false
}
}
else {
//ERROR
return false
}
}
func getCurrentObject(songID id: String) -> [FavoriteSong] {
let context = persistentContainer.viewContext
let request: NSFetchRequest<FavoriteSong> = FavoriteSong.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id)
do {
let song = try context.fetch(request)
return song
} catch {
return []
}
}
func fetchByBrand(brand: String) -> [FavoriteSong] {
let context = persistentContainer.viewContext
let request: NSFetchRequest<FavoriteSong> = FavoriteSong.fetchRequest()
request.predicate = NSPredicate(format: "brand == %@", brand)
do {
let song = try context.fetch(request)
return song
} catch {
return []
}
}
@discardableResult
func deleteSong(id: String) -> Bool {
let data = getCurrentObject(songID: id)
guard !data.isEmpty else { return false }
let object = data[0] as NSManagedObject
self.context.delete(object)
do {
try self.context.save()
return true
} catch {
//ERROR
return false
}
}
// 애창곡으로 되어 있는지만 확인하기 위한 용도
func isExist(songID id: String) -> Bool {
let request: NSFetchRequest<FavoriteSong> = FavoriteSong.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id)
do {
let data = try self.context.fetch(request)
if data.isEmpty { return false }
return true
} catch {
print(error.localizedDescription)
return false
}
}
}
3. 데이터 이전 작업
위에 만들어 놓은 PersistenceManager를 통해 기존에 데이터를 저장, 변경, 불러오는 코드는 다 대체를 했다.
이제 마지막으로 남은것이 데이터를 이전하는 것인데, 맘 같아서는 그냥 버리고 새로 데이터 쌓게 하지 뭐.. 싶었지만
버그 많은 별거 없는 앱도 다운수가 1.5K가 넘어가는 걸 보고 이 중 한 명정도는 저장해 둔 데이터를 요긴하게 쓰겠지 싶어 데이터 이전 작업을 하고 UserDefaults는 구현 코드 정도만 남겨두기로 했다.
func migrationToCoreData() {
//데이터 이전 작업 UserDefaults -> CoreData
var songs: [Song] = []
if let data = UserDefaults.standard.data(forKey: UserDefaultsManager.Key.tjFavorite.rawValue) {
songs.append(contentsOf: (try? PropertyListDecoder().decode([Song].self, from: data)) ?? [])
}
if let data = UserDefaults.standard.data(forKey: UserDefaultsManager.Key.kyFavorite.rawValue) {
songs.append(contentsOf: (try? PropertyListDecoder().decode([Song].self, from: data)) ?? [])
}
for song in songs {
if !PersistenceManager.shared.isExist(songID: song.id) {
let result = PersistenceManager.shared.addFavoriteSong(song: song)
if !result {
print("Failed Migration: \(song.id), \(song.title)")
}
}
}
}
SceneDelegate에서 윈도우 생성 후 바로 동작하도록 함수로 만들어 호출했다.
UserDefaults에 브랜드별로 데이터가 따로 저장되어 있었는데 이를 모두 가져와서 CoreData에 저장하는 방식으로 했다.
UserDefaults를 DB로 쓴 덕에 이런 경험도 해보고 나름 뿌듯?했다.
지금 코드를 보니 이전작업이 끝나도 끊임없이 UserDefaults에 있는 데이터를 가져와 같은 작업을 반복하게 될텐데,,,
이 코드는 나중에 완벽히 이전이 됐는지 불러온 데이터와 CoreData에 저장된 데이터를 교차 검증 하는 코드를 넣고
UserDefaults에 이전을 완료했는지를 확인하는 값을 저장해서 앱을 실행할 때 마다 데이터 이전 작업을 하지 않게 만들어야겠다.
이 작업 외에도 API 변경으로 Response를 받지 못하는 문제, 최신곡 날짜 선택 UI 등 추가한 것이 몇가지 있어 몇 가지 더 정리하고 나면 API 변경시 대처 외에는 더 이상 작업할 일이 없을 거 같다..
계속 유지할 수 있는 앱을 만들고 싶었지만,,, 정말 간단하고 더 추가할 수 있는게 없는 컨텐츠여서 아쉽다.
지금까지 버그 있는데도 다운받아주신 분들 죄송합니다.. 이제 완벽하게 잘 동작하니 가볍게 써주시면 감사하겠습니다!
https://apps.apple.com/kr/app/%EB%85%B8%EB%9E%98%EB%B0%A9book/id1672848960
노래방Book
[주요 기능] - 최신곡 매일 확인 가능 - 인기차트 - 노래 저장기능 (애창곡) - 노래 이름 및 가수 검색 기능 - 유튜브에 올라와 있는 MR검색 [검색 가능한 노래방] - TJ, 금영
apps.apple.com