 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/Stanford iOS Programming (SwiftUI)

Lecture 5 Review Part3: Properties Layout @ViewBuilder

singularis7 2021. 8. 23. 17:24
반응형

Property Observers

"Watching" a var and reacting to changes

이전 데모 영상에서 computed property 개념을 다루었는데 많은 사람들이 property observer와 computed property의 개념을 혼동하는 경우가 많다고 한다. 이번 기회에 두 개념을 양옆에 두고 차이점을 이해해보는 시간을 가져보자! 두 개념은 완전히 다른 개념이기 때문이다.

이전에 우리는 Swift가 struct의 변동 여부를 알 수 있다는 점을 여러 측면에서 봐왔다. 그중 가장 중요한 두 지점이 있는데 ViewModel에서 struct로 선언된 model이 연결될 때 @Published 키워드를 선언함으로써 model이 변경될때마다 @Published는 자동으로 세계에 무언가 변경되었다고 말하였다. 이와 같이 구조체 변경을 감지하는 방식은 구조체가 변경되고 있음을 감지하는 Swift의 내장된 기능이다.

Swift가 struct의 변동여부를 알 수 있었던 다른 지점은 UI 코드에서도 찾아볼 수 있다. CardView는 Card를 인자로 받고있으며 우리가 카드를 만들기 위해서 body를 rebuild할 때 card가 변경되지 않은 상태라면 CardView의 body를 재생성하지 않는다. 따라서 SwiftUI의 효율성에대한 요점은 변경되지 않은 body에 대하여 rebuilding 작업을 수행하지 않는다는 점이다. 

Swift는 프로그래머에게도 구조체가 실제로 변경되었을 때 행위를 정의할 수 있는 기능을 제공해주고 있다. 이것이 바로 propery observer이다. 사용 예시는 다음과 같다.

 

isFaceUp 변수는 computed property가 아니라 실제로 메모리 어딘가에 값이 존재하는 일반적인 변수이다. 다만 중괄호 이후에 willSet 부분에 어느 행위가 정의되어있는데 이 행위는 isFaceUp 변수를 감시하고 있다가 isFaceUp값이 newValue로 변경되기 직전에 호출된다. 위 코드의 의도는 매번 카드의 isFaceUp이 변경될 때마다 보너스 타임을 시작하고 중지할 것이다. 

위 예시를 통해 Swift가 코드 등에서 일어나는 일을 시작하기 위해 구조체가 변경되었음을 알고있다는 사실을 활용하는 힘을 조금이나마 엿볼 수 있기를 기대한다.

비슷한 개념으로 didset이 존재한다. didset을 활용하면 값이 변경된 직후에 호출되며 암묵적으로 사용되는 oldset 변수를 통해 변경되기 이전 값에 접근할 수 있다. 앞으로 데모에서 활용 예시를 계속 볼 수 있으니 큰 걱정하지 않아도 된다.

Layout

How does SwiftUI figure out what goes where on screen

이번 강좌의 주요한 주제는 바로 레이아웃이다. 어떻게 화면의 공간을 모든 View에게 나누어줄 것인가? 누가 어떤 공간을 차지할지 어떻게 결정할 것인가?에 대한 질문이다. 아래의 내용은 우리가 UI를 배치하는 방법이다.

  1. Container View는 내부에 있는 View에게 공간을 "제공"해준다.
  2. 내부에 있는 View들은 제공된 공간에서 자신이 원하는 만큼의 자체 크기를 선택한다. 
  3. Container VIew는 내부에 있는 View의 크기를 알고 있으므로 공간에 배치한다.
  4. (2번과 유사함) Container View 또한 다른 Container에 소속되어있을 수 있기에 자신의 크기를 선택한다.

SwiftUI에서 제공하는 Container View에 위와 같은 사항이 어떻게 적용되고 있는지 살펴보자! 

General HStack & VStack Combiner

