Back to Blog

[UIKit] UIStackView - 오토 레이아웃으로 다양한 뷰 감싸기

2024년 10월 27일
13분
Swift
UIStackView, UIKit, iOS, 글또 10기, Swift
[UIKit] UIStackView - 오토 레이아웃으로 다양한 뷰 감싸기

그동안 많은 프로젝트를 하면서 여러 뷰를 하나의 뷰로 감쌀 때 UIView를 사용해 왔는데요.
뷰를 감싸기 위한 UIStackView가 있는 것을 알면서도 더 자유롭게 뷰를 배치하기 위해 사용하지 않았습니다.
또, UIStackView를 사용하면서 여러 제약 조건에 충돌이 생기기도 했고, 이러한 충돌을 어떻게 해결해야 할 지 감이 오지 않아서 빠르게 프로젝트를 진행해야 한다는 핑계로 UIStackView의 사용을 더 피하기도 했습니다.

그러던 중 최근 새로운 프로젝트를 하면서 뷰 간의 일관된 간격을 위해 UIStackView를 사용하게 됐는데요.
이번 시간에는 UIStackView를 사용하면서 공부한 내용을 기록하려 합니다.

UIStackView

@MainActor
class UIStackView: UIView

우선 공식 문서부터 살펴보자면 UIStackView열 또는 행에 뷰 컬렉션을 배치하기 위한 간소화된 인터페이스라고 합니다. 조금 더 쉽게 풀어보자면 여러 뷰를 가로 방향, 혹은 세로 방향으로 순서대로 배치하기 위한 UIView라고 할 수 있겠네요.

여러 뷰를 순서대로 배치하는 것은 UIView와 제약 조건을 사용해서도 충분히 만들 수 있는데요.
그렇다면 UIStackView가 왜 필요한 것일까요?

UIStackView는 오토 레이아웃의 강력한 기능을 활용하여 기기의 방향, 화면 크기 및 사용 가능한 공간의 변경 사항에 따라 동적으로 조정할 수 있는 사용자 인터페이스를 만들 수 있습니다.

UIView 안에 다른 여러 뷰를 배치할 때는 안에 배치하는 뷰 하나하나마다 일일이 제약 조건을 걸어야 했는데요. UIStackView를 사용한다면 UIStackView의 프로퍼티를 사용해 뷰 하나하나에 제약 조건을 걸어야 하는 수고를 덜 수 있습니다.

UIStackView의 내부 뷰 관리

UIStackView는 내부에 여러 뷰를 정렬할 수 있는데요. UIStackView는 이러한 내부에 정렬된 뷰를 관리하기 위해 아래의 네 가지 방법을 사용할 수 있습니다.

  • var arrangedSubviews: [UIView] { get }
  • func addArrangedSubview(_ view: UIView)
  • func insertArrangedSubview(_ view: UIView, at stackIndex: Int)
  • func removeArrangedSubview(\_ view: UIView)

1. var arrangedSubviews: [UIView] { get }

arrangedSubvies 프로퍼티는 UIStackView의 내부에 정렬된 뷰의 목록입니다.

UIStackViewUIView를 상속하고 있기 때문에 subviews 배열을 갖는데, arrangedSuviews 배열은 이러한 subviews 배열의 하위 집합입니다.
따라서 arrangedSubviews 배열에 새로운 뷰가 추가되면 subviews 배열에도 해당 뷰가 추가됩니다.
하지만 subviews 배열에 새로운 뷰가 추가된다고 해서 arrangedSubviews에 해당 뷰가 추가되는 것은 아닙니다.
또, arrangedSubviews 배열에서 뷰가 제거된다면 subviews 배열에서는 해당 뷰가 함께 제거되지 않습니다. 하지만 subviews 배열에서 뷰가 제거된다면 arrangedSubviews에서도 해당 뷰는 함께 제거됩니다.

2. func addArrangedSubview(_ view: UIView)

addArrangedSubview 메서드는 arrangedSubviews 배열의 끝과 subviews 배열의 끝에 view 파라미터의 뷰를 추가하는 메서드입니다.

