Haru's 개발 블로그

[SwiftUI] Coredata 사용해보기 본문

IOS/SwiftUI

[SwiftUI] Coredata 사용해보기

Haru_29 2022. 5. 4. 23:53

전에 Coredata를 활용하기 위한 기본셋팅으로 CoreDataManager에 대하여 알아 보았습니다.

근데 CRUD를 활용하기 위한 Coredata를 사용할려면 어떻게 해야되는지 전체 적으로 알아보도록 합시다.

먼저 CRUD를 활용하기 위해 여러 방법이 있습니다.

예를 들어 Sqlite,Coredata,Realm 아니면 파일입출력 형식으로도 표현을 할수도 있습니다.

각각의 장단점은 다음에 파악하는 것으로 하고 일단 저는 Coredata를 사용해보록 하겠습니다.

일단 Coredata를 활용하기 위해서 기본적인 체크를 한번 하도록하겠습니다.

Use Core data 클릭

먼저 project파일을 생성할때 use coredata를 클릭을 하도록 하겠습니다.

그러면 모델 파일이 생성이 되는데 저는 Attributes에 genre,singer,tite,dateCreated를 생성을 하였습니다.

그러면 사전에 생성한 Coredatamanger를 EnvironmentObject로 생성하도록 합시다.

그러면 조금 다른 길로 샐수도 있지만 State, Binding, ObservedObject, EnvironmentObject의 차이점에 대하여 설명해보도록 하겠습니다.

일단 State와 Binding의 차이점을 간단하게 집고 넘어 가도록 하겠습니다.

- 공통점으로는 뷰 내부에서 특정 View의 상태를 나타내는 변수

- 단, 뷰내부에서 밖으로 사용이 불가능하기 때문에 Private로 선언

- 하위 뷰나 다른 뷰에서 참조하기 위해선 @Binding 해야한다

- State property에 해당하는 변수 값이 변경되면 view를 다시 랜더링한다 -> 따라서 매번 최신값을 가진다.

- 뷰 전체가 다시 랜더링 되는 일을 막기위해 하위뷰로 데이터 변동이 반영되는 뷰만 따로 빼준다 -> 따로 뺀 뷰에 state property를 Binding 해준다.

근데 state나 binding은 부모, 자식뷰가 한정적이어서 여러개의 view에 활용하기에는 적합하지가 않습니다.

이때 ObserableObject와 EnvironmentObject를 활용해보록하겠습니다.

일단 예시로 ObserableObject의 class를 생성해보도록 하겠습니다.

class CountRepo: ObservableObject {
    @Published var count: Int = 0
}

이 코드는 버튼을 클릭하면 숫자가 1씩 증가하는 코드입니다.

그러면 하위뷰에 이 class를 받아오기 위한 ObseredObject는 이런식으로 구현을 하면됩니다.

struct ContentView: View {

    @ObservedObject var countRepo = CountRepo()

    var body: some View {
        VStack {
            Text("\(self.countRepo.count)").font(.largeTitle)
            
           ￿ Button("숫자증가") {
                self.countRepo.count += 1
            }
        }
    }
}

그러면 count 앞에 있는 Published어노테이션의 값이 변동하였을때 즉각적으로 View에게 알려주는 Annotation입니다.

class CountRepo: ObservableObject {

    var count:Int = 0 {
        willSet(newVal){
            print(newVal % 5)
            if(newVal % 5 == 0){
                objectWillChange.send()
            }
        }
    }

}

별개의 코드입니다만, objectWillChange.send()는 SwiftUI에 값이 변동됐음을 알려주는 매서드를 5단위로 설정하였기 때문에 5번 누르게 된다면 +1씩 텍스트가 바뀌게 됩니다.

이제 EnvironmentObject로 넘어가도록 하겠습니다. 일단 EnvironmentObject 요놈은 과연 무엇인가?

일단 EnvironmentObject는 별도로 값을 전달해주지 않아도 상속받는 부모로부터 함께 적용되는 오브젝트입니다. 일단 최상위뷰에 할당해주기 위하여 SceneDelegate.swift로 이동해보도록 하겠습니다.

let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(CountRepo()))

여기에 SceneDelegate 클래스의 Scene 메서드를 보면 window.rootViewController에 UIHostingController을 이용하여 ContentView를 최상위뷰로 할당하고 있습니다.

자, 이제 enviromentObject 메서드를 붙여 CountRepo를 넘겨 주도록 합시다. 이제 자식뷰들에게 CountRepo를 사용할 수 있습니다.

struct ContentView: View {
    var body: some View {
        VStack {
            ChildTextView()
            ChildButtonView()
        }

    }
}

