Mac 창 크기 조절 중 SwiftUI 콘텐츠가 화면 밖으로 벗어나면, 먼저 그리기가 레이아웃 범위를 벗어나는지, 콘텐츠가 축소를 거부하는지, 또는 내장된 AppKit 뷰가 컨테이너 추적을 중단하는지 확인하세요. 그런 다음 해당 경계를 수정하세요.
이 가이드는 자체 Mac 앱의 크기 조절 가능한 인터페이스를 수정하는 개발자를 위한 것입니다: 레이블을 읽기 쉽게 유지하고, 컨트롤에 접근 가능하게 하며, 더 긴 콘텐츠를 스크롤 가능하게 만듭니다. 아래 진단 시퀀스는 합성 레이블을 사용하며 SwiftUI 레이아웃, 윈도우 크기 조절, AppKit 텍스트 래핑을 구별합니다.
크기 조절 중 어떤 경계가 실패하는지 식별
SwiftUI 레이아웃은 부모로부터의 크기 제안으로 시작합니다. 자녀는 이에 응답하여 자신의 크기를 선택합니다; 제안은 모든 것을 해당 사각형으로 클리핑하라는 지시가 아닙니다. Apple은 ProposedViewSize에서 최소, 최대, 이상적 크기 제안을 문서화합니다. 이것이 창을 더 좁게 만든다고 해서 모든 자손이 반드시 더 좁아지는 것은 아닌 이유를 설명합니다.
반복 가능한 테스트로 시작하세요: 앱의 창을 열고, 첫 번째 실패가 나타날 때까지 좁혔다가, 다시 넓히세요. 최종 배치가 아닌 첫 번째 영향을 받은 요소를 관찰하세요. 레이블이 컨테이너를 넘어 확장되나요? 전체 패널이 이동하나요? 텍스트가 이전 래핑 너비를 유지하나요?
의심되는 자녀에 .border(Color.orange)을, 해당 프레임이나 컨테이너에 .border(Color.blue)을 임시로 추가하세요. 서로 다른 레벨에 모디파이어를 배치하여 사각형을 비교할 수 있게 하세요. Apple의 offset 문서는 이 테두리 기법을 보여주며 원래 레이아웃 위치와 이동된 그리기를 구별합니다.
| 관찰 사항 | 먼저 검사할 것 | 유용한 다음 조치 |
|---|---|---|
| 안정적인 사각형 밖으로 그리기가 이동 | offset, position, 좌표 계산 |
배치 모디파이어를 개별적으로 제거 |
| 컨테이너가 좁아져도 레이블이 넓은 상태 유지 | 수평 fixedSize, 명시적 너비 |
레이블이 제약된 너비를 받아들이도록 허용 |
| 컨트롤이 가로 행에 맞지 않음 | 결합된 컨트롤 너비와 간격 | 세로 대안 제공 |
| 높이가 감소하면 하단 콘텐츠 사라짐 | 오버플로 동작 없음 | 콘텐츠 영역을 스크롤 가능하게 만듦 |
| 호스팅된 SwiftUI 패널이 AppKit 내에서 이동 | 호스팅 뷰 경계와 크기 제약 | 호스트 크기와 콘텐츠 요구 사항 비교 |
| AppKit 텍스트 뷰가 좁아지지만 줄은 넓은 상태 유지 | 텍스트 컨테이너 너비 추적 | 텍스트 뷰와 텍스트 컨테이너 구성 확인 |
이는 문서화된 동작에서 파생된 진단 가설이며, 특정 증상이 단일 원인을 가진다고 보장하지 않습니다. 한 번에 하나의 의심 사항을 변경하고 동일한 크기 조절 제스처를 반복하세요. 복잡한 자녀를 Text("Sample panel")로 교체하여 실패가 제거되면, 해당 자녀의 레이어를 점진적으로 복원하세요.
창 크기 조절에 저항하는 배치 및 크기 규칙 제거
오프셋과 중심 기반 위치 지정 확인
offset는 뷰의 원래 크기를 변경하지 않고 표시된 콘텐츠를 이동합니다. 따라서 양의 수평 오프셋은 부모가 여전히 원래 공간을 예약하고 있어도 그리기를 오른쪽 가장자리 너머로 밀 수 있습니다. 오프셋을 임시로 제거하세요; 그리기가 컨테이너 안으로 돌아오면, 스택, 패딩, 프레임 정렬을 통해 의도된 관계를 표현하는 배치로 교체하세요. Apple의 offset 동작을 참조하세요.
position(x:y:)는 뷰의 중심을 부모의 좌표 공간에서 좌표에 배치합니다. 해당 좌표를 좌상단 모서리로 처리하면 자녀의 크기와 관련된 오류가 발생합니다. 한 번 계산된 위치는 부모가 크기를 변경하면 부적합해질 수도 있습니다. 일반적인 레이블-컨트롤 패널의 경우, 컨테이너와의 의도된 관계를 표현하는 정렬을 선호하세요. Apple은 position(x:y:)에서 중심 기반 동작을 정의합니다.
예를 들어, .frame(maxWidth: .infinity, alignment: .leading)은 유연한 프레임 내에서 선두 정렬을 표현합니다. 이는 과대 크기 자손의 자체 크기 요구 사항을 제거하지 않습니다. Apple의 유연한 프레임 문서는 프레임이 제안을 제약하고 자녀를 배치하는 방식을 설명합니다.
고정 너비와 이상적 크기 텍스트 확인
실패하는 분기의 명시적 너비 설정과 fixedSize의 수평 사용을 검사하세요. Apple은 특별히 .fixedSize(horizontal: true, vertical: false)이 텍스트가 전체 문자열에 필요한 이상적 너비를 보존하도록 하여 잠재적으로 부모의 경계를 초과할 수 있음을 문서화합니다. 래핑되는 본문 텍스트의 경우, 해당 수평 요구 사항을 제거하세요. 래핑된 텍스트의 전체 높이를 보존하는 것이 적절한 경우, .fixedSize(horizontal: false, vertical: true)을 고려하세요. fixedSize(horizontal:vertical:)를 참조하세요.
수직 크기 보존에는 해당 높이가 갈 곳이 필요합니다. 짧고 스크롤되지 않는 컨테이너 안의 수직으로 늘어나는 문단은 여전히 접근 불가능해질 수 있습니다. 모든 텍스트에 대한 접근이 필요한 작업에는 수직 스크롤 영역을 사용하세요; Apple은 ScrollView에서 지원되는 스크롤 축을 문서화합니다.
기하학 기반 계산 재확인
GeometryReader는 자신의 크기와 좌표 공간을 사용하여 콘텐츠를 정의하고, 부모에게 유연한 선호 크기를 반환합니다. 뷰 주위에 하나를 추가하면 레이아웃 구조가 변경되므로 중립적인 측정 래퍼가 아닙니다. 기하학 계산 도입 후 드리프트가 시작되었다면, 해당 분기를 일반 스택과 프레임으로 임시 대체하세요. Apple은 GeometryReader에서 이러한 속성을 설명합니다.
진정으로 기하학이 필요한 비례 레이아웃의 경우, 관련 현재 컨테이너에서 치수를 도출하세요. 초기 등장 시 캡처된 너비를 저장하는 상태가 있는지 검사하세요. 또한 측정된 치수가 측정 중인 동일한 영역의 크기를 조정하는 데 사용되는지 확인하세요; 해당 의존성을 제거하는 것이 유용한 격리 테스트입니다.
명시적 오버플로 동작으로 콤팩트 레이아웃 구축
다음 완전한 샘플은 세 가지 독립적인 선택을 보여줍니다: 텍스트 래핑, 필요 시 컨트롤 배치 전환, 수직 오버플로 접근 가능 유지. 그 치수는 예시적 설계 입력이며, Apple이 규정한 값이 아닙니다.
Xcode에서 macOS SwiftUI 앱을 생성하고, 배포 대상을 macOS 13 이상으로 설정한 후, 생성된 앱과 콘텐츠 뷰 선언을 이 단일 파일로 교체하세요. @main 선언은 하나만 유지하세요. 이 샘플은 macOS 13에서 사용 가능한 ViewThatFits와 윈도우 크기 조절 가능 API를 사용합니다. Apple은 ViewThatFits와 windowResizability(_:)에서 이들의 동작을 문서화합니다.
import SwiftUI
@main
struct ResizeExampleApp: App {
var body: some Scene {
WindowGroup {
ResizeExampleView()
.frame(minWidth: 280, minHeight: 220)
}
.windowResizability(.contentMinSize)
}
}
struct ResizeExampleView: View {
@State private var selection = "Sample A"
private let details = Array(
repeating: "Sample notes wrap as the panel becomes narrower.",
count: 12
).joined(separator: " ")
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Sample panel")
.font(.title2)
Text(details)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
.fixedSize(horizontal: true, vertical: false)
VStack(alignment: .leading, spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
}
Text("Selected: \(selection)")
}
.padding(20)
.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .leading
)
}
}
}
ViewThatFits는 대안을 선언된 순서대로 평가하고 제약된 축을 따라 이상적 크기가 맞는 첫 번째 것을 선택합니다. 여기서 수평 맞추기는 버튼 행과 수직 스택 사이에서 선택합니다. 행은 의도적으로 수평 이상적 크기를 보존하여 압축된 행이 선호 결과가 되지 않게 합니다. 두 대안을 사용 가능한 작업에서 동등하게 유지하세요. Apple의 선택 규칙을 참조하세요.
외부 프레임은 콘텐츠 열이 사용 가능한 너비를 따르도록 허용하며, 수직 스크롤 뷰는 래핑으로 높이가 증가할 때 접근을 제공합니다. 이 배치는 개별 컨트롤이 선택된 최소 너비에 맞는다고 가정합니다. 더 긴 레이블이나 더 큰 글꼴은 다른 최소값이나 다른 콤팩트 배치를 요구할 수 있습니다.
이 샘플은 일관된 선언과 API 가용성 측면에서 검토되었으나, Swift 컴파일러를 사용할 수 없어 여기서는 컴파일되지 않았습니다. macOS에서 실행하여 실제 창과 텍스트 동작을 검증하세요.
콘텐츠 레이아웃 수정 후 윈도우 한계 설정
유용한 최소 윈도우 크기는 인터페이스가 지원되는 배치보다 작아지는 것을 방지합니다. .contentMinSize를 사용하면 SwiftUI가 윈도우의 최소 크기를 콘텐츠에서 도출하고 최대 크기는 부과하지 않습니다. 샘플에서 루트 프레임이 최소 너비와 높이 입력을 제공합니다. contentMinSize를 참조하세요.
가장 긴 지원 레이블로 콤팩트 배치를 테스트하여 최소값을 선택하세요. 잘못 배치된 뷰가 보일 때까지 최소값을 늘리면 원래 레이아웃 오류를 숨길 수 있습니다. 먼저 의도치 않은 변위와 과대 크기 자녀를 제거한 다음, 의도된 작업이 여전히 사용 가능한 가장 작은 치수로 설정하세요.
AppKit이 윈도우를 소유하고 SwiftUI를 호스팅하는 경우, 호스팅 경계를 별도로 검사하세요. macOS 13 이상에서 NSHostingView.sizingOptions는 SwiftUI 콘텐츠에서 파생된 제약을 제어합니다. 호스트가 윈도우의 contentView인 경우, 윈도우의 콘텐츠 최소 및 최대 크기도 업데이트합니다. Apple은 또한 호스트의 프레임이 SwiftUI 콘텐츠에서 요구하는 크기와 다를 때 콘텐츠가 중앙에 정렬됨을 문서화합니다. 보상 오프셋을 추가하기 전에 이러한 관계를 확인하세요. NSHostingView sizingOptions를 참조하세요.
실패하는 뷰가 이를 사용할 때만 AppKit과 TextKit 확인
SwiftUI가 표현된 루트 뷰의 크기를 정하도록 허용
NSViewRepresentable의 경우, SwiftUI는 표현된 AppKit 뷰의 frame과 bounds을 제어합니다. Apple은 이러한 속성을 직접 설정하면 SwiftUI와 충돌하여 미정의 동작이 발생한다고 명시적으로 경고합니다. 크기 조절 처리기가 표현된 루트의 프레임을 반복적으로 할당하는 경우, 해당 경쟁 소유권을 제거하고 SwiftUI 경계를 통해 크기 조절을 표현하세요. NSViewRepresentable을 참조하세요.
사용자 정의 측정이 필요한 경우, macOS 13 이상은 sizeThatFits(_:nsView:context:)을 제공합니다. 한 레이아웃 패스에서 여러 제안을 받을 수 있으며, nil를 반환하면 기본 크기 조절을 요청합니다. 결과를 이전 윈도우 너비가 아닌 현재 제안과 콘텐츠에 의존하게 만드세요. 지정되지 않은 제안 차원은 자동으로 0으로 처리하는 대신 의도적인 측정 정책이 필요합니다. 표현 가능 크기 조절을 참조하세요.
Swift의 옵셔널 바인딩은 제안된 차원을 사용하기 전에 확인하고 추출하는 방법을 제공합니다. 이는 옵셔널 너비나 높이를 강제 언래핑하지 않고 크기 조절 정책을 구현할 때 유용합니다. 언어 동작은 The Basics: Optional Binding에서 문서화됩니다.
텍스트 래핑 너비와 텍스트 뷰 너비 분리
실패하는 자녀가 NSTextView인 경우, 현재 너비를 텍스트 컨테이너의 너비와 비교하세요. Apple은 NSTextContainer.widthTracksTextView가 기본적으로 false로 설정됨을 문서화합니다; 이를 true으로 설정하면 텍스트 뷰의 너비가 변경될 때 컨테이너가 너비를 조정합니다. 줄이 텍스트 뷰와 함께 리플로우되어야 할 때 이것이 관련 제어입니다. widthTracksTextView를 참조하세요.
또한 isHorizontallyResizable를 검사하세요. 이는 텍스트 뷰가 텍스트에 맞게 너비를 변경하는지 제어하며, 이는 할당된 너비 내에서 텍스트를 래핑하는 것과는 다른 요구 사항입니다. 수직 스크롤, 래핑 텍스트 영역의 경우, 이 두 정책을 일관되게 구성하고 포함 레이아웃이 실제로 텍스트 뷰의 크기를 조정하는지 확인하세요. isHorizontallyResizable를 참조하세요.
구현에서 사용하는 텍스트 엔진을 보존하세요. Apple은 NSTextView나 그 텍스트 컨테이너에서 layoutManager에 접근하면 NSLayoutManager를 사용하는 호환 모드로 전환될 수 있음을 문서화합니다. TextKit 2 뷰를 진단하면서 해당 접근을 가볍게 추가하지 마세요. 기존 엔진과 배포 대상에 적합한 API를 사용하세요. NSTextView를 참조하세요.
너비와 높이 변경 전반에서 수리 검증
자체 앱에서 이 최종 시퀀스를 사용하세요:
- 원래 실패 지점을 천천히 좁혔다가 다시 넓히세요.
- 높이를 독립적으로 줄이고 모든 필수 콘텐츠가 여전히 접근 가능한지 확인하세요.
- 긴 합성 레이블과 여러 줄 텍스트로 반복하세요.
- 넓고 콤팩트한 배치 모두에서 각 컨트롤을 활성화하세요.
- AppKit 텍스트 뷰의 경우, 래핑이 양쪽 크기 조절 방향에서 현재 너비를 따르는지 확인하세요.
성공적인 수리는 배치를 현재 컨테이너에 연결하고, 콘텐츠에 실행 가능한 콤팩트 배치를 제공하며, 오버플로에 대한 접근을 제공합니다. 이러한 동작이 반복된 크기 조절에서 유지되면, 임시 테두리를 제거하고 인터페이스로 정당화된 최소 윈도우 크기를 유지하세요.
출처
- ProposedViewSize — Apple
- offset(x:y:) — Apple
- position(x:y:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- ScrollView — Apple
- GeometryReader — Apple
- ViewThatFits — Apple
- windowResizability(_:) — Apple
- contentMinSize — Apple
- sizingOptions — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- The Basics — Swift.org
- widthTracksTextView — Apple
- isHorizontallyResizable — Apple
- NSTextView — Apple