[SwiftUI] SwiftUI의 View Layout 결정

Eric·2022년 11월 7일
0
post-thumbnail

도입부

여느 때랑 다름없이 사지방에 앉아서 코딩을 하고 있었다.
힘들어서 잠깐 쉬면서 이것저것 만지고 있었는데, 갑자기 맨날 보던 화면이 문득 이상하다는 생각이 들었다.

저 Hello World!가 왜 프리뷰 중앙에 있지??

좌측 코드를 보면 알겠지만, 나는 Text("Hello World!")에게 위치를 특정해주는 어떠한 modifier(.position, .offset)도 사용하지 않았다. 그냥 Text하나만 달랑 썼는데 자동으로 중앙으로 배치되어 있는 것이다.
물론 정서상(?)으로 중앙이 맞긴 하지만, 처음 코딩을 배울 때, 저 텍스트가 좌측 상단에 표시되었어도 전혀 이상하지 않았을 거 같다는 생각이 들었다.

나는 이런 화면 배치에 고의성이 느껴져서 정리된 내용이 있는지 찾아봤는데, 아니나 다를까 한국어로 정리된 글도 엄청 많고, 아예 WWDC에서 광고를 광고를 했는데도 나만 모르고 있었다.

이렇게 알게된 이 글의 주제는 바로 SwiftUI가 View의 레이아웃을 결정하는 과정이다.

Layout Basics

뷰의 레이아웃을 결정하는 과정을 배우기 전에 먼저 뷰 계층(view hierarchy)에 관한 이해가 필요합니다.
이해를 위해 WWDC 19에 나온 간단한 뷰의 동작을 살펴보겠습니다.

아래 코드에는 3개의 뷰가 동작하고 있습니다.

뷰 계층의 맨 아래에는 Text가 있고,

그 위에는 ContentView가 있습니다.

이 때, 레이아웃 중립(Layout neutral)이라는 개념이 나오는데,
body를 가진 모든 상위 뷰는 그들의 경계가 body의 경계와 동일한 것을 말합니다.
쉽게 말하면, 뷰 레이아웃에 아무런 영향을 끼치지 않는 것이라고 이해해도 좋을 것 같습니다.

위 예시에서는 body를 가진 ContentView를 레이아웃 중립이라고 부르고, 실제로 bodyContentView는 동일한 경계를 가지고 있습니다.
그래서 예시 이미지 우측에 표시되는 경계가 Text 와 같음을 볼 수 있습니다.

그리고 그 위엔 Root View가 있습니다.

레이아웃 중립인 ContentViewText와 같은 경계를 나타내기 때문에 생략하고 위처럼 표현할 수 있습니다.

Layout Process

레이아웃을 결정하는 원칙은 크게 3단계로 구성됩니다.

1. Parent ViewChild View에게 사이즈를 제안한다.
2. Child View는 자신의 사이즈를 결정한다.
3. Parent ViewChild View를 자신의 좌표 공간에 배치한다.

Parent View는 뷰 계층에서 상대적으로 상위에 있는 뷰를 말하고,
Child View는 뷰 계층에서 상대적으로 하위에 있는 뷰를 말합니다.

1. Parent View의 제안

뷰 계층의 최상단에 위치한 Parent가 Child에게 차례차례 사이즈를 제안해옵니다.

예시에서는 Parent ViewRoot ViewChild ViewText에게 사용 가능한 모든 영역을 제안합니다.

2. Child View의 사이즈 결정

하지만 Text는 제안된 영역을 다 사용하는게 아니라 자신에게 필요한 영역만 선택합니다. 위 예시에서는 Hello World가 들어갈 수 있는 공간만을 차지하겠다고 응답합니다.
그리고 SwiftUI에서는 이런 Child View의 size를 강제할 수 있는 방법이 없기 때문에 Parent View는 이 결정을 들어 주어야만 합니다.

3. Parent View에 Child View를 배치

Root View는 크기를 결정한 Text를 자신의 공간에 중앙에서부터 배치합니다. >> 내 궁금증은 이거였는데, 크게 특별한 이유는 없었다.(ㅠㅠ)

SwiftUI의 모든 레이아웃은 위 과정을 통한 Parent와 Child의 상호작용으로 나타나게 됩니다.

Child choose its own size

세 개의 원칙 중에서 두 번째 과정에서 Text가 자신에게 필요한 사이즈를 String의 크기를 고려해 결정한 것처럼 각 뷰들은 자신의 사이즈를 조절하는 방법 및 시기를 결정할 수 있습니다.

import SwiftUI

struct ContentView: View {
	var body: some View {
     VStack {
         Image(systemName: "globe")     
             .foregroundColor(.accentColor)
         Text("Hello world!")
         	.background(Color.red)
      }
   }
}

위 예시 코드의 뷰 계층을 그려보면 TextImage에 modifier들이 있어서 기존 원칙을 어떻게 적용해야할지 고민되는데, modifier 역시 뷰 계층에 적용됩니다.

Modifier를 어떻게 뷰 계층에다가 그린다는 건가요? 저게 뷰인가요?

