2020 Lecture 12: Core Data
마찬가지로 코드를 다 이해하지 못해서 포스팅을 쓸 지 말 지를 엄청 고민했는데 나름대로 Core Data 를 쓰기 위한 최소한은 이해했다는 생각이 들어서 정리해 보기로...
지난 시간과 동일한 Enroute 앱, 근데 이제 CoreData 를 곁들인
# Persistence
- 코어데이터에 저장된 데이터에 접근하기 위해서는
NSManangedObjectContext라는 일종의 코어데이터에 접근할 수 있게 하는 창을 이용할 건데, 코어데이터와 상호작용이 필요한 모든 뷰의@Environment에context를 전달해줘야 한다.
- 아래에서
FlightList는FlightsEnrouteView의 자식View이므로 자동으로 환경을 공유하기 때문에managedObjectContext를 별도로 넘겨줄 필요가 없지만,FilterFlights는sheet을 통해 새로운 환경의View가 나타나는 것이므로 명시적으로 넘겨줘야 한다.sheet이나popover,ViewModifier등으로 새로운View를 띄울 때에는 기존View와 특정 환경 프로퍼티를 같게 설정해주려면 반드시 주입해줘야 한다!
struct FlightsEnrouteView: View {
@Environment(\.managedObjectContext) var context
@State var flightSearch: FlightSearch
var body: some View {
NavigationView {
FlightList(flightSearch) // 여기
.navigationBarItems(leading: simulation, trailing: filter)
}
}
@State private var showFilter = false
var filter: some View {
Button("Filter") {
self.showFilter = true
}
.sheet(isPresented: $showFilter) {
FilterFlights(flightSearch: self.$flightSearch, isPresented: self.$showFilter)
.environment(\.managedObjectContext, self.context) // 여기
}
}
}
# @ObservableObject
- 데이터베이스에 생성한 객체(이하 엔티티)는
ObservableObject이므로, 사실상 미니ViewModel과 같다. 대신 데이터베이스 상에서 무언가가 변할 때 변화했다는 사실을 자동으로 알려주지 않으므로(Publisher가 디폴트로 있지는 않은것 같다) 변화를 감지하기 위해서는objectWillChange()를 써서 변화가 발생했다는 것을 명시적으로 알려야 한다. 이로 인해FetchRequest를 더 많이 쓴다.
# @FetchRequest
@FetchRequest는 일종의standing query로 데이터베이스로부터 지정한 조건에 부합하는 데이터만Collection으로 리턴한다. 이때, 한번 반환하고 끝나는 게 아니라 데이터베이스에 변화가 생기면 이를 반영한 결과를 지속적으로 다시 리턴한다. 예컨대 새로운 인스턴스가 추가되었는데 내가 지정한 조건에 부합한다면@FetchRequest가 리턴값에 이를 포함시키고 이에 맞춰 UI도 업데이트된다.
ManagedObjectContext는 해당View의Environment에 들어있는 값을 쓴다.
# 데이터베이스에서 데이터 불러오기
- 데이터베이스로부터 데이터를 가져오기 위해서는
NSFetchRequst를 사용하는데,predicate프로퍼티로 가져올 데이터의 조건을 설정하고sortDescriptor프로퍼티로 리턴된Collection이 정렬될 기준을 지정할 수 있다. 이렇게request의 조건을 설정한 다음,fetch()메서드를 사용해서 데이터베이스에서 데이터를 가져온다- 우리가 데이터를 불러온 다음 정렬하는 것보다 SQL 데이터베이스에 시키는 게 훨씬 더 빠르기 때문
# 옵셔널과 옵셔널

