#15 Complex Interfaces
- SwiftUI는 UI 디자인의 새로운 패러다임을 제시한다.
- UIKit 이나 AppKit 이나 다른 framework에서 제공하는 기능이 전부 같진 않다.
- 좋은 뉴스는 UIKit 이나 AppKit으로 만든 모든걸 SwiftUI로 재창조 할 수 있다는 점이다.
- SwiftUI 이전에 앱을 만들었다면 이미 만들어 놓은 custom control들이 있을 것 이다.
- SwiftUI는 UIkit 이나 AppKit과 협력하여 view 와 view controllers에 있던 view 를 재사용 할 수 있다.
- SwiftUI는 framework 기반으로 build 하고 없는 기능을 추가할 수 있다.
이 기능을 사용하면 기본 프레임워크 내에서 기능을 복제하거나 확장할 수 있다.
이번 챕터에서는 UIKit으로 된 open source 를 SwiftUI 에 추가해본다. 그리고 grid 에 표시할 수 있는 재사용 가능한 view 를 만들어 본다.
Integrating with other frameworks
상황: 적당히 복잡한 앱을 SwiftUI 프로토타입에 통합하는 것
MapKit과 같은 빌트인 framework는 동일한 compoenet가 SwiftUI 에 없기 때문에 상황이 발생
이미 앱에 사용한 서드파티 control 들도 있을 것
이번장에는 UITableView 를 위해 만들어진 timelineview 를 보여주는 opens source 를 가지고 SwiftUI 앱에 통합시켜 본다.
Zheng-Xiang Ke’s TimelineTableViewCell control 을 사용해 하루의 비행편 타임라인을 만들어 볼 것 이다.
SPM 을 지원하기 때문에 쉽게 프로젝트에 추가할 수 있다. (starter project 는 이미 가지고 있음)
SPM 이 현재 bundling resources 를 지원하지 않기 때문에 (TimeLineTableViewCell control 을 만들 때 사용한 custom nib file 포함) 메인 프로젝트의 UI group 에 복사된 nib file을 볼 수 있다.
UIViews 와 UIViewControllers 를 SwiftUI 에서 다룰려면
UIViewRepresentable 과 UIViewControllerRepresentable 프로토콜을 준수하는 type으로 만들어야 한다.
developer.apple.com/documentation/swiftui/uiviewrepresentable
developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable
SwiftUI는 이러한 view 들의 life cycle을 관리해서 우리가 해야될 것은 view 를 만들고 framework 가 알아서 하도록 configure 만 하면 된다.
starter project 에서 FlightTimeline.swift 인 Swift 파일을 만든다.
먼저 TimelineTableViewCell 패키지를 import 한다.
다음 UITableViewController 를 wrpping 할 type을 만든다.
SwiftUI 는 views, view controller 와 다른 app framework compoenet과 통합할 수 있는 프로토콜을 제공한다.
SwiftUI 뷰에 전달하는 것 처럼 FlightInformation 배열을 전달할 것 이다.
UIViewControllerRepresentable 프로토콜 이 필요한 두가지 함수는
makeUIViewController(context:) 와 updateUIViewController(_:context:) 이다.
flight 파라미터 아래에 추가한다.
SwiftUI는 view 를 표시할 준비가 되면 makeUIViewController(context:) 를 호출한다.
여기서 UiTableViewController를 코드로 만들어 리턴한다.
모든 UiKit ViewController 가 사용 가능하다. AppKit 과 WatchKit 과 다른 views에서도 플랫폼에 맞는 비슷한 protocol들이 존재한다.
두번째 메서드를 구현한다.
SwiftUI는 현재 표시된 view controller 의 설정을 업데이트하고 싶을 때 updateUIViewController(_:context:) 를 호출한다.
UIKit 에서 viewDidLoad() 안에 작성했던 코드들이 이 메서드에 들어간다.
timeline cell을 위한 nib를 불러오고 viewController를 통해 UITableView 에 cell 을 등록한다.
여기서 사용한 Nib 은 main app bundle 에 포함된 Nib이다. SPM 이 layout 파일도 포함하기를 희망해본다.
똑같이 따라했는데 에러가 뜬다.
UIViewControllerRepresentable 을 준수하지 않는다고 하는데 나는
makeUIViewController 메서드와 updateUIViewController 메서드 둘다 정의했다.
그래서 이미 있는 두개를 없애고 fix를 눌러보면
기대와는 다르게 typealias 를 쓰라고 한다. UITableViewController를 써주면
다시 fix 하겠냐고 뜨는데 fix 하면
이렇게 필요한 메서드가 나온다. 하나 다른점은 나는 makeUIViewController 에서 리턴을 some UIViewController 로 했는데 자동으로 만들어진 곳에서는 UITableViewController를 리턴한다.
UITableViewController 를 리턴하면 오류는 없어지는데 typealias 를 지워도 잘 실행된다.
UIViewControllerRepresentable를 준수하는 구조체를 만들때
- makeUIViewController : 사용할 view controller 의 type 을 반환할 때 명시
- updateUIViewController : 그냥 쓰면됨
- typalias 를 쓰면 makeUIViewController 할 때 자동으로 반환 type 지정해주는데 안쓰면 some UIViewController 로 되어 에러가 발생함
Connecting delegates, data sources and more (Coordinator)
UITableView에 익숙하다면 어떻게 UITableVIewController에 data source 와 delegates 를 제공하는지 궁금할 것 이다.
struct 내부에 필요한 데이터가 있으나 UIKit에서 직접적으로 데이터에 접근할 경우 app crash 난다.
대신 NSObject 파생 클래스로 Coordinator object를 만들어야 한다.
이 Coordinator class는 SwiftUI 내부에 있는 데이터와 외부 framework 사이의 transition(전환) 또는 bridge(다리) 역할을 한다.
updateUIViewController(_:context:)를 보면 두번째 파라미터로 context가 절달되는 걸 알 수 있다.
클래스에 비행 정보를 전달하기 위해 사용자 지정 initializer 와 함께 클래스를 만들었다.
이 Coordinator를 사용하면 UITableView에 대한 delegate와 data source를 연결할 수 있습니다.
사용자 이벤트를 처리하는 데 사용할 수도 있습니다.
SwiftUI에게 Corrdinator class 에 대해 알려줘야 한다.
FlightTimeline struct 의 top 에 코드 추가한다.
이렇게 하면 coordinator가 생성되고 SwiftUI 프레임 워크로 반환되어 필요한 곳에 전달된다.
SwiftUI는 makeUIViewController (context :) 전에 makeCoordinator ()를 호출하므로 SwiftUI가 아닌 구성 요소를 만들고 구성하는 동안 사용할 수 있습니다.
이제 UITableViewDelegate 와 UITableViewDataSource 를 Coordinator class 내부에 구현할 수 있다.
UITableVIewDelegate를 이번에는 사용하지 않을 것 이지만 UITableViewDataSource는 구현해야 한다.
이 두 메서드는 UITableView에 대한 데이터를 제공한다.
tableView (_, numberOfRowsInSection)은 배열의 항목 수를 테이블의 항목 수로 반환한다.
tableView (_ : cellForRowAt :)에서 updateUIViewController (_ : context :) 메서드에 등록 된 TimelineTableViewCell을 사용하여 해당 항공편에 대한 타임 라인 셀을 생성하고 반환한다.
이 클래스와 컨트롤은 SwiftUI에 대해 아무것도 모른다.
여기에서 사용한 코드는 UIKit 에서처럼 작동한다.
이제 UITableViewDataSource를 구현 했으므로 UITableView에 대해 설정할 수 있다.
updateUIViewController (_ : context :) 맨 위에 다음 줄을 추가한다.
그리고 ContentViews.swift 에서 방금 만든 view 로 가는 NavigationLink를 만든다.
정리: UITableViewController (UIKit) 을 SwiftUI 에 Integration 방법
- UIViewControllerRepresentable 을 준수하는 구조체를 만든다.
- 해당 프로토콜이 필요로 하는 메서드 2개 (makeUIViewController, updateUIViewController)를 작성한다.
- Coordinator 를 통해 data source 랑 delegate 를 연동한다.
- SwiftUI에서 NavigationLink 등의 방법으로 view present 로직 구현
Building reusable views
- SwiftUI는 더 작은 뷰에서 뷰를 구성한다는 아이디어를 기반으로 한다.
- 이 때문에 종종 뷰 내의 뷰 내에서 막대한 뷰 블록과 코드 화면에 걸쳐있는 SwiftUI 뷰로 끝날 수 있다.
- 구성 요소를 별도의 뷰로 분할하면 코드가 더 깔끔해진다.
- 또한 여러 위치와 여러 앱에서 구성 요소를 더 쉽게 재사용 할 수 있다.
- 이 장에서는 Awards 뷰를 현재 List 에서 Grid로 수정한다..
- 앱을 빌드하고 실행합니다. Awards 버튼을 탭하여 Award보기를 불러 오면 Awards 목록이 하나만 표시된다.
- 과도한 스크롤로부터 사용자를 구하기 위해 List 대신 Grid에 표시해보자
- UIKit 앱에서는 UICollectionView를 사용하여 그리드를 만들 수 있습니다.
- 안타깝게도 현재 버전의 SwiftUI에는 UICollectionView 를 지원하지 않는다.
- 따라서 UICollectionView 대신 SwiftUI에서 Custom Grid 를 만들어 본다.
- 먼저 현재의 모든 수상 내역에 대한 정보를 담은 배열을 만든다.
- 처음 세 개의 수상내역은 13 장에서 만든 것들이다.
- 나머지는 전달 된 매개 변수에서 곡선을 그릴것이다.
- 또한 각 수상에 대한 제목과 설명을 제공하고 테스트를 위해 모든 상을 수상한걸로 정한다.
- 두 번째 속성은 전체 배열을 수상한 상만 표시하도록 필터링하여 보여준다.
UICollectionView 를 완전히 대체할 수 있는 view 가 아닌 UICollectionView grid 와 비슷한 view 를 만들어 본다.
GridView.swift 라는 새로운 SwiftUI 파일을 만든다.
다음 이 list 를 grid로 만들어 본다.
Displaying a grid
- 그리드를 구성하는 방법에는 여러 가지가 있지만 가장 일반적인 방법은 여러 열로 구성된 행 집합을 만드는 것이다.
- 그리드의 항목은 첫 번째 행과 첫 번째 열에서 시작하여 첫 번째 행을 가로 질러 가로로 계속되고
- 다음 행이 첫 번째 행이 멈추는 곳을 선택한다.
- 표시 할 항목의 끝에 도달 할 때까지 반복된다.
- 마지막 행을 채우는 데 필요한 것보다 적은 항목이있는 경우 그리드는 항목을 비워 둔다.
- SwiftUI 용어로 각 행에 대한 HStack view로 구성된 VStack으로 그리드를 만들 수 있다.
- HStack의 내용은 그리드의 column에 해당된다.
item 프로퍼티 전에 column의 갯수를 담는 변수를 추가해라
VStack 내부의 ForEach 루프는 배열의 각 요소를 통과한다.
VStack이 이제 그리드의 각 row을 래핑하므로 대신 row을 통과하도록 루프를 변경한다.
이 코드는 items 의 갯수에 따라 몇개의 coulumn이 필요할지 계산한다.
그리고 ForEach 루프를 수정한다.
- 이제 VStack 내부의 행을 반복한다. 0에서 시작하여 계산 된 행 수보다 적은 1까지 행에 번호를 매긴다.
배열과 마찬가지로 3 개의 행을 0, 1, 2 행으로 지정한다. - columns 수를 알고 있으므로 0에서 시작하여 columns 수보다 적은 숫자 1에서 끝나는 각 열을 다시 반복한다.
- 배열의 요소를 표시하려면 행과 열에 해당하는 인덱스를 계산합니다.
배열은 0에서 시작하므로 0에서 행과 열을 계산하기 시작할 때 계산이 더 간단하다.
각 행의 시작에 대해 행 번호에 각 행의 열 수를 곱한다.
그리드의 첫 번째 행은 0으로 시작한다.
그런 다음 열 번호를 추가하여 해당 행의 첫 번째 열이 인덱스 0에 있고 두 번째 열이 인덱스 1에 있도록하는 식이다,
다음 행은 다음 요소와 일치하는 index 에서 시작한다.
만약 여기서 item 을 하나 더 추가하면 preview 에서 에러가 난다.
Debug preview 를 보면 "Fatal error : Index out of range" 가 원인이다.
루프 행을 통과하는 마지막 row는 3이되고 column은 1이된다.
루프는 7 번째 요소에 접근하려고 시도한다.
그런데 배열에는 0에서 6까지의 7 개 요소 만 있으므로 "out of range" 인 것이다
다음 요소가 존재하지 않는지 확인하는 함수를 추가 할 수 있습니다.
만약 갯수가 column 의 배수가 아니면 마지막 row에 빈 요소가 있다는 것 이다.
이 함수는 현재 행과 열을 가져와 이전과 동일한 계산을 수행한다.
그런 다음 인덱스가 배열에서 유효한 위치인지 확인하고 그렇지 않은 경우 nil을 반환한다.
그리고 ForEach 내부 Text 에 대해서도 수정한다.
오류 메시지가 표시된다고 한다.
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
제어 흐름 문을 포함하는 클로저는 함수 빌더 'ViewBuilder'와 함께 사용할 수 없습니다.
현재 SwiftUI는 optional 을 다루지 못하고 조건만 다룬다고 해서 code를 바꿔라고 하는데
나는 오류 없이 잘 된다. 업데이트 된건가
잘되도 시키는대로 코드를 바꾼다.
마지막 1이 이상하다. 빈 공간이 안생긴다. nil 이면 empty Text를 넣자
조금 왼쪽으로 이동했다.
이제 columns 와 items 를 바꿔도 gridview 가 잘? 나온다.
Using a ViewBuilder
현재 GridView 는 Text 를 표시한다.
Text 를 위한 GridView, Image를 위한 GridView 다 따로 만들 수도 있지만 그러나 호출자가 grid 각 셀에 표시 할 내용을 정하는 것이 더 효율적이다.
이때 SwiftUI 의 ViewBuilder가 등장한다.
ForEach(0..<items.count) { index in
Text("\(self.items[index])")
}
ForEach 루프 내부의 view를 제공한다. ForEach는 ViewBuilder를 사용하여 view 생성 인클로저에 대한 매개 변수를 만든다.
GridView를 업데이트하여 이러한 인클로저를 사용하여 그리드에있는 각 셀의 내용을 정의할 수 있다.
GridView의 정의를 바꿔라
그리고 item 변수 다음에 방금 정의한 Content를 저장하는 파라미터를 만들어라
custom initializer로 만들어라
- initializer는 이전 Column 수 및 정수 배열과 함께 Content라는 인클로저를 허용한다.
- 또한 인클로저가 단일 Int 매개 변수를 받도록 정의한다.
- 이러한 변경으로 이제 GridView에 대한 인클로저를 지정할 수 있다.
- 그러면 루프가 grid의 각 요소에 대한 인클로저를 표시 할 수 있다.
- 매개 변수를 사용하여 어레이의 현재 요소를 인클로저에 전달할 수 있다.
이제 content parameter 를 사용하기 위해 grid 를 바꿔볼 것 이다.
ForEach loop 를 수정해라
- 루프 내에 뷰를 포함하는 대신 엔클로저를 포함하는 콘텐츠 뷰를 호출하고 정수 배열의 현재 요소를 엔클로저에 매개 변수로 전달한다.
- 두 케이스가 단일 요소로 작동하도록 그룹 내에서 조건부를 wrapping 한다.
이렇게 ForEach 내부에 작성된 View를 Grid 로 나타내는 게 아닌 ViewBuilder를 사용해서 view 생성 enclosure에 대한 매개변수를 만들어 외부로 부터 View를 받아서 Grid 에 표시하는 방법을 공부해봤다.
마지막 줄이 이상하다. Spacing 으로 수정해보자
Spacing the grid
이번 grid 는 크기를 column으로 나눈다.
GeometryReader를 통해 view 의 크기를 가져올 수 있다.
ScrollView 주위의 코드를 수정해 GridView 의 ScrollView 를 GeometryReader 로 wrapping 한다.
그리고 제일 하단의 items의 width 와 height 를 geometry size를 활용해 column 등분 한다.
*그리드를 사용할 때 그리드의 column 수가 내용을위한 충분한 공간을 제공하는지 확인해야한다.
이제 Grid 완성한것 같지만 현재는 데이터를 정수 배열만 쓸 수 있다.
여러 데이터 를 쓸 수있도록 제네릭으로 바꿔주자
Making the grid generic
Generics를 사용하면 사용중인 데이터 유형을 구체적으로 지정하지 않고도 코드를 작성할 수 있다.
함수를 한 번 작성하면 모든 데이터 유형에 사용할 수 있다.
GridView 정의를 수정한다.
여기서는 구조체에게 제네릭 유형을 사용한다고 알린다.
Int, String 또는 다른 유형을 지정하는 대신 이제 T를 지정할 수 있다.
이제 Int 배열의 인스턴스를 대신 T type의 배열로 변경해서 사용한다.
그리고 enclosure 에 전달하는 파라미터의 type 도 T로 바꿔준다.
custom initializer 또한 T 타입으로 바꿔준다.
끝이다.
Using the grid
gridView 를 다 만들었으니 사용해 보자
AirportAwards.swift 에서 수정한다.
Key ponts
- Representable 프로토콜을 사용하여 뷰를 작성하여 SwiftUI를 다른 Apple 프레임 워크와 통합
- 이러한 프로토콜에는 view를 만들고 configure 작업을 수행하는 데 필요한 두 가지 방법이 있음
- Controller 클래스는 SwiftUI 뷰의 데이터를 이전 프레임 워크의 뷰와 연결하는 방법을 제공함.
이를 사용하여 delegate 및 관련 패턴을 관리 할 수 있음 - SwiftUI 뷰 내에서 Controller를 인스턴스화하고 Controller 클래스 내에 다른 프레임 워크 코드를 배치함
make, update 메서드 - VStack, HStack 및 ZStack을 결합하면 더 복잡한 레이아웃을 만들 수 있음
- ViewBuilder를 사용하여 반복을 수행 할 때 뷰를 다른 뷰로 전달할 수 있음
- 제네릭을 사용하면 특정 유형을 하드 코딩하지 않고도 뷰가 작동함
Challenge
방금까지는 view 를 균등으로 분할해서 정사각형으로 크기를 설정했다.
그 대신에 계산된 grid cell 크기를 enclosure 에 전달하고 레이아웃을 결정해봐라
---
enclosure에 전달할 파라미터를 추가한다.
그리고 루프 내에서 self.content에 대한 호출을 변경하여 계산 된 너비를 인클로저에 적용하는 대신 인클로저에 전달한다.
이제 enclosure 안에서 width 를 사용할 수 있다.
왜 grid Width 를 파라미터로 전달하려는 걸까
forums.swift.org/t/style-explicit-self-when-referencing-instance-member/21173
여기서도 self 명시적으로 붙이라고 warning 이 뜬다.
hcn1519.github.io/articles/2017-09/swift_escaping_closure