struct ChildTextView:View{
    @EnvironmentObject var countRepo:CountRepo
    var body: some View{
        Text("\(self.countRepo.count)").font(.largeTitle)
    }

}

struct ChildButtonView:View{
    @EnvironmentObject var countRepo:CountRepo
    var body: some View{
        Button("숫자증가") {
            self.countRepo.count += 1
        }
    }

}

이러면 최상위뷰에 EnviromentObject를 정해주었기때문에 하위 뷰들은 어디서든 EnviromentObject 어노이테이션을 이용하여 값에 접근을 할수 있게 됩니다. 

이때, 유의사항이 있습니다. 아래의 코드를 보도록 합시다.

struct ContentView: View {

    var body: some View {

        VStack {
            ChildTextView().environmentObject(CountRepo())
            ChildButtonView() //ChildButtonView에서는 CountRepo가 바인딩되지 않았기때문에 에러가 발생합니다.
        }

    }
}

최상위 뷰가 아닌 서브 뷰에도 environmentObject를 할당할 수 있도록 하는데 부모로부터 자식 뷰로만 전달하기 부모뷰로부터 자식 뷰로만 전달되기 때문에 형제 뷰인 ChildTextView와 ChildButtonView는 서로 값을 공유하지 않기 때문에 오류를 발생하게 됩니다.

이를 유의하면서 사용하도록 합시다.

 

자, 좀 뺑돌아 왔지만 environmentObject를 활용하여 CoreDataManger의 값을 자식 뷰에다그 받아 오도록 합시다.

import SwiftUI

@main
struct MusicPalace_Nano_App: App {
    let persistenceController = CoreDataManger.shared.persistentContainer
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.viewContext)
            
        }
    }
}

그러면 coredata를 활용하기 위한 셋팅은 완료가 되었습니다. 자 그럼 현재 Coredata에 들어가있는 Entity를 불러와 보도록 하겠습니다.

import SwiftUI
import Foundation

struct MainView: View {
    @State var singer : String = ""
    @State var title : String  = ""
    @State private var recordButton = false
    var genres = ["발라드","댄스","팝","인디","락","힙합"]
    @State private var selectedGenre = "발라드"
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(entity: TaskData.entity(), sortDescriptors: [NSSortDescriptor(key: "dateCreated", ascending: true)])
    private var allTasks: FetchedResults<TaskData>
    var currentMainDate: Date = Date()

이 내용은 MainView의 일부를 나타냅니다. 저는 여기에 방금 저장한 TaskData의 entity를 받기 위해서 FetchRequest를 받고 이를 정렬하기 위해서 sortDescriptors를 통하여 key값과 ascending으로 오름차순으로 할지 내림차순으로 할지 결정하도록 합니다.

자 그럼 coredata를 받아오기 위한 FetchRequest에 대하여 좀 더 알아보도록 합시다.

일단 @FetchRequest에 최소한 2개의 값을 제공하도록 합니다. 읽기 원하는 entity와 데이터를 정렬하기 위한 설명이 필요하고 필터링(filter)와 조건(predicate)을 선택적으로 제공하도록 합니다. 이때 @FetchRequest는 @ObservedObject이므로 기본 데이터가 변경이 된다면 자동으로 갱신이 됩니다. 앞서  entity와 sortDescriptors는 앞에 설명을 하였고 조건을 추가하여 NSPredicate를 만듭니다.

@FetchRequest( 
	entity: User.entity(), 
	sortDescriptors: [ 
    NSSortDescriptor(keyPath: \User.name, ascending: false), 
    ], 
    
    predicate: NSPredicate(format: "surname == %@", "Hudson") 
) var users: FetchedResults<User>

참고로 NSPredicate의 문법이 헷갈릴때를 위해 링크를 하나 추가하도록 하겠습니다.

https://onelife2live.tistory.com/35

 

[iOS] NSPredicate 문법 정리

NSArray를 필터링 할 때, CoreData를 사용할 때 Predicate 문법을 사용하여 필터링 하곤 합니다. 이 때 주로 사용하는 문법들을 정리해보겠습니다. let request: NSFetchRequest = Entity.fetchRequest() let pred..

onelife2live.tistory.com

 

그러면 일단 CRUD중 MainView에서 저장과 삭제를 구현한 코드를 보여주도록 하겠습니다.

private func saveTask() {
        
        do {
            let task = TaskData(context: viewContext)
            task.singer = singer
            task.title = title
            task.genre = selectedGenre
            task.dateCreated = currentMainDate
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
    }

    private func deleteTask(at offsets: IndexSet) {
        offsets.forEach { index in
            let task = allTasks[index]
            viewContext.delete(task)
            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }

localizedDesciption Apple 공식 홈페이지 설명

 

Comments