Mac 앱에서 레이블을 늘리지 않고 측정하려면 살펴볼 뷰에 onGeometryChange를 연결하세요. 이전 배포 대상에서는 GeometryReader를 배경에 넣으세요. 수정자의 위치에 따라 읽게 되는 너비가 달라집니다.
이 안내에서는 합성 레이블과 그 여백을 측정하면서, 주변 레이아웃이 결과에 영향을 받지 않게 합니다. 또한 레이블의 현재 너비와 이상적인 너비, 컨테이너의 너비를 구분하는 방법도 설명합니다.
GeometryReader를 사용하면 레이아웃이 확장되는 이유는 무엇인가요?
SwiftUI 레이아웃은 제안과 응답으로 이루어집니다. 부모 뷰가 공간을 제안하면 자식 뷰는 계산된 크기를 보고합니다. 프레임을 추가하면 계층 구조에 또 다른 뷰가 들어가므로 레이블과 프레임의 너비가 서로 다를 수 있습니다. Apple은 간단한 뷰 레이아웃하기에서 이 과정을 설명합니다.
GeometryReader는 선호 크기가 유연한 컨테이너입니다. 따라서 작은 레이블을 그 안에 감싸면 부모 뷰에 전달되는 레이아웃 참여자가 달라집니다. 측정하는 대상은 텍스트에 필요한 공간보다 훨씬 넓은 공간을 차지할 수 있는 리더의 컨테이너입니다. Apple의 GeometryReader 문서를 참조하세요.
관찰을 위해서는 기존 뷰에 측정을 연결하세요. 배경 리더가 필요한 경우에는 배치가 중요합니다. Apple 문서에 따르면 배경은 자신이 연결된 뷰의 크기를 받습니다. 따라서 레이블이 측정 영역을 결정할 수 있습니다. 뷰에 배경 추가하기를 참조하세요.
구현 방법을 선택하기 전에 인터페이스에서 어떤 질문에 답해야 하는지 정하세요.
| 질문 | 적절한 방법 |
|---|---|
| 현재 레이아웃에서 이 레이블의 너비는 얼마인가요? | 레이블의 기하 정보를 관찰합니다. |
| 레이블을 감싼 패딩 배지의 너비는 얼마인가요? | 패딩을 적용한 뒤 관찰합니다. |
| 레이블을 포함하는 행의 너비는 얼마인가요? | 행이나 행의 프레임을 관찰합니다. |
| 텍스트 전체가 원하는 너비는 얼마인가요? | 이상적인 크기를 별도로 살펴봅니다. |
| 여러 표시 방식 중 어떤 것이 들어맞나요? | ViewThatFits를 고려합니다. |
| 여러 자식 뷰를 함께 측정하고 배치하려면 어떻게 해야 하나요? | 사용자 지정 Layout을 고려합니다. |
이 구분은 흔한 실수를 막아 줍니다. 즉, 제한되거나 잘릴 수 있는 레이블의 너비를 문자열 전체를 표시하는 데 필요한 공간으로 착각하지 않게 합니다.
macOS 13 이상에서 현재 너비 측정하기
Apple의 최신 문서에 따르면 단일 값 동작 클로저를 받는 onGeometryChange(for:of:action:) 오버로드는 macOS 13 이상에서 사용할 수 있습니다. 이 오버로드는 기하 프록시를 동등성 비교가 가능한 값으로 변환하고, 그 값이 바뀌면 동작을 호출합니다. 이 API가 포함된 SDK를 사용하고 여기 나온 단일 매개변수 동작을 유지하세요. onGeometryChange(for:of:action:)를 참조하세요.
Xcode에서 SwiftUI를 사용하고 배포 대상이 macOS 13 이상인 macOS 앱 프로젝트를 만드세요. 생성된 앱 및 콘텐츠 뷰 선언을 다음 전체 프로그램으로 바꾸고, 대상에는 @main 앱 선언을 하나만 남기세요. 이 예제는 로컬에서 만든 합성 레이블만 사용합니다.
import SwiftUI
import Foundation
@main
struct WidthDemoApp: App {
var body: some Scene {
WindowGroup {
WidthDemoView()
}
}
}
struct WidthDemoView: View {
@State private var measuredWidth: CGFloat = 0
@State private var offeredWidth: Double = 260
@State private var showLongLabel = false
private var label: String {
showLongLabel
? "Sample collection of blue circles"
: "Blue circles"
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(label)
.font(.system(size: 16))
.lineLimit(1)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.12))
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newWidth in
measuredWidth = newWidth
}
.frame(
width: CGFloat(offeredWidth),
alignment: .leading
)
Text(String(
format: "Measured badge: %.1f pt",
Double(measuredWidth)
))
.monospacedDigit()
.frame(width: 340, alignment: .leading)
Slider(value: $offeredWidth, in: 120...340)
Button("Change sample label") {
showLongLabel.toggle()
}
}
.frame(width: 360, alignment: .leading)
.padding(20)
}
}
관찰자는 proxy.size.width을 읽습니다. Apple은 GeometryProxy.size를 컨테이너 뷰의 크기로 정의합니다. 여기서는 관찰 대상 뷰에 텍스트와 패딩이 포함되고, 뒤에 오는 프레임은 너비 제안을 독립적으로 제어합니다. GeometryProxy.size를 참조하세요.
측정값은 별도의 읽기 영역에 표시됩니다. 측정 대상 배지의 너비를 설정하지 않습니다. 고정된 데모 영역과 읽기 영역의 너비 덕분에 진단 텍스트가 주변 스택의 크기를 바꾸지 않아 결과를 더 쉽게 살펴볼 수 있습니다.
CGFloat만 반환하면 너비에만 초점을 맞춰 관찰할 수 있습니다. 높이만 바뀌어도 변환된 값은 바뀌지 않습니다. Apple은 기하 정보가 자주 바뀔 수 있으므로 필요한 특정 데이터로 변환할 것을 권장합니다. onGeometryChange(for:of:action:)를 참조하세요.
이 예제들은 가져온 API 선언을 기준으로 확인했습니다. 사용 가능한 환경이 Swift 툴체인이 없는 Linux라 여기서 컴파일하거나 실행하지는 않았습니다. Mac의 Xcode에서 빌드하고 인터페이스를 살펴보세요.
측정 수정자는 어디에 배치해야 하나요?
수정자 체인은 감싸인 뷰가 이어지는 과정으로 읽으세요. 포함하려는 크기 변경 수정자 중 마지막 수정자 바로 뒤에 측정을 둡니다. Apple의 레이아웃 설명은 프레임이 뷰를 감싸고 패딩이 콘텐츠에 사용할 수 있는 공간을 어떻게 바꾸는지 설명합니다. 간단한 뷰 레이아웃하기를 참조하세요.
| 예제의 측정 위치 | 관찰 대상 |
|---|---|
| 두 패딩 수정자 앞 | 텍스트 뷰의 현재 레이아웃 너비 |
| 두 패딩 수정자 뒤 | 패딩이 적용된 배지의 현재 너비 |
| 명시적 너비 프레임 뒤 | 바깥쪽 프레임의 너비 |
주변 VStack에 적용 |
스택의 너비 |
예를 들어 텍스트 뷰가 현재 제안에서 96포인트라고 보고한다고 가정해 보세요. 양쪽 가로 가장자리에 12포인트씩 추가하면 패딩이 적용된 너비는 120포인트가 됩니다. 바깥쪽 프레임 너비 260포인트는 또 다른 별개의 측정값입니다. 이 숫자들은 수정자 경계를 설명하기 위한 것이며, 예제 문자열을 측정한 결과가 아닙니다.
예상과 다른 값이 나오면 관찰자와 같은 수정자 체인 위치에 테두리를 임시로 배치하세요. 그 경계를 의도한 대상과 비교합니다. 리더를 행 전체에 연결하면 보이는 레이블이 행의 일부만 차지해도 행의 너비를 올바르게 보고할 수 있습니다.
macOS 12 대상에서 배경 리더 사용하기
macOS 12 배포 대상을 사용하는 경우 배경 GeometryReader를 사용해 환경 설정 값으로 너비를 전달하세요. 아래에서 사용하는 후행 클로저 방식의 배경 API는 macOS 12부터 사용할 수 있습니다. 수정된 뷰가 레이아웃을 주도해야 할 때 배경을 사용하라고 Apple은 권장합니다. background(alignment:content:)를 참조하세요.
다음은 완전한 대체 프로그램입니다. 두 번째 앱 선언을 추가하지 말고 이전 예제를 이 코드로 바꾸세요.
import SwiftUI
import Foundation
private struct BadgeWidthKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(
value: inout CGFloat,
nextValue: () -> CGFloat
) {
value = nextValue()
}
}
@main
struct LegacyWidthDemoApp: App {
var body: some Scene {
WindowGroup {
LegacyWidthDemoView()
}
}
}
struct LegacyWidthDemoView: View {
@State private var measuredWidth: CGFloat = 0
@State private var showLongLabel = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(showLongLabel ? "Blue circles collection" : "Blue circles")
.font(.system(size: 16))
.lineLimit(1)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.12))
.background {
GeometryReader { proxy in
Color.clear.preference(
key: BadgeWidthKey.self,
value: proxy.size.width
)
}
}
.onPreferenceChange(BadgeWidthKey.self) { newWidth in
measuredWidth = newWidth
}
Text(String(
format: "Measured badge: %.1f pt",
Double(measuredWidth)
))
.frame(width: 340, alignment: .leading)
Button("Change sample label") {
showLongLabel.toggle()
}
}
.frame(width: 360, alignment: .leading)
.padding(20)
}
}
PreferenceKey는 값을 상위 뷰로 전달하고, onPreferenceChange은 해당 값의 변경을 받습니다. 리더는 콘텐츠 클로저 안에서 상태를 할당하는 대신 측정값을 내보냅니다. Apple의 PreferenceKey 및 onPreferenceChange(_:perform:) 문서를 참조하세요.
이 축약 함수는 값을 내보내는 배지가 하나라고 가정합니다. 같은 키를 여러 자식 뷰에 재사용하면 값이 합쳐지고, 이 구현에서는 축약 후 마지막 값이 유지됩니다. 레이블을 각각 추적하려면 하나의 스칼라 값으로 여러 뷰를 나타내지 말고 키가 있는 측정값 컬렉션을 설계하세요.
측정값이 레이아웃과 독립적인지 어떻게 확인할 수 있나요?
첫 번째 프로그램을 간단한 진단 순서로 사용하세요.
Blue circles와 넉넉한 제안 너비로 시작합니다. 색이 있는 배지와 읽기 영역을 비교하세요.- 슬라이더를 드래그합니다. 바깥쪽 프레임은 독립적으로 바뀌며, 공간이 충분한 동안 배지는 작은 크기를 유지할 수 있습니다.
Change sample label을 선택한 다음 제안 너비를 줄입니다. 문자열 전체의 이상적인 너비를 기대하지 말고, 현재 제한된 배지를 살펴보세요.- 관찰자를 명시적 프레임 아래로 옮깁니다. 이제 프레임 너비가 보고되어야 합니다.
- 배지 측정값을 다른 곳에 사용하기 전에 관찰자를 프레임 위로 되돌립니다.
관찰한 너비를 같은 뷰의 너비를 결정하는 공식에 넣지 마세요. 예를 들어 프레임 너비를 관찰한 값에 패딩을 더한 값으로 설정하면 더 큰 결과가 계속 레이아웃에 반영될 수 있습니다. 이는 직접 만드는 의존 관계에서 비롯됩니다. 관찰이 상태를 바꾸고, 상태가 크기를 바꾸며, 새 크기가 다시 관찰을 일으킵니다.
실제 작업이 전체 레이블과 간결한 대안 중 하나를 선택하는 것이라면 macOS 13 이상에서 ViewThatFits를 사용해 제한된 축에 이상적인 크기가 들어맞는 첫 번째 자식 뷰를 선택할 수 있습니다. 이렇게 하면 너비를 저장할 필요가 없을 수도 있습니다. ViewThatFits를 참조하세요.
여러 자식 뷰를 함께 측정하고 배치해야 한다면 macOS 13 이상에서 사용자 지정 Layout을 사용하세요. 측정 및 배치 메서드를 사용하면 컨테이너가 자식 뷰를 배치하기 전에 조회할 수 있으므로, 해당 결정이 레이아웃 과정 안에서 이뤄집니다. Layout을 참조하세요.
이상적인 크기나 AppKit 텍스트 지표로 같은 질문에 답할 수 있나요?
텍스트의 이상적인 너비를 유지하려는 경우에만 .fixedSize(horizontal: true, vertical: false)를 사용하세요. Apple 문서에 따르면 이 수정자를 사용하면 텍스트가 부모 뷰의 경계를 넘어설 수 있습니다. 측정하려고 이를 추가하면 표시된 뷰의 크기 동작이 바뀌므로 원래의 제한된 레이아웃이 유지되지 않습니다. fixedSize(horizontal:vertical:)를 참조하세요.
문자 수로 너비를 측정하는 것도 적절하지 않습니다. Swift의 String.count는 확장된 문자소 클러스터로 표현되는 Character 값을 셉니다. 타이포그래피상의 크기를 반환하지는 않습니다. 문자 수에 추정 문자 너비를 곱해도 SwiftUI 뷰의 측정값은 얻을 수 없습니다. 문자열과 문자를 참조하세요.
AppKit 그리기 작업에서는 NSAttributedString.size()가 해당 속성 문자열을 그리는 데 필요한 크기를 반환하며, 소수점 이하 크기가 포함될 수 있습니다. Apple은 이 결과로 뷰 크기를 정할 때 올림하라고 안내합니다. 이는 텍스트 그리기에 관한 질문에 답하지만 SwiftUI 뷰의 패딩이나 바깥쪽 프레임은 포함하지 않습니다. size()를 참조하세요.
기존 TextKit 1 레이아웃에서 NSLayoutManager.usedRect(for:)은 텍스트 컨테이너에 현재 배치된 글리프가 차지하는 영역을 보고합니다. 이 메서드 자체가 글리프를 생성하거나 레이아웃을 수행하지는 않습니다. 레이아웃을 먼저 완료해야 한다면 ensureLayout(for:)를 호출하세요. 이 API들은 구성된 텍스트 컨테이너를 측정하므로 결과를 SwiftUI 기하 정보와 분리해 두세요. usedRect(for:) 및 ensureLayout(for:)를 참조하세요.
SwiftUI Mac 인터페이스에 이미 표시된 레이블은 필요한 경계에서 기하 정보를 관찰하고, 읽기 영역을 독립적으로 유지하며, 콘텐츠와 사용 가능한 공간을 모두 바꿔 가며 테스트하세요. 그러면 살펴보려는 레이아웃을 유지하면서 유용한 너비를 얻을 수 있습니다.
출처
onGeometryChange(for:of:action:) — Apple
간단한 뷰 레이아웃하기 — Apple
GeometryReader — Apple
뷰에 배경 추가하기 — Apple
size — Apple
background(alignment:content:) — Apple
PreferenceKey — Apple
onPreferenceChange(_:perform:) — Apple
ViewThatFits — Apple
Layout — Apple
fixedSize(horizontal:vertical:) — Apple
문자열과 문자 — Swift.org
size() — Apple
usedRect(for:) — Apple
ensureLayout(for:) — Apple