HStack과 VStack은 가장 많이 사용되는 레이아웃 메커니즘 container view일 것이다. HStack과 VStack의 특징은 View에 제공되는 내부의 공간을 명확하게 나누어주는데 least flexible한 subview에 먼저 공간을 제공한다. least flexible한 view에 대하여 이해해보기 위해 몇가지 예시를 생각해보자!

  • 어떤 View는 Image처럼 inflexible 하다. 즉, 해당 View는 Image가 자연스럽게 보일 수 있는 고정 크기를 원한다.
  • 어떤 View는 Text처럼 inflexible 하다. 즉, 자신이 가진 text의 크기에 딱 알맞은 크기를 원한다.
  • 가장 flexible한 View의 예시로 RoundedRectangle이 존재한다. 제공된 모든 공간을 최대한 사용해 버린다.

이렇듯 HStack과 VStack이 동작하는 방식은 가장 유연하지 않은 View 부터 공간을 나눠주기 시작해서 마지막에 가장 유연한 View가 나머지 공간을 채우도록 설계되어있다. 따라서 마지막에 남는 가장 유연한 뷰들은 남은 공간을 균등하게 나누어 갖게 될 것이다. 

이제 HStack과 VStack의 내부에 있는 모든 view의 크기가 결정되었으니 스택의 크기 또한 이에 맞춰 자체적으로 조정된다. 이때 만약 스택 내부에 있는 모든 View가 Flexible할 경우 stack 또한 flexible 하게 동작한다. 

HStack과 VStack을 사용할 때 써먹기 좋은 몇가지 View에 관해 소개해본다.

  • Spacer: 아주 Flexible한 View 중 하나로 이것에 할당된 모든 공간을 채워버리지만 아무것도 그려내지 않는다. 
  • Divider: HStack이나 VStack에서 다른 View를 구분할 수 있는 선을 그려준다. 예를들어 HStack에서는 수직선을 볼 수 있다. 유연하지 않으며 선을 그리기 위한 최소한의 공간을 사용한다.

앞서 소개했듯이 일반적으로 VStack과 HStack을 사용할 때에는 Least Flexible한 View를 우선으로 공간 할당을 진행한다고 했는데, layoutPriority 메서드를 활용하여 우선순위를 수동으로 조절할 수 있다. 파라미터로 아무것도 지정하지 않으면 0이 기본값이며 부동소수점도 인자로 넣을 수 있다. 

위 예시의 경우 Text View가 가장 먼저 공간을 할당받고 이후에 가장 덜 유연하게 동작하는 Image View가 다음 순서로 공간을 가져가게 되며 마지막으로 "Unimportant" String이 담긴 Text View가 마지막으로 공간을 확보하게 된다. 만약 이 과정이 수행되면서 가장 마지막에 담긴 Text View에 할당된 메시지가 전부 표시될 만큼 공간이 할당되지 않는다면 String을 말 줄임표 형태로 표현하여 일부 텍스트를 생략하여 출력한다. "Unimportant"가 "Unimp..." 이런식으로 말이다.

HStack과 VStack의 마지막 측면은 Alignment(정렬)이다. 예를 들어 VStack을 활용하여 View의 열을 만들때 표시할 Sub View가 VStack의 컬럼에 전체 너비를 채우지 않는다면 어떻게 배치할 것인가? 정답은 프로그래머가 위치를 구체화해주는 것이다.

alignment를 지정해주지 않으면 center 정렬을 기본값으로 갖는다. 다른 종류도 선택할 수 있는데 가장 흥미로운 것은 ".leading"과 ".trailing"이다. 왜 .left 혹은 .right가 아닌 이런 선택지가 존재하는가? 왜냐하면 Stack은 사용자의 언어 환경에 맞추어 View를 정렬하기 때문이다! (상상하지 못한 일이다!!)

예를들어 한국어에서의 텍스트는 왼쪽에서 오른쪽으로 흐른다면 아랍어나 히브리어 계통의 세상에서는 반대로 텍스트가 오른쪽에서 왼쪽으로 흐른다는 점이다. 위에서 언급된 .leading과 같은 키워드들은 세상의 이런 차이점을 반영하여 사용자의 언어 환경에 맞추어 스스로 View의 위치를 정렬하도록 도와주는 것이었다!