만약, view 파라미터의 뷰가 arrangedSubviews 배열에 이미 존재한다면 새로운 뷰를 추가하지 않고 기존에 있던 뷰의 순서를 배열의 끝으로 옮깁니다.
하지만, subviews 배열에 view 파라미터의 뷰가 이미 존재한다면 subviews 배열의 순서는 변경되지 않습니다.

3. func insertArrangedSubview(_ view: UIView, at stackIndex: Int)

insertArrangedSubview 메서드는 arrangedSubviews 배열의 startIndex 파라미터 인덱스에 view 파라미터의 뷰를 추가하는 메서드입니다.

startIndex의 인덱스가 이미 사용 중인 경우 UIStackViewarrangedSubviews 배열의 크기를 늘리고 인덱스 값 이상의 모든 콘텐츠를 다음 칸으로 이동시킵니다. 그 후에 view 파라미터의 뷰를 startIndex 파라미터의 인덱스에 해당하는 칸에 저장합니다.

이 때, startIndex 파라미터의 값은 현재 arrangedSubviews 배열의 크기보다 크지 않아야 합니다. 만약, 인덱스가 범위를 벗어난 경우 insertArrangedSubviewinternalInconsistencyException 예외를 발생시킵니다.

insertArrangedSubview 메서드로 arrangedSubviews 배열에 view 파라미터의 뷰가 추가될 때, subviews 배열에도 뷰가 함께 추가되는데, subviews 배열에 추가될 때는 특정 인덱스에 삽입되는 것이 아니라, subviews 배열의 끝에 추가됩니다.
즉, startIndexarrangedSubviews 배열의 순서에만 영향을 주고, subviews 배열의 순서에는 영향을 미치지 않습니다.

4. func removeArrangedSubview(_ view: UIView)

removeArrangedSubview 메서드는 arrangedSubviews 배열에서 view 파라미터의 뷰를 제거하는 메서드입니다.

removeArrangedSubview 메서드로 제거된 뷰는 UIStackView가 더 이상 위치와 크기를 관리하지 않습니다.
하지만, removeArrangedSubview 메서드로 제거된 뷰는 arrangedSubviews 배열에서만 제거될 뿐, subviews 배열에서는 제거되지 않습니다. 따라서 해당 뷰는 여전히 뷰 계층구조의 일부로 표시됩니다.

만약, removeArrangedSubview로 제거된 뷰가 화면에 표시되지 않도록 하려면 뷰의 removeFromSuperview() 메서드를 호출하여 하위 뷰 배열에서 뷰를 명시적으로 제거하거나, 뷰의 isHidden 프로퍼티를 true로 설정해 뷰를 더 이상 보이지 않게 하는 방법을 사용할 수 있습니다.

UIStackView의 위치 및 크기 조정

UIStackView를 사용하면 오토 레이아웃을 사용하지 않고도 콘텐츠를 정렬할 수 있지만, UIStackView 자체의 위치를 지정하려면 반드시 오토 레이아웃을 사용해야 합니다.
일반적으로 UIStackView의 두 개 이상의 인접한 가장자리를 고정하여 위치를 정의합니다.

추가 제약 조건이 없다면 시스템은 UIStackView 내부의 콘텐츠를 기반으로 스택의 크기를 자동으로 계산합니다.
즉, UIStackView의 축 방향의 스택 크기는 스택 내부에 정렬된 모든 뷰의 크기와 뷰 사이의 공간을 더한 값의 합과 같고, 축에 수직인 스택 크기는 정렬된 뷰 중 가장 큰 정렬된 뷰의 크기와 같습니다.

추가 제약 조건을 설정한다면 UIStackView의 높이, 너비 도는 둘 모두를 지정할 수 있습니다. 이 경우, UIStackView는 지정된 영역을 채우도록 정렬된 뷰의 레이아웃과 크기를 조정하게 됩니다.

UIStackView의 레이아웃 구성

UIStackView는 오토 레이아웃을 사용하여 정렬된 뷰의 위치와 크기를 지정하는데, 정렬된 첫 번째 및 마지막 뷰를 UIStackView의 축을 따라 가장자리에 정렬합니다.

