본문 바로가기
iOS/iOS

Core Data

by 패쓰킴 2023. 1. 4.
728x90

앱에서 데이터를 관리하는데 사용하는 프레임워크로 userDefault와 달리 좀 더 복잡한 데이터를 저장하는데 사용한다. 얼핏 DB라고 생각할 수 있으나 DB가 아니다. 

CoreData는 앱이 설치된 해당 기기에서 저장된 데이터를 사용하므로 앱이 삭제되면 데이터도 삭제 되지만,

DB는 데이터를 관리하는 시스템으로 여러 사용자나 응용프로그램과 공유 및 동시 접근이 가능하다.

 

*raywenderlich의 Core Data 공부 내용을 바탕으로 작성되었습니다.*

 

 

- 기본적으로, 코어데이터는 SQLite database를 영구적인 저장소로 사용한다.

- Xcode에서 프로젝트를 생성할 때 'Use Core Data' 박스를 체크하면 AppDelegate.swift에서 `NSPersistentContainer` 코드를 생성한다.

 

NSManagedObject

: 코어데이터에 저장된 단일 객체를 나타내며, 데이터의 생성/수정/저장/삭제 하는데 사용된다.

NSManagedObjectModel

: 데이터 모델에 있는 각 객체 유형, 속성, 관계를 나타낸다.

NSPersistentStore

: 데이터를 읽고 쓰는데 사용된다.

NSPersistentStoreCoordinator

: NSManagedObjectModel과 NSPersistentStore를 연결해주는 역할을 한다.

  NSPersistentStore 구성 방식에 대한 세부 정보를 숨기고 NSManagedObjectContext에 대한 간단한 인터페이스를 제공한다.

  데이터모델을 이해하고 데이터를 주고받는 실제 DB 작업이 이루어진다.

NSManagedObjectContext

: 데이터 객체가 존재하는 영역으로 객체의 생명주기를 관리하는 역할을 한다.

  fetch, 편집, 유효성 검사 등 보다 강력한 기능을 담당한다.

NSPersistentContainer

: 코어데이터에서 정보를 저장하고 가져오는 일을 수월하게 해주는 객체의 집합으로 이루어져 있다. 

 

----- BASIC -----

사용

Person이라는 객체의 name 속성을 이용한다면,

1. xcdatamodeld파일에서 ENTITIES는 Person으로, Attributes에는 String 타입의 name을 추가해준다.

2. name에 데이터를 넣자.

    새로운 데이터의 저장이 필요한 곳에 save()를 호출하여 managed object context에 저장한다.

// CoreData import 한다.
inport CoreData

class ViewController: UIViewController {
  // 데이터를 가질 프로퍼티를 생성한다.
  var people: [NSManagedObject] = []
  
  // 들어오는 name 문자열을 저장할 함수
  func save(name: String) {
    // 1
    // coredata를 사용하려면 먼저, NSManageObjectContext가 필요하다.
    // 코어데이터에 데이터를 저장하려면, 새로운 객체를 'managed object context'에 넣어야 한다.
    // (이 managed object context는 NSPersistentContainer 프로퍼티 안에 존재 한다)
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
    let managedContext = appDelegate.persistentContainer.viewContext
    
    // 2
    // 코어데이터의 엔티티를 연결
    let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!
    let person = NSManagedObject(entity: entity,insertInto: managedContext)
    
    // 3
    // NSManagedObject를 연결하고 나면 KVC key를 정확히 넣어주어야 한다.
    person.setValue(name, forKeyPath: "name")
    
    // 4
    // managed object context에 새로운 데이터를 저장
    // 저장에 실패할 경우 error를 프린트한다.
    do {
      try managedContext.save()
      people.append(person)
    } catch let error as NSError {
      print("error : \(error), \(error.userInfo)")
    }
  }

}

 

3. 저장되어 있는 데이터를 가져온다.

person의 name에 접근(이 필요한 위치에 작성)

// Person entities에 저장되어 있는 name 속성에 접근하여 각 인덱스 자리에 해당하는 값을 가져온다.
let person = poeple[인덱스넘버]
person.value(forKeyPath: "name") as? String

 

