Skip to content

Conversation

@moonkey48
Copy link

📌 Summary

데이터저장(CoreData 활용 기본) #7


✨ Description


CoreData 세팅 및 기본

1. .xcdatamodeld 파일 추가

  1. 프로젝트 생성 시 Use Core Data를 체크해서 생성하면 자동으로 .xcdatamodeld 파일이 생성된다.
  2. 진행중인 프로젝트의 경우 New File - Data Model을 찾아서 추가하면 .xcdatamodeld 파일을 생성할 수 있다.
스크린샷 2023-10-09 오후 1 09 49 스크린샷 2023-10-09 오후 1 10 28

2. Entity, Attribute 정의

위에서 생성한 .xcdatamodeld 파일을 보면 Entity와 Attribute 등을 확인할 수 있다. 해당 데이터 모델은 객체와 객체들 사이의 관계에 대한 정보를 담고 있다.

CoreData에서 Entity는 class나 struct, Attribute는 Entity에 대한 프로퍼티라고 생각할 수 있다.

  1. Entity를 추가한다. 예시로 People과 Person이라는 두가지 Entity를 추가했다.
  2. People안에 Person이 종속 될 수 있게 Property를 생성한다. 이때 아차 싶다. 왜냐하면 Attribute에는 Person이라는 타입이 없기 때문이다. 이때 사용하는 것이 Relationships 개념이다.
    1. Relationships: Entity들 사이에 어떻게 관련이 있는지, 변경사항이 Entity간에 어떻게 전달될지를 정의한다.
    2. Attributes 하단에 Realtionships에서 프로퍼티 이름과 Destination(상대 Entity)를 설정해 준다.
    3. Relationships은 관련된 Entity ahen 설정해주어야 하나의 값이 변했을 때 Core Data에서 인지해서 값을 바꿔줄 수 있어서 Inverse에 연결된 값을 명시해주어야 한다.
    4. People members의 Relationships Type은 연결 관계가 1:n이기 때문에 Too Many로 변경해주어야 한다.
  3. 추가로 우측에 Attributes에 대해 optional로 기본으로 되어있어서 해제해주어야 한다.
스크린샷 2023-10-09 오후 1 24 44 스크린샷 2023-10-09 오후 1 24 48 스크린샷 2023-10-09 오후 2 59 56

3. Core Data Model로부터 class 생성

Model 파일의 우측에 보면 Class에 관한 정보를 설정할 수 있다. 그 중 CodeGen을 바꿔서 class를 생성하는 방법을 설정할 수 있다.

  • Manul/None: 하나의 Entity에 대해 CoreDataClass파일과 CoreDataProperties파일을 만든다.
    • CoreDataClass: Entity를 표현하는 클래스. 생성된 클래스는 자동으로 NSManagedObject를 상속한다.
    • CoreDataProperties: CoreDataClass의 익스텐션. Attribute들을 프로퍼티로 갖고 있다.
  • Class Definition: 기본 설정인 Class Definition은 위의 두 파일을 자동으로 만들어 알아서 관리하기 때문에 개발자가 해당 파일을 볼 수 없어 커스텀 코드를 추가할 수 없다.
  • Category/Extension: CoreDataClass 파일만 만들어주고 CoreDataProperties 파일은 Xcode가 자동으로 관리해준다.

4. Core Data Stack Setting

📑 Core Data Stack이란 앱의 모델 레이어를 협력해서 서포트하는 객체들로 Core Data를 사용하기 위해서는 Core Data Stack이 반드시 필요하다.

코어 데이터 스택 구조