이러한 UIStackView의 정확한 레이아웃은 다양한 프로퍼티에 의해 달라집니다.
아래의 6가지 프로퍼티는 UIStackView의 레이아웃을 구성할 수 있습니다.

  • var axis: NSLayoutConstraint.Axis { get set }
  • var alignment: UIStackView.Alignment { get set }
  • var distribution: UIStackView.Distribution { get set }
  • var spacing: CGFloat { get set }
  • var isBaselineRelativeArrangment: Bool { get set }
  • var isLayoutMarginsRelativeArrangement: Bool { get set }

1. var axis: NSLayoutConstraint.Axis { get set }

axis 프로퍼티는 UIStackView에 정렬된 뷰가 배치되는 축입니다.
axis 프로퍼티에 따라 정렬된 뷰의 방향을 결정하게 되며, NSLayoutConstraint.Axis.vertical 값을 할당하면 세로 방향으로, NSLayoutConstraint.Axis.horizontal 값을 할당하면 가로 방향으로 뷰를 정렬하게 됩니다.
기본값은 NSLayoutConstraint.Axis.horizontal 입니다.

2. var alignment: UIStackView.Alignment { get set }

alignment 프로퍼티는 UIStackView의 축에 수직으로 정렬된 뷰의 정렬 방식을 결정합니다.
alignment 프로퍼티의 값은 UIStackView.Alignmnet enum에 속하는 8가지의 값으로 설정할 수 있으며, 기본값은 UIStackView.Alignment.fill입니다.

enum UIStackView.Alignment: Int, @unchecked Sendable {
    static var top: UIStackView.Alignment { get }
    static var bottom: UIStackView.Alignment { get }
 
    case fill = 0
    case leading = 1
    case firstBaseline = 2
    case center = 3
    case lastBaseline = 5
    case trailing = 4
}

2-1. case fill

fillUIStackView가 정렬된 뷰의 크기를 조정하여 축에 수직인 사용 가능한 공간을 채우는 레이아웃입니다.
가로 스택 뷰에서는 뷰의 높이를 조정하며, 세로 스택 뷰에서는 뷰의 너비를 조정하여 스택 뷰의 빈 공간을 채웁니다.

가로 스택 뷰 fill Alignment
이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.fill

2-2. case center

centerUIStackView가 정렬된 뷰의 중심을 축의 중심에 맞게 정렬하는 레이아웃입니다.

가로 스택 뷰 center Alignment
이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.center

2-3. case leading, case trailing

leading은 정렬된 뷰의 앞쪽 가장자리를, trailing은 뒤쪽 가장자리를 각각 UIStackView의 앞, 뒤쪽 가장자리에 맞게 정렬하는 세로 스택용 레이아웃입니다.
일반적으로 많이 사용되는 왼쪽에서 오른쪽으로 정렬되는 스택 뷰에서는 왼쪽이 앞쪽, 오른쪽이 뒤쪽이 되지만, 오른쪽에서 왼쪽으로 정렬되는 스택 뷰에서는 오른쪽이 앞쪽, 왼쪽이 뒤쪽이 됩니다.

leading Alignment trailing Alignment
세로 스택 뷰 leading Alignment 세로 스택 뷰 trailing Alignment
이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.leading 이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.trailing

2-4. static var top, static var bottom

top은 정렬된 뷰의 위쪽 가장자리를, bottom은 정렬된 뷰의 아래쪽 가장자리를 각각 UIStackView의 위, 아래쪽 가장자리에 맞게 정렬하는 가로 스택용 레이아웃입니다.

top Alignment ottom Alignment
가로 스택 뷰 top Alignment 가로 스택 뷰 bottom Alignment
이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/top 이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/bottom

2-5. case firstBaseline, lastBaseline

firstBaselineUIStackView가 정렬된 뷰의 첫 번째 행의 아래쪽을 기준으로, lastBaseline은 정렬된 뷰의 마지막 행의 아래쪽을 기준으로 뷰를 정렬하는 가로 스택용 레이아웃입니다.

firstBaseline Alignment lastBaseline Alignment
가로 스택 뷰 firstBaseline Alignment 가로 스택 뷰 lastBaseline Alignment
이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.firstBaseline 이미지 출처: Apple Developer Documentation/UIKit/UIStackView/UIStackView.Alignment/UIStackView.Alignment.lastBaseline

3. var distribution: UIStackView.Distribution { get set }

