[SwiftUI] SwiftUI 의 layout 3 단계

SwiftUI 가 View의 크기를 결정하고 배치하는 방법을 알아보자

naljin
14 min readSep 20, 2022

들어가기 전에

SwiftUI 에서는 뷰의 위치를 이동시키기 위해 offset()position() 이라는 두가지 방법을 제공합니다.

Text("눈물의 layout")
.background(Color.red)
.offset(x: 100, y: 100)
.background(Color.blue)
Text("눈물의 layout")
.background(Color.red)
.position(x: 100, y: 100)
.background(Color.blue)

코드는 비슷하지만 나타나는 결과는 꽤나 다른 것을 확인할 수 있는데요

이런 차이점들이 왜 발생하는지 이해하기 위해서는, SwiftUI 가 뷰의 크기를 결정하고 배치하는 원리를 먼저 알아야합니다.

그럼 알아보러 가보져 ㄱㄱ

Layout Basics

[WWDC19] Building Custom Views with SwiftUI 에서 설명하는 layout 의 기본 동작을 살펴보겠습니다. 아무래도 그것이 근-본 이니까요.

아래 코드에서는 사실 3개의 View 가 동작합니다

View hierarchy 의 맨 아래에 Text 가 있고요

body 와 항상 동일한 bounds 를 갖는 ContentView 가 있습니다

마지막으로 Root View 가 있습니다.

Root View 라고 해서 무조건 device 의 전체 영역을 말하는게 아니라 “특정 View (ContentView) 가 위치할 수 있는 상위 레이어” 라고 생각하면 더 이해가 쉬울듯

이 경우에 Root View 는 디바이스에서 safe area 를 뺀 영역이 됩니다. 물론 edgesIgnoringSafeArea 같은 modifier 를 통해 해당 영역도 Root View 영역에 포함 가능하지만, 기본적으로는 safety zone 내에 있습니다.

body 가 있는 view 의 최상위 layer (여기서는 ContentView) 는 항상 layout neutral (레이아웃 중립) 입니다.

레이아웃 중립의 bounds 는 하위 뷰의 bounds 를 따라갑니다. 따라서 ContentView 의 bounds 는 body 에 의해 결정됩니다.

이 둘은 동일하게 동작하기 때문에 레이아웃의 목적을 위해 동일한 뷰로 취급할 수 있습니다

이제 우리가 관심을 가져야 할 뷰는 두 개뿐입니다. 이를 기반으로 레이아웃 프로세스를 살펴보겠습니다.

Layout Procedure

레이아웃 프로세스는 세 단계로 구성됩니다.

  1. Parent 는 Child 에게 size 제안
  2. Child 는 Parent 의 크기 제안을 고려해서 (완전히 무시도 가능) 자신의 size 를 결정
  3. Parent 는 child 의 결정을 존중 후 자신의 좌표 공간 내에 child 를 위치

단계별로 살펴보겠습니다.

먼저 RootView 는 Text 에 제안된 크기(proposed size)를 제공하며, 이는 두 개의 큰 화살표로 표시됩니다.

여기서는 전체 safe area 의 size 를 제공합니다.

그 다음 Text 는 Root View 에게 “고맙긴한데, 난 사실 Hello world 만큼만 있으면 돼” 라고 응답합니다.

부모는 이렇게 결정된 child 의 크기 선택을 존중해야만 합니다. SwiftUI 에서 child 에 size를 강제할 수 있는 방법은 없습니다.

이제 Root View 는 child 를 자신 좌표 공간내의 가운데 배치합니다.

이게 전부입니다.

모든 layout 인터렉션은 이와 동일한 parents — children 간의 상호작용으로 이뤄집니다.

여기서 저는 두 번째 단계를 다시 주의 깊게 살펴보겠습니다. 왜냐하면 이 동작은 우리가 이미 익숙해져 있을 것과 다를 수 있으며, 중요한 결과를 가져오기 때문입니다.

“2. Child 가 자신의 size 를 결정한다” 는 것은 view 에 크기를 조정할 수 있는 동작(behavior)이 있음을 의미합니다.

모든 view 는 자신의 size 를 제어하기 때문에, view 를 build 할때 크기를 조정하는 방법 및 시기를 결정할 수 있습니다.

예를 들어 아래 뷰는 고정된 size frame 으로 인해 50 x 10 points 로 고정됩니다.

또 다른 예시는 가로, 세로 비율이 1:1 로 항상 똑같습니다. (size 는 flexible 하긴 하지만)

이렇게 뷰의 크기는 뷰의 definition 에 의해 정의됩니다.

우리는 view 가 자신의 size 를 String 크기 만큼 결정하는 것을 Text 에서도 살펴보았습니다. 따라서 SwiftUI 에서 텍스트의 bounds는 표시된 선의 높이와 너비를 넘지 않습니다.

보기 좋은 UI 를 만드는 데 중요한 역할을 하는 레이아웃의 마지막 단계가 하나 더 남아 있는데요, 이 사항은 SwiftUI 에서 알아서 처리하기 때문에 굳이 신경쓰지 않아도 되지만 알아둘 가치는 있습니다.

마지막 사항은 바로 SwiftUI 는 view 의 모서리를 가장 가까운 pixel 로 반올림한다는 것입니다.

따라서 이런 anti-aliasing 대신에,

우리는 항상 crisp하고 clear한 edge를 갖게 됩니다

Delicious Avocado Toast

이제 아까의 Text 예제를 “Avocado Toast” 로 바꾸고

녹색 배경을 더해보겠습니다.

이제 background modifier 는 Color view 를 secondary child 로 갖는 Background view 로 Text view 를 감쌉니다.