Lazy HStack & VStack Combiner

이전에 데모앱을 구현하면서 Combiner 중에서 LazyHStack, LazyVStack을 사용해본 기억이 있을 것이다. Lazy 버전의 Stack에는 일반적인 Stack과 두가지의 주요한 차이점이 존재한다.  

  • 화면에 존재하지 않는 View의 body를 Build하지 않는다.

위 사항은 ScrollView를 활용하여 HStack혹은 VStack 콘텐츠를 넣을 때 사용하게 된다. 예를들어 10,000개의 항목이 있고 그중에 12개만 화면에 표시가 된다면 나머지 9,900개 항목이 화면에 표시되지 않기 때문에 이를 모두 계산하고 싶지 않을 것이다. LazyHStack과 LazyVStack은 바로 그 일을 한다. 

  • Lazy Stack 시리즈는 Flexible 하게 동작하지 않는다. 내부에 Flexible한 보기가 있는 경우일지라도 내부의 보기에 제공된 공간 중 모든 공간을 가져가지 않으며 가능한 가져가는 공간을 작게 만든다. 

왜 위와 같이 동작하는 것일까? 대부분 Lazy Stack 시리즈는 ScrollView와 함께 사용되고있다.Scroll View는 무한히 커질 수 있는 개념이니 모두가 flexible하게 동작해버리면 가능한 공간을 계속 채우려고 동작할 것이고 결국 모든 View가 아주 거대해져 버릴 것이다. 따라서 LazyHStack과 LazyVStack은 크기 자체가 유연하지 않다. 

ScrollView

 ScrollView 는 자신에게 제공된 모든 공간을 사용한다. ScrollView 내부의 View들은 그 주위를 스크롤하는 만큼 알맞게 위치하게 된다. 

그외 나머지 지금 보지 않을 개념들...

List, Form, OutlineGroup이라는 개념도 존재한다. VStack계통의 아주 똑똑한 친구들이다. 아주아주 중요하지만 추후의 강의에서 다루도록 한다. 

ZStack

ZStack은 자신이 갖고 있는 child view에 자신의 크기를 맞춘다. 즉, 자신이 갖고있는 children 중 하나라도 완전히 flexible한 크기를 갖고 있다면 ZStack 또한 마찬가지로 완전이 flexible한 크기를 갖도록 동작하기 때문에 ZStack을 사용할 때에 반드시 생각해보아야 한다.

ZStack 개념을 활용하기 위한 몇가지 대안이 존재한다. 우선 background modifier를 사용해보는 것이다. 이것은 인자로 받는 View에 대하여 메서드가 호출된 주체 View의 배경처럼 동작하도록 만들어준다. 따라서 서로 곂쳐진 ZStack과 유사하게 동작하게 된다. 

여기서 ZStack 컴바이너를 사용하여 rectangle과 text를 묶는 경우와 위와 같은 경우에 대해서 분명한 차이점이 존재한다. 위와 같은 경우는 text의 크기에 맞춰 배경 rectangle이 결정되지만 전자의 경우 rectangle이 유연한 친구이기 때문에 Z-Stack도 마찬가지로 유연하게 동작하여 화면을 가득 채우게 된다.

.overlay modifier도 사용해볼 수 있을 것이다. background modifier과 같은 규칙이 적용되지만 주체 View의 뒷쪽이 아닌 앞쪽으로 쌓인다는 특징이 있다. 언급된 두개의 modifier는 container 처럼 동작하며  작은 단위의 ZStack을 만들때 자주 사용된다고 하니 기억해두자!

Modifiers

.padding과 같은 View Modifier 함수들은 VIew를 리턴하며 리턴된 그 View는 개념적으로 어떻게 되었든지 수정사항을 반영한다. 예를 들어 .padding 을 생각해보자!

padding은 View에 해당되는데 View를 수정하여 다른 View를 생성하지만 그 자체가 View이고 공간이 제공된다. 예를들어 padding의 인자로 10이 들어가면 주어진 공간에서 가장자리부분에 10pt 만큼을 제외하고 결과값을 리턴한다. 

