SwiftUI 스크롤 뷰가 짧은 콘텐츠에 맞도록 하려면 내부 콘텐츠를 측정하고, 뷰포트 높이를 그 측정값과 지정한 한도 중 더 작은 값으로 설정하세요. 콘텐츠가 더 길면 한도 내에서 스크롤됩니다. 이 안내에서는 Mac 앱에 사용할 작은 레이블 패널에 이 동작을 구현합니다.
짧은 스크롤 뷰에 빈 공간이 남는 이유는 무엇인가요?
먼저 두 가지 크기를 구분하세요. 콘텐츠의 높이와 콘텐츠를 표시하는 뷰포트의 높이입니다. Apple은 ScrollView를 기본 콘텐츠의 일부를 표시하고 스크롤에 따라 보이는 부분을 조정하는 뷰로 설명합니다. 레이블이 몇 개뿐이어도 원하는 뷰포트 높이가 저절로 정해지지는 않습니다. Apple의 ScrollView 문서에서 이 차이를 설명합니다.
작은 데스크톱 패널에는 명시적인 크기 규칙을 정하세요. viewportHeight = min(contentHeight, maximumHeight). 패딩을 스크롤 콘텐츠에 포함해야 한다면, 실제로 적용되는 너비에서 패딩까지 포함해 콘텐츠를 측정합니다. 측정 결과를 바깥쪽 스크롤 뷰에 적용하세요. Swift의 min(_:_:)는 더 작은 값을 반환하며, 높이 인수를 지정하면 SwiftUI의 frame(width:height:alignment:)가 고정 높이를 설정합니다. Swift min(_:_:), SwiftUI 고정 프레임.
예를 들어 최대 높이가 240포인트라고 가정해 보겠습니다. 높이가 86포인트인 콘텐츠에는 86포인트 뷰포트가 만들어지고, 높이가 410포인트인 콘텐츠에는 240포인트 뷰포트가 만들어져 나머지 콘텐츠를 스크롤로 볼 수 있습니다. 이는 이해를 돕기 위한 입력값이며, 아래 예제에서 측정한 값은 아닙니다.
| 변경 항목 | 제어하는 항목 | 원하는 컴팩트 높이를 설정하나요? |
|---|---|---|
| 측정한 콘텐츠 높이를 제한한 뒤 스크롤 뷰에 적용 | 뷰포트 높이 | 예, 아래의 제약된 레이아웃에서 설정합니다 |
.scrollBounceBehavior(.basedOnSize) |
콘텐츠 크기에 따른 바운스 동작 | 아니요 |
레이블의 .fixedSize(horizontal: false, vertical: true) |
콘텐츠의 이상적인 세로 크기 | 측정에 도움이 되지만, 바깥쪽 뷰포트에는 여전히 높이 규칙이 필요합니다 |
Apple은 .basedOnSize를 바운스 구성으로, fixedSize(horizontal:vertical:)를 선택한 축에서 뷰를 이상적인 크기로 고정하는 기능으로 설명합니다. 어느 쪽도 콘텐츠에서 계산한 뷰포트 높이를 설정하지 않습니다. 스크롤 바운스 동작, 축별 고정 크기.
최대 스크롤 높이가 있는 컴팩트 패널 만들기
단일 값 형식의 onGeometryChange(for:of:action:) 오버로드를 제공하는 macOS SDK가 포함된 최신 Xcode를 사용하세요. Apple 문서에 따르면 이 오버로드는 macOS 13.0부터 사용할 수 있습니다. 아래의 완전한 예제는 macOS 13.3 이상을 대상으로 합니다. macOS 13.3부터 사용할 수 있는 scrollBounceBehavior(_:axes:)도 사용하기 때문입니다. Geometry 변경 콜백, 바운스 수정자 사용 가능 버전.
SwiftUI를 사용해 macOS 앱 프로젝트를 만드세요. 생성된 앱 선언과 콘텐츠 뷰를 아래 단일 파일 프로그램으로 바꾸고, 대상에 @main 선언은 하나만 남겨 두세요. 모든 레이블은 예시용 로컬 문자열입니다. Show more labels 토글을 켜고 끄면 콘텐츠가 짧은 상태와 긴 상태 사이에서 전환됩니다.
import SwiftUI
@main
struct CompactLabelsApp: App {
var body: some Scene {
WindowGroup {
CompactLabelsView()
}
}
}
struct CompactLabelsView: View {
@State private var showMore = false
@State private var viewportHeight: CGFloat = 240
private var labels: [String] {
if showMore {
return (1...18).map { number in
"Sample label \(number): a longer description that can wrap when the panel becomes narrower."
}
}
return ["Sample label A", "Sample label B"]
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Show more labels", isOn: $showMore)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 8) {
if labels.isEmpty {
Text("No labels to display")
} else {
ForEach(labels.indices, id: \.self) { index in
Text(labels[index])
}
}
}
.font(.body)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.onGeometryChange(for: CGFloat.self) { proxy in
min(proxy.size.height.rounded(.up), 240)
} action: { newHeight in
viewportHeight = newHeight
}
}
.frame(height: viewportHeight)
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
.border(Color.secondary.opacity(0.4))
Text("The label panel grows up to 240 points.")
.font(.caption)
}
.padding(20)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.frame(minWidth: 300, minHeight: 360)
}
}
프로그램은 검토했지만, 이 환경에는 Swift 컴파일러나 macOS 런타임이 없어 컴파일하거나 실행하지 않았습니다. 기존 인터페이스에 적용하기 전에 Xcode에서 빌드하고 아래 항목을 확인하세요.
측정이 스크롤 뷰 안에서 이뤄져야 하는 이유
geometry 콜백은 패딩이 적용된 내부 스택을 관찰합니다. 바깥쪽 스크롤 뷰를 측정하면 이미 지정한 뷰포트 크기가 보고됩니다. 이는 콘텐츠에 필요한 높이를 알아내는 대신 크기 규칙이 순환하는 결과를 만듭니다.
변환은 반올림한 뒤 제한한 높이만 반환합니다. Apple은 geometry를 필요한 값으로 변환하고, 자주 바뀌는 geometry에 광범위한 업데이트를 피할 것을 권장합니다. 여기서는 스크롤 위치가 계산에 영향을 주지 않으며, 제한보다 높은 콘텐츠도 계속 240포인트라는 같은 결과를 냅니다. Apple의 geometry 변경 안내.
올림하면 측정된 콘텐츠보다 아주 조금 낮은 뷰포트를 의도적으로 지정하는 일을 피할 수 있습니다. 초기 240포인트 값은 측정 결과가 도착하기 전에도 사용할 수 있는 뷰포트를 제공합니다. 이는 측정된 콘텐츠 높이가 아니라 초기값입니다. 콜백이 이후 이 값을 대체합니다.
너비와 수정자 순서가 중요한 이유
가로 크기는 유연하게 유지하세요. 예제에서는 .fixedSize(horizontal: false, vertical: true)를 사용해 레이블이 이상적인 세로 크기를 유지하면서 사용 가능한 너비를 활용하도록 합니다. Apple은 이상적인 크기를 고정하면 뷰가 부모의 경계를 넘어설 수 있다고 경고합니다. 따라서 너비가 좁은 패널에서 줄바꿈되는 레이블에는 두 축을 모두 고정하는 방법이 적절한 기본값이 아닙니다. Apple의 fixedSize(horizontal:vertical:) 문서.
측정 수정자보다 앞에 12포인트 패딩을 두어 측정되는 콘텐츠 높이에 패딩이 포함되도록 합니다. 바깥쪽 높이를 지정한 다음에 테두리를 적용해 뷰포트 경계를 표시합니다. 이 구성은 사용하지 않는 공간이 스크롤 패널에 속하는지, 주변 창에 속하는지 더 쉽게 확인할 수 있게 합니다.
짧은 콘텐츠, 줄바꿈, 축소 확인하기
예제에서 동작을 확인한 뒤, 앱의 실제 컨테이너에 패널을 배치하고 다음 순서로 다시 확인하세요.
Show more labels를 끈 상태로 실행합니다. 테두리가 두 레이블과 패딩을 감싸고, 캡션이 바로 아래에 표시되어야 합니다.Show more labels를 켭니다. 패널이 240포인트 제한까지 커져야 합니다. 스크롤해 마지막 레이블까지 볼 수 있는지 확인합니다.- 창의 너비를 줄였다가 늘립니다. 줄바꿈된 레이블의 높이가 바뀌어도 마지막 줄이 잘리지 않아야 합니다.
- 토글을 다시 끕니다. 패널이 짧은 콘텐츠 높이로 줄어들어야 합니다.
- 짧은 배열을
[]로 잠시 바꿉니다. 명시적인표시할 레이블이 없습니다메시지에 따라 콘텐츠 높이가 결정되어야 합니다. - 실제 패널에서 사용하는 텍스트 스타일과 주변 컨트롤로 다시 확인합니다.
빈 상태의 표현은 의도적인 인터페이스 선택입니다. 이 예제는 배열이 비어 있어도 메시지와 패딩을 유지합니다. 레이블이 없을 때 패널 전체를 없애고 싶다면 레이블이 없는 경우 스크롤 뷰 전체를 조건부로 제외하세요. 이는 비어 있지 않은 패널의 높이 측정과는 별개의 제품 결정입니다.
주변 예제 창은 최소 높이를 유지합니다. 이 코드는 레이블 뷰포트의 크기를 조정하지만, 레이블이 없어질 때마다 Mac 창을 줄이도록 요청하지는 않습니다. 캡션 아래의 공간은 테두리가 있는 스크롤 뷰 바깥의 상위 레이아웃에 속합니다.
남는 여백이나 잘린 레이블 진단하기
패널 높이가 그대로라면 먼저 측정 수정자가 어디에 붙어 있는지 확인하세요. 내부 패딩을 적용한 뒤 콘텐츠 스택을 관찰해야 합니다. 그런 다음 패널 주변에 공간을 확보하는 명시적 높이 또는 최소 높이가 조상 뷰에 있는지 살펴보세요. Apple 문서에 따르면 지정된 프레임 높이는 고정 크기이므로, 별도의 바깥 프레임이 내부 뷰포트가 줄어들어도 공간을 유지할 수 있습니다. SwiftUI 프레임 크기 지정.
콘텐츠 자체가 예상보다 계속 높다면 측정 대상 하위 뷰에서 세로 확장을 제거하세요. 이처럼 작은 레이블을 다루는 패턴에서는 세로 스페이서나 최소 높이가 뷰포트에서 계산되는 콘텐츠 프레임을 피하세요. 이런 선택은 콘텐츠 높이가 뷰포트에 따라 바뀌게 만들어, 크기 규칙에 필요한 독립적인 측정을 어렵게 합니다.
아래쪽 패딩이 잘린다면 측정 전에 패딩이 포함되었는지 확인하세요. 통합 후 텍스트 줄바꿈이 달라졌다면 새 부모 뷰가 실제로 제공하는 너비를 살펴보세요. 한 너비에서 측정한 높이를 다른 너비에서도 변하지 않는 높이로 취급해서는 안 됩니다.
주변 인터페이스에 맞게 240포인트 제한을 설정하세요. 예제에서는 컨트롤, 패딩, 캡션, 제한된 패널을 모두 담을 수 있도록 창의 최소 크기를 정합니다. 컨테이너가 더 짧다면 높이 예산을 줄이거나 주변 레이아웃을 바꿔야 합니다. 이 고정 제한은 다른 컨트롤 아래에 남은 공간을 자동으로 계산하지 않습니다.
즉시 레이아웃되는 레이블이 적은 경우에 이 방법을 사용하세요. Apple은 LazyVStack을 화면에 표시할 항목만 필요할 때 생성하는 스택으로 설명합니다. 따라서 내부 스택을 지연 스택으로 바꿔도 모든 콘텐츠를 완전히 측정하는 전략이 그대로 유지된다고 가정하지 마세요. 항목이 많은 컬렉션에는 안정적인 뷰포트와 지연 콘텐츠를 사용하는 설계가 대개 더 적합합니다. Apple의 LazyVStack 문서.
AppKit과 TextKit 측정은 언제 적용하나요?
이 SwiftUI 레이블 패널에서는 렌더링된 SwiftUI 하위 뷰를 측정하세요. AppKit 텍스트 측정을 사용하면 표시된 레이블과 구성이 일치해야 하는 별도의 레이아웃이 추가됩니다.
앱이 이미 NSScrollView를 소유하고 있다면 문서와 뷰포트의 구분을 유지하세요. Apple은 documentView을 스크롤되는 뷰로, contentSize을 스크롤 뷰의 콘텐츠 뷰 크기로 정의합니다. 이름과 달리 contentSize는 전체 문서의 크기를 측정하지 않습니다. AppKit documentView, AppKit contentSize.
NSLayoutManager를 중심으로 만든 기존 TextKit 레이아웃에서는 usedRect(for:)이 텍스트 컨테이너에 이미 배치된 글리프의 경계 사각형을 반환합니다. Apple은 이 값을 조회해도 글리프를 생성하거나 레이아웃을 수행하지 않는다고 명시합니다. 컨테이너 전체를 배치해야 할 때는 ensureLayout(for:)가 먼저 레이아웃을 강제하는 메서드로 문서화되어 있습니다. TextKit usedRect(for:), TextKit ensureLayout(for:).
그렇더라도 글리프 사각형은 전체 텍스트 뷰 프레임과 다릅니다. Apple은 textContainerInset를 통해 텍스트 컨테이너 주변에 추가 공간이 생기며, 컨테이너 내부에도 줄 조각 패딩이 있다고 설명합니다. 글리프 높이를 그대로 스크롤 뷰포트에 적용하지 말고 실제 텍스트 뷰 구성을 반영하세요. 이 메서드들은 NSLayoutManager를 사용하는 방식이며, 모든 텍스트 구성 요소에 적용할 수 있는 범용 크기 지정 방법은 아닙니다. AppKit textContainerInset.
작은 SwiftUI 레이블 패널에는 너비를 유연하게 유지하고, 패딩을 포함한 내부 콘텐츠를 측정한 뒤, 바깥쪽 뷰포트 높이에 상한을 두는 것이 실용적인 해결책입니다. 짧은 콘텐츠와 긴 콘텐츠에서 결과를 확인한 다음, 실제 부모 레이아웃 안에서도 주변 제약 조건이 같은 컴팩트 동작을 뒷받침하는지 시험하세요.
출처
- ScrollView — Apple
- min(::) — Apple
- frame(width:height:alignment:) — Apple
- scrollBounceBehavior(_:axes:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- onGeometryChange(for:of:action:) — Apple
- LazyVStack — Apple
- documentView — Apple
- contentSize — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- textContainerInset — Apple