iOS/SwiftUI

#8 Introducing Stacks & Containers

HaningYa 2020. 10. 22. 00:35
728x90

이전까지 TextField, Button, Slider, Toglgle 과 같이 흔히 쓰이는 SwiftUI control 에 대해서 배워봤다.
이번 챕터에서는 연관된 view들을 묶는 container views 에 대해서 배워볼 것이다. 

Layout and priorities

UIKit 과 AppKit 에서는 view 에 제약조건을 주기 위해 Autolayout을 사용했다.
보통의 규칙은 parent view 기준으로 child view의 사이즈를 결정하고 contraint 를 추가하는 것 이였다. 
(child view의 width 와 height 가 명시되있지 않으면)

반면에 SwiftUI는 반대로 동작한다.
child view 가 parent 가 제시한 size 에 따라 크기를 선택한다.

만약 Text를 View 안에서 사용할 경우 Text에게 View가 rendered 되었을 때 Text 에게 Size 에 대한 제안이 주어진다.
하지만 Text는 표시하고자 하는 문자열의 사이즈를 계산해서 필요한 만큼의 크기를 선택할 것 이다.

왜 여기서 proposed라는 표현을 썼을까? 결국 Child view 는 본인이 선택한 size대로 나타나는데 parent 가 size 를 propose 한다는 의미가 뭐지??

Layout  for views with a single child

Practice/ChallengeView.swift 파일을 연다.
Hello world가 적혀진 걸 볼 수 있다.

* 모든 view 는 default로 parent의 center 에 위치된다.

background modifier를 통해 Text 의 frame size를 눈으로 확인해 보자

frame 을 확인해 보면 내용을 보여줄 수 있는 최소한의 공간만을 사용하고 있는걸 알 수 있다.
더 긴 문장으로 내용을 바꿔도 마찬가지이다.

SwiftUI가 parent view 와 child view 의 size를 결정하기 위해 적용하는 rule 들은 아래와 같다.

  • parent view 는 사용 가능한 frame을 결정한다.
  • parent view 는 결정된 frame 을 child view 에 제안한다.
  • parent view 의 제안에 따라 child view 는 크기를 선택한다.
  • parent view 는 child view를 포함하도록 자체 크기를 조정한다.

위 과정은 root view 로 부터 view 계층의 마지막 view 까지 recursive 하게 진행된다.

view 에 적용된 modifier들은 원본 view 를 embed 한 새로운 view를 생성한다.
위에 설명된 규칙은 각각의 component에도 적용되고 modifier에 의해 만들어진 view 에게도 적용된다

이 과정을 눈으로 보기위해 코드를 추가해서 보면

흥미롭게도 Text가 사이즈가 있고 .frame modifier로 부터 생성된 size과 다르다는걸 알 수 있다.
왜냐하면 4가지 규칙이 적용되었기 때문이다.

  1. frame view 는 150x50의 고정된 크기를 가진다.
  2. frame view 는 Text에게 size 를 제안한다.
  3. Text는 제안받은 size 에서 내용을 표시하기 위한 최소한의 방법을 생각해낸다.
  4. 생략(왜냐하면 frame 은 이미 size 가 정의되어 있기 때문에)

Text는 자동으로 내용이 짤리는 것을 방지하기 위해 자동으로 2줄로 내용을 표시하게 된다.

만약 frame size 350x50으로 늘려보면 Text 는 한줄로 표시할 수 있는 공간이 생겨 아래와 같이 표시되게 된다.

하지만 여전히 Text는 본인이 꼭 필요한 영역(red)만을 사용하고 있고 frame 은 고정된 사이즈(yellow)로 존재한다.

만약 childview 를 표시하기 힘들 정도로 parent view 가 작다면 어떻게 되겠는가?
frame 의 size 를 100x50으로 바꾸면 문자열은 짤린채로 표시된다.