이 부분은 어려워서 이후 정리를 한번 더 해야할 것 같습니다. 당장 몰라도 구현은 가능하지만 이후에는 필요해서 추가로 정리하겠습니다.

  1. NSManagedObjectModel
    1. Entities라고 불리는 모델 객체와 다른 Entity들과의 관계를 정의한다.
    2. 데이터 모델을 로드하고 Core Data Stack에 노출한다.
  2. NSManagedObjectContext
    1. 데이터베이스에 있는 객체를 보고 접근하게 해주는 window이다.
    2. Core Data Stack의 핵심. managed object context로 Core Data Stack과 소통하기 때문에 이 객체를 개발자가 가장 많이 사용한다.
  3. NSPersistentStoreCoordinator
    1. Core Data Stack의 연결을 도와준다. managed obejct model과 managed object context에 대한 참조를 유지한다.
    2. managed object model을 통해서 데이터 모델을 이해하고, persistent store를 관리한다.
  4. NSPersistentContainer
    1. Core Data Stack을 캡슐화한 컨테이너. 즉, NSPersistentContainer 클래스가 NSManagedObjectModelNSManagedObjectContextNSPersistentStoreCoordinator를 프로퍼티로 가지고 있다
    2. 코어 데이터의 데이터베이스라고 볼 수 있다.

5. Core Data Stack 추가

위에 Core Data Stack에 대해 설명했지만 NSPersistentContainer가 캡슐화를 하고 있어서 NSPersistentContainer만 만들어주면 나머지 것들을 관리할 수 있다.

struct PersistenceController {
  static let shared = PersistenceController()

  let container: NSPersistentContainer

  init(inMemory: Bool = false) {
		// 여기 name은 .xcdatamodeld의 파일명과 같이야 한다. 
    container = NSPersistentContainer(name: "ModelName")
    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    }
  }
	// context 변화에 대해 저장하는 method 생성
	func saveContext() {
	    let context = container.viewContext
	    if context.hasChanges {
	      do {
	        try context.save()
	      } catch {
	        let nserror = error as NSError
	        fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
	      }
	    }
	 }
}

6. 뷰 연결하기

  1. ProjectNameApp.swift 파일에서 싱글톤으로 만든 persistentController를 Environment로 ContentView에 전달한다.

    struct testApp: App {
        let persistenceController = PersistenceController.shared
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environment(\.managedObjectContext, persistenceController.container.viewContext)
            }
        }
    }
  2. CoreData를 사용할 파일에 접근(또는 생성하여) Environment로 제공된 context에 접근한다.

    //CoreDataView.swift
    @Environment(\.managedObjectContext) var managedObjectContext 
  3. 저장된 데이터를 가져오기 위해 @fetchrequest Property Wrapper를 사용해서 데이터를 가져온다.

    1. @fetchrequest는 데이터의 변경이 생길 때마다 fetch하기 때문에 데이터와 UI를 동기화하는데 좋다.
    2. 어떤 데이터를, 어떤 순서로, 어떤 조건으로 가져올 것인지 predicate를 통해 정의할 수 있다.
    3. predicate refercence
    4. predicate refercence
    //CoreDataView.swift
    @FetchRequest(
      entity: Person.entity(), 
    	sortDescriptors:[
          NSSortDescriptor(keyPath: \Person.age, ascending: true)
      ]
    ) var people: FetchedResults<Person>
  4. CRUD 메소드를 구현한다.

    //CoreDataView.swift
    func saveContext() {
        do {
          try managedObjectContext.save()
        } catch {
          print("Error saving managed object context: \(error)")
        }
    }
    func addPerson(name: String, age: Int32) {
        let newPerson = Person(context: managedObjectContext)
    
        newPerson.name = name
        newPerson.age = age
    
        saveContext()
    }
    
    func deletePerson(at offsets: IndexSet) {
      offsets.forEach { index in
        let person = self.people[index]
        self.managedObjectContext.delete(person)
      }
      saveContext()
    }
  5. 추가 제거를 위한 간단한 UI를 구현한다.

    //CoreDataView.swift
    VStack {
        Spacer()
            .frame(height: 50)
        VStack {
            ForEach(people, id: \.self) { p in
                HStack {
                    Text("\(p.name ?? "defaultName")")
                    Spacer()
                    Text("\(p.age)")
                }
                .font(.title)
                .padding(.bottom, 20)
            }
        }
        Spacer()
        Button {
            addPerson(name: "person\(people.count)", age: Int32(people.count + 20))
        } label: {
            Text("add person")
                .frame(width: 300, height: 50)
                .background(.yellow)
        }
        Button {
            deletePerson(at: IndexSet([people.count - 1]))
        } label: {
            Text("delete")
                .frame(width: 300, height: 50)
                .background(.orange)
        }
        Spacer()
            .frame(height: 30)
    }
    .padding()