텍스트 주위에 공간을 조금 더 두기 위해 여기에 padding 을 넣겠습니다.

SwiftUI는 플랫폼, dynamic type size 및 환경에 적합한 패딩 양을 선택합니다. 다만 여기서는 명시적으로 10 point 만큼의 padding 을 요구해보겠습니다.

아까의 hello world 예제보다 조금 더 흥미로워졌는데요, 이 경우 레이아웃 프로세스가 어떻게 작동하는지 살펴보겠습니다.

먼저 Root View 는 사용 가능한 전체 크기를 Background View에 제안합니다.

Background view 는 layout neutral(중립)이므로 이 크기 제안을 Padding 뷰에 전달합니다.

Padding 뷰는 child 의 side에 10 포인트를 더할 것이라는 것을 알고 있기 때문에 Child, 즉 Text 에게 훨씬 적은 공간을 제공합니다.

그리고 Text 는 자신에게 필요한 width 를 가져와서 Padding view 에게 반환합니다 (이 경우에는 Avocado Toast 만큼).

Padding 뷰는 각 side에서 child 보다 10 point 더 큰 크기를 Background 에 보고합니다. 그리고 Text 를 자신의 좌표 공간에 적절하게 배치합니다 (기본적으로 center 에 배치).

Background 뷰는 레이아웃 중립이라고 했으므로 자식 뷰에게 보고 받은 크기를 그대로 사용할 것입니다.

사용할 크기를 상위 뷰에 보고하기 전에, secondary child, 즉 Color 뷰에도 이 크기를 제공합니다.

Color View 에서는 이렇게 제공된 size 를 받아들임으로써 Padding view 와 같은 size 를 갖게 됩니다.

마지막으로, BackgroundRoot 뷰에 크기를 보고하고

Root 뷰는 전과 같이 Background view 를 자신의 center 에 배치합니다.

이것이 레이아웃의 전 과정입니다.

Scrumptious Avocado

간단하지만 중요한 다른 예시를 살펴보겠습니다.

이 경우 뷰의 body는 20x20 크기의 고정된 이미지입니다.

여기서 전체 view 가 절반정도 커졌으면해서 30 x 30 으로 frame modifier 를 적용해보겠습니다.

이미지 자체의 크기는 고정되어있기 때문에 겉보기에는 변화가 없지만, 그 주변을 30 x 30 크기로 감싸는 것을 확인할 수 있습니다.

이 사이즈가 우리 view body 의 size 로, modifier 를 추가하기 전보다 50% 가 더 커졌습니다.

SwiftUI 에서는 frame이 제약 조건(constraint) 이 아니라는 것을 인식하는 것이 중요합니다. 그냥 액자처럼 생각할 수 있는 하나의 뷰입니다.

위 예시에서 Frame 은 child 에게 고정된 치수를 제안하지만, 다른 모든 view 와 마찬가지로 child 에서 궁극적인 자신의 크기를 선택하기 때문에 이미지는 20 x 20 사이즈를 갖습니다.

Ruler

“Child 에서 자신의 size 를 선택하고, parent 는 이를 존중해야한다.” 의 의미를 확실히 하기 위해 한가지 예시를 더 살펴보겠습니다.

먼저 Root View 는 자식에게 사용 가능한 공간을 제안합니다.

Border 뷰는 layout 중립이므로 하위 뷰에게 size 를 물어봅니다.

Frame 은 10 x 10 을 자신이 사용할 사이즈로 응답할 것입니다. 그 전에 자식 뷰를 배치하기 위한 layout 프로세스를 마저 수행합니다.

Image 는 상위 뷰에서 10 x 10 의 사이즈를 제안했음에도, 이를 무시하고 자신만의 사이즈를 사용하기로 결정합니다.

Parent 는 Child 의 사이즈 결정을 존중하고 Parent 좌표 공간의 가운데 배치합니다.

Parent 가 Child 의 size 를 강제할 방법은 없습니다. 그저 Child 의 결정을 받아들입니다. 따라서 ruler 이미지가 frame 범위 내에서 잘려보이지 않습니다.

이제 Frame View 는 상위 뷰에게 자신의 size 를 알립니다.

.border 수정자는 Color view 를 secondary child 로 갖는 Border view 로 Frame View 를 감쌉니다. 여기서 Border view 는 layout 중립이기 때문에, 자식과 동일한 사이즈를 사용합니다.

참고로 modifier 에 대해 이야기할 때 Frame “View”, Border “View” 처럼 부르는 것은, modifier 적용시 어떤 일이 일어나는지 이해에 도움을 주는 훌륭한 mental model 입니다 — modifier 를 적용하면, 기존의 view 를 수정하는게 아니라 새로운 view 를 만듭니다.

이제 Border View 가 사용할 크기를 상위 뷰에 보고하기 전에, secondary child, 즉 Color 뷰에도 이 크기를 제안합니다.

Color view 는 layout 중립입니다. 하지만 이때 Color view 의 Child view 는 없습니다. 이렇게 view hierarchy 가 완전히 레이아웃 중립인 경우, 사용 가능한 모든 공간을 자동으로 차지합니다. (즉, Border View 에서 제안한 공간을 모두 차지합니다.)

Color View 의 사이즈가 정해졌으니, 이제 Border View 는 자신의 가운데 Color 뷰를 배치합니다.

그리고 Root View 에게 자신이 사용할 Size 를 전달합니다.

Root 뷰는 자신의 좌표공간 가운데에 Border View 를 배치합니다.

마무리?

위에서 살펴본 내용을 바탕으로,, offset 과 position 의 동작까지 살펴보기에는 포스트가 너무 길어지져?? 다른 글로 빼뒀으니 아래 포스팅을 참고해주시라요..!

출처

--

--

Responses (1)