2021 강의에는 Core Data에 대해 설명만 하고 넘어가시는데 2020 강의에서는 직접 다루고 넘어가서 해당 파트를 따로 수강했다. 비행 정보를 받아오는
Enroute
라는 새로운 앱을 베이스로 진행되는데 11강에서 간략하게 어떠한 앱인지 설명을 해주시고 12강에서 Core Data를 적용해본다. 사실 앱 전체 구조를 이해한 것은 아니라 포스팅을 할 지 말 지 고민했는데 그냥 배운 것 위주로 작성하는 것으로...!
11강의 주제는 Enroute의 메인 화면에 filter
버튼을 추가해서 원하는 조건(출발지, 도착지, 항공사, 현재 비행중인지)에 따라 비행 정보를 필터링해서 나타낼 수 있도록 하는 것
Comparable
프로토콜에 순응하게 하면 Array
의 내장 메서드인 sorted()
사용 시 해당 프로토콜에서 정의한 바에 따라 정렬된다!CustomStringConvertible
프로토콜을 따르기 위해서는 String
타입인 description
변수를 선언해줘야 하는데, String(describing:)
이니셜라이저와 print(_:)
함수의 인자로 CustomStringConvertible
프로토콜에 순응하는 객체의 인스턴스를 넣어주는 경우 description
프로퍼티가 사용된다고 보면 된다!
description
프로퍼티 자체에 직접 접근하는 것은 지양해야 한다즉, 해당 객체의 representaion in String
을 내 마음대로 커스텀할 수 있게 해주는 친구
struct FAFlight: CustomStringConvertible {
var description: String {
if let departure = self.departure {
return "\(ident) departed \(origin) at \(departure) arriving \(arrival)"
} else {
return "\(ident) scheduled to depart \(origin) at \(filed) arriving \(arrival)"
}
}
}
print("\(FAFlight)) // flight1AB departed LA at 10:00AM arriving NY
출발지, 도착지, 현재 운항 여부, 항공사 등의 조건을 설정해서 조건에 맞는 비행 정보만 불러올 수 있게 해주는 게 목표.
지금까지는 sheet
을 사용해서 팝업을 띄우고 해당 팝업에서 TextField
로 한 객체의 여러 프로퍼티 값을 입력 받을 때 각 프로퍼티마다 @State
변수를 선언해줬는데 그냥 인스턴스를 @State
변수로 선언해줘도 된다는 것을 배웠다. 이렇게 하면 초기화도 그냥 인스턴스 자체를 바로 받아와서 쓰면 되기 때문에 더 깔끔하다
@Binding
과 @State
는 실제로 구조체를 생성하고, 우리는 구조체 내부의 wrappedValue
를 computed property
로 접근해서 get/set 하고 있는 것. init
을 할 때에는 아직 wrappedValue
가 없으므로 구조체 자체에 접근해서 초기화를 해줘야 하는데 변수 이름 앞에 언더바 "_"
를 붙이면 구조체 자체에 접근 할 수 있다. 그리고 바인딩 자체는 제네릭이므로 우리가 어떤 타입에 바인딩할 것인지 지정해줘야 한다.
struct FilterFlights: View {
@State private var draft: FlightSearch
init(flightSearch: Binding<FlightSearch>, isPresented: Binding<Bool>) {
_flightSearch = flightSearch
_isPresented = isPresented
_draft = State(wrappedValue: flightSearch.wrappedValue)
}
}
Picker(label:selection:content)
는 이름 그대로 여러 선택지 중에 하나를 선택할 수 있게 하는 컨트롤이다. 이때 각 content
에 모든 선택가 들어가며 content
는 View
이다. selection
바인딩의 타입과 일치하도록 각 View
에 tag(_:)
을 달아서 식별하는데, 이 특정 View
를 선택했을 때 이 tag
을 이용해서 Picker
에게 어떠한 View
를 선택했는지 알려준다.
Picker
의 선택지는 ForEach(data:content:)
로도 생성할 수 있는데 ForEach
의 content
인자는 Collection
인 data
의 각 요소를 인자로 하는 클로저를 받는다. 이때 data
의 element
의 타입이 Picker
의 selection
바인딩과 같으면 자동으로 tag
이 생성되지만, 아닌 경우 명시적으로 tag(_:)
을 이용해서 달아줘야 한다.
element
와 selection
이 바인딩하고 있는 타입이 같아서 tag
이 필요 없는 경우 private var destinationSection: some View {
Picker("Destination", selection: $draft.destination) {
ForEach(allAirports.codes, id: \.self) { airport in
Text("\(self.allAirports[airport]?.friendlyName ?? airport)")
}
}
}
map(_:)
을 이용해 element
인자 자체의 타입을 selection
에 맞게 바꿔버린 경우 private var originSection: some View {
Picker("Origin", selection: $draft.origin) {
Text("Any").tag(String?.none)
// map version
ForEach(allAirports.codes.map { Optional($0) }, id: \.self) { airport in
Text("\(self.allAirports[airport]?.friendlyName ?? airport ?? "Any")")
}
}
}
tag(_:)
을 이용해서 타입을 맞춰준 경우 private var airlineSection: some View {
Picker("Airline", selection: $draft.airline) {
Text("Any").tag(String?.none)
// tag version
ForEach(allAirlines.codes, id: \.self) { airline in
Text("\(self.allAirlines[airline]?.friendlyName ?? airline)")
.tag(Optional(airline))
}
}
}
ForEach(data:content)
에서 content
클로저의 인자를 바인딩에 맞게 바꾸는 방식인데, 클로저의 인자를 데이터와 안 맞춰도 컴파일이 작동하는 지에 대해 의문을 가질 수 있다. ForEach
는 단순히 data
의 element
를 인자로 보낼 수 있는 클로저를 원하는 것 뿐이고, 현재의 경우 element
는 String
이고 클로저는 인자로 String?
을 받으므로 작동 가능하다! Picker("Airline", selection: $draft.airline) {
Text("Any").tag(String?.none)
ForEach(allAirlines.codes, id: \.self) { (airline: String?) in
Text("\(self.allAirlines[airline]?.friendlyName ?? airline ?? "Any")").tag(airline)
}
}
Text("Any").tag(String?.none)
라고 되어있는 부분이 있다 도착지나 항공사 등을 선택할 때 모두를 선택할 수 있도록 선택지를 하나 더 만들기 위한 부분이다. (그렇다, Picker
의 content
로 ForEach
와 그냥 바로 Text
를 넣는 방법을 병행해서 사용할 수 있다!) tag
의 value
로 nil
을 보내주고 해당 경우는 모든 선택지를 다 서치하도록 할 건데 그냥 nil
을 넣으면 String
타입의 nil
인지, Int
타입의 nil
인지 유추할 수 있는 context
가 부족하다고 뜬다. 따라서 String?.none
으로 String
타입의 nil
임을 전달한다. Form에 안 넣은 경우
Picker
를 Form
안에 넣으면 레이아웃이 깔끔하게 바뀐다. 이는 SwiftUI가 자동으로 컨트롤(e.g. Picker, Button, etc.)들의 UI를 현재 환경에 맞게 변형하기 때문이다. 그리고 지금 Picker
의 완전한 변형을 위해서는 NavigationView
안에 넣어줘야 한다!Form
에 넣으면 PickerStyle
이 그냥 inline
스타일에서 선택 화면으로 넘어가는 방식으로 바뀌기 때문. NavigationView
안에 넣어주지 않으면 각 픽커를 눌렀을 때 선택창으로 안 넘어간다.State
변수를 초기화 할 때 무식한 방법...으로 하고 있었음을 느낄 수 있었다...ㅎㅎsheet
에서 역으로 정보를 받아와야 하는 경우(오늘 어떤 기준으로 필터링 할 지 기준들을 받아온 것처럼) 바인딩을 두 개 써서 하나는 sheet
을 띄울 지 말 지를 받아오고, 하나는 정보를 받아오는 데 쓸 수 있음을 배웠다. @State var draft
Binding and State creates Structs, and we can access the actual struct using the underbar version, during init we need to use the underbar version. Binding is a generic, with a don't care which is the type of the thing it's binding to
SwiftUI adapts its controls(e.g. Picker, Button, etc.) to the environment that they're in! This is the main reason that we use Buttons instead of Text + onTapGesture. So Picker adapts when it's inside a Form(this adaptation requires NavigationView...!)
when making a Picker, u r actually picking on a "View" and this is being reported back to u(i.e. the binding) via the tag