전체 코드

//
//  testApp.swift
//  test
//
//  Created by Seungui Moon on 2023/09/26.
//

import CoreData
import SwiftUI

struct PersistenceController {
static let shared = PersistenceController()

let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "TestModel")
        container.loadPersistentStores { _, error in
          if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
          }
        }
    }

    func saveContext() {
      let context = container.viewContext
      if context.hasChanges {
        do {
          try context.save()
        } catch {
          let nserror = error as NSError
          fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
      }
    }
}

@main
struct testApp: App {
    let persistenceController = PersistenceController.shared
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}
//
//  CoreDataView.swift
//  test
//
//  Created by Seungui Moon on 10/9/23.
//

import SwiftUI

struct CoreDataTestView: View {
    @Environment(\.managedObjectContext) var managedObjectContext

    @FetchRequest(
        entity: Person.entity(), sortDescriptors:[
            NSSortDescriptor(keyPath: \Person.age, ascending: true)
        ]
    ) var people: FetchedResults<Person>

    var body: some View {
        VStack {
            Spacer()
                .frame(height: 50)
            VStack {
                ForEach(people, id: \.self) { p in
                    HStack {
                        Text("\(p.name ?? "defaultName")")
                        Spacer()
                        Text("\(p.age)")
                    }
                    .font(.title)
                    .padding(.bottom, 20)
                }
            }
            Spacer()
            Button {
                addPerson(name: "person\(people.count)", age: Int32(people.count + 20))
            } label: {
                Text("add person")
                    .frame(width: 300, height: 50)
                    .background(.yellow)
            }
            Button {
                deletePerson(at: IndexSet([people.count - 1]))
            } label: {
                Text("delete")
                    .frame(width: 300, height: 50)
                    .background(.orange)
            }
            Spacer()
                .frame(height: 30)
        }
        .padding()
    }
    
    func saveContext() {
        do {
          try managedObjectContext.save()
        } catch {
          print("Error saving managed object context: \(error)")
        }
    }
    func addPerson(name: String, age: Int32) {
        let newPerson = Person(context: managedObjectContext)

        newPerson.name = name
        newPerson.age = age

        saveContext()
    }

    func deletePerson(at offsets: IndexSet) {
      offsets.forEach { index in
        let person = self.people[index]
        self.managedObjectContext.delete(person)
      }
      saveContext()
    }
}

#Preview {
    CoreDataTestView()
}

reference


📸 Screenshot

기능 스크린샷
Create
Delete

🗒️ Review Point

- 데이터가 많아지게 될 경우 Entity간의 관계가 깊어지게 될텐데 그럴 경우 Model 구조를 어떻게 짜야하는지 고민해봐야할 것 같습니다.
- CoreData에 대해서는 공부했지만 Firebase를 사용하게 될 경우 json이 필요할 것 같습니다. 서버를 운영할지, Firebase로 일단 진행할지 논의가 필요할 것 같습니다. 

@moonkey48 moonkey48 added ✨ feature 기능개발 📁 data 데이터와 관련이 있음 labels Oct 9, 2023
@moonkey48 moonkey48 added this to the Baseline Project milestone Oct 9, 2023
@moonkey48 moonkey48 self-assigned this Oct 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📁 data 데이터와 관련이 있음 ✨ feature 기능개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants