#3 Understanding SwiftUI
선언형 프로그래밍
와 데이터 의존성 알아보고 어떻게 앱 디자인을 다르게 생각할 수 있을지 배운다.
왜 SwiftUI
IB의 단점
- Interface Builder와 스토리보드는 앱을 빠르게 만드는데 있어 많은 도움이 되었고 적응형 유저 인터페이스와 네이게이션을 위한 segues를 세팅하는걸 쉽게 만들어 주었다.
- 하지만 많은 개발자들은 view를 코드로 작성하고 싶어했고 왜냐하면 UI 를 코드로 작성했을 때 더 효과적으로 복사하거나 편집을 할 수 있기 때문
- 대부분 IB 와 스토리보드는 내부적으로 코드들이 적용되어 IBAction 이나 IBOutlet 을 코드에서 지우거나 이름을 수정했을 때 앱이 크래쉬나는걸 볼 수 있었다. (왜냐하면 IB는 코드의 변화를 보지 못하기 때문이다.)
- segues의 경우도 문자열 타입의 식별자를 사용하여 Xcode 가 미리 체크할 수 없는 어려움이 있었다.
SwiftUI의 장점
- SwiftUI는 IB 와 스토리보드를 무시하면서 UI를 만드는데 세세한 것 까지 코딩하지 않아도 되게 도와준다.
- SwiftUI 의 preview 를 코드와 함께 나란히 볼 수 있고 한 쪽에서의 변화는 다른쪽의 뷰를 업데이트 시켜 변경사항을 적용시켜 싱크해준다.
- 오류가 발생할 문자열 식별자도 없고 코드로 짤수 있으면서 UIKit보다 코드양도 적고 이해하고 수정하고 디버깅 하기 쉽다.
- SwiftUI API 들은 플랫폼별 일관성을 가진다. 그래서 멀티 플랫폼을 지원해야하는 앱을 개발할 때 강점을 가진다. (macOS, watchOS, tvOS)
SwiftUI 는 UIKit을 대체할 순 없다. 그래서 Swift 와 objc와 같이 동일한 앱에서 둘다 사용 가능하다.
이번 챕터에서는 swiftUI가 아닌 class 를 data source 로 사용해볼 것이다.
선언형 앱 개발
SwiftUI 는 선언형 앱 개발을 지원해준다. 어떻게 다르게 생각하는 지만 배우게 된다면 훨씬더 좋은 앱을 빠르게 개발 할 수 있다.
선언형 앱개발의 뜻은 너가 UI의 뷰들이 어떻게 보여질 지와 어떤 데이터에 의존하는지를 둘다 선언해 주는 것이다.
SwiftUI 프레임워크는 뷰를 만드는 것과 언제 뷰가 나타날지, 언제 업데이트가 될지, 의존하는 데이터의 변화에 따라서 바뀌게 도와준다.
뷰의 외형에 영향을 주는 state 를 선언하고 어떻게 SwiftUI가 데이터에 대해 반응할 지 선언해준다는 개념을 곱씹어 보면
reactive 프로그래밍의 느낌을 받을 수 있다.
그래서 만약 reactive programming frameworks에 익숙하다면 SwiftUI를 배우는데 더 쉬울 것이다.
Views
선언형 UI는 문자열 타입의 식별자가 없이 항상 코드와 동기화 되어있다.
View들을 navigation과 데이터를 위한 presentation logic을 캡슐화 하는 레이아웃으로 사용해라.
선언형 UI의 또다른 장점은 크로스 플랫폼 간의 API 가 일정하다는 것이다.
Controls는 외형이 아닌 role (역할)을 설명해서 같은 컨트롤이 적용되는 플랫폼에서 적절히 보여지도록 한다.
Data
- 선언형 데이터 디펜던시는 view들을 데이터가 바뀔 때 업데이트 시킨다.
- SwiftUI Framwork 는 view와 그 하위 자식 뷰들 모두를 다시 계산하여 변경사항에 대해 렌더링을 진행한다.
- view state 는 데이터에 의존되어 있어 view 가 어떻게 데이터를 사용하는지, 데이터가 변경되었을 경우 어떻게 view 가 반응해야 할지, 또는 뷰가 데이터를 어떻게 바꾸는지 선언한다.
- view가 가능한 state 별로 뷰의 모양을 선언해줘야한다.
Navigation
Conditional subviews는 navigation으로 대체될 수 있다.
Integration
SwiftUI 를 UIKit 앱에 통합하거나 그 반대도 쉽다.
Primitive views
- control, layout, paint 와 다른 view들의 리스트 들이다.
- 대부분의 것 특히 control views 의 경우 UIKit 요소와 많이 비슷하지만 몇몇은 SwiftUI에만 특별히 존재한다.
Modifiers
- controls, effects, layout, text등등 메소드로써 존재하는 view 에서 새로운 뷰를 만든다. 아무 view나 modifier들을 chaining 하여 pipeline을 구성해 view를 커스텀 할 수 있다.
- SwiftUI 는 작은 재사용 가능한 뷰를 만들어 modifier를 통해 커스텀 하여 특정 context 에서 사용할 수 있도록 권장한다.
- SwiftUI는 modified view 를 효율적인 데이터 구조로 만들어 collapses해놔서 이런 모든 편안함을 성능저하 없이 사용할 수 있다.
- 다양한 modifier들을 다양한 타입의 view 에 적용할 수 있다.
- 때로는 그 순서가 중요할 때가 있다.
Environment values
몇몇의 환경 값들은 전체 앱에 영향을 미친다. 대부분 이 것들은 디바이스 사용자 세팅, 예를들어 접근성, 로컬, 캘린더, 컬러 스키마이다. 이러한 환경 값들을 previews를 통해 시도해 볼 수 있고 유저 디바이스 세팅에 따라 일어날 수 있는 문제점을 예상할 수 있다.
Local Envieronment
view level 환경 값들을 통해 child view 에 영향을 줄 수 있다.
특정 child view 에 대해서 default 환경값을 override 할 수 있다.
Modifying reusable views
color slider 의 경우 아래와 같이 padding 이 들어가 있다.
vertical padding 을 넣을 경우에 어떻게 해야할까.
위 세개의 colorSlider는 logical unit이기 떄문에 상식적으로 각각의 ColorSlider 에 padding 을 주기 보단 unit 에 padding 을 주는 것이 맞다. 그러기 위해서 VStack 으로 감싸주고 해당 Container 인 VStack 에 padding 을 준다.
해당 작업을 통해 좀더 padding 값을 다양하게 줄 수 있는 재사용성이 높은 ColorSlider를 만들 수 있었다.
Adding modifiers in the right order
SwiftUI 는 modifiers를 너가 추가하는 순서대로 적용한다.
background color를 추가하고 padding을 주는 것과 padding을 먼저주고 background color 를 추가하는 것은 다른 시각적 효과를 준다.
*라인 순서를 위로 올리는 단축키: option-command-[
왜 cornerRadius가 적용되지 않았냐면 사실은 적용이 된건데 round corner 가 적용된 레이어 아래에 아무것도 없어서 티가 나지 않는 것이다. 그래서 background color는 전체 사각형에 칠해지게 된다.
Showing conditional views
.alert(isPresented: $showAlert) 에서 showAlert 와 같은 Bool 타입의 Status 변수를 통해서 조건을 가지는 view 를 보여 줄 수 있지만
좀더 명시적으로도 가능하다. 예를들어 showAlert가 true, 즉 Hitme 로 정답을 점수를 계산해봤을 때 Match this Color 에 정답을 표시해주고 아닌 경우 원본 텍스트를 보여주는 코드를 작성할 수있다.
ZStack
어떤 View 의 Center 에 subview 를 만들고 싶다면 ZStack 을 사용해라
ZStack 에서 Z 방향은 스크린의 수직방향(고도느낌)이다. ZStack 클로져에서 아래에 있는 Item 은 StackView 에서는 더 높이 있는 것 처럼 표현된다.
window가 down 방향일때 Y축 양의 방향
예를들어 위의 코드에서 Text를 ZStack 클로져에서 Color 위로 올리게 되면 Text는 Color 뒤에 있으므로 보이지 않게 된다.
텍스트 뷰의 outline은 볼 수 있으나 Color 에 가려진 상태이다.
좀더 꾸며보자면
ZStack 에 aliment 를 주어 center 정렬이 가능하고 Text에 padding과 background, 를 주고 동그랗게 masking 해서 꾸밀 수 있다.
Debugging
Live preview 에서 디버깅을 하려면 Debug preview 에 들어간다.
조금 기다리면 일반적인 디버깅 툴과 환경을 override 하는 툴과 runtime issue scanning 과 breakpoint 걸 수 있는 툴들이 나온다.
*디버깅 세션은 preview 생애주기에 묶여 있으므로 만약 view debugger를 새로운 에디터를 창으로 열었을 경우 preview 를 항상 열어놔야된다.
developer.apple.com/videos/play/wwdc2019/412/?time=539
위 WWDC 세션은 SwiftUI에서 디버깅하는 내용을 다루고 있다. 시청필수!
Declaring data dependencies
SwiftUI는 앱의 데이터 흐름을 관리하는데 크게 두가지의 가이드라인을 제공한다.
- Data access = dependency: view 에서 데이터를 읽는 것은 view 안에 있는 데이터의 의존성을 만든다. 모든 view는 해당 data dependencies의 function 이다.
- Single source of truth: view 가 읽는 모든 데이터들은 진실의 근원이다,,,? view 에 의해 소유되거나 view 외부에 있거나. 그 진실의 근원이 어디에 있던지 너는 항상 단 하나의 진실의 근원을 가지고 있어야 한다. 이것이 바로 왜 @State 값을 ColorSlider 내부에 선언하지 않은 이유이다. 내부에 선언을 하게되면 duplicate 된 진실의 근원이 존재함으로 rValue를 계속 동기화 해줘야 한다. 그 대신에 @Binding 값을 통해 view 가 다른 뷰로 부터 온 @State 에 depend 하게 만들어 줬다.
UIKit 에서는 view controller 들은 항상 model 과 view 를 동기화 해줬어야 했다.
하지만 SwiftUI와 같은 선언형 view 계층 구조와 단 하나의 진실의 근원은 더이상 view controller 를 필요 없게 만들어 준다.
Tools for data flow
SwiftUI는 데이터의 흐름을 관리하기 위해 여러가지 툴을 제공해 준다.
Property wrappers는 변수의 행동을 늘려준다. SwiftUI 에 특정되는 wrappers 예를들어 @State 나 @Binding @ObservedObject, @EnviromentObject는 데이터에 대한 view의 의존성을 변수에 의해 표현되게 만들어 준다.
각각의 wrapper는 다른 데이터 소스를 나타낸다.
@State
@State 변수는 view 가 소유한다. @State var 는 지속적인 스토리지에 할당되기 때문에 값을 초기화 해주어야 한다.
Apple 은 @State을 private 으로 선언하여 @State 변수가 해당 특정 view 에 의해 소유되고 관리되어진다라는 점을 강조하길 권한다.
* SceneDelegate 에서 파라미터를 전달하지 않기 위해 @State 변수를 ContentView 내에서 초기화 할 수 있다.
private 으로 선언하면 ContentView를 root view 로 초기화 하지 못할 것 이다?
@Binding
Binding은 다른 view 가 소유하고 있는 @State 에 의존한다는 걸 선언한다. $ prefix를 통해 binding 을 다른 view 에 전달한다. 받는 view 의 입장에서 @Binding 변수는 데이터에 대한 reference 이다. 그래서 초기화가 필요하지 않다. 이 reference는 해당 데이터에 의존하고 있는 모든 view 를 수정할 수 있게 해준다.
@ObservedObject
ObservableObject 프로토콜을 준수하는 reference type 에 의존성을 선언한다. objectWillChange 프로퍼티를 구현하여 데이터에게 변경사항을 publish 한다.
@EnvironmentObject
shared data 에 대한 의존성을 선언한다. shared data란 앱 전체에서 보여지는 데이터이다. parentview 에서부터 child, grandchild 로 하데이터를 전달하는 대신 데이터를 indirectly 하게 전달하는 편리한 방법이다. (특히 childview 가 필요로 하지 않을 때)
보통 재사용가능한 view 에서는 @State를 쓰지 않고 @Binding 이나 @ObservedObject를 대신 사용한다.
@State 의 경우 private 으로 선언하여 오직 view 만이 데이터를 소유할 수 있도록 해야한다. 데이터가 parent view 에 의해 소유되야 하는지 external source 에 의해 소유되야 하는지 생각해 보라.
Observing a reference type object
Combine을 이용해서 Timer 를 만들어 볼 것이다.
TImerCounter.Swift 파일을 만들고 Combine 을 import 한다.
TimerCounter는 publisher 역할을 할 것 이고 ContentView 는 subscribe 를 할 것이다.
ObservableObject 프로토콜과 Published property wrapper를 통해 counter 가 바뀔 때 마다 해당 작업을 구독한 subscriber들에게 매번 publish 를 해준다.
*ObservableObject - Published 는 일반적으로 Combine 을 사용하기 위해 제공된다. 특정 작업을 위한 observe subscribe 가 필요하다면 예를들어 Timer class의 TimerPublisher 와 같이 목적이 있는 걸 쓰면 되는데 Combine 책에서 배우면 된다.
Timer에 필요한 init 과 타이머 종료 메서드를 완성한다.
다음은 ContentView 로 돌아와 해당 Observable 을 subscribe 할 것이다.
먼저 새로운 프로퍼티를 선언한다.
@ObservedObject var timer = TimeCounter()
이로써 ObservableObject 프로토콜을 준수하는 TimerCounter class 에 대한 data 의존성을 선언하는 셈이다.
Combine 용어로는 TimerCounter publisher 에 subscribing 한다 라고 설명한다.
요약
- 선언형 앱 개발은 어떻게 view UI에서 보여질 지와 어떤 데이터에 의존하는지 두가지를 선언하는 것이다. SwiftUI 프레임워크는 view 를 만드는 걸 도와주고 언제 보여지고 업데이트 될지 어떤 데이터에 의존해서 바뀔지를 담당해준다.
- Library는 primitive view 와 modifier method들의 리스트 이다.
- 몇몇의 modifier들은 모든 view 타입에 적용 가능하지만 몇몇은 특정 view 타입에만 적용이 가능하다. modifier의 순서 또한 중요하다.
- Data access = dependency : view 내에서 데이터를 읽는 것은 그 뷰와 데이터사이 의존성이 만들어 지는 것 이다.
- Single source of truth: 모든 데이터는 내부적으로나 외부적으로 진실의 근원이다. 단 하나의 진실의 근원만이 있어야 한다.
- property wrapper 는 변수의 활동 범위를 넓혀준다. 위에 예시 있었다.
- @Binding은 다른 view 에 의해 소유된 @State 에 대한 의존성을 선언한다. @ObservableObject는 ObservableObject 프로토콜을 준수하는 러퍼런스 타입에 의존성을 선언한다. @Envierment 는 shared data에 대한 의존성
- 런타임 디버깅하는 방법에 대해서도 알아보았다.