2020 Lecture 11: Picker

sun·2021년 12월 4일
0

강의 링크

2021 강의에는 Core Data에 대해 설명만 하고 넘어가시는데 2020 강의에서는 직접 다루고 넘어가서 해당 파트를 따로 수강했다. 비행 정보를 받아오는 Enroute 라는 새로운 앱을 베이스로 진행되는데 11강에서 간략하게 어떠한 앱인지 설명을 해주시고 12강에서 Core Data를 적용해본다. 사실 앱 전체 구조를 이해한 것은 아니라 포스팅을 할 지 말 지 고민했는데 그냥 배운 것 위주로 작성하는 것으로...!

11강의 주제는 Enroute의 메인 화면에 filter 버튼을 추가해서 원하는 조건(출발지, 도착지, 항공사, 현재 비행중인지)에 따라 비행 정보를 필터링해서 나타낼 수 있도록 하는 것

# Comparable 프로토콜

  • Comparable 프로토콜에 순응하게 하면 Array 의 내장 메서드인 sorted() 사용 시 해당 프로토콜에서 정의한 바에 따라 정렬된다!

# CustomStringConvertible

  • 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

# FilterFlights

  • 출발지, 도착지, 현재 운항 여부, 항공사 등의 조건을 설정해서 조건에 맞는 비행 정보만 불러올 수 있게 해주는 게 목표.

  • 지금까지는 sheet 을 사용해서 팝업을 띄우고 해당 팝업에서 TextField 로 한 객체의 여러 프로퍼티 값을 입력 받을 때 각 프로퍼티마다 @State 변수를 선언해줬는데 그냥 인스턴스를 @State 변수로 선언해줘도 된다는 것을 배웠다. 이렇게 하면 초기화도 그냥 인스턴스 자체를 바로 받아와서 쓰면 되기 때문에 더 깔끔하다

  • @Binding@State 는 실제로 구조체를 생성하고, 우리는 구조체 내부의 wrappedValuecomputed 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 그리고 Tag

  • Picker(label:selection:content) 는 이름 그대로 여러 선택지 중에 하나를 선택할 수 있게 하는 컨트롤이다. 이때 각 content 에 모든 선택가 들어가며 contentView 이다. selection 바인딩의 타입과 일치하도록 각 Viewtag(_:) 을 달아서 식별하는데, 이 특정 View 를 선택했을 때 이 tag 을 이용해서 Picker 에게 어떠한 View 를 선택했는지 알려준다.

  • Picker 의 선택지는 ForEach(data:content:) 로도 생성할 수 있는데 ForEachcontent 인자는 Collectiondata 의 각 요소를 인자로 하는 클로저를 받는다. 이때 dataelement 의 타입이 Pickerselection 바인딩과 같으면 자동으로 tag 이 생성되지만, 아닌 경우 명시적으로 tag(_:) 을 이용해서 달아줘야 한다.

  1. elementselection 이 바인딩하고 있는 타입이 같아서 tag 이 필요 없는 경우
    private var destinationSection: some View {
        Picker("Destination", selection: $draft.destination) {
            ForEach(allAirports.codes, id: \.self) { airport in
                Text("\(self.allAirports[airport]?.friendlyName ?? airport)")
            }
        }
    }
  1. 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")")
            }
        }
    }
  1. 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))
            }
        }
    }
  1. 교수님이 강의해서 설명해주신 경우
    • 결국 2번 방법과 동일한데 어디서 형변환을 하느냐의 차이
    • ForEach(data:content) 에서 content 클로저의 인자를 바인딩에 맞게 바꾸는 방식인데, 클로저의 인자를 데이터와 안 맞춰도 컴파일이 작동하는 지에 대해 의문을 가질 수 있다. ForEach 는 단순히 dataelement 를 인자로 보낼 수 있는 클로저를 원하는 것 뿐이고, 현재의 경우 elementString 이고 클로저는 인자로 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)
                    }
                }

# nil with context

  • 위의 코드들을 보다면 Text("Any").tag(String?.none) 라고 되어있는 부분이 있다 도착지나 항공사 등을 선택할 때 모두를 선택할 수 있도록 선택지를 하나 더 만들기 위한 부분이다. (그렇다, PickercontentForEach 와 그냥 바로 Text 를 넣는 방법을 병행해서 사용할 수 있다!) tagvaluenil 을 보내주고 해당 경우는 모든 선택지를 다 서치하도록 할 건데 그냥 nil 을 넣으면 String 타입의 nil 인지, Int 타입의 nil 인지 유추할 수 있는 context 가 부족하다고 뜬다. 따라서 String?.none 으로 String 타입의 nil 임을 전달한다.

# Picker 와 Form

Form에 안 넣은 경우

  • 그리고 별 건 아닌데 PickerForm 안에 넣으면 레이아웃이 깔끔하게 바뀐다. 이는 SwiftUI가 자동으로 컨트롤(e.g. Picker, Button, etc.)들의 UI를 현재 환경에 맞게 변형하기 때문이다. 그리고 지금 Picker 의 완전한 변형을 위해서는 NavigationView 안에 넣어줘야 한다!
    • Form 에 넣으면 PickerStyle 이 그냥 inline 스타일에서 선택 화면으로 넘어가는 방식으로 바뀌기 때문. NavigationView 안에 넣어주지 않으면 각 픽커를 눌렀을 때 선택창으로 안 넘어간다.

☀️ 느낀점

  • 그동안 객체를 위해 State 변수를 초기화 할 때 무식한 방법...으로 하고 있었음을 느낄 수 있었다...ㅎㅎ
  • 그리고 sheet 에서 역으로 정보를 받아와야 하는 경우(오늘 어떤 기준으로 필터링 할 지 기준들을 받아온 것처럼) 바인딩을 두 개 써서 하나는 sheet 을 띄울 지 말 지를 받아오고, 하나는 정보를 받아오는 데 쓸 수 있음을 배웠다.






FilterFlights

  • @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

# Picker and Form

  • 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

profile
☀️

0개의 댓글

관련 채용 정보