- 위 사진에서 보면 엔티티의 애트리뷰트에 옵셔널이 체크되어있는데 이는
String?등의 옵셔널 타입을 의미하는 게 아니라, 해당 애트리뷰트가 데이터베이스 상에서 옵셔널한 지를 의미한다. 이와 별개로 엔티티의 애트리뷰트는 데이터베이스가 오염될 수 있어 기본적으로 (String?과 같은) 옵셔널 타입이다.
# 맵 생성
- 코어데이터의 핵심인 엔티티와 엔티티 간의 관계를 담고 있는 맵 만들기!
- 그동안은 그냥 상위 개체 - 하위 개체의 종속 관계로
Model을 나타냈다면, 코어 데이터에서는 두 개체 모두 별도로 존재하고relationship을 이용해서 관계를 설정한다.
- 아래와 같은 맵? 스타일 뷰에서는
ctrl키를 누르면 관계를 설정할 수 있다.

- 오른쪽 창에서 일대일, 일대다, 다대다 등의 관계, 삭제 규칙 등도 설정 가능

- 오른쪽 창에서 일대일, 일대다, 다대다 등의 관계, 삭제 규칙 등도 설정 가능
# 코어데이터에서 엔티티 찾기
withICAO(_:context:)함수는 인자로 받은icao의 공항 엔티티가 이미 데이터베이스에 있는 지 확인하고, 있다면 해당 인스턴스를 리턴하고, 없다면 새로운 인스턴스를 생성하는 함수다. 구조 자체는 그냥fetchRequest를 만들고,fetch를 시도해 본 다음, 적절한 결과를 리턴하면 된다.
-
다만 주의할 점은 새로운 객체를 생성하게 되는 경우, 객체를 생성하고 나서
API로부터 정보를 받아와서 애트리뷰트를 업데이트하는Airport.fetch(_:perform:)함수는async하게 수행되므로 과정 상 일단은icao프로퍼티 외에는 다 비어있는Airport객체가 반환되고 이후에Airport.fetch(_:perform:)함수가 실행 완료되면서 정보가 다 채워진다!- 개인적으로 항상
async에 대해서 다시 공부해야겠다고 느꼈는데, 하나의 블록 내에서 함수가 호출되면 다async하게 수행된다고 보면 되는 것 같다.
- 개인적으로 항상
-
참고로
fetch가 실패하는 경우는 데이터베이스에 연결을 못하는 경우 등이 있다고 한다. 여기서는fetch에 실패해도 그냥 API에서 정보를 받아와서 새로운 객체를 생성하는 방식으로 해결을 해서let airports = (try? context.fetch(request)) ?? []이 줄에서 실패하면 일단nil을 리턴하게 하고, 닐 코알리싱으로 빈Collection으로 바꿔줬다.
extension Airport: Comparable {
static func withICAO(_ icao: String, context: NSManagedObjectContext) -> Airport {
// sun
// need to try this b/c fetch could fail due to lost connection to database etc...
let airports = (try? context.fetch(request)) ?? []
if let airport = airports.first {
let airport = Airport(context: context)
airport.icao = icao
AirportInfoRequest.fetch(icao) { airportInfo in
self.update(from: airportInfo, context: context)
}
return airport
}
}
}
# 수동으로 변화 공지하기
- 위에서 얘기했듯이 각 엔티티는
ObservableObject이므로,View가 다시 그려져야 하는 변화가 발생했을 때는 이를 수동으로objectWillChange.send()를 사용해서 공지해야 한다.relationship으로 연결된 엔티티도 필요한 경우 꼭 따로 알림을 보내준다!
extension Airport: Comparable {
static func update(from info: AirportInfo, context: NSManagedObjectContext) {
// fetch the empty airport created previously
if let icao = info.icao {
let airport = self.withICAO(icao, context: context)
airport.latitude = info.latitude
airport.longitude = info.longitude
airport.name = info.name
airport.location = info.location
airport.timezone = info.timezone
airport.objectWillChange.send()
airport.flightsTo.forEach { $0.objectWillChange.send() }
airport.flightsFrom.forEach { $0.objectWillChange.send() }
try? context.save()
}
}
}
# NSSet을 Set 으로 바꿔주기
-
fetchRequest의 결과로Airport객체(들)를 불러오면,flightsFrom과flightsTo는Flight들이 담긴NSSet으로 리턴된다. 우리가View코드를 작성할 때는Set<Flight>이 필요하므로 형변환을 해줘야 하는데, 매번 하기 귀찮으므로 이런 경우extension에서 연산 프로퍼티로 해결하면 쉽다. -
교수님의 팁에 의하면 이처럼 코어데이터로부터 반환된 값이 우리가 원하는 것과 다른 형태거나, 옵셔널 언래핑이 필수적인 경우 엔티티의 애티리뷰트 뒤에는 언더바("_") 를 붙이고,
extension에서 원하는 대로 형변환을 한 연산 프로퍼티를 선언하면 편리하다고 한다.- 솔직히 강의 들으면서는 체감이 잘 안됐는데 플젝하면서 연산 프로퍼티 선언해서 닐 코알리싱 해놓으니까 코어 데이터 쓸 때
View에서 지옥의 옵셔널 언래핑에서 벗어날 수 있어서 진짜 편했다..
- 솔직히 강의 들으면서는 체감이 잘 안됐는데 플젝하면서 연산 프로퍼티 선언해서 닐 코알리싱 해놓으니까 코어 데이터 쓸 때