이 현상은 다른 조건, 예를들어 minimumScaleFactor modifier 과 같이 필요하다면 글자 크기를 줄어들게 하는 조건이 없을때 이렇게 짤리게 된다.

minimumScaleFactor 적용한 모습

일반적으로 component는 항상 parent가 제시한 size 에 맞도록 노력할 것이다.
만약 compoenet가 제시된 size 공간이 적어서 맞추지 못할 경우 component 유형에 따라 엄격하게 종속 된 규칙을 적용한다.

이것을 통해 SwiftUI는 각각의 view 가 본인의 크기를 결정하는 컨셉을 더욱 강화한다.
parent가 제시한 크기를 고려해보고 최대한 맞추기 위해 노력하지만 항상 componet 유형에 따라 독립적이다.

이미지를 예로 들어 다른 제약조건이 없을 경우 원본 해상도를 유지한다. 

파란색 경계가 100x50 의 frame 을 나타내지만
image 는 frame 이 제시하는 size를 완전히 무시하고 원본 해상도에 맞게 화면에 표현된걸 확인할 수 있다.


Image 에 resizable() modifier만 추가해 줘도 

이렇게 제시된 size 에 맞게 표현되는 걸 볼 수 있다.

  • 결론적으로 parent view 는 child view 에게 size를 강요할 수 없다. 대신 size를 제안할 수 있는데 그 제안은 child가 더 크거나 작게 본인의 size를 결정하는데 영향을 미치지 못한다.
  • Text 와 같은 몇몇의 component 는 parent가 제안한 size 에 적응(adaptive)하기 위해 노력할 것이지만 여전히 표시하는 내용의 size 를 고려한다. (2줄로 바꾼다던가)
  • Image 와 같은 몇몇의 compoenet 의 경우 는 그냥 parent 가 제시한 size 를 버린다.
  • 둘의 중간쯤에 중립, size를 고를 필요가 없는 것 들이 있다. 그들은 그냥 size를 본인의 child 에게 전달해서 child 를 감싸는 size를 가진다. 
    .padding modifier가 예시이다. (intrinsic size) 단순하게 child size를 받아와서 명시된 padding 값을 각 모서리에 적용하여 새로운 view를 만든다. (child 를 embed 할 수 있는)

Stack views

이전 챕터에서 Stackview를 사용했을 것 이다. 
두개 이상의 child view 를 container view 에서 나타낼 때는 다음 규칙을 따른다.

  1. container view는 사용가능한 frame을 결정하는데 대부분 parent 가 제시한 size를 따른다.
  2. container view 는 child view 중 가장 제한적인 제약조건이 있는 child view 를 선택하거나 동일한 제약조건일 경우 크기가 가장 작은 child view 를 선택한다.
  3. container view 는 childview 에게 size 를 제안한다. 제안된 size는 사용 가능한 공간을 child view 의 숫자로 균등하게 나눈 값이다.
    제안된 size = 사용가능한공간/childview 숫자
  4. child view 는 parent view 가 제안한 size를 바탕으로 크기를 정한다.
  5. container view 는 child view가 선택한 사용가능한 frame 을 뽑아내서 step2로 다시 진행된다.
    (모든 child view 가 size 를 고를 때 까지 반복된다.)

하나의 child 를 가지는 view 와 다른점은 step5 이다.

눈으로 확인해 보자

두개의 Text가 있으니 둘다 같은 size를 차지 할 것 같지만 사실은 그렇지 않다.

왜냐하면

  1. stack 은 parent 로 부터 size를 제안받고 2개의 균등한 부분으로 나눈다.  
  2. 지금 Text 는 두개가 동일하기 때문에 첫번째 Text (왼쪽) 에게 우선권을 준다. 
  3. 우선권을 받은 왼쪽 Text 는 제공받은 size(parent 의 반) 보다 contents 보여주는데 필요한 size 가 작으므로 딱 내용을 보여줄 수 있는 만큼의 공간만을 size로 선택한다.
  4. 그 다음 차례인 오른쪽 Text는 남은 공간을 사용하게 된다.