distribution 프로퍼티는 UIStackView의 축을 따라 정렬된 뷰를 배치하는 방식을 결정합니다.
distribution 프로퍼티의 값은 UIStackView.Distribution enum에 속하는 5가지의 값으로 설정할 수 있으며, 기본값은 UIStackView.Distribution.fill입니다.

enum UIStackView.Distribution: Int, @unchecked Sendable {
    case fill = 0
    case fillEqually = 1
    case fillProportionally = 2
    case equalSpacing = 3
    case equalCentering = 4
}

3-1. case fill

fillUIStackView가 정렬된 뷰의 크기를 조정하여 스택의 축을 따라 사용 가능한 공간을 채우는 레이아웃입니다.
정렬된 뷰가 스택 뷰의 축의 크기보다 크다면 compression resistance priority에 따라 뷰의 크기를 줄이고, 정렬된 뷰가 스택 뷰의 축의 크기보다 작다면 hugging priority에 따라 뷰의 크기를 늘립니다.
모호한 부분이 있는 경우 UIStackViewarrangedSubviews 배열의 인덱스에 따라 정렬된 뷰의 크기를 적절하게 조정합니다.

가로 스택 뷰 fill Distribution
이미지 출처: Apple Developer Documentation/UIStackView/UIStackView.Distribution/UIStackView.Distribution.fill

3-2. case fillEqually

fillEquallyfill과 마찬가지로 UIStackView가 정렬된 뷰의 크기를 조정하여 스택의 축을 따라 사용 가능한 공간을 채우는 레이아웃입니다.
하지만, fill이 compression resistance, huggin priority에 따라 각각의 뷰의 크기를 적절하게 조정하는 것과 달리 fillEqually는 스택 내부의 모든 정렬된 뷰의 크기를 같도록 조정합니다.

가로 스택 뷰 fillEqually Distribution
이미지 출처: Apple Developer Documentation/UIStackView/UIStackView.Distribution/UIStackView.Distribution.fillEqually

3-3. case fillProportionally

fillProportionally 또한 fill, fillEqually와 마찬가지로 UIStackView에 정렬된 뷰의 크기를 조정하여 스택의 축을 따라 사용 가능한 공간을 채우는 레이아웃입니다.
fillProportionally를 사용하는 경우 스택의 축을 기준으로 각각의 정렬된 뷰의 크기에 따라 비례적으로 크기를 조정합니다.

가로 스택 뷰 fillProportionally Distribution
이미지 출처: Apple Developer Documentation/UIStackView/UIStackView.Distribution/UIStackView.Distribution.fillProportionally

3-4. case equalSpacing

equalSpacingUIStackView에 정렬된 뷰의 크기를 조정하여 스택의 축을 따라 사용 가능한 공간을 채우는 레이아웃이지만, equalSpacing을 사용하는 경우 정렬된 뷰가 스택의 크기를 채우지 못하면 뷰 사이의 간격을 균등하게 조정하여 스택을 채웁니다.
정렬된 뷰가 스택의 크기보다 크다면 fill과 마찬가지로 compression resistance priority에 따라 뷰를 줄이고, 모호한 부분이 있는 경우 arrangedSubviews 배열의 인덱스에 따라 정렬된 뷰의 크기를 줄입니다.

가로 스택 뷰 equalSpacing Distribution
이미지 출처: Apple Developer Documentation/UIStackView/UIStackView.Distribution/UIStackView.Distribution.equalSpacing

3-5. case equalCentering

equalCenteringUIStackView의 축을 따라 정렬된 뷰 간 가운데 간격을 동일하게 맞춰 배치하는 레이아웃으로, 뷰 간 spacing 프로퍼티의 거리를 유지하면서 배치합니다.
정렬된 뷰가 스택의 크기보다 크다면 spacing 프로퍼티에 정의된 최소 간격에 도달할 때까지 간격을 축소하며, 최소 간격에 도달했지만, 여전히 정렬된 뷰가 더 크다면 compression resistance priority에 따라 정렬된 뷰의 크기를 줄이고, 모호한 부분이 있는 경우 arrangedSubview 배열의 인덱스에 따라 뷰의 크기를 줄입니다.

가로 스택 뷰 equalCentering Distribution
이미지 출처: Apple Developer Documentation/UIStackView/UIStackView.Distribution/UIStackView.Distribution.equalCentering

