iOS/SwiftUI

#15 Complex Interfaces

HaningYa 2020. 10. 27. 08:37
728x90
  • 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 을 사용해 하루의 비행편 타임라인을 만들어 볼 것 이다.

 

kf99916/TimelineTableViewCell

Simple timeline view implemented by UITableViewCell - kf99916/TimelineTableViewCell

github.com

SPM 을 지원하기 때문에 쉽게 프로젝트에 추가할 수 있다. (starter project 는 이미 가지고 있음)

SPM 이 현재 bundling resources 를 지원하지 않기 때문에 (TimeLineTableViewCell control 을 만들 때 사용한 custom nib file 포함) 메인 프로젝트의 UI group 에 복사된 nib file을 볼 수 있다.

UIViews 와 UIViewControllers 를 SwiftUI 에서 다룰려면
UIViewRepresentableUIViewControllerRepresentable 프로토콜을 준수하는 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가 절달되는 걸 알 수 있다.

 

Any vs AnyObject vs NSObject in Swift 3

What is the difference between these three enigmatic types? A sometimes confusing topic, and to confuse things further Swift 3 has shaken it up by removing implicit bridging between Foundation and …

craiggrummitt.com

클래스에 비행 정보를 전달하기 위해 사용자 지정 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 방법

  1. UIViewControllerRepresentable 을 준수하는 구조체를 만든다.
  2. 해당 프로토콜이 필요로 하는 메서드 2개 (makeUIViewController, updateUIViewController)를 작성한다. 
  3. Coordinator 를 통해 data source 랑 delegate 를 연동한다.
  4. 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를 바꿔라고 하는데

나는 오류 없이 잘 된다. 업데이트 된건가

이제 써도된다고 한다.

 

Unwrapping optional in SwiftUI View

I try to unwrap my optional property, but get this Error message: Closure containing control flow statement cannot be used with function builder 'ViewBuilder' I don't see whats wrong with my code

stackoverflow.com

잘되도 시키는대로 코드를 바꾼다.

마지막 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의 배열로 변경해서 사용한다.

원래 Int 였음

그리고 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 이 뜬다.

 

[Style] Explicit self when referencing instance member

This was discussed previously either to always require self SE-0009 (rejected), or have an optional compiler warning (rejected). Let's discuss whether and when this should be a recommended practice. Required vs Optional There is only one case where you are

forums.swift.org

 

hcn1519.github.io/articles/2017-09/swift_escaping_closure

 

Swift Escaping Closure 이해하기

Swift의 Escaping Closure에 대해 알아봅니다.

hcn1519.github.io

 

 

 

728x90