aspect ratio 개념도 생각해볼 수 있다. 이 함수는 제공된 공간을 가져온 뒤에 주어진 비율에 맞추어 크기를 조정한다. fit으로 설정하면 비율에 맞추어 화면에 알맞게 크기가 줄어들며 fill으로 설정하면 공간외부로 나가 종횡비에 맞추어 가득 채워버린다. 

위 코드를 해석해보자! 초록색으로 칠해진 가장 바깥쪽 컨테이너인 HStack에 padding(10)으로 수정되었기 때문에 바깥쪽 컨테이너에서 10만큼을 제외한 나머지 공간을 내부에 있는 View들이 사용할 수 있다. 또한 내부의 View들에 대하여 foregroundcolor를 제공한다. 이 친구는 공간, 레이아웃을 수정하지 않기 때문에 HStack에 제공된 공간과 동일한 공간을 제공하게 된다. HStack은 내부의 카드뷰에 대하여 aspectratio를 통과한 view에 대하여 모두에게 균등하게 공간을 할당시켜준다. 이 때 HStack이 각 카드에 할당해준 너비가 있을텐데 이 너비를 기준으로 aspect ratio 를 맞추어 결과물을 출력하게된다. 혹은 반대로 주어진 공간에 높이를 최대한 맞춘 뒤에 폭을 비율에 맞춰 골라오는 방법도 있다. 이러한 방식으로 view가 배치되니 알아두어야 할 것 같다. 

마지막으로 모든 공간을 차지하는 View에 대하여 생각해보자! CardView와 같이 작성된 사용자 정의 View들은 일반적으로 자신에게 제공된 공간을 차지하려고 시도해야한다. 그러나 실제로 제공되는 공간에 자신을 맞춰야 할 때도 있다. 좋은 예시로 Memorize 앱에서 CardView가 이모지로하여금 공간을 가득 채우도록 하는 폰트 크기를 선택하기를 원한다. 이것을 구현하려면 자신에게 주어진 공간의 크기가 얼마인지 알 수 있어야 한다.  이 기능을 GeometryReader가 제공한다.

Geometry Reader View는 다른 View를 둘러싸는 방법을 통해 사용할 수 있다. Geometry Reader의 유일한 인자(geometry)는 @ViewBuilder이며 이를 통해 둘러쌀 다른 View를 구체적으로 지정할 수 있다.  

geometry 파라미터는 GeometryProxy이며 GeometryProxy는 작은 구조체로 정의되어있다. 그중 눈에 띄는 것은 var size 이다. size 변수는 컨테이너에 의해 우리에게 제공되는 공간의 양이다. 이제 내부에 view는 자신에게 주어진 공간의 크기를 알고 있으니 이에 맞추어 자신의 크기를 조정할 수 있을 것이다.

여기서 빨간 부분을 이해하는 것이 중요한데 Geometry의 크기 자체는 항상 제공된 모든 공간에 맞춘다. 즉 Maximum Flexible하다. GeometryReader은 제공된 것보다 적은 공간을 사용하지 않는다. GeometryReader 안에 넣은 View가 확실하게 완전히 Flexible한 View가 되기를 원한다. 

공부를하다보면 Safe Area 라는 개념을 볼 수 있다. 그렇다면 화면에서 안전하지 않은 영역은 무엇인가? 대표적으로 iPhone X의 노치 부분을 생각해 볼 수 있을 것이다. 상단에 작은 노치 부분은 화면에 보이지 않기 때문에 그릴 수는 있지만 의미는 없을 것이다. 따라서 이렇게 안전하지 않은 영역을 자동으로 제거하여 사용자에게 제공하는 기능이 바로 Safe Area 이다. 하지만 때에 따라 safe area를 무시하고 싶을 때가 있을 수 있다. 예를 들어 사진 혹은 동영상 앱의 경우 사용자는 화면 전체를 가득 채워서 영상을 볼 수 있는 것을 기대할 것이다. 

간단한 modifier 하나를 사용하여 인자로 주어진 부분에 대한 SafeArea를 무시하도록 설정할 수 있다. 

반응형