안녕하세요 후르륵짭짭 입니다.
이번에 Core Data에 대해 알아보려고 합니다.
Core Data는 아이폰 내부에 데이터를 저장하는 방법 입니다.
내용이 많을 수 있으니,,, 후우,,,
** CoreData 생성하기 **
프로젝트를 만들 때, coreData 생성하기 버튼을 누르면 AppDelegate에 CoreData 관련 코드와 .xcdatamodeld 파일이 생깁니다.
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "CoreData_Tuto")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
이렇게 생기는데, persistentContainer는 CoreData의 내용을 수정 관리 해주는 프로퍼티 입니다.
그리고 persistentContainer는 NSPersistentContainer 타입인데, 여기에 viewContext가 있습니다.
// An instance of NSPersistentContainer includes all objects needed to represent a functioning Core Data stack, and provides convenience methods and properties for common patterns.
@available(iOS 10.0, *)
open class NSPersistentContainer : NSObject {
open class func defaultDirectoryURL() -> URL
open var name: String { get }
open var viewContext: NSManagedObjectContext { get }
open var managedObjectModel: NSManagedObjectModel { get }
open var persistentStoreCoordinator: NSPersistentStoreCoordinator { get }
open var persistentStoreDescriptions: [NSPersistentStoreDescription]
,,, 생략
}
여기서 viewContext는 coreData의 내용을 관리해주는 프로퍼티 입니다.
이제 본격적으로 데이터를 생성해보도록 하겠습니다.
이렇게 Add Entity 버튼을 눌러주면 Person 과 같은 객체를 생성 할 수 있습니다.
오른쪽에 Name이 객체의 이름을 정해주는 부분 입니다.
그리고 Codegen이 CoreData의 데이터들을
Manual/None - 수동으로 관리 , Class Definition - 자동으로 관리, Category/Extension - 부분 관리
이렇게 되어 있습니다.
그런데 여기서 저는 Manual/None - 수동으로 관리로 해주겠습니다.
그러면 아래 처럼 두개의 파일이 생기는데,
import Foundation
import CoreData
@objc(Person)
public class Person: NSManagedObject {
}
이 부분은 코드에 새로운 함수를 정의하거나, 특별한 기능을 만들 때 사용하고
extension Person {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
return NSFetchRequest<Person>(entityName: "Person")
}
@NSManaged public var age: String?
@NSManaged public var birth: Date?
@NSManaged public var name: String?
@NSManaged public var todoList: NSSet?
}
// MARK: Generated accessors for todoList
extension Person {
@objc(addTodoListObject:)
@NSManaged public func addToTodoList(_ value: Todo)
@objc(removeTodoListObject:)
@NSManaged public func removeFromTodoList(_ value: Todo)
@objc(addTodoList:)
@NSManaged public func addToTodoList(_ values: NSSet)
@objc(removeTodoList:)
@NSManaged public func removeFromTodoList(_ values: NSSet)
}
extension Person : Identifiable {
}
이 부분은 기본적으로 제공 되는 객체의 프로퍼티와 기본 제공 함수들이 있습니다.
그런데 저는 Relation을 가진 함수를 만들 것이기 때문에 위에 Person 객체와 Todo 객체를 생성 했습니다.
그래서 관계를 표현 하면 Person 객체는 여러개의 Todo 객체를 가질 수 있도록 되어 있습니다.
그러니깐 Person은 Todo를 여러 N 개 , Todo는 Person을 하나만 가지게 되어 있습니다.
그럼 이제 이것을 생성 해보도록 하겠습니다.
** Relation Data 생성하기 **
이렇게 Todo 객체를 생성한 다음 Person 객체에서 relationship에 + 버튼을 눌러 줍니다.
이제 위에 그림에서 그린 것 처럼 서로의 관계를 정의 해줘야합니다.
relationship(관계 변수)에는 각 관계의 이름을 지정하는 것이고
Destination(관계 목적지)에는 관계의 객체를 지정하고
Inverse(역 관계)는 서로의 역관계를 이루고 있는 관계 변수를 지정해주는 것 입니다.
그래서 위에 처럼 Person은 Todo와 관계를 가지고 있고 그 관계 이름이 todolist 인 것이고
Todo객체에서 Person과 역 관계를 이뤄져 있고 그 관계 변수 이름이 master인 것 입니다.
그럼 이제 삭제 법칙(Delete Rule)에 대해 알아보록 하겠습니다.
Deny - Department 객체를 삭제 할 때, 만약 Department객체가 employee 객체 하나라도 가지고 있다면 삭제 하지 않는다.
그러니깐, 삭제하는 객체가 관계된 객체가 하나라도 존재한다면 삭제하지 않는 것을 의미합니다.
Nullify - employees의 객체를 하나 삭제하더라도 그와 연결된 Department는 삭제하지 않는 것
그러니깐, 하위 객체를 삭제할 때 부모 객체는 건들지 않는 것을 의미한다.
Cascade - 목적지에 해당하는 Department 객체를 삭제 할때, 그 하위 객체인 Employees를 다 삭제하는 것
그러니깐, 하위 객체를 모두 삭제할 때 주로 사용된다.
No Action은 삭제 할때 어떠한 영향도 주지 않는 것을 의미합니다.
이제 관계법칙(Type)에 대해 설명해보도록 하겠습니다.
To one - 연산관계가 하나만 해당하는 것을 의미합니다.
To many - 연산관계가 여러개를 형성할 때 의미합니다.
이렇게 모든 관계를 정의하고 나면 Editor -> Create NSManagedObject Subclass에 들어가서 필요한 객체들을 생성해주면 됩니다.
기본적인 방법에 대해 알게 됐으니, 코드로 CRUD를 만드는 방법에 대해 적어보도록 하겠습니다.
** viewContext에 접근하기 **
우리는 appDelegate에 PersistenceContainer에 접근해서 CoreData를 접근해야합니다.
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
이렇게 전역변수로 context를 생성해주므로서 Core Data의 관리 프로퍼티를 생성 해줍니다.
** 단일 객체의 CRUD **
각 객체는 fetch함수가 있습니다.
@nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
return NSFetchRequest<Person>(entityName: "Person")
}
이것은 Person 객체의 모든 데이터를 가져오는 것을 의미합니다.
그래서 아래 처럼 Fetch를 하면 모든 Person 객체를 가져오게 됩니다.
//READ
func fetchItems(){
do{
let temp = try context.fetch(Person.fetchRequest())
items = temp as? [Person]
DispatchQueue.main.async {
self.maintableView.reloadData()
}
}
catch (let error){
print( "fetch Data Error :\(error.localizedDescription)")
}
}
근데, 이때 temp는 [Any] 타입이기 때문에 타입 캐스팅으로 [Person]으로 변경해줘야합니다.
//CREATE
@objc private func plusBtn(){
let alert = UIAlertController(title: "Add Person", message: "What is there name?", preferredStyle: .alert)
alert.addTextField { (textfeld) in
textfeld.placeholder = "Insert name"
}
let submitButton = UIAlertAction(title: "Add", style: .default, handler: { (action) in
let textField = alert.textFields![0]
//Create a new Person Object -> context가 추적한다.
let newPerson = Person(context: self.context)
newPerson.name = textField.text
newPerson.age = "25"
newPerson.birth = Date()
//Save the Data
do{
try self.context.save()
}
catch(let error){
print( "Save Data Error :\(error.localizedDescription)")
}
self.fetchItems()
})
alert.addAction(submitButton)
present(alert, animated: true, completion: nil)
}
이제 CoreData에 데이터를 저장하는 방법에 대해 알아보도록 하겠습니다.
//Create a new Person Object -> context가 추적한다.
let newPerson = Person(context: self.context)
newPerson.name = textField.text
newPerson.age = "25"
newPerson.birth = Date()
//Save the Data
do{
try self.context.save()
}
catch(let error){
print( "Save Data Error :\(error.localizedDescription)")
}
이것을 보면 newPerson = Person(context: self.context)로 되어 있습니다.
이렇게 하면 위에서 전역변수로 선언한 context에 새로운 Person객체를 생성하고
Person 객체의 모든 정보를 추적해서 데이터를 생성합니다.
그리고 마지막에 context에 save를 해서 저장할 수 있도록 합니다.
//DELETE
let delete = UIContextualAction(style: .normal, title: "삭제") { (action, view, nil) in
let personToRemove = self.items![indexPath.row]
self.context.delete(personToRemove)
do{
try self.context.save()
}
catch (let error){
print( "Delete Data Error :\(error.localizedDescription)")
}
self.fetchItems()
}
이제 삭제를 하도록 하겠습니다.
위와 같이 Person 타입에 해당하는 personToRemove를
context.delete(삭제할 CoreData 객체) 를 넣어주면
Core Data에서 삭제를 해줍니다.
그리고 context.save를 해주면 됩니다.
//UPDATE
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let personItem = self.items![indexPath.row]
let saveButton = UIAlertAction(title: "Save", style: .default, handler: {_ in
let textField = alert.textFields![0]
personItem.name = textField.text
do{
try self.context.save()
}
catch (let error){
print( "Update Data Error :\(error.localizedDescription)")
self.context.rollback()
}
self.fetchItems()
})
,,, 생략
}
이제 업데이트를 알아보도록 하겠습니다.
다른 것과 다른 것 없이 Person 객체에 해당하는 personItem을 가져온 다음에
그 personItem의 정보만 변경해주고 context.save를 해주면 됩니다.
** 관계 객체의 CRUD **
일단 관계 객체의 CRUD를 보기 전에 Person 객체의 구성을 보도록 하겠습니다.
extension Person {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
return NSFetchRequest<Person>(entityName: "Person")
}
@NSManaged public var age: String?
@NSManaged public var birth: Date?
@NSManaged public var name: String?
@NSManaged public var todoList: NSSet?
}
// MARK: Generated accessors for todoList
extension Person {
@objc(addTodoListObject:)
@NSManaged public func addToTodoList(_ value: Todo)
@objc(removeTodoListObject:)
@NSManaged public func removeFromTodoList(_ value: Todo)
@objc(addTodoList:)
@NSManaged public func addToTodoList(_ values: NSSet)
@objc(removeTodoList:)
@NSManaged public func removeFromTodoList(_ values: NSSet)
}
이렇게 보면 우리가 생성했던 todo객체를 담고 있는 todoList가 있습니다.
그런데 NSSet으로 되어 있습니다.
이것은 OBJ - C로 되어 있고 Set 타입이라는 것을 알 수 있습니다.
그리고 밑에 여러가지 함수가 있는데,
위에 value : Todo는 하나의 객체만 다루는 것이고
value : NSSet은 여러개의 객체를 다루는 것을 의미합니다.
그리고 함수의 이름을 보면 add(추가) , remove(삭제)인 것을 쉽게 알 수 있습니다.
//READ
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let showList = UIContextualAction(style : .normal, title : "내용출력"){ (_,_,_) in
let personInfo = self.items![indexPath.row]
if let list = personInfo.todoList?.allObjects as? [Todo] , !list.isEmpty{
print("Not empty")
for item in list{
print(item.name!)
}
}
else{
print("It's empty")
}
}
,,, 생략
}
어렵지 않게 데이터에 접근 할 수 있습니다.
일단 Person에 해당하는 객체를 personInfo로 가져옵니다.
그리고 personInfo에 todoList 에 allObject를 사용해서 현재 Person과 연결된 모든 Todo 객체를 가져옵니다.
그런데 [Any]으로 되어 있기 때문에 [Todo] 타입으로 형 변환 한 뒤에 사용해주면 됩니다.
//CREATE
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let personItem = self.items![indexPath.row]
let alert = UIAlertController(title: "Edit Person", message: "Edit Name", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.text = personItem.name
}
let saveButton = UIAlertAction(title: "Save", style: .default, handler: {_ in
let textField = alert.textFields![0]
let newTodo = Todo(context: self.context)
newTodo.name = textField.text
personItem.addToTodoList([newTodo])
do{
try self.context.save()
}
catch (let error){
print( "Update Data Error :\(error.localizedDescription)")
self.context.rollback()
}
self.fetchItems()
})
,,,, 생략
일반적인 단일 객체와 다른 것 없이 person 객체를 가져온 다음
CoreData의 Todo 객체를 생성하고 그것을
Person.addToTodoList 함수를 이용해서 넣어준 다음에
context.save()를 해주면 됩니다.
//Delete
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "내부 1번 삭제") { (_, _, _) in
let personToRemove = self.items![indexPath.row]
if let item = personToRemove.todoList?.allObjects as? [Todo] , !item.isEmpty{
self.context.delete(item[0])
do{
try self.context.save()
}
catch (let error){
print( "Delete Data Error :\(error.localizedDescription)")
}
self.fetchItems()
}
}
삭제하는 것도 다르지 않습니다.
모든 CoreData 객체는 Class로 이뤄져 있기 때문에
주소값을 가지게 됩니다. 즉, 참조 형태이기 때문에,
위에 처럼 personToRemove라는 Person 객체를 가지고 옵니다.
그리고 personToRemove.todoList.allObject의 첫번째 요소를 가져 온 다음
context.delete(item[0])을 해주면 CoreData에서 모두 삭제가 됩니다.
그리고 context에 저장을 해주면 끝! 입니다.
소스 코드 :
github.com/HururuekChapChap/Xcode_TestProj/tree/master/CoreData_Tuto/CoreData_Tuto
참고 사이트 :
Relation 관계에 대한 이론적 설명 :
Relation 관계의 CRUD를 코드로 작성하는 방법 :
CoreData와 Relation에 대한 좋은 설명이 있는 한글 블로그 :
ios-development.tistory.com/93
부모객체에서 하위 객체의 정보를 가져오는 방법 :
stackoverflow.com/a/35056797/13065642
'Xcode > Swift - PlayGround' 카테고리의 다른 글
PlayGround) Class func 와 Static func의 차이가 머지? (0) | 2021.01.24 |
---|---|
PlayGround) 참조값으로 받을 수 있는 함수와 클로저의 놀라운 기능! (0) | 2021.01.14 |
PlayGround) Codable ANY 타입 처리 방법 (0) | 2020.12.19 |
PlayGround) DateFormatter를 이용해서 날짜를 변경하자 (2) | 2020.12.18 |
PlayGround ) Closure의 기능은 무엇인가? (0) | 2020.12.08 |
댓글