애플 공식 문서를 보면 modifier를 이렇게 정의하고 있습니다.

modifier(_:)
뷰에 modifier를 적용해서 새로운 뷰를 리턴한다.

그리고 예시에 적용된 foregroundColorbackground의 문서를 확인해보면 역시 리턴 타입이 View인걸 확인할 수 있습니다.


결국 이 modifier들이 새로운 뷰를 반환하니까 그 뷰도 화면에 그려지기 위해서는 앞서 설명한 원칙을 지켜야합니다. 위 예시에서 modifier를 고려한 뷰 계층은 다음과 같습니다.

background modifier는 Color 뷰를 secondary child로 갖습니다. 이 뷰는 Text 뷰의 사이즈가 결정되면, 그 경계까지만 적용됩니다.
이제 레이아웃 원칙을 고려하면서 레이아웃 결정 과정을 살펴보기 전에!!
여러 뷰를 담고 있는 VStack이 레이아웃을 결정할 때 일어나는 과정을 이해하면 전체적인 과정을 이해하기 좀 더 쉬워집니다.


Stack(HStack, VStack) Layout Process

HStack, VStack은 Parent에게 제안받은 사이즈를 일련의 과정을 거쳐서 자식뷰들에게 제안합니다.
VStack이 부모에게 400x400의 크기를 제안받았다고 가정해보겠습니다.

1. Child View들끼리 가지는 spacing의 크기만큼 Parent에게 제안받은 사이즈에서 뺀다.
위 예시에서는 ImageText 사이의 spacing이 될 것입니다. spacing이 20이라면 VStack은 제안받은 400에서 20을 뺀 380을 Child View들에게 제공하는 것입니다.

2. Child View의 수만큼 영역을 똑같은 크기로 나눈다.
위 예시에서 Child View가 2개 이므로 400x380을 400x190으로 나눕니다.
(VStack이니 가로 너비는 나누지 않습니다.)

3. 우선순위가 높은 Child View부터 사이즈를 제안합니다.
우선순위가 정해지지 않았다면 가장 경직적인 뷰부터 제안을 받습니다. Image는 특정 modifier들이 없으면 자신의 크기를 항상 고정시키기에 경직적이라고 말합니다.

4. 모든 Child View의 사이즈가 결정되면 처음에 계산했던 spacing과 함께 Child View들을 정렬한다.
정렬기준을 미리 정해주지 않을경우 center로 정렬합니다.

5. 자신의 크기를 Child View들을 수용할 정도로만 결정하고 Parent에게 응답한다.


자세한 건 후에 다루기로 하고, 이제 위 예시의 레이아웃 과정을 살펴보겠습니다.

  1. Root ViewContentView에게 사이즈를 제안합니다.
  2. ContentView는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 VStack에게 다시 제안합니다.
  3. VStack은 앞서 설명한 과정을 거치고 경직적인 Image에게 먼저 사이즈 제안을 합니다.

Image

  1. foregroundColor는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Image에게 제안합니다.
  2. Image는 자신의 크기를 결정하고 상위 뷰에게 알립니다.
  3. foregroundColor는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 VStack에게 제안합니다. 추가적으로foregroundColor는 SwiftUI 에게 Image의 색을 자신이 정한 색으로 렌더링 하도록 말합니다.

Text

  1. background는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Text에게 제안합니다.
  2. Text는 자신에게 필요한 크기를 정하고(Hello, world!만큼의 크기) 상위 뷰에게 알립니다.
  3. background는 사이즈를 상위 뷰에게 알리기 전에, 자신의 Secondary Child인 Color에게 사이즈를 제안합니다. Color 역시 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 하위 뷰에 전달하려고 하지만, Color는 하위 뷰가 없습니다. 이렇게 뷰 계층에서 완전히 레이아웃 중립인 경우, 사용 가능한 모든 공간을 차지합니다.
  4. Color의 사이즈가 정해졌으므로, background는 자신의 중간에 Color를 배치합니다. 그리고 자신의 사이즈를 VStack에게 알립니다.

  1. VStack은 두 개의 하위 뷰에서 받은 사이즈와 정해놓은 spacing의 크기를 종합해 자신의 사이즈를 정하고, ContentView에게 알립니다.
    6.ContentView는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Root View에게 다시 제안합니다.
  2. Root ViewContentView를 자신의 공간의 중앙에 배치합니다.

마무리

맨날 보던 화면에서 이런 디테일이 숨어있을거라곤 생각도 못했다. Swift나 SwiftUI를 이제 어느정도 안다고 생각하고 있었는데, 이번 경험을 통해 내가 인지못하고 있는 무언가들이 많을 거라고 느꼈다. 코딩은 알게 되면 알게될수록 겸손해지는 것 같다.😶‍🌫️

참고

[SwiftUI] SwiftUI 의 layout 3 단계

[SwiftUI] Modifier와 적용순서

Apple Documentation

WWDC19 Building Custom Views with SwiftUI

profile
IOS Developer DreamTree

0개의 댓글