만약 m 을 없애서 뒤 Text를 짧게 만들면

위와 같이 적은 공간을 차지하는 오른쪽 Text가 우선권을 받아 내용에 딱 맞는 크기를 선택하게 되고 왼쪽은 남는 size 를 사용하게 된다.

Layout prioirty

container view는 children 을 restriction 의 갯수대로 정렬한다.
그리고 restriction 이 같다면 크기가 작은 child 부터 선택권을 준다.

하지만 이 순서를 바꾸고 싶은 경우 두가지 방법이 있다.

  • modifier를 통해 view behavior 를 바꾼다.
  • layout priority 를 통해 바꾼다.

Modifier

modifier를 통해  view 가 size 를 고르는 성향을 바꿀 수 있다.
예를들어

  • Image 는 제일 말안듣는 녀석이다. (parent 제안을 완전히 무시하기 때문)
    하지만 resizable modifier를 붙이면 세상 말 잘듣는 애가 된다.
  • Text의 경우 말을 잘듣는 녀석이다. (되도록 parent 안에서 표시하기 위해 line 수도 바꿈)
    하지만 maximum number line 을 lineLimit modifier을 붙이면 세상 말 안듣는 애가 된다.

이러한 adaptivity degree change 는 sorting 의 순서에 영향을 끼친다.

Priority

.layoutPrioity modifier를 통해 우선순위를 바꿀 수 있다. 
명시적으로 sorting 할때 control 의 weight를 바꿀 수 있다.
Double 값으로 양수 음수값을 가지고 따로 priority 가 명시되 있지 않는경우 0으로 간주한다.

두번째 Text에 layoutpriority를 줬을 때

첫번째 Text 에 layoutpriority -1 을 줬을 때

layout priority 를 정해주는 것은 sort order 뿐만 아니라 제안되는 size또한 변한다는걸 알 수 있다.

같은 우선순위를 가지는 view들에 대해서 parent view 는 child 숫자로 나눈 균등한 size를 제시한다.

다른 우선순위를 가질 경우 parent view 는 다른 알고리즘을 사용한다.
우선 순위가 낮은 모든 자식의 최소 크기를 빼고
그 결과 크기를 레이아웃 우선 순위가 가장 높은 자식 (또는 둘 이상인 경우 자식)에게 제안한다.

-1 을 준 HStack 을 보면 너비가 가장 제약조건이 심한 size 이기 때문에 child view 는 너비를 위해 경쟁하게 되고 반면에 높이에 대해서는 아무 제약조건이 없게된다. 
너비에 집중해서 보면

  1. HStack은 우선순위가 낮은 child view 가 필요로 하는 최소한의 너비를 계산한다.
    (-1 의 우선순위를 가지는 Text가 그래서 한글자만 표현할 수 있는 너비를 가지고 높이가 늘어나게 된 것 이다.)
  2. HStack 은 그다음 가장 우선순위가 높은 중간(두번째)에 있는 Text를 찾는다.
  3. HStack 은 최상위 우선순위 보다 낮은 child view 들에게 가상의 최소한의 너비를 전달한다.
    (첫번째 와 세번째 Text)
  4. 주어진 너비에 맞게 각각의 낮은 우선순위를 가지는 child view 는 필요한 최소한의 너비를 뺴서 (Step 1에서 계산된) 가장 높은 우선순위를 가지는 중간 Text에게 너비를 제시한다.
  5. 가장 높은 우선순위를 가지는 중간 Text는 필요한 만큼의 size 를 가져가게 된다.(한줄로 표현될 수 있는)

이 다음 시점부터 stack 은 다음 view 에 대해서 진행 (우선순위가 0인 view) 수 있게 된다.
알고리즘은 같지만 이제 width 가 다르다.

  1. HStack이 가진 너비
  2. 에서 우선순위 1이 가져간 너비를 빼고
  3. 에서 우선순위 -1인 Text 가 필요한 최소한의 너비를 뺀다.