extension Airport: Comparable {
var flightsTo: Set<Flight> {
get { (flightsTo_ as? Set<Flight>) ?? [] }
set { flightsTo_ = newValue as NSSet }
}
var flightsFrom: Set<Flight> {
get { (flightsFrom_ as? Set<Flight>) ?? [] }
set { flightsFrom_ = newValue as NSSet }
}
var icao: String {
get { icao_! } // TODO: maybe protect against when app ships?
set { icao_ = newValue }
}
var friendlyName: String {
let friendly = AirportInfo.friendlyName(name: self.name ?? "", location: self.location ?? "")
return friendly.isEmpty ? icao : friendly
}
}
# 나는 내 출처를 알고있다
- 코어데이터에서 불러온 엔티티의 인스턴스는 자신이 어떤
ManagedObjectContext출신인지 알고 있기 때문에 해당 엔티티의extension에서 선언한static이 아닌 메서드에서는 인자로 따로 넘겨주지 않더라도managedObjectContext프로퍼티를 이용해서 데이터베이스에 접근 할 수 있다. 매번 받아올 필요가 없다는 게 핵심이자 장점
extension Airport: Comparable {
func fetchIncomingFlights() {
Self.flightAwareRequest?.stopFetching()
if let context = managedObjectContext {
Self.flightAwareRequest = EnrouteRequest.create(airport: icao, howMany: 90)
Self.flightAwareRequest?.fetch(andRepeatEvery: 60)
Self.flightAwareResultsCancellable = Self.flightAwareRequest?.results.sink { results in
for faflight in results {
Flight.update(from: faflight, in: context)
}
do {
try context.save()
} catch(let error) {
print("couldn't save flight update to CoreData: \(error.localizedDescription)")
}
}
}
}
}
# @FetchRequest 실전
- 진짜 별 거 없다. 그냥 아래와 같이 선언해주고,
init할 때는property wrapper이므로_버전에 접근해서 초기화해주면 끝. 알아서 데이터베이스에 변화가 생기면 반영한다.FetchedResult인 엔티티Collection을 이용해서 개별 엔티티를ObservedObject를 필요로 하는 곳(e.g.FlightListEntry(flight:)에 넘길 수도 있다.- 엔티티를 필요로 하는 모든 곳에서
FetchRequest를 해야하는 지 의문이었는데, 기존 결과를 다시 사용할 수 있는 경우ObservableObject로 넘기면 될 것 같다.
- 엔티티를 필요로 하는 모든 곳에서
struct FlightList: View {
@FetchRequest var flights: FetchedResults<Flight>
init(_ flightSearch: FlightSearch) {
let request = Flight.fetchRequest(flightSearch.predicate)
_flights = FetchRequest(fetchRequest: request)
}
var body: some View {
List {
ForEach(flights, id: \.ident) { flight in
FlightListEntry(flight: flight)
}
}
.listStyle(PlainListStyle())
.navigationBarTitle(title)
}
}
struct FlightListEntry: View {
@ObservedObject var flight: Flight
...
}
# 모두 불러오거나 아무것도 불러오지 않기
- 코어데이터 사용 전에는 모든
Airport를Airport클래스에서static으로 선언한shared프로퍼티를 사용해서 가져왔으나, 이제FetchRequest를 사용해서 불러올 수 있다.
struct FilterFlights: View {
@FetchRequest(fetchRequest: Airport.fetchRequest(.all)) var airports: FetchedResults<Airport>
@FetchRequest(fetchRequest: Airline.fetchRequest(.all)) var airlines: FetchedResults<Airline>
}
TRUEPREDICATE은 모든 인스턴스를 리턴하고,FALSEPREDICATE은 아무 인스턴스도 리턴하지 않는다. 즉, 빈Collection을 리턴한다.
extension NSPredicate {
static var all = NSPredicate(format: "TRUEPREDICATE")
static var none = NSPredicate(format: "FALSEPREDICATE")
}
☀️ 느낀점
- 항상 코어데이터에 저장된 데이터를 어떻게 가져와서 어떻게 활용하는지 잘 이해가 안 갔는데 조금 감이 잡힌 느낌이다. 기본적으로 일단 각 엔티티가
ViewModel의 역할을 하는데, 알리고 싶은 변화가 발생하면 꼭objectWillChange.send()로 공지해야 한다. 그리고View에서는 쿼리가 필요하다/넘겨받을 곳이 없다면FetchRequest를 쓰고, 넘겨받을 수 있다면ObservedObject로 넘겨받기라고 생각하고 있다.context는 새로운 환경인 경우 반드시 넘겨줘야 되고, 저장하고 싶은 변화가 생기면 반드시context.save()를 이용해서 저장을 시도한다. 플젝에서 써보면서 좀 더 정리해서 별도로 포스팅해야지...
-
the objects we create in the database are ObservableObjects, so they're essentially mini ViewModels. they do not fire automatically when things change in the database so if u want them to change u need to explicitly call objectWillChange
- this is one reason that we usually use FetchRequest instead- cf. FetchRequest is originally meant to fetch Collection
-
FetchRequest is kind of a standing query that's constantly trying to match whatever the criteria ur talking about are and returning whatever's in the databse. So as things get added to the database, if they match the criteria of that FetchRequest, then it's gonna update ur UI
-
sortDescriptors : when we make a request to the database, it comes back as an Array, and it has to be sorted. so we specify the sortDescriptors so the sorting can happen on the database side b/c the SQL datbase is super good at sorting things
-
fetch : go out to the data base and find all the objects that meet our predicate and return them to us
-
using sheet or popover is like putting up a new environment of Views so we have to pass the environment in
- cf. all the Views that are in it's body get the same evironment -
below the option
optionalhas nothing to do with optional in swift(e.g. String?, Int? etc.) it means whether the attribute is optional in the database! but the vars will actually be of optional type(i.e. with ?) because the database might be corrrupted etc.(29분)

Fetching data:
- check if threre
- 50분 : 새 객체를 생성 시 identifier 를 제외한 나머지는 API로부터 받아오고, async하기 때문에 일단은 빈 상태로 객체가 만들어짐에 유의
NSSet을 Set 으로 바꿔주기
- 54분 Aiports, 등은 NSset이므로 computed property로 바꿔줘야 함
@FetchedRequest
-
FetchedResults<\Flight> -> some type of collection
-
this fe
-
we can make Objects in the database be observable objects
-
코어데이터 오브젝트의 익스텐션은 자신의 출처를 알고 있으므로
managedObjectContext()를 이용해서 접근 가능
// when u have an instance from the databse in ur hand,
// u can always get the context from the database b/c
// the instance knows where it came from
// and so use that context to add/fetch other objects
13 분에러
context in environment is not connected to a persistence store coordinatore
Author And Source
이 문제에 관하여(2020 Lecture 12: Core Data), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sunnysideup/2020-Lecture-12-Core-Data저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)