4. var spacing: CGFloat { get set }

spacing 프로퍼티는 UIStackView에 정렬된 뷰의 인접한 가장자리 사이의 포인트 단위 거리입니다.

spacing 프로퍼티는 UIStackView.Distribution.fillProportionally에서 정렬된 뷰 사이의 정확한 간격을 정의하며, UIStackView.Distribution.equalSpacingUIStackView.Distribution.equalCentering에서는 정렬된 뷰 사이의 최소 간격을 나타냅니다.

뷰끼리 겹치는 것을 허용하려면 음수 값을 사용하며, 기본값은 0.0입니다.

5. var isBaselineRelativeArrangment: Bool { get set }

isBaselineRelativeArrangement는 뷰 사이의 세로 간격을 기준선에서부터 측정할지 여부를 결정하는 불 값으로, 기본값은 false입니다.
true로 설정하면 뷰 간의 세로 간격을 텍스트 기반 뷰의 마지막 기준선에서 그 아래 뷰의 첫 번째 기준선까지로 측정합니다. 상단 및 하단 뷰도 가장 가까운 기준선이 스택 뷰의 가장자리에서 지정된 거리만큼 떨어져있도록 배치됩니다.
isBaselineRelativeArrangement는 세로 스택 뷰에서만 사용되며, 가로 스택 뷰에서 뷰의 기준선을 정렬하기 위해서는 alignment 프로퍼티를 사용할 수 있습니다.

6. var isLayoutMarginsRelativeArrangement: Bool { get set }

isLayoutMarginsRelativeArrangementUIStackView가 레이아웃 margins을 기준으로 정렬된 뷰를 배치할지 여부를 결정하는 불 값으로, 기본값은 false입니다.
true로 설정하면 UIStackView는 레이아웃 margins을 기준으로 정렬된 뷰를 레이아웃하며, false로 설정하면 bounds를 기준으로 정렬된 뷰를 배치합니다.

UIStackView 내부 뷰 개별 간격 조정

UIStackView는 일반적으로 스택 내부에 정렬된 뷰 간의 간격을 동일하게 설정하지만, UI를 구성하다 보면 스택 내부의 일부 뷰의 간격을 다르게 해야 할 때도 있습니다.
UIStackView는 이러한 경우 일부 뷰의 간격을 별도로 조정하기 위한 아래의 4가지 메서드를 지원합니다.

  • func customSpacing(after arrangedSubview: UIView) -> CGFloat
  • func setCustomSpacing(\_ spacing: CGFloat, after arrangedSubview: UIView)
  • class let spacingUseDefault: CGFloat
  • class let spacingUseSystem: CGFloat

1. func customSpacing(after arrangedSubview: UIView) -> CGFloat

customSpacing(after:) 메서드는 arrangedSubview 파라미터에서 지정된 뷰 뒤의 사용자 지정 간격을 반환합니다.

2. func setCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView)

setCustomSpacing(\_:after:) 메서드는 arrangedSubview 파라미터에서 지정된 뷰 뒤에 spacing 파라미터에서 지정된 사용자 지정 간격을 적용합니다.
일부 뷰 간의 간격을 지정하기 위해 사용할 수 있습니다.

3. class let spacingUseDefault: CGFloat

spacingUseDefault 프로퍼티는 UIStackView 내부의 정렬된 뷰의 기본 간격을 의미하는 프로퍼티입니다.

4. class let spacingUseSystem: CGFloat

spacingUseSystem 프로퍼티는 인접한 뷰에 대한 시스템 정의 간격을 의미합니다.

마무리

이번 시간에는 UIStackView에 대해 알아보았습니다.
그동안은 UIStackView 내부의 간격을 커스텀하는 방법을 알지 못해서 UIView를 사용했었는데, 이번 기회에 setCustomSpacing 등의 새로운 메서드를 알게 되어서 앞으로는 UIStackView를 활용해서 레이아웃을 구성하는 데 잘 사용할 수 있을 것 같습니다.

iOS의 모든 컴포넌트에는 각각에 맞는 용도와 사용법이 있다는 걸 다시 한번 느낄 수 있었던 것 같습니다.

참고자료

댓글 및 반응

GitHub 계정으로 댓글을 남기거나 반응을 남길 수 있습니다