우선 순위가 0 인 텍스트는 텍스트를 4 줄로 래핑하여 원하는대로 크기를 최대한 활용한다. 

------------------205쪽

The HStack and the VStack

  • HStack: subview 를 수평으로 배치
  • VStack: subview 를 수직으로 배치

UIKit 과 AppKit에도 UIStackView라는 비슷한 component가 존재하고 수평 수직 둘다로 동작한다.
(subview 가 배치되는 방식에 따라 자동으로 결정됨)

VStack 과 HStack을 이전 챕터에서 써봤지만 사실 두개의 추가적인 파라미터가 필요하다. 

// HStack
init(
alignment: VerticalAlignment = .center, spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
)
// VStack
init(
alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content
)
  • aligment 는 HStack 과 VStack 각각에게 subviews 를 정렬하는데 쓰이는데 .center가 디폴트이다.
  • spacing 은 children 사이의 거리이다. nil 이 디폴트 값이고 platform 별 독립적인 거리가 사용된다. 그래서 0을 원할경우 명시적으로 넣어줘야 한다.

content 파라미터는 child view 를 만드는 closure 이다. 
하지만 container 들은 대부분 하나 이상의 child 를 만든다.
@ViewBuilder 어트리뷰트가 이것을 가능하게 만들어 준다 : 그것은 childview를 return 하는 clousre를 활성화 하여 대신 여러개의 children view 를 제공합니다.

The @ViewBuilder attribute is what enables that: It enables a closure that returns a child view to provide multiple children views instead.

해석이 안된다.

A note on aliment

VStack 정렬은 3개의 값을 줄 수 있다.
.center .leading .trailing

HStack 의 위 세개를 포함해 몇가지 더 있다.

  • firstTextBaseline: 최상단 text의 baseline에 view base 를 맞춘다.
  • lastTextBaseline: 최하단 text 의 baseline 에 view base를 맞춘다.

세개의 children 은 vertically center 되있는걸 볼 수 있다.

bottom 으로 하면 아래로 맞춰지는데 완벽하지 못하다.

.firstTextBaseline 으로 하면 baseline 이 제일 위에 있는 Text 에 맞춰지게 된다.

ZStack

ZStack 에서 children은 선언 된 위치에 따라 정렬된다.
즉, 첫 번째 하위 뷰는 스택의 맨 아래에 렌더링되고 마지막 하위 뷰는 맨 위에 렌더링 된다.

.layoutPriority modifier는 Z order 에 대해서는 영향을 미치지 못한다. 그래서 선언된 위치에 따라 표시되는 걸 modifier 로 바꿀 수 없다.
ZStack 또한 default 로 children view 를 center 로 정렬한다.

크기에 대해 말하자면,
HStack의 높이가 가장 높은 child view에 의해 결정되고
VStack이 너비가 가장 넓은 child view에 의해 결정되고
ZStack은 너비와 높이는 각각 가장 넓은 child view와 가장 높은 child view에 의해 결정된다.

Other container views

어떤 view 들도 하나 이상의 childview 를 가지는 container 가 될 수 있다.
Button 과 같은 컴포넌트의 경우 label view를 가질 수 있고 Text 부터 Image 까지 적용할 수 있다. 
단순히 StackView 에 넣어서 multi view content 를 만들 수 있다.

복잡한 UI를 만들기 위해 nested 된 view 도 만들 수 있지만 만약 view 가 너무 복잡해 진다면 작은 단위로 쪼개야할 것 이다.

*Stack 이 10개 이상의 child 를 가질 수 없다는 루머가 있는데 문서에 나와있진 않지만 10개 이상 뷰를 직접 넣어보면 compiler 가 error 메시지를 띄우긴 한다.

Back to Kunchi

이론 끝 실습 시작

The Congratulations View

