-
SwiftUI의 레이아웃 가이드
SwiftUI의 레이아웃 핵심 개념을 살펴보세요. 시스템과 원활하게 연동하는 유연한 동적 뷰를 구축하는 방법을 알아보고, 작업 흐름을 간소화할 수 있는 필수 디버깅 기법을 살펴보세요.
이 세션은 원래 Apple과의 만남 활동 ‘SwiftUI 기초: SwiftUI로 멋진 앱 빌드하기'의 일부로 진행되었습니다. 전체 비디오를 시청하여 더 많은 인사이트와 관련 세션을 확인하세요.리소스
-
비디오 검색…
좋은 아침입니다. 여러분, 안녕하세요. 제 이름은 캣이고, 애플에서 개발 도구 엔지니어로 일하고 있습니다.
지난 몇 년간 저는 Apple Watch의 ‘바이탈스’ 앱은 물론, iPhone과 iPad의 ‘건강’ 앱에 포함된 수면 기능과 같은 프로젝트들을 담당해 왔습니다. 네, 이러한 기능들은 직관적으로 느껴져야 할 뿐만 아니라 시각적으로도 아름다워야 합니다. 손목을 흘끗 보든, 휴대폰을 들여다보든 말이죠.
이러한 작업을 통해 저는 탄탄한 UI 레이아웃 기초가 이를 가능하게 하는 토대라는 것을 깨달았습니다.
오늘은 어떤 플랫폼에서 작업하든 상관없이 SwiftUI 레이아웃의 보편적인 구성 요소가 되는 원칙들을 다뤄보겠습니다. 어떤 플랫폼에서 작업하든 상관없이 말이죠.
먼저, 레이아웃에 특히 중요한 SwiftUI의 개념적 구성 요소들을 다시 살펴본 후, SwiftUI
레이아웃의 작동 원리에 대해 알아보겠습니다.
그 다음에는 SwiftUI를 사용하여 예측 가능한 결과를 얻는 방법에 대한 예제를 단계별로 안내해 드리겠습니다.
그럼 기본 개념인 뷰(view)부터 시작해 보겠습니다.
오늘 아침 레아가 설명했듯이, SwiftUI의 뷰는 화면에 무엇이 표시될지를 설명하는 경량 템플릿입니다.
뷰는 위치, 크기, 계층 구조를 나타내는 템플릿입니다.
SwiftUI는 화면에 표시될 내용을 설명하기 위해 경량 뷰 구조체를 생성하고, 이를 폐기한 뒤 다음 업데이트 시 새로운 구조체를 생성합니다.
SwiftUI 업데이트 엔진은 변경된 UI 부분만 업데이트하여, 상태 변화에 따라 예측 가능하고 효율적인 결과를 보장합니다.
이러한 효율성은 SwiftUI의 로컬 업데이트 모델에서 비롯됩니다. 뷰 계층 구조의 한 부분에서 발생한 변경 사항은 관련 없는 하위 트리로 연쇄적으로 전달되지 않습니다. 따라서 영향을 받은 서브뷰만 다시 렌더링됩니다.
그리고 SwiftUI에서 뷰 템플릿은 위치 지정과 크기 조정부터 렌더링 및 상태 업데이트에 이르기까지 모든 것을 제어합니다.
이제 뷰가 무엇이며 SwiftUI의 업데이트 엔진이 어떻게 작동하는지 살펴보았으니, SwiftUI가 레이아웃을 수행할 때 이러한 개념들이 어떻게 결합되는지 그 메커니즘을 보여드리겠습니다.
레이아웃 메커니즘을 설명하면서, 위시리스트 앱에서 만든 여행 카드에 집중할 것입니다.
이 여행 카드는 SwiftUI가 다양한 상태에 따라 동적 레이아웃을 어떻게 처리하는지 보여주는 훌륭한 예시입니다.
SwiftUI에서 레이아웃은 상태의 함수입니다. 여기서 말하는 ‘상태’는 at state 속성 래퍼를 의미하는 것이 아닙니다. 여기서 말하는 ‘상태’는
일반적인 상태를 의미하며, 예를 들어 ‘Cali coastal
trails’와 같은 여행 이름이나 여행 이미지의 URL 등을 말합니다. SwiftUI는 본질적으로 상태를 레이아웃으로부터 추상화합니다.
다른 UI 프레임워크에서도 이것이 가능하지만, 상당한 노력과 체계적인 관리가 필요합니다. 반면 SwiftUI에서는 이러한 추상화가 핵심적인 부분으로 자리 잡고 있습니다.
상태는 UI를 동적으로 제어함으로써 레이아웃을 주도합니다.
또한 상태는 화면에 표시되는 문자열이나 이미지처럼 각 뷰 내부의 내용을 채웁니다.
다른 프레임워크에서는 상태가 변경될 때 UI의 특정 부분을 다시 레이아웃하기 위해 수동으로 업데이트해야 합니다.
SwiftUI에서는 상태 변경 시 영향을 받는 뷰 템플릿이 자동으로 재생성되어 항상 최신 상태를 유지합니다.
이제 여기 표시된 것처럼 위시리스트 앱의 여행 카드 레이아웃을 어떻게 구축했는지 설명하겠습니다.
이를 통해 상태로부터 레이아웃을 추상화하는 방법을 설명하고자 합니다. 여행 카드(trip card)는 단색의 어두운 배경 위에 표시됩니다.
이 카드에는 여행을 나타내는 이미지와 여행 이름이 담긴
텍스트 뷰를 포함하는 VStack이 있습니다. 여행 이미지 뷰에는 활동 수를 표시하는 오버레이가 있지만,
활동 개수가 0보다 클 때만 표시됩니다. 그렇지 않은 경우, 해당 텍스트는 생성되지 않으며 SwiftUI는 활동 오버레이를 렌더링하지 않습니다.
뷰의 상태는 모델의 ‘여행’ 객체에 의해 독립적으로 제어됩니다.
여행 모델에는 여행 제목을 나타내는 이름 문자열이 있습니다.
그리고 여행에 대해 어떤 이미지를 렌더링할지 결정하는 사진 URL이 있습니다.
마지막으로, 활동 목록을 담은 사전이 있습니다. 사전 내 활동 개수를 사용하여 활동 오버레이를 표시할지 여부를 결정합니다.
이것은 상태가 SwiftUI 레이아웃을 제어하는 방식과 값이 어떻게 활용되는지에 대한 간단한 예시입니다. 이 예제에서는 활동의 개수를 사용하여 뷰의 레이아웃을 맞춤 설정합니다.
다음으로, SwiftUI에서 레이아웃이 어떻게 작동하는지 설명하겠습니다.
SwiftUI 레이아웃의 기본 전제는 컨테이너가 자식 뷰에 크기를 제안한다는 것입니다. 이 제안된 크기는 컨테이너 뷰와 그 상위 모든 컨테이너 뷰에 의해 제약받습니다.
각 자식 뷰는 이에 대응하여 선호하는 크기를 계산하여 보고합니다.
SwiftUI의 뷰마다 선호하는 크기가 다릅니다.
컬러 뷰는 제안받은 크기를 항상 모두 차지합니다. 사용 가능한 공간을 채우도록 크기가 커집니다.
이미지 뷰는 제안받은 공간이 더 적더라도 항상 전체 크기를 차지합니다. 이는 이미지가 포함된 뷰의 경계를 넘어설 수 있음을 의미합니다.
이를 재정의하려면 resizable 수식어를 사용하세요. 그러면 제안받은 공간을 모두 차지하게 됩니다. 여기 해안가의 꽃들처럼 이미지가 찌그러져 보이게 되더라도, 색상 뷰와 마찬가지로 말입니다.
이미지의 원래 종횡비를 유지하려면 콘텐츠 모드를 ‘fit’ 또는 ‘fill’로 설정하세요.
텍스트는 최소한 줄임표(…)를 표시할 수 있는 공간만큼을 차지합니다. 텍스트는 콘텐츠에 필요한 공간만 차지합니다.
레이아웃과 SwiftUI는 계층적으로 이루어집니다.
컨테이너 뷰는 각 서브뷰에 크기를 제안합니다.
제안된 크기가 구체적으로 지정되어 있다고 가정해 봅시다. 예를 들어, 이 주황색 상자는 제안이 어떻게 보일 수 있는지 보여줍니다.
컨테이너 뷰가 앱의 루트 뷰인 경우, 제안된 크기는 기기 크기이거나, 프레임 수정자를 적용하면 제안된 크기는 프레임의 크기입니다.
반면, 때로는 제안된 크기가 한 방향 또는 양방향 모두에서 지정되지 않은 경우도
있습니다.
이는 컨테이너 뷰가 하위 뷰가 전혀 제약받지 않는다면 얼마나 많은 공간을 원할지 알고 싶어 한다는 것을 의미합니다. 이것이 바로 하위 뷰의 이상적인 크기입니다.
각 하위 뷰는 제안된 크기를 바탕으로 선호하는 크기를 응답합니다.
이는 다른 뷰 시스템과 비교했을 때 SwiftUI만의 특별하고 독특한 동작입니다. 제안은 아래로만 전달되는 것이 아니라 위로도 전달됩니다.
이 레이아웃 과정은 재귀적입니다. 제안은 루트 뷰에서 시작하여 전체 뷰 계층 구조를 따라 아래로 전달됩니다.
그런 다음 응답이 다시 위로 올라옵니다.
크기 정보가 포함된 상태로 말이죠. 루트 뷰는 모든 서브뷰에 어디에 그려져야 하는지 지시합니다. 서브뷰들은 다시 자신의 서브뷰들에게 지시하고, 모든 뷰가 그려질 때까지 이 과정이 반복됩니다.
이제 동일한 탐색 과정을 다시 한 번 살펴보겠습니다. 이번에는 여기 여행 카드에 실제 뷰와 실제 값을 적용해 보겠습니다.
여행 카드는 가로 스크롤 뷰 내부의 가로 스택에 표시됩니다.
소스 이미지의 크기나 여행 이름의 길이와 상관없이, ‘칼리’, ‘교토’ 등 모든 여행 카드의 높이가 동일하게 유지되기를 원합니다.
고정 높이 프레임을 사용하면 이를 달성할 수 있습니다.
SwiftUI는 VStack에 무제한 너비와 220의 고정 높이를 제공합니다. 여행 카드의 루트에서.
스택은 여행 이미지뷰에 높이 220을 할당합니다. 다음은 고정 높이 프레임의 예시입니다.
이미지뷰는 필요한 정확한 크기인 185 × 150 포인트를 반환합니다.
외부 스택은 반환된 높이를 뺀 후, 남은 공간을 여행 이름이 포함된 텍스트에 할당합니다.
이름 텍스트는 필요한 공간인 15 × 130 포인트를 반환합니다. 마지막 뷰에 도달했습니다.
이제 각 뷰가 필요한 크기를 응답했습니다.
이는 액티비티 오버레이를 제외한 모든 뷰에 해당합니다. 액티비티 오버레이에 대해서는 프레젠테이션 후반부에서 다시 다루겠습니다. 프로세스를 마무리합니다. VStack은 내부 이미지, 뷰, 텍스트가 요청한 크기를 합산하여 뷰의 최종 요청 크기를 반환합니다.
이것이 바로 SwiftUI 레이아웃이 작동하는 방식입니다.
정리하자면, SwiftUI 레이아웃은 위에서 아래로 순회됩니다. 그리고 크기 정보는 루트까지 역방향으로 전달됩니다.
어떤 수준에서든 사용 가능한 공간은 컨테이너 뷰가 제공합니다.
필요한 공간은 서브뷰가 결정하며, 경우에 따라 자체 서브뷰들을 확인하기도 합니다.
레이아웃은 상태가 변경될 때만 재계산됩니다.
뷰 템플릿은 항상 새로 생성됩니다.
상태가 콘텐츠를 제어하므로, 상태 값이 변경되면 뷰가 자동으로 변경됩니다.
정말 멋지네요. 이제 멋진 SwiftUI 뷰를 만들어 볼 시간입니다.
제가 SwiftUI를 막 시작했을 때, 뷰를 만들면 때때로 기대했던 결과를 얻지 못하곤 했습니다. 그래서 다음에는 레이아웃을 단순하고 조합 가능하게 유지하여 예측 가능한 결과를 얻을 수 있는 몇 가지 지침을 공유하겠습니다.
예측 가능한 레이아웃을 위한 첫 번째 고려 사항은 레이아웃 우선순위입니다.
레이아웃 우선순위는 간격 조정 및 크기 조정을 위해 뷰가 배치되는 순서를 결정하는 데 사용됩니다.
뷰의 요청을 충족할 공간이 충분하지 않을 경우, 레이아웃 우선순위가 더 높은 뷰에 더 많은 공간이 할당됩니다. 레이아웃 우선순위는 레이아웃 우선순위 수정자를 통해 설정되며, 기본값은 0입니다. 값이 높을수록 우선순위가 높아집니다. 예를 들어, 우선순위 3인 뷰는 우선순위 1인 뷰보다 우선순위가 높습니다.
이는 버그 큐에서 익숙하게 접하는 방식과는 정반대일 수 있습니다. 버그 큐에서는 p1이 가장 높은 우선순위이지만, 이 경우에는 그 반대입니다. 숫자가 클수록 우선순위가 높아집니다.
위시리스트 앱에서 저는 카드의 너비가 더 넓은 ‘여행’ 섹션을 만들었습니다.
위시리스트 앱 디자이너인 마호와 이야기를 나누던 중, 이 넓은 여행 카드가 여유 공간을 활용해 여행의 부제목과 생성 날짜를 표시하기에 안성맞춤이라고 생각했습니다.
때로는 컨테이너 뷰 내에 여러 텍스트 뷰가 포함될 때가 있습니다. 사용 가능한 공간에 따라 텍스트가 잘리거나 줄바꿈될 수 있습니다.
이 예시에서, 부제목과 날짜를 추가하자 카드 왼쪽 하단에 있던 ‘하와이, 미국’이라는 텍스트가 잘렸습니다.
사용 가능한 공간이 얼마나 되는지 알 수 없는 경우가 있습니다. 예를 들어, 기기 크기에 따라 달라질 수 있습니다.
또한 필요한 공간이 얼마나 되는지 알 수 없을 수도 있습니다. 일부 여행의 부제목 문자열은 더 길기 때문에, 레이아웃에서 각 텍스트의 중요도를 지정하면 어떤 텍스트를 잘라낼지 결정할 수 있습니다.
이를 위해 텍스트의 레이아웃 우선순위를 기본값인 0에서 1과 같은 더 높은 값으로 높이면 됩니다.
앞서 말씀드렸듯이, 스택은 우선순위가 높은 뷰에 먼저 공간을 할당하므로
여행명 자막은 필요한 공간을 모두 확보하지만, 이로 인해 오른쪽 하단 모서리의 날짜 텍스트가 잘리게 됩니다.
저는 이 레이아웃 문제를 우리 디자이너 마호에게 공유했습니다. 그녀는 공간이 부족할 때 생성 날짜를 숨겨서 여행명 자막이 완전히 보이도록 하라고 제안했습니다.
문자열이 잘리는 것을 방지할 수 있으니 정말 훌륭한 조언이라고 생각합니다. 이제 그 방법을 보여드리겠습니다.
저는 스택을 ‘View that fits’ 뷰 안으로 이동시켰습니다. ‘View that fits’는 서브뷰들을 평가합니다. 초기화자에 전달한 순서대로,
제안된 크기 내에 이상적인 크기가 맞는 첫 번째 서브뷰를 선택합니다. 즉, 선호하는 순서대로 뷰를 제공해야 합니다. 보통 이 순서는 가장 큰 것부터 가장 작은 것까지입니다.
그래서 서브타이틀만 포함된 텍스트 뷰를 추가했습니다. 뷰가 배치되면, 스택이 잘리는 대신 ‘뷰 핏’이 텍스트 뷰를 선택하게 됩니다.
이러한 변경 사항이 우리 디자이너 마호 씨를 기쁘게 해줄 것이라고 확신합니다.
다음으로, 스택 정렬 기능은 뷰들이 정렬 방향이 일치하도록 배치합니다. 기본 정렬은 중앙 정렬이지만, 다른 정렬 방식도 지정할 수 있습니다.
중앙 정렬 외에도, H 스택 내의 뷰들은 상단이나 하단에 수직으로 정렬될 수 있으며, 첫 번째 텍스트 기준선이나 마지막 텍스트 기준선에 맞춰 정렬될 수도 있습니다.
V 스택 내 뷰들은 가로 방향으로 앞쪽 가장자리, 뒤쪽 가장자리 등에 맞춰 정렬될 수도 있습니다.
위시리스트 앱의 여행 컬렉션 섹션 헤더가 완벽한 예시입니다. 기본적으로 스택은 뷰를 정렬할 때 중앙 정렬을 사용하지만,
일련의 요소가 있을 때 이 방식이 정렬이 어긋난 것처럼 보일 수 있습니다. 이 경우, 모든 요소를 베이스라인에 맞춰 정렬하는 것이 가장 좋습니다.
여기서는 섹션 부제목의 하위 뷰들이 기본 정렬 방식을 사용하고 있습니다. 즉, ‘모두 보기’가 부제목 텍스트가 형성하는 시각적 선 위에 떠 있는 상태입니다.
정렬 방식을 ‘첫 번째 텍스트 기준선’으로 변경함으로써, ‘모두 보기’를 부제목이 형성하는 시각적 선과 같은 높이로 맞춥니다.
이제 SwiftUI에서 레이아웃을 구축할 때 고려해야 할 몇 가지 사항을 더 공유하겠습니다.
가능한 한 명시적인 레이아웃 대신 적응형 레이아웃을 만드세요. 적응형 레이아웃을 사용하면 다양한 기기 크기, 창 크기 및 플랫폼에 더 쉽게 대응할 수 있습니다.
뷰에 명시적인 높이와 너비를 지정하는 대신, 뷰 프레임을 명시적으로 조작하는 것은 피하십시오. 뷰가 사용 가능한 공간을 채우도록 확장되도록 하세요.
원하는 레이아웃을 적응적이고 유연한 방식으로 구현할 수 없는 경우에만 frame이나 position과 같은 뷰 수정자를 사용하십시오.
Zstack과 유사합니다. 뷰에 깊이감을 더하려면 background 또는 overlay 수정자를 사용할 수 있습니다.
overlay 및 background 수정자는 레이아웃 크기 계산에 포함되지 않으며, 항상 수정 대상 뷰와 동일한 크기를 가집니다.
이것이 앞서 레이아웃 트리 탐색 예제에서 activities 오버레이가 레이아웃 계산에 포함되지 않았던 이유입니다. 이 오버레이는 컨테이너 뷰의 계산된 크기를 상속받았습니다. 기본적으로 이미지 뷰입니다.
레이아웃 문제는 발생하기 마련입니다. 저도 여전히 가끔 그런 문제를 겪곤 합니다. 이제 저는 이러한 레이아웃 문제를 파악하고 해결하는 데 사용하는 몇 가지 기법을 공유하겠습니다.
이 중 상당수는 신속한 프로토타이핑을 위한 SwiftUI의 강력한 기능을 기반으로 합니다. 약간의 조정을 가하면 결과가 즉시 나타납니다.
간단한 기법 중 하나는 여기 트립 카드에서 제가 한 것처럼 이미지에 빨간색, 텍스트에 파란색과 같은 색상 테두리를 추가하는 것입니다.
이 기법은 스택 레이아웃과 뷰 주변의 패딩을 이해하는 데 특히 유용합니다.
때로는 임시 테두리가 겹치기도 하는데, 이럴 때는 서브뷰의 크기와 위치를 더 명확하게 표현하고 싶을 때가 있습니다. 이 경우, 반투명한 색상의 오버레이 수정자를 사용하여 서브뷰의 전체 범위와 레이어 구조를 명확히 드러냅니다.
뷰가 렌더링될 때 속성의 값을 출력하는 것도 유용할 수 있습니다.
print를 사용해 보셨다가 컴파일러 오류로 인해 실망하셨을 수도 있습니다.
여기서 오류 메시지는 ‘빌드 표현식을 사용할 수 없습니다. 이 표현식은 뷰를 준수하지 않습니다.’라고 나옵니다.
이 오류는 Swift의 print 표현식이 반환 값이 없어 반환
유형이 void이기 때문입니다. void는 뷰가 아니지만, 뷰의 본문은 변수 선언을 지원합니다.
플레이스홀더 변수를 선언하여 이 오류를 수정할 수 있습니다. 이제 등호 오른쪽에 `print` 표현식을 작성할 수 있습니다. 이렇게 하면 오류 없이 컴파일되며, 실행 시 콘솔에 출력이 표시됩니다.
다음으로, Xcode는 미리보기 캔버스를 제공하며, 이를 통해 어떤 코드가 어떤 뷰와 연결되는지 빠르게 확인할 수 있습니다. 캔버스 하단의 마우스 포인터 아이콘을 클릭하면 미리보기를 선택할 수 있게 됩니다.
그러면 에디터에서 코드를 선택하면 미리보기에서 관련 뷰가 강조 표시되고, 미리보기에서 뷰를 선택하면 Xcode가 에디터에서 해당 코드를 선택합니다.
레이아웃 문제를 디버깅할 때 제가 사용하는 가장 강력한 도구는 Xcode의 뷰 디버거입니다. 앱을 실행한 후 뷰 디버거로 중지하면 모든 뷰를 확장한 다음, 집중하고 싶은 개별 서브뷰를 선택할 수 있습니다. 이 기법은 SwiftUI와 UKit을 함께 사용할 때 특히 효과적입니다. 이 점과 관련해 중요한 사항을 하나 말씀드리겠습니다.
SwiftUI는 UKit 레이아웃과 호환됩니다. Alltrails의 James Graham이 나중에 이에 대해 자세히 설명해 드릴 것입니다. 이에 대해 더 자세히 알고 싶으시다면 WWDC 22 영상을 확인해 보세요. UIKit과 함께 SwiftUI를 사용해 보세요.
SwiftUI 레이아웃의 기본 사항을 공유했으니, 이제 이러한 개념을 실제로 적용해 볼 차례입니다.
배우는 가장 좋은 방법은 직접 만들어 보는 것입니다. 저는 마음에 드는 앱에서 뷰 하나를 골라 SwiftUI로 재구현하는 것으로 시작했습니다.
Xcode 미리보기 기능은 다양한 레이아웃과 접근 방식을 빠르게 시험해 볼 수 있는 훌륭한 방법입니다.
print 및 색상 오버레이와 같은 간단한 기법을 사용하여 발생하는 레이아웃 문제를 진단해 보세요. 문제를 얼마나 빠르게 파악하고 해결할 수 있는지 놀라실 겁니다.
오늘 함께해 주셔서 감사합니다. 이제 SwiftUI로 특별한 무언가를 만들어 보시고, 남은 세션도 즐겁게 감상하시기 바랍니다.
-