📌 개념 정리
UIViewController
의 생명주기는 뷰 컨트롤러가 생성되어 화면에 표시되고 사라질 때까지의 일련의 과정이다. 이를 이해하고 사용하면 앱 상태 관리, 뷰 초기화, 데이터 처리 등을 효율적으로 구현할 수 있다.
viewDidLoad
는 새로운 프로젝트를 만들거나 새 파일을 만들 때 cocoa touch
파일을 만들 경우 자동으로 재정의 되는 메소드이기 때문에 자주 사용하고, 자주 보는 메소드이다.
viewDidLoad
는 뷰 컨트롤러의 뷰가 메모리에 로드된 후 한 번만 호출된다. 때문에 주로 뷰의 초기 설정을 진행하거나 데이터를 로드 해야할 때, UI 요소를 초기화할 때 사용한다.
// viewDidLoad 사용 예시
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad: 뷰가 메모리에 로드됨")
}
viewWillAppear
는 뷰가 화면에 표시되기 직전에 호출된다. 만약 뷰를 업데이트 해야 하거나 화면 표시가 되기 전에 필요한 작업이 있을 경우 viewWillAppear
메소드 내에서 선언하여 사용하면 된다.
// viewWillAppear 사용 예시
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear: 뷰가 곧 표시됨")
}
viewDidAppear
는 뷰가 화면에 표시된 직후에 호출된다. 주로 애니메이션이 시작되거나 네트워크 호출, 사용자 인터랙션을 활성화 할 때 viewDidAppear
에서 선언하여 사용한다.
// viewDIdAppear 사용 예시
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear: 뷰가 화면에 표시됨")
}
viewWillDisappear
는 뷰가 화면에서 사라지기 직전에 호출된다. 주요 용도로는 데이터의 저장, UI 상태 저장, 애니메이션 종료 처리 등으로 사용된다.
// viewWillDisappear 사용 예시
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("viewWillDisappear: 뷰가 곧 사라질 예정")
}
viewDidDisappear
은 뷰가 화면에서 사라진 직후 호출된다. 주요 용도로는 리소슷 해체, 백그라운드 작업 시작, 상태 복원을 준비할 때 사용된다.
// viewDidDisappear 사용 예시
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("viewDidDisappear: 뷰가 화면에서 사라짐")
}
viewWillLayoutSubviews
는 뷰의 서브뷰 레이아웃이 설정되기 직전에 호출되는 메소드이다. 커스텀 레이아웃 조정이나 제약 조건 등의 수정이 필요할 때 주로 사용된다.
// viewWillLayoutSubviews 사용 예시
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("viewWillLayoutSubviews: 서브뷰 레이아웃 직전")
}
viewDidLayoutSubviews
는 뷰의 서브뷰 레이아웃이 모두 설정된 직후에 호출된다. 주로 최종 레이아웃의 상태를 확인하거나 복잡한 레이아웃을 변결할 때 사용된다.
// viewDidLayoutSubviews 사용 예시
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews: 서브뷰 레이아웃 완료")
}
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
func loadData() {
// API 호출이나 로컬 데이터 로드
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshUI()
}
func refreshUI() {
// 최신 데이터 반영
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
adjustLayout()
}
func adjustLayout() {
// 특정 뷰의 위치나 크기 조정
}
// 뷰가 생성될 때
viewDidLoad() → viewWillAppear() → viewDidAppear()
// 뷰가 사라질 때
viewWillDisappear() → viewDidDisappear()
// 서브뷰 레이아웃 조정 시
viewWillLayoutSubviews() → viewDidLayoutSubviews()
📌 개념 정리
CoreData
란 Apple의 프레임워크로, iOS 및 macOS에서 데이터를 영구적으로 저장하고 관리하는데 사용된다. 주로 객체 관계형 데이터베이스 관리 시스템(ORM)으로, 데이터 모델을 정의하고, 객체를 영구 저장소에 저장하거나 검색하는 역할을 한다.
CoreData
의 주요 구성 요소는 아래와 같다.
Managed Object Model (NSManagedObjectModel)
.xcdatamodeld
파일을 통해 시각적으로 구성하거나 코드로 생성할 수 있다.Managed Object Context (NSManagedObjectContext)
Persistent Store Coordinator (NSPersistentStoreCoordinator)
Managed Object (NSManagedObject)
먼저 새로운 프로젝트를 생성하고 프로젝트 이름이나 팀 등 기본 설정을 완료한 후 마지막에 있는 Storage
를 CoreData
로 설정한다
그리고 프로젝트를 생성하면 아래 사진처럼 .xcdatamodel
이라는 확장자를 가진 파일이 생성된 것을 확인할 수 있다.
이 파일이 데이터베이스가 될 CoreData
이다.
이제 이 파일을 누르고 화면을 보면 아무것도 없을텐데, 여기서 우리가 직접 데이터베이스를 생성해줘야 한다.
먼저 화면 하단의 Add Entity
버튼을 눌러 새로운 엔티티를 생성해준다.
엔티티를 생성하면 화면의 좌측에 새로운 엔티티가 생성되며 아래 사진과 같이 변하는 모습을 볼 수 있다.
우리는 엔티티의 이름을 원하는 대로 수정할 수 있는데, 만들고 싶은 데이터베이스에 맞는 이름을 설정해주면 좋을 것 같다.
오른쪽 화면을 보면 Attributes
, Relationships
, Fetched Properties
라는 항목이 존재하고 각각 다른 옵션을 가지고 있는 것을 볼 수 있는데 이것들은 무엇일까??
우리는 CoreData
에서 데이터 모델을 설계할 때 Entity
를 생성했다. 엔티티에는 저장할 데이터에 대한 정의를 해야하는데, Attributes
, Relationships
, Fetched Properties
라는 3가지 옵션으로 데이터를 저장할 수 있다.
먼저 Attributes
는 속성이라는 뜻으로, 엔티티 내의 데이터를 표현하는 개별 필드이다. 뜻 그래도 객체의 속성이라고 할 수 있으며, 데이터베이스의 열(column)에 해당한다.
사용 예시
User
엔티티에 name
, age
, email
속성을 추가.지원하는 데이터 타입
속성 추가 예시
Relationships
는 엔티티 간의 연결을 나타낸다. 이는 관계형 데이터베이스에서 테이블 간 관계를 정의하는 것과 같다. 두 엔티티가 어떻게 연결되는지를 설정하며, 1:N (One-to-Many), N:N (Many-to-Many) 등 다양한 관계 설정이 가능하다.
사용 예시
User
엔티티와 Order
엔티티가 있다고 가정했을 때, User
가 여러 개의 Order
를 가질 수 있는 관계를 정의할 때 사용한다.User
↔️ Order
: One-to-Many
관계 설정Student
↔️ Course
: Many-to-Many
관계 설정관계 설정 속성
Destination
: 관계가 연결되는 엔티티를 설정Inverse Relationship
: 반대 방향의 관계를 설정Delete Rule
: 관계된 객체가 삭제될 때의 행동을 설정 (예: Cascade
, Nullify
, Deny
)관계 설정 예시
User
엔티티에 orders
라는 관계 추가:Order
To-Many
Fetched Properties
는 동적으로 다른 엔티티에서 데이터를 가져오는 역할을 한다. 직접적인 관계가 설정되어 있지 않은 엔티티에서도 데이터를 쿼리하여 가져올 수 있다.
사용 예시
Department
엔티티에 속한 특정 Employee
객체들을 동적으로 가져올 때 사용설정 방법
Destination
: 참조할 엔티티를 설정Predicate
: 데이터를 필터링할 조건Employee
엔티티에서 특정 조건을 만족하는 직원만 가져오기Fetched Properties 예시
Department
엔티티의 activeEmployees
:Employee
status == "active"
위의 내용을 정리하면 아래 표와 같다.
옵션 | 설명 | 사용 예시 |
---|---|---|
Attributes | 엔티티의 개별 데이터 필드를 정의 | User 엔티티에 name , age , email 속성을 추가 |
Relationships | 엔티티 간의 관계 설정 | User ↔️ Order : One-to-Many 관계 설정 |
Fetched Properties | 동적으로 다른 엔티티에서 조건에 맞는 데이터를 가져옴 | Department 에서 특정 조건의Employees 목록 가져오기 |
엔티티의 데이터 설정까지 마무리하면 NSManagedObject subclass
파일로 만들어야 하는데, 그 전에 Code Generation
에 대한 설정이 필요하다.
Code Generation
은 Core Data
모델 파일(.xcdatamodel)에서 정의한 엔티티를 Swift 또는 Objective-C 코드로 변환하는 방법을 결정하는 것이다.
Manual/None
은 수동으로 코드를 생성한다는 뜻으로, 개발자가 직접 NSManagedObject subclass
파일을 만들어야 한다.
Xcode가 자동으로 파일을 생성하거나 업데이트하지 않기 때문에, Custom Logic을 작성하거나 Entity 클래스를 수동으로 관리하고 싶은 경우에 적합하다.
주의해야할 점은 데이터베이스에 변경 사항이 있을 때마다 개발자가 클래스 파일을 수동으로 업데이트 해야 한다는 점이다. 또, 모델에 새로운 속성이나 관계가 추가되면 해당 클래스를 직접 수정해야 한다.
Category/Extension
은 NSManagedObject
의 기본 클래스를 Xcode가 생성하고, 그 클래스의 확장(extension)을 사용하여 커스텀 코드를 작성할 수 있도록 하는 방식이다.
기본 속성 및 관계에 대한 코드는 자동 생성되지만, 커스텀 코드는 Extension 파일에 작성하도록 유도된다.
주로 엔티티에 대한 기본 코드는 자동으로 관리하면서 커스텀 메소드나 추가 로직을 작성해야 할 때 사용한다. 이 방식은 코드와 데이터를 분리하여 관리하기 때문에 유지보수에 용이하다.
Class Definition
는 NSManagedObject subclass
의 전체 정의를 Xcode가 자동으로 생성하는 방식이다.
모든 Attributes
와 Relationships
가 포함된 클래스 파일이 생성되며, 필요에 따라 커스텀 코드를 추가할 수 있다.
이 방식은 빠르게 모델을 구현하고 싶거나 프로젝트 규모가 작아 기본적인 CRUD(Create-Read-Update-Delete) 연산만 필요한 경우, 혹은 자동 생성된 코드에 커스텀 로직을 추가하고 싶은 경우 사용하기 적합하다.
단, 자동으로 생성된 파일을 직접 수정할 수 있지만, 모델 변경 시 Xcode가 이 파일을 덮어쓸 수 있으므로 커스텀 코드를 안전하게 유지하려면 Category/Extension
방식을 사용하는 것이 더 좋다.
옵션 | 특징 | 사용 상황 |
---|---|---|
Manual/None | 클래스 파일을 직접 작성 및 관리 | 커스텀 로직이 많거나, 완전히 수동으로 제어하고 싶을 때 |
Category/Extension | 자동 생성된 클래스와 확장 파일로 커스텀 코드 분리 | 기본 모델 관리는 자동으로 하고, 확장에서 커스텀 코드 작성시 |
Class Definition | 전체 클래스를 자동 생성, 커스텀 로직도 같은 파일에 작성 | 빠르게 구현하고 싶거나, 작은 프로젝트에서 기본적인 CRUD 작업만 필요한 경우 |
엔티티를 어떻게 변환할지 Cord Generator
까지 결정했다면 이제 데이터 베이스를 코드로 변환하는 작업을 진행한다.
화면 상단의 Editor
를 선택하고, 아래쪽에 있는 Create NSManagedObject Subclass...
옵션을 선택한다.
이후 데이터 베이스가 있는 프로젝트를 선택한 후 코드로 변환할 엔티티를 선택해준다.
이후 파일을 생성할 폴더를 선택하고 create를 누르면 해당 폴더에 데이터베이스 파일이 생성된다.
CoreDataClass | CoreDataProperties |
---|---|
우리는 프로젝트를 생성할 때 Storage
를 CoreData
로 선택했기 때문에 AppDelegate
파일을 확인해보면 새로운 코드가 추가되어 있는 모습을 볼 수 있다.
여기서 persistentContainer
라는 변수를 정의하는 코드가 있는데, 이 변수는 CoreData
의 핵심 개체로, NSPersistentContainer
클래스의 인스턴스를 의미한다. 이 개체는 CoreData
스택을 설정하고 관리하는 역할을 한다.
또, saveContext
라는 항목이 있는데, 우리가 데이터 베이스에 값을 추가하거나 삭제하는 등 변동을 주면 반드시 데이터 베이스에 해당 사실을 알려야 하기 때문에 saveContext
를 활용하여 데이터의 변동사항을 저장해야 한다.
persistentContainer
의 주요 역할은 아래와 같다.
Core Data 스택 관리:
Persistent Store 로드:
Managed Object Context 제공:
우리는 CoreData
를 활용하기 위해 persistentContainer
를 프로젝트 내부에 선언하여 사용해야 한다.
그러기 위해 먼저 ViewController
에 새로운 프로퍼티를 만들어준다.
private var container: NSPersistentContainer!
여기서 container
는 반드시 정의해줄 것이기 때문에 강제 옵셔널 언래핑으로 설정한다. NSPersistentContainer
타입은 AppDelegate
에 있던 persistentContainer
의 타입으로 클래스로 정의되어 있다.
이제 viewDidLoad()
메소드에서 위에서 만든 프로퍼티에 값을 정의해준다.
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
self.container = appDelegate.persistentContainer
}
먼저 AppDelegate
타입으로 캐스팅한 변수를 선언해주고, 해당 변수의 persistentContainer
를 위에서 만든 프로퍼티 container
의 값으로 정의해준다.
이렇게 하면 이제 CoreData
를 쓰기 위한 기초 준비가 완료된 것이다.
이번에는 메소드를 이용하여 CoreData
에 데이터를 추가하는 방법을 알아볼 차례이다.
CoreData
에 데이터를 추가하기 위해서는 우선 데이터 베이스와 연결된 Entity
를 정의하여 사용해야 한다.
private func createData(name: String, phoneNumber: String) {
guard let entity = NSEntityDescription.entity(forEntityName: "PhoneBook", in: self.container.viewContext) else { return }
}
위의 코드는 entity
라는 새로운 변수를 guard
문으로 옵셔널 체이닝하는 과정이다.
이 때, entity
의 값은 CoreData
와 연결된 엔티티로 정의하기 위해 엔티티 이름을 입력하고, 어떤 데이터 베이스의 엔티티인지 파라미터에 값을 입력한다.(위의 코드에서는 위에서 만든 container의 엔티티로 선언)
엔티티는 있을 수도 있고, 없을 수도 있기 때문에 옵셔널 타입이기 때문에 옵셔널 바인딩 하여 사용할 수 있다.
이제 메소드 내부에서 데이터 베이스에 값을 추가하는 코드를 입력하면 된다.
// 데이터 베이스 연결
let newPhoneBook = NSManagedObject.init(entity: entity, insertInto: self.container.viewContext)
newPhoneBook.setValue(name, forKey: "name")
newPhoneBook.setValue(phoneNumber, forKey: "phoneNumber")
위의 코드는 newPhoneBook
이라는 변수를 선언하고, 이 변수를 사용해서 데이터 베이스에 값을 추가하기 위해 이와 관련된 동작을 수행할 수 있는 NSManagedObject
타입으로 정의한 뒤 setValue
메소드를 사용하여 데이터 베이스에 값을 추가하는 코드이다.
여기서 파라미터에는 위에서 만든 엔티티를 넣고, 어떤 데이터 베이스에 넣을 것인지 설정해준다.
그리고 메소드의 매개변수로 받는 name
과 phoneNumber
를 각각 데이터 베이스의 "name"
, "phoneNumber"
키에 저장한다.
이제 마지막으로 추가한 값을 데이터 베이스에 저장하면 된다.
do {
// 변동사항을 데이터 베이스에 저장
try self.container.viewContext.save()
print("Data storage successful")
} catch {
print("Failed to save data", error)
}
데이터를 저장할 때는 반드시 try
를 사용해야 하는데, 그 이유는 데이터를 저장하는 save()
메소드가 에러를 던질 수 있는 throws
의 형태이기 때문이다.
이제 이 메소드를 사용하면 데이터 베이스에 쉽게 데이터를 저장할 수 있다. 하지만 이 메소드는 값을 저장만 할 수 있기 때문에 잘 저장이 되었는지 확인할 수 없다.
그럼 이제부터 데이터를 읽는 메소드를 제작해보자.
위에서 만든 메소드로 저장한 데이터를 확인하기 위해 Read 메소드를 만들어보자.
우선 메소드를 만들고 do-try-catch
문을 사용해 데이터 베이스의 값을 가져오자.
private func readAllData() {
do {
// 데이터 베이스의 모든 데이터를 가져옴
let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
} catch {
print("Failed to call")
}
}
위 코드는 뷰 컨트롤러에 정의한 persistentContainer
의 fetch
메소드를 사용하여 데이터 베이스에 저장된 값을 새로 정의한 변수에 저장하는 코드이다. 이 때, fetch
는 에러를 던지는 throws
타입이기 때문에 try
를 사용하여 호출해야 한다.
fetch
메소드의 파라미터인 PhoneBook
은 Create NSManagedObject Subclass... 으로 엔티티를 코드화 했을 때 생성된 클래스이다. 이 클래스는 NSManagedObject
클래스를 상속하고 있기 때문에 fetchRequest()
메소드를 사용할 수 있는데, 해당 메소드는 엔티티를 코드화 했을 때 같이 생성된 파일인 CoreDataProperties
파일에 정의되어 있다.
extension PhoneBook {
@nonobjc public class func fetchRequest() -> NSFetchRequest<PhoneBook> {
return NSFetchRequest<PhoneBook>(entityName: "PhoneBook")
}
// 생략...
}
이 메소드는 데이터 베이스와 연결된 엔티티의 데이터를 NSFetchRequest<PhoneBook>
의 형태로 가공하여 리턴하는 메소드이다. NSFetchRequest<ResultType>
은 제네릭 타입을 가지는 클래스로, 여기서는 해당하는 엔티티의 데이터를 데이터 베이스의 클래스 타입(여기서는 PhoneBook 클래스)의 배열로 반환해주는 메소드라고 이해하면 된다.
이제 데이터를 불러왔으니 해당 데이터를 출력해보도록 하자.
위에서 데이터를 배열의 형태로 불러왔으니 다양한 방법으로 데이터를 출력할 수 있는데, 여기서는 for-in
문으로 데이터를 출력해보도록 하겠다.
// 가져온 데이터를 NSManagedObject 타입으로 캐스팅
// 캐스팅된 데이터를 모두 출력
for phoneBook in phoneBooks as [NSManagedObject] {
// Key값으로 데이터를 불러와 String 타입 캐스팅
if let name = phoneBook.value(forKey: "name") as? String,
let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String {
// 출력
print("name: \(name), phoneNumber: \(phoneNumber)")
}
}
phoneBooks
는 데이터 베이스의 클래스 타입인 PhoneBook
타입이고, 이 클래스는 NSManagedObject
클래스를 상속 중이기 때문에 타입 캐스팅이 가능하다.
타입 캐스팅을 하는 이유는 value(forKey:)
메소드를 사용하기 위함이다. 이렇게 하면 해당 키의 데이터를 가져올 수 있는데, 그냥 가져오면 Any?
타입을 가지기 때문에 타입 캐스팅으로 String
타입으로 변환한 뒤 출력한다.
이렇게 하고 위에서 만든 create 메소드와 같이 read 메소드를 사용하여 데이터의 저장이 잘 이루어졌는지 확인해보자.
createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
readAllData()
사진처럼 콘솔 창에 데이터가 잘 출력되는 것을 보면 데이터가 잘 저장되었다는 것을 확인할 수 있다.
그런데 나는 코드를 분석하며 한가지 의문이 들었다.
for-in
문으로 데이터를 출력하는데 꼭 타입 캐스팅을 사용해야 하는걸까??
데이터 베이스의 클래스를 보면 데이터 베이스의 Key
값이 아래와 같이 정의되어 있다.
@NSManaged public var name: String?
@NSManaged public var phoneNumber: String?
클래스의 프로퍼티로 키를 갖고 있다면 굳이 타입 캐스팅을 안해도 되지 않을까?
그래서 실험을 해봤다.
for phoneBook in phoneBooks {
if let name = phoneBook.name ,
let phoneNumber = phoneBook.phoneNumber {
print(name, phoneNumber)
}
}
문제 없이 출력 되었다.
이렇게 하면 타입캐스팅을 3번이나 진행해야 했던 지난 코드와 달리 if-let
만 사용하여 간단히 원하는 데이터를 출력할 수 있다.
강의에서 타입캐스팅을 하라고 해서 그대로 했지만... 왜 타입캐스팅이 필요한지 잘 모르겠다.
추측을 해보자면, 이번의 경우 데이터 베이스의 클래스인 PhoneBook
이 데이터 베이스의 Key
값을 프로퍼티로 가지고 있었기 때문에 위와 같이 축약 형태의 출력을 할 수 있었다.
하지만, 엔티티의 Code Generation
옵션에 따라 프로퍼티가 생길 수도, 안 생길 수도 있다고 생각한다면, 혹은 데이터 베이스를 수정한 탓에 새로운 Key
값이 생겼지만, Xcode가 그것을 자동으로 업데이트 하지 않을 수도 있기 때문에 value(forKey:)
메소드로 불러오는 편이 더욱 안전할 수 있다.
때문에 타입 캐스티을 해서 Key
값을 통해 데이터를 불러오는 방식을 사용하라고 한 것이 아닐까? 하고 생각해 보았다.
이것은 차차 공부해 보도록 하자
그럼 이제 데이터를 업데이트 하는 방법에 대해 알아보자
데이터 베이스의 값은 항상 같은 값을 가진 채 유지 되지 않는다. 즉, 어떠한 이벤트에 의해 데이터 베이스의 값이 변경될 가능성이 있다는 뜻이다.
그렇다면 우리는 어떻게 데이터 베이스의 값을 업데이트 해줄 수 있을까??
지금부터 데이터 베이스를 업데이트 하는 메소드를 만들어보자.
우선 메소드를 정의하고 데이터 베이스의 모든 값을 가져올 수 있도록 fetchRequset()
메소드를 호출한다.
private func updateData(currentName: String, updateName: String) {
let fetchRequest = PhoneBook.fetchRequest()
}
이렇게 하면 fetchRequest
변수는 PhoneBook
의 데이터 베이스에 담긴 모든 데이터를 가져오기 위한 객체, NSFetchRequest<PhoneBook>
타입이 된다.
이번 메소드에서는 name
이라는 key
를 변경할 것이기 때문에 필터링을 설정한다.
fetchRequest.predicate = NSPredicate(format: "name == %@", currentName)
여기서 NSpredicate
는 CoreData
에서 데이터를 필터링할 때 사용되는 조건식 객체이다. 주어진 조건을 기반으로 데이터 베이스에서 원하는 레코드를 가져오거나 수정할 때 유용하게 사용된다.
이 때, 조건 설정에 대해 format: "name == %@", currentName
으로 설정했는데, 이것은 name
이라는 필드가 currentName
과 일치하는 객체를 찾는다는 뜻이다.
여기서 %@
와 같은 형식을 포맷 문자열이라고 하며 종류는 아래와 같다.
포맷 문자열의 종류
%@
: 객체(문자열, 숫자 등)%d
or %i
: 정수형 숫자%f
: 부동소수점 숫자%K
: 키 경로(동적으로 필드 이름을 삽입할 때 사용)필터를 통해 원하는 데이터를 불러왔으니 이제 이 데이터를 업데이트(가공)해보도록 하자.
do {
// 필터링 된 데이터를 모두 가져옴
let result = try self.container.viewContext.fetch(fetchRequest)
// 가져온 데이터를 모두 수정
for data in result as [NSManagedObject] {
data.setValue(updateName, forKey: "name")
}
// 변동사항을 데이터 베이스에 저장
try self.container.viewContext.save()
print("Data Update Successful")
} catch {
print("Failed to update data", error)
}
먼저 Read 때처럼 fetch
메소드를 통해 필터링 된 데이터를 모두 result
에 저장한다.
그리고 for-in
문을 사용하여 데이터를 업데이트 할 것인데, 이 때 setValue
메소드를 사용해야 하기 때문에 NSManagedObject
로 타입 캐스팅을 진행한 후 데이터를 업데이트 한다.
이제 아래 코드로 테스트를 해보자.
createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
readAllData()
updateData(currentName: "crois", updateName: "sparta")
readAllData()
문제 없이 기존 데이터가 업데이트 된 모습을 볼 수 있다.
그럼 마지막으로 데이터 베이스의 값을 삭제하는 방법을 알아보도록 하자.
우리는 위에서 데이터 베이스에 값을 추가하고, 값을 불러오고, 값을 업데이트 하는 방법에 대해 학습했다.
그렇다면 데이터 베이스에서 값을 삭제하려면 어떻게 하면 될까??
지금부터 데이터 베이스의 값을 삭제하는 메소드를 함께 만들어보자.
우선 메소드를 만들고 Updata 때처럼 fetchRequest
를 만들어준다.
private func deleteData(name: String) {
let fetchRequest = PhoneBook.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
}
우리는 name
이라는 Key
를 기준으로 값을 삭제할 것이기 때문에 필터링도 name
을 사용한다.
이제 do-try-catch
를 사용해서 데이터 삭제를 진행한다.
do {
// 필터링된 데이터 모두 가져오기
let result = try self.container.viewContext.fetch(fetchRequest)
// 가져온 데이터를 NSManagedObject 타입으로 캐스팅
// 데이터 베이스에서 해당하는 값 삭제
for data in result as [NSManagedObject] {
self.container.viewContext.delete(data)
}
// 변동사항을 데이터 베이스에 저장
try self.container.viewContext.save()
print("Data deletion successful")
} catch {
print("Failed to delete data", error)
}
전체적인 과정은 Update를 할 때와 유사하다. 다만 for-in
문 내부에서 실행하는 작업은 데이터 베이스의 delete
메소드를 사용한다는 점만 다르다.
여기서 delete
메소드는 아래와 같이 정의되어 있다.
파라미터로 NSManagedObject
를 받는데... 그렇다면 이번에도 타입캐스팅 없이 삭제가 가능하지 않을까??
for data in result {
self.container.viewContext.delete(data)
}
이렇게 코드를 작성하니 에러는 발생하지 않는다.
그럼 실제로 작동하는지 확인을 위해 아래처럼 코드를 작성하고 실행을 해보자.
createPhoneNumber(name: "crois", phoneNumber: "010-1111-2222")
readAllData()
deleteData(name: "crois")
readAllData()
콘솔창의 출력 상태를 보면 문제 없이 삭제가 된 모습을 볼 수 있다.
타입캐스팅을 하는 것은 이번에도 아마 안정성의 문제가 아닐까 싶다. 예제에서는 데이터 베이스도 한 개이고, 별다른 커스텀을 진행하지 않아 문제가 없지만, 프로젝트 규모가 커지고 데이터 베이스도 여럿을 사용하게 되거나 커스텀하여 사용하게 된다면 타입 캐스팅을 하여 사용하는 편이 안정성이 높을 것이다.
위에서 예시 코드를 보며 불편함을 느낀 사람이 있을까?
아마 있었을지도 모르겠다.
예를 들어 아래 코드를 보자
if let name = phoneBook.value(forKey: "name") as? String,
let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String {
// 생략...
}
이 코드는 Read 메소드를 만들 때 작성한 코드이다.
여기서 주시할 부분은 value(forKey)
부분이다. Key
에 대한 값을 String
타입으로 정의하고 있는데, 만약 키 값이 바뀐다면? 오타가 난다면?? 여러 상황을 상정해 보았을 때 String
타입으로 Key
값을 입력하는 것은 안정적이지 않다.
그럼 어떻게 할 수 있을까??
엔티티를 코드로 변환했을 때 생긴 파일 중 클래스 파일을 확인해보자.
import Foundation
import CoreData
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
}
아마 이런 형식으로 작성되어 있을 것이다.
이제 여기에 코드를 작성해주자.
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
static let className: String = "PhoneBook"
enum Key {
static let name: String = "name"
static let phoneNumber: String = "phoneNumber"
}
}
먼저 데이터 베이스 클래스 타입의 이름을 정의하는 상수를 만들고 Key
의 이름을 정의하는 상수를 열거형을 이용하여 정의한다.
이 값은 모두 static
으로 선언되어 다른 파일에서 쉽게 불러와 사용할 수 있다.
그 말은 즉 이렇게 사용할 수 있다는 뜻이다.
if let name = phoneBook.value(forKey: PhoneBook.Key.name) as? String,
let phoneNumber = phoneBook.value(forKey: PhoneBook.Key.phoneNumber) as? String {
// 생략...
}
guard let entity = NSEntityDescription.entity(forEntityName: PhoneBook.className, in: self.container.viewContext) else { return }
위 코드에서는 Key
값이 필요할 때 PhoneBook.Key.name
등의 형태를 사용하여 값을 사용했다. 또, 클래스의 이름이 필요할 때는 PhoneBook.className
코드를 사용했다.
이처럼 클래스에서 static
을 사용하여 값들을 정의하면 자동완성을 통해 휴먼 에러를 줄일 수 있고, 값의 변경을 일괄적으로 적용할 수 있어 유지보수 측면에서도 우수하다.
Core Data의 장점
Core Data의 단점
오늘은 UIKit에서 UIController의 생명주기와
CoreData, UserDefaults에 대해 학습했다.
원래는 UserDefaults에 대해서도 정리하려고 했으나,
코어 데이터의 분량이 너무 많기도 하고
유저디폴트는 비교적 쉽기 때문에 굳이 정리할 필요성을 못 느껴
코어 데이터 까지만 정리하기로 했다.
코어 데이터는 이번에 처음 써보는데, 익숙해지려면 시간이 많이 필요할 것 같다.
그래도 잘 활용하면 프로젝트를 만들 때 유용하게 활용할 수 있을 것 같다는 생각이 든다.
헉 강의내용에서 viewWillLayoutSubviews 는 못 들은 것 같은데 어떻게 알게 되셨나요? 내가 졸았던것인가ㅠㅜ