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
optional
has 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.)