4. 저장된 데이터에 항상 접근되도록 fetch 해준다.

    왜 viewWillAppear에서 작업하는지 이해가 되지 않는다면

    view의 생명주기에 대해 공부해보면 된다.

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  
  //1
  // manageObjectContext에 접근하기 위해 위와 동일하게 진행해준다
  guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  let managedContext = appDelegate.persistentContainer.viewContext
  
  //2
  // NSFetchRequest는 코어데이터에서 fetch 해주는 클래스
  // init(entityName:)으로 초기화하면 특정 엔터티의 모든 개체를 가져옵니다.
  // 여기에선 Person을 가져오기 위한 작업
  let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
  
  //3
  // 가져온 Person을 managed object context에 fetch해준다.
  // fetch() 또한 error를 발생 시킬 수 있으니 do-catch문을 사용해준다.
  do {
    people = try managedContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("Could not fetch. \(error), \(error.userInfo)")
  }
}

 

----- STARTER -----

BASIC에서 엔티티의 키값에 접근하기 위해 KVC방법을 이용했지만 이는 오타 발생시 에러의 위험성을 야기한다.

따라서, Person.name 처럼 프로퍼티로 접근하기 위한 방법을 알아본다.

 

먼저, 다양한 Type을 다루면서 방법을 알아보려 한다.

 

- 이미지

데이터의 타입을 이미지로 설정하고 싶다면 'Binary Data'를 사용한다.

Binary Data는 이지미 뿐만 아니라 PDF파일도 될 수있다. (0과 1로 시리얼라이즈 가능한 모든 것)

 

하지만, Binary Data타입은 엔티티에 액세스할 때마다 메모리에 로드되어 많은 양이 저장되면 앱 성능을 떨어 뜨린다. 때문에, Binary Data type의 속성에는 'Allows External Storage' 옵션을 활성화 해주어야 한다.

이 옵션을 활성화 하면, 코어데이터는 데이터를 데이터베이스에 직접 저장해야 하는지 별도의 URI를 저장해야 하는지 결정해준다.

 

- 색상

UIColor로 타입을 지정해도 되지만,

UIColor는 Data도 Binary Data타입으로도 시리얼라이즈 되지만,

UIColor <-> Data <-> Binary Data 와 같은 작업이 필요하다면 Transformable 타입이 적합하다.

다만, Transformable을 이용하기 위해서는 해줘야 하는 작업이 있다.

 

본론, 

엔티티.프로퍼티 접근 ( color 기준 )

1. 엔티티 속성의 Codegen 값을 변경한다.

    엔티티를 추가하고 컴파일을 한번 한 후에 작업 해주어야 한다.

    Manual/None으로

2. Cocoa Touch template의 subclass of: NSSecureUnarchiveFromDataTransformer 파일을 새로 생성한다.

3. '엔티티명+CoreDataProperties.swift'가 생성이 된다.

4. 이 파일에 아래의 소스를 넣어준다.

import UIKit

class 클래스명: NSSecureUnarchiveFromDataTransformer {

  //1
  // 디코드 할 수 있는 클래스 목록을 리턴해준다.
  // UIColor를 디코드 하기 위해 UIColor를 배열로 갖도록 추가해준다.
  override static var allowedTopLevelClasses: [AnyClass] {
    [UIColor.self]
  }

  //2
  // ValueTransformer를 통해 이 클래스의 등록이 가능하다.
  // ValueTransformer는 키-값을 매핑해주는 역할을 한다.
  static func register() {
    let className =
      String(describing: 클래스명.self)
    let name = NSValueTransformerName(className)

    let transformer = 클래스명()
    ValueTransformer.setValueTransformer(
      transformer, forName: name)
  }
}

5. 앱 실행 시 등록 작업이 이루어 지도록 AppDelegate의 didFinishLaunchingWithOptions 함수에 호출해준다.

func application(_ application: UIApplication, didFinishLaunchingWithOptions
  launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

  ColorAttributeTransformer.register()
  return true
}

6. 그리고 이 클래스가 'color'에 적용 되도록 속성을 변경 해준다.

7. Xcode의 메뉴에서 Editor - Create NSManagedObject Subclass... - 엔티티 명이 체크되어 있는지 확인 후 Create!

8. '엔티티명+CoreDataClass.swift' 파일이 생성된다.

    CoreData를 import 해준다.

import CoreData

 

이제 생성한 엔티티를 적용해본다.

데이터를 segmentControl을 사용하여 각 버튼별로 각 데이터가 보이도록 해본다.

 

1. ViewController.swift 파일에

import CoreData

