마찬가지로 코드를 다 이해하지 못해서 포스팅을 쓸 지 말 지를 엄청 고민했는데 나름대로 Core Data 를 쓰기 위한 최소한은 이해했다는 생각이 들어서 정리해 보기로...
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
이므로, 사실상 미니 ViewModel
과 같다. 대신 데이터베이스 상에서 무언가가 변할 때 변화했다는 사실을 자동으로 알려주지 않으므로(Publisher
가 디폴트로 있지는 않은것 같다) 변화를 감지하기 위해서는 objectWillChange()
를 써서 변화가 발생했다는 것을 명시적으로 알려야 한다. 이로 인해 FetchRequest
를 더 많이 쓴다. @FetchRequest
는 일종의 standing query
로 데이터베이스로부터 지정한 조건에 부합하는 데이터만 Collection
으로 리턴한다. 이때, 한번 반환하고 끝나는 게 아니라 데이터베이스에 변화가 생기면 이를 반영한 결과를 지속적으로 다시 리턴한다. 예컨대 새로운 인스턴스가 추가되었는데 내가 지정한 조건에 부합한다면 @FetchRequest
가 리턴값에 이를 포함시키고 이에 맞춰 UI도 업데이트된다.ManagedObjectContext
는 해당 View
의 Environment
에 들어있는 값을 쓴다. NSFetchRequst
를 사용하는데, predicate
프로퍼티로 가져올 데이터의 조건을 설정하고 sortDescriptor
프로퍼티로 리턴된 Collection
이 정렬될 기준을 지정할 수 있다. 이렇게 request
의 조건을 설정한 다음, fetch()
메서드를 사용해서 데이터베이스에서 데이터를 가져온다 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()
}
}
}
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)")
}
}
}
}
}
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
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분)
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