여느 때랑 다름없이 사지방에 앉아서 코딩을 하고 있었다.
힘들어서 잠깐 쉬면서 이것저것 만지고 있었는데, 갑자기 맨날 보던 화면이 문득 이상하다는 생각이 들었다.
저 Hello World!가 왜 프리뷰 중앙에 있지??
좌측 코드를 보면 알겠지만, 나는 Text("Hello World!")
에게 위치를 특정해주는 어떠한 modifier(.position
, .offset
)도 사용하지 않았다. 그냥 Text
하나만 달랑 썼는데 자동으로 중앙으로 배치되어 있는 것이다.
물론 정서상(?)으로 중앙이 맞긴 하지만, 처음 코딩을 배울 때, 저 텍스트가 좌측 상단에 표시되었어도 전혀 이상하지 않았을 거 같다는 생각이 들었다.
나는 이런 화면 배치에 고의성이 느껴져서 정리된 내용이 있는지 찾아봤는데, 아니나 다를까 한국어로 정리된 글도 엄청 많고, 아예 WWDC에서 광고를 광고를 했는데도 나만 모르고 있었다.
이렇게 알게된 이 글의 주제는 바로 SwiftUI가 View의 레이아웃을 결정하는 과정이다.
뷰의 레이아웃을 결정하는 과정을 배우기 전에 먼저 뷰 계층(view hierarchy)에 관한 이해가 필요합니다.
이해를 위해 WWDC 19에 나온 간단한 뷰의 동작을 살펴보겠습니다.
아래 코드에는 3개의 뷰가 동작하고 있습니다.
뷰 계층의 맨 아래에는 Text
가 있고,
그 위에는 ContentView
가 있습니다.
이 때, 레이아웃 중립(Layout neutral)이라는 개념이 나오는데,
body
를 가진 모든 상위 뷰는 그들의 경계가 body
의 경계와 동일한 것을 말합니다.
쉽게 말하면, 뷰 레이아웃에 아무런 영향을 끼치지 않는 것이라고 이해해도 좋을 것 같습니다.
위 예시에서는 body
를 가진 ContentView
를 레이아웃 중립이라고 부르고, 실제로 body
와 ContentView
는 동일한 경계를 가지고 있습니다.
그래서 예시 이미지 우측에 표시되는 경계가 Text
와 같음을 볼 수 있습니다.
그리고 그 위엔 Root View
가 있습니다.
레이아웃 중립인 ContentView
는 Text
와 같은 경계를 나타내기 때문에 생략하고 위처럼 표현할 수 있습니다.
레이아웃을 결정하는 원칙은 크게 3단계로 구성됩니다.
1.
Parent View
가Child View
에게 사이즈를 제안한다.
2.Child View
는 자신의 사이즈를 결정한다.
3.Parent View
는Child View
를 자신의 좌표 공간에 배치한다.
Parent View
는 뷰 계층에서 상대적으로 상위에 있는 뷰를 말하고,
Child View
는 뷰 계층에서 상대적으로 하위에 있는 뷰를 말합니다.
뷰 계층의 최상단에 위치한 Parent가 Child에게 차례차례 사이즈를 제안해옵니다.
예시에서는 Parent View
인 Root View
가 Child View
인 Text
에게 사용 가능한 모든 영역을 제안합니다.
하지만 Text
는 제안된 영역을 다 사용하는게 아니라 자신에게 필요한 영역만 선택합니다. 위 예시에서는 Hello World가 들어갈 수 있는 공간만을 차지하겠다고 응답합니다.
그리고 SwiftUI에서는 이런 Child View
의 size를 강제할 수 있는 방법이 없기 때문에 Parent View
는 이 결정을 들어 주어야만 합니다.
Root View
는 크기를 결정한 Text
를 자신의 공간에 중앙에서부터 배치합니다. >> 내 궁금증은 이거였는데, 크게 특별한 이유는 없었다.(ㅠㅠ)
SwiftUI의 모든 레이아웃은 위 과정을 통한 Parent와 Child의 상호작용으로 나타나게 됩니다.
세 개의 원칙 중에서 두 번째 과정에서 Text
가 자신에게 필요한 사이즈를 String의 크기를 고려해 결정한 것처럼 각 뷰들은 자신의 사이즈를 조절하는 방법 및 시기를 결정할 수 있습니다.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.foregroundColor(.accentColor)
Text("Hello world!")
.background(Color.red)
}
}
}
위 예시 코드의 뷰 계층을 그려보면 Text
와 Image
에 modifier들이 있어서 기존 원칙을 어떻게 적용해야할지 고민되는데, modifier 역시 뷰 계층에 적용됩니다.
애플 공식 문서를 보면 modifier를 이렇게 정의하고 있습니다.
modifier(_:)
뷰에 modifier를 적용해서 새로운 뷰를 리턴한다.
그리고 예시에 적용된 foregroundColor
와 background
의 문서를 확인해보면 역시 리턴 타입이 View
인걸 확인할 수 있습니다.
결국 이 modifier들이 새로운 뷰를 반환하니까 그 뷰도 화면에 그려지기 위해서는 앞서 설명한 원칙을 지켜야합니다. 위 예시에서 modifier를 고려한 뷰 계층은 다음과 같습니다.
background
modifier는 Color
뷰를 secondary child로 갖습니다. 이 뷰는 Text
뷰의 사이즈가 결정되면, 그 경계까지만 적용됩니다.
이제 레이아웃 원칙을 고려하면서 레이아웃 결정 과정을 살펴보기 전에!!
여러 뷰를 담고 있는 VStack
이 레이아웃을 결정할 때 일어나는 과정을 이해하면 전체적인 과정을 이해하기 좀 더 쉬워집니다.
HStack, VStack은 Parent에게 제안받은 사이즈를 일련의 과정을 거쳐서 자식뷰들에게 제안합니다.
VStack이 부모에게 400x400의 크기를 제안받았다고 가정해보겠습니다.
1. Child View들끼리 가지는 spacing의 크기만큼 Parent에게 제안받은 사이즈에서 뺀다.
위 예시에서는 Image
와 Text
사이의 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에게 응답한다.
자세한 건 후에 다루기로 하고, 이제 위 예시의 레이아웃 과정을 살펴보겠습니다.
Root View
가 ContentView
에게 사이즈를 제안합니다.ContentView
는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 VStack
에게 다시 제안합니다.VStack
은 앞서 설명한 과정을 거치고 경직적인 Image
에게 먼저 사이즈 제안을 합니다.Image
foregroundColor
는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Image
에게 제안합니다.Image
는 자신의 크기를 결정하고 상위 뷰에게 알립니다.foregroundColor
는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 VStack
에게 제안합니다. 추가적으로foregroundColor
는 SwiftUI 에게 Image
의 색을 자신이 정한 색으로 렌더링 하도록 말합니다.Text
background
는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Text
에게 제안합니다.Text
는 자신에게 필요한 크기를 정하고(Hello, world!만큼의 크기) 상위 뷰에게 알립니다.background
는 사이즈를 상위 뷰에게 알리기 전에, 자신의 Secondary Child인 Color
에게 사이즈를 제안합니다. Color
역시 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 하위 뷰에 전달하려고 하지만, Color
는 하위 뷰가 없습니다. 이렇게 뷰 계층에서 완전히 레이아웃 중립인 경우, 사용 가능한 모든 공간을 차지합니다.Color
의 사이즈가 정해졌으므로, background
는 자신의 중간에 Color
를 배치합니다. 그리고 자신의 사이즈를 VStack
에게 알립니다.VStack
은 두 개의 하위 뷰에서 받은 사이즈와 정해놓은 spacing의 크기를 종합해 자신의 사이즈를 정하고, ContentView
에게 알립니다.ContentView
는 레이아웃 중립이기 때문에, 제안받은 사이즈를 그대로 Root View
에게 다시 제안합니다.Root View
는 ContentView
를 자신의 공간의 중앙에 배치합니다.맨날 보던 화면에서 이런 디테일이 숨어있을거라곤 생각도 못했다. Swift나 SwiftUI를 이제 어느정도 안다고 생각하고 있었는데, 이번 경험을 통해 내가 인지못하고 있는 무언가들이 많을 거라고 느꼈다. 코딩은 알게 되면 알게될수록 겸손해지는 것 같다.😶🌫️
[SwiftUI] SwiftUI 의 layout 3 단계