5개의 정답을 맞추면 보여주는 축하 화면을 만들어 볼 것이다.
EmptyView()를 지우고 VStack 을 넣어준다.

VStack은 최소 한개의 childview 를 가져야 하기 때문에 비어있는 Stack 을 작성하면 컴파일러 에러가 뜬다.

기본 화면을 만든다.

User avatar

사용자 아바타를 만들어 보자

디자인 나와있다.

  1. 배경을 두개로 나눠서 다른 색을 넣는다.
  2. user avatar 를 넣는다.
  3. user name 을 넣는다.

ZStack 을 통해 구현해준다.

The Spacer view

user name 을 표시하는 VStack에서 Spacer를 제거하면 Text가 center 정렬이 된다.
Text를 아래로 위치시키기 위해 Space()를 사용하는데 Spacer는 주축을 중심으로 확장해서 Text를 아래로 민다.

Space()가 적용되는 방식은 다음과 같다.

  1. VStack은 parent view 인 ZStack 으로 부터 권장된 사이즈이다.
  2. VStack 은 child view 중에서 덜 유연한 레이아웃이 Text임을 알게되고 size 를 제안한다. layout priority 없이 제안된 size는 그 크기의 절반이다.
  3. Text는 필요한 size를 계산하여 VStack 에게 다시 전달한다.
  4. VStack은 Text가 요청한 size를 빼고 나머지를 Spacer 에게 제안한다.
  5. Spacer 는 유연하기 때문에 해당 제안(나머지 view 크기) 를 받아들인다.

 

Challenge : Play button 을 하단으로 내리자

Button 위에 Spacer() 를 넣고 제일 위에도 Spacer() 를 넣는다.

 

Completing the challenge view

문제를 보여주고 정답 보기를 보여주는 기능을 한다.

QuestionView.swift 와 ChoicesView.swift 에 predefine 된 view 를 사용한다.
정답view 의 경우 숨겨져 있다가 사용자가 탭을 하면 보여지는 형태이다.

먼저 ChallengeView.swift 코드작성

  • preview mode 에서 challengTest를 만든다.
  • view 생성자에 test를 넘겨준다.

ChallengeView 는 PracticeView에서도 사용되기 떄문에 해당 코드도 수정해준다.

Reworking the App launch

  • 앱의 시작화면을 바꿔준다.
  • WelcomeView 를 수정한다.

먼저 SceneDelegate에서 시작화면을 StarterView로 바꿔준다.
그리고 StarterView 를 보면 proxy view 역할을 담당한다.

@ViewBuilder는 하나 이상의 View 를 리턴하기 위해 사용된다. 코드 조건상 무조건 하나의 view 가 반환되긴 하지만 두개의 view 가 선언되어 있기에 attribute 를 붙여줘야한다.

이제 WelcomeView 를 작성한다.

Key points

  • SwiftUI 는  레이아웃을 더 쉽게 다룰 수 있다. (오토레이아웃 보다)
  • view 는 본인의 크기를 본인이 결정한다 (부모뷰가 강요할 수 없다.)
  • Text의 경우 적응하려 노력하고 Image의 경우 parent 를 무시하고 표시된다.
  • StackView 는 HStack VStack ZStack 이 있다.
  • Stack은 least adaptive 한 애부터 size를 제시한다.
  • layoutpriority를 통해 우선권을 바꿀 수 있다.

참고링크

WWDC 2019: Session 237 "Building Custom Views with SwiftUI"

http://apple.co/developer.apple.com/videos/play/wwdc2019/237/

 

Building Custom Views with SwiftUI - WWDC 2019 - Videos - Apple Developer

Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high...

developer.apple.com

Stack Views: Official documentation

developer.apple.com/documentation/swiftui/view-layout-and-presentation

 

Apple Developer Documentation

 

developer.apple.com

There are a few other container views that have not been covered in this chapter:

Form
Group
GroupBox

728x90