[TIL] UIStackView 제약 문제 트러블슈팅

Eden·2025년 9월 12일

StatsHeaderView에서 categoryButtontodayButton 사이에 spacerView를 두고, todayButton을 오른쪽 끝에 고정하려고 했지만
버튼을 isHidden = true/false로 토글할 때 버튼 정렬이 가운데로 흔들리는 문제가 발생했습니다.
이 문서는 contentHuggingPrioritycontentCompressionResistancePriority(이하 Hugging/Compression)로 해결하는 과정을 정리합니다.


증상

  • todayButton을 숨겼다가 다시 표시하면, categoryButton이 가운데로 이동하거나 레이아웃이 들쭉날쭉해 보임
  • UIStackViewdistribution = .fill 상태에서 여유 공간을 누가 가져가느냐가 매번 달라짐

원인

  1. UIStackView의 레이아웃 규칙
    • .fill에서는 가장 낮은 Hugging을 가진 뷰가 여유 공간을 먼저 가져갑니다.
    • 추가로, 가장 낮은 Compression을 가진 뷰가 먼저 줄어듭니다.
  2. hidden 토글의 특성
    • arrangedSubviews에서 isHidden = true가 되면 해당 뷰는 레이아웃에서 제외됩니다.
    • 이때 나머지 서브뷰들끼리 우선순위를 다시 비교하며, 공간 배분이 달라질 수 있습니다.
  3. spacerView의 역할 부족
    • spacer가 명확히 여유 공간만 흡수하도록 우선순위를 설정하지 않으면, 텍스트 길이/아이콘 유무 등에 따라
      categoryButton이 공간을 가져가며 가운데로 치우칠 수 있습니다.

해결 전략

핵심은 오른쪽 버튼은 고정, 가운데 여백은 spacer만 흡수하도록 우선순위를 설계하는 것입니다.

우선순위 설정

categoryButton.setContentHuggingPriority(.defaultLow, for: .horizontal)
categoryButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
spacerView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

todayButton.setContentHuggingPriority(.required, for: .horizontal)
todayButton.setContentCompressionResistancePriority(.required, for: .horizontal)
  • todayButtonrequired(1000) 로 설정해 오른쪽 끝에서 크기/위치가 흔들리지 않도록 고정합니다.
  • spacerView낮은 Hugging/Compression으로 두어 여유 공간을 전담 흡수하게 만듭니다.
  • categoryButton도 낮게 설정해 필요 시 줄어들 수 있도록 합니다. (제목 길이에 따라 레이아웃이 밀리지 않도록)

StackView 구성

private lazy var hStack = UIStackView(arrangedSubviews: [
    categoryButton, spacerView, todayButton
]).then {
    $0.axis = .horizontal
    $0.alignment = .center
    $0.distribution = .fill
}
  • .fill + spacer 조합은 spacer가 “빈 공간만” 차지하도록 만드는 기본 패턴입니다.

상황별 동작 확인

1) todayButton이 보이는 경우

  • todayButton: required → 오른쪽 끝 고정
  • spacerView: 낮은 우선순위 → 중앙 여유 공간 흡수
  • categoryButton: 컨텐츠 크기 유지, 필요 시 줄어듦

2) todayButton을 숨긴 경우

todayButton.isHidden = true
UIView.animate(withDuration: 0.25) {
    self.layoutIfNeeded()
}
  • todayButton이 레이아웃에서 제외되면 spacer가 여전히 중앙 공간을 담당하고, categoryButton은 왼쪽 정렬 유지
  • 가운데로 흔들리지 않음

체크리스트

  1. StackView의 alignment = .center, distribution = .fill인지 확인
  2. spacer는 낮은 Hugging/Compression으로 지정 (여유 공간 흡수 전담)
  3. 고정하고 싶은 버튼(여기서는 todayButton)은 required로 지정
  4. isHidden 토글 후 애니메이션이 필요하다면 layoutIfNeeded() 래핑
  5. 텍스트 길이가 긴 버튼은 줄임표최대 길이 제한으로 레이아웃 폭주 방지
    categoryButton.titleLabel?.lineBreakMode = .byTruncatingTail
  6. 이미지/타이틀 인셋이 과도하면 intrinsicContentSize가 커져 배분이 틀어질 수 있으니 점검

대안 패턴

  • 고정 폭 spacer: spacer.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)로 시작해 상황에 따라 multiplier를 조정
  • 두 개의 spacer: 좌·우에 spacer를 두고 가운데 버튼을 고정하는 패턴 (이 케이스에는 불필요)
  • Constraint 기반 정렬: StackView 대신 Auto Layout으로 leading/trailing 고정, 중앙 공간을 별도 제약으로 제어

정리

  • 문제의 본질은 여유 공간을 누가 가져가느냐입니다.
  • spacer가 전담하여 흡수, todayButton은 required로 고정하면 hidden 토글에도 가운데로 흔들리지 않습니다.
  • 위 우선순위 조합은 UILabel 길이 변화, 이미지 유무 등 변동 상황에서도 예측 가능한 레이아웃을 보장합니다.
profile
iOS Dev

0개의 댓글