// 전역변수 선언
var managedContext: NSManagedObjectContext!

 

2. 화면에 보여줄 데이터를 Property List 파일을 생성하여 작성해준다.

(2개를 보여준다는 가정하에 ->)

이런식으로

3. 코어데이터에 데이터를 넣을 수 있도록 별수 함수를 작성해준다.

func inserData() {
  let fetch = Person.fetchRequest()
  // NSPredicate(format: )은 코어데이터에서 데이터를 검색(fetch)할 때 사용하는 방식 
  fetch.redicate = NSPredicate(format: "searchKey != nil")
  let dataCount = (try? managedContext.count(for: fetch)) ?? 0
  
  if dataCount > 0 { return }
  
  // forResource에는 아까 생성한 propertyList 파일명을 넣어준다.
  let path = Bundel.main.path(forResource: "PersonData", ofType: "plist")
  let dataArr = NSArray(contentsFile: path!)!
  
  for item in dataArr {
    let entity = NSEntityDescription.entity)forEntityName: "Person", in: managedContext)!
    let perston = Person(entity: entity, inserInto: managedContext)
    let dict = item as! [String:Any]
    
    let color = dict["color"] as! [String:Any]
    let imageName = dict["img"] as? String
    let img = UIImage(named: imageName!)
    
    person.name = dict["name"] as? String
    person.searchKey = dict[searchKey"] as? String
    person.color = UIColor.color(dict: color)
    person.photo = img?.pngData()
  }
  
  try? managedContext.save()
}

 

4. UIColor에 대한 extenstion을 작성해준다.

private extension UIColor {
  
  static func color(dict: [String: Any]) -> UIColor? {
    guard 
      let red = dict["red"] as? NSNumber,
      let green = dict["green"] as? NSNumber,
      let blue = dict["blue"] as? NSNumber else {
        return nil
    }
    
    return UIColor(
      red: CGFloat(truncating: red) / 255.0,
      green: CGFloat(truncating: green) / 255.0,
      blue: CGFloat(truncating: blue) / 255.0,
      alpha: 1)
  }
}

 

5. 데이터 fetch 해오기

override func viewDidLoad() {
  super.viewDidLoad()
  
  // entity 접근 준비
  let appDelegate =  UIApplication.shared.delegate as? AppDelegate
  managedContext = appDlegate?.persistentContainer.viewContext
  
  // 데이터 넣어주기
  inserData()
  
  // searchKey를 통해 데이터 요청
  let request = Person.fetchRequest()
  let firstTitle = segmentedControl.titleForSegment(at:0) ?? ""
  request.predicate = NSPredicate(format: "%K = %@", argumentArray: [#keyPath(Person.searchKey), firstTitle])
  
  do {
    let results = try managedContext.fetch(request)
    if let person = results.first {
      populate(person: person)
  } catch let error as NSError {
    print("fetch error: \(error),\(error.userInfo)")
  }
  
}
func populate(person: Person) {
  guard
    let imageData = person.photo as Data?,
    let color = person.color else { return }
  
  이미지뷰.image = UIImage(data: imageData)
  이름레이블.text = person.name
  self.view.backgroundColor = color
}

 

6. 현재 눌려있는 버튼의 데이터를 가지고 있을 전역 변수를 선언한다.

var current: Person!

 

7. populate 호출부 위에 현재 데이터를 대입해준다.

do {
    let results = try managedContext.fetch(request)
    if let person = results.first {
      current = person
      populate(person: person)
  } catch let error as NSError {
    print("fetch error: \(error),\(error.userInfo)")
  }

 

8. segmentedControl의 버튼이 눌리면 해당 버튼의 데이터가 보이도록 해준다.

@IBAction func segmentedControl(_ sender: UISegmentedControl) {
  guard let selectedValue = sender.titleForSegment(
    at: sender.selectedSegmentIndex) else {
      return
  }

  let request = Person.fetchRequest()
  request.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(Person.searchKey), selectedValue])

  do {
    let results = try managedContext.fetch(request)
    current = results.first
    populate(person: current)
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

 

728x90

'iOS > iOS' 카테고리의 다른 글

설정앱의 특정 화면으로 이동??  (0) 2023.01.06
CustomView의 super  (0) 2023.01.04
UIViewController PopUp  (0) 2022.12.21
UITableView  (0) 2022.12.19
UIDatePicker  (0) 2022.12.08

댓글