가로세로 비율이 적용된 SwiftUI 미리보기에 측면 컨트롤을 맞추려면, 사용 가능한 창 공간에 맞게 조정된 미리보기 크기를 계산한 다음 컨트롤에도 같은 높이를 지정하세요. 이 macOS 예제는 합성 미리보기의 비율을 유지하고, 넘치는 컨트롤은 스크롤되도록 합니다.
작업 대상은 왼쪽에 미리보기, 오른쪽에 너비가 고정된 컨트롤 패널이 있는 크기 조정 가능한 데스크톱 편집기입니다. 여기서 “미리보기”는 실행 중인 앱 내부의 콘텐츠를 뜻합니다. 목표는 창 크기가 바뀌어도 두 패널의 위쪽과 아래쪽 레이아웃 가장자리를 정렬하는 것입니다.
컨트롤은 어느 높이에 맞춰야 할까요?
먼저 맞추려는 사각형을 정의하세요. 가로세로 비율에 맞게 조정된 미리보기는 주변 컨테이너보다 공간을 덜 차지할 수 있습니다. 컨트롤 높이를 컨테이너에 맞추면 두 뷰가 올바르게 배치되어 있어도 컨트롤이 보이는 미리보기 영역 아래로 내려갈 수 있습니다.
Apple은 가로세로 비율을 너비를 높이로 나눈 값으로 정의합니다. 크기를 조정할 수 있는 콘텐츠에서 .fit을 적용한 aspectRatio(_:contentMode:)은 제안된 공간 안에 콘텐츠를 맞추면서 해당 비율을 유지합니다. 문서의 타원 예제는 비율에 맞게 조정된 도형과 이를 둘러싼 정사각형 프레임의 크기가 왜 다를 수 있는지 보여 줍니다. Apple의 가로세로 비율 문서.
이 구현에서는 비율에 맞게 조정된 사각형을 명시적으로 계산합니다. 컨트롤 너비와 패널 사이 간격을 고정한 다음, 남은 너비와 사용 가능한 높이에 미리보기를 맞춥니다. 두 패널 모두 계산된 미리보기 높이를 사용합니다.
이 예제에서는 미리보기 높이에 따라 컨트롤 뷰포트 높이를 정하는 레이아웃 정책을 사용합니다. 컨트롤 목록이 길면 미리보기를 더 높이는 대신 해당 뷰포트 안에서 스크롤됩니다. Apple은 ScrollView를 스크롤 가능한 영역 안에서 기본 콘텐츠의 이동 가능한 일부를 표시하는 뷰로 설명합니다. Apple의 스크롤 뷰 문서.
맞춤 사각형을 한 번만 계산하기
W와 H은 바깥쪽 여백을 뺀 사용 가능한 콘텐츠 크기, C은 컨트롤 너비, G는 간격, R은 미리보기의 너비 대 높이 비율이라고 하겠습니다. 계산식은 다음과 같습니다.
remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight
첫 번째 제한은 미리보기가 컨트롤의 가로 공간까지 차지하지 않도록 합니다. 두 번째 제한은 창이 넓을 때 미리보기 높이가 사용 가능한 영역보다 커지지 않도록 합니다. 선택된 너비로 높이를 계산하면 두 경우 모두 비율이 유지됩니다.
예를 들어 여백을 적용하기 전 콘텐츠 영역이 960 × 600포인트라고 가정해 보겠습니다. 모든 가장자리에 16포인트 여백을 두고, 컨트롤 패널 너비를 220포인트, 간격을 16포인트, 미리보기 비율을 16:9로 설정하면 사용 가능한 영역은 928 × 568포인트입니다. 남는 미리보기 너비는 692포인트이므로 미리보기 높이는 389.25포인트입니다. 따라서 컨트롤 뷰포트 크기는 220 × 389.25포인트가 됩니다.
이 값은 설명을 위한 입력값이며, 실제 앱에서 측정한 결과가 아닙니다. 또한 컨트롤 높이를 사용 가능한 전체 높이인 568포인트에 맞추면 정렬이 어긋나는 이유도 보여 줍니다.
레이아웃 크기에는 CGFloat를 사용하고, 비율은 양수이면서 유한한 값이어야 합니다. Swift에서 let ratio: CGFloat = 16.0 / 9.0을 사용하면 부동소수점 계산 의도를 명확히 할 수 있습니다. 나중에 앱에서 비율을 설정할 수 있도록 한다면 나눗셈 전에 값을 검증하세요.
완전한 macOS 예제 만들기
Xcode에서 macOS SwiftUI 앱을 만들고 배포 대상을 macOS 13 이상으로 설정하세요. 생성된 앱과 콘텐츠 뷰 선언을 이 단일 파일로 바꾸고, 타깃에 @main 선언이 정확히 하나만 있도록 하세요. 이 예제에는 에셋이나 외부 패키지가 필요하지 않습니다.
이 배포 대상 요구사항은 여기서 사용하는 창 크기 조정 API 때문입니다. 이 씬은 .contentMinSize를 사용합니다. Apple 문서에 따르면 이 API는 콘텐츠를 기준으로 창의 최소 크기를 정하고 최대 크기는 제한하지 않습니다. Apple의 창 크기 조정 문서, Apple의 콘텐츠 최소 크기 문서.
import SwiftUI
@main
struct MatchedPreviewApp: App {
var body: some Scene {
WindowGroup {
PreviewEditor()
.frame(minWidth: 640, minHeight: 360)
}
.windowResizability(.contentMinSize)
}
}
struct PreviewEditor: View {
@State private var intensity: Double = 0.5
@State private var showsLabel = true
private let ratio: CGFloat = 16.0 / 9.0
private let controlsWidth: CGFloat = 220
private let gap: CGFloat = 16
var body: some View {
GeometryReader { geometry in
let availableHeight = max(0, geometry.size.height)
let remainingWidth = max(
0,
geometry.size.width - controlsWidth - gap
)
let previewWidth = min(
remainingWidth,
availableHeight * ratio
)
let previewHeight = previewWidth / ratio
HStack(alignment: .top, spacing: gap) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2 + intensity * 0.7))
.aspectRatio(ratio, contentMode: .fit)
.overlay {
Text("Sample composition")
.font(.headline)
.opacity(showsLabel ? 1 : 0)
}
.frame(
width: previewWidth,
height: previewHeight
)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Preview controls")
.font(.headline)
Toggle("Show label", isOn: $showsLabel)
VStack(alignment: .leading, spacing: 8) {
Text("Color intensity")
Slider(value: $intensity, in: 0...1)
}
ForEach(1...8, id: \.self) { index in
Text("Sample setting \(index)")
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(
width: controlsWidth,
height: previewHeight
)
.background(Color.gray.opacity(0.08))
}
.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: .center
)
}
.padding(16)
}
}
GeometryReader는 편집기에 할당된 영역의 크기를 제공합니다. Apple은 이를 자체 크기와 좌표 공간을 바탕으로 콘텐츠를 정의하며, 선호 크기가 유연한 컨테이너로 설명합니다. 여백을 리더 바깥에 두면 계산에 여백이 적용된 콘텐츠 영역의 크기가 사용됩니다. Apple의 GeometryReader 문서.
두 개의 고정 프레임으로 높이를 맞춥니다. Apple은 frame(width:height:alignment:)이 지정된 크기의 보이지 않는 프레임을 만든다고 설명합니다. 컨트롤 배경은 해당 프레임 뒤에 배치되므로, 목록이 짧을 때 남는 공간을 포함해 컨트롤 뷰포트 전체를 덮습니다. Apple의 고정 프레임 문서.
마지막 프레임은 리더 안에서 행 전체를 가운데 정렬합니다. 두 패널의 크기를 키우지는 않습니다. 따라서 창이 높으면 컨트롤 높이는 미리보기와 계속 일치하면서 행 위아래에 사용되지 않는 공간이 남을 수 있습니다.
크기 조정 중 상태와 생명 주기 동작 유지하기
사용자가 수정하는 값과 계산된 기하 정보를 분리하세요. 예제에서 intensity와 showsLabel는 상태이며, 미리보기 너비와 높이는 현재 사용 가능한 크기에서 도출되는 로컬 값입니다.
Apple은 상태를 변경 시 뷰 계층의 종속된 부분이 업데이트되는 SwiftUI 관리 저장소로 설명합니다. 따라서 슬라이더와 토글 값은 이를 소유하는 편집기에 두면 됩니다. Apple의 상태 문서.
레이아웃 계산에는 상태 할당, 표시 콜백, 비동기 작업 또는 측정 피드백이 없습니다. 크기 조정 시 동일한 뷰 구조에 적용되는 치수만 바뀝니다. 레이블은 계속 표시되며 토글을 바꾸면 투명도가 바뀝니다.
기존 편집기에 이 방식을 적용할 때는 모델 소유권과 바인딩 타입을 유지하세요. 미리보기와 컨트롤 주변에 크기 조정 정책을 적용하고, 기하 계산 안으로 모델 생성을 옮기지 마세요. 이 예제의 산술에는 사용 가능한 크기와 선택한 비율만 필요합니다.
크기 조정, 넘침, 수정자 순서 확인하기
아래 크기를 계산 확인에 사용하세요. 이 값은 창 외부 프레임이 아니라 예제의 바깥쪽 여백을 적용하기 전 콘텐츠 영역을 나타냅니다. 예상값은 소수점 둘째 자리까지 반올림했습니다.
| 콘텐츠 영역 | 미리보기 크기 | 컨트롤 뷰포트 |
|---|---|---|
| 960 × 600포인트 | 692 × 389.25 | 220 × 389.25 |
| 640 × 360포인트 | 372 × 209.25 | 220 × 209.25 |
| 1200 × 400포인트 | 654.22 × 368 | 220 × 368 |
| 700 × 900포인트 | 432 × 243 | 220 × 243 |
작은 Python 계산으로 산술을 확인하고, 비율에 맞게 조정된 미리보기가 사용 가능한 높이를 넘지 않으며 두 패널과 간격이 가로로 맞는지도 단언문으로 확인했습니다. 조사 환경에서는 Swift와 Xcode를 사용할 수 없어 전체 SwiftUI 프로그램을 검토했지만 컴파일하거나 실행하지는 못했습니다.
Xcode에서 앱을 실행하고 창을 넓고 낮거나, 높고 좁은 형태로 조정하세요. 패널 가장자리가 계속 정렬되는지, 콘텐츠가 뷰포트보다 길 때 컨트롤이 스크롤되는지, 크기 조정 중 슬라이더와 토글 값이 유지되는지 확인하세요.
예제를 적용한 뒤에도 정렬이 어긋난다면 다음 조건을 확인하세요.
- 패널에 추가 여백 적용: 한 패널의 고정 프레임 바깥에만 적용된 여백은 해당 패널의 전체 공간을 늘립니다. 예제처럼 컨트롤 여백은 스크롤 뷰 안에 두세요.
- 더 큰 미리보기 래퍼: 미리보기 주위에 더 큰 프레임을 추가한다면 컨트롤을 해당 래퍼에 맞출지, 비율에 맞게 조정된 콘텐츠에 맞출지 결정하세요. 이 계산은 비율에 맞게 조정된 사각형에 맞춥니다.
- 넘치는 사용자 지정 콘텐츠: 프레임은 레이아웃 크기를 정하지만, 그 바깥에 그려지는 내용을 자동으로 숨기지는 않습니다. 의도적으로 넘치는 미리보기 내용을 숨기려면 Apple 문서에 설명된
.clipped()을 사용하세요. 컨트롤은 스크롤 가능하게 유지해 모든 설정에 접근할 수 있도록 하세요. Apple의 클리핑 문서. - 호스트 공간 부족: 이 예제는 최소 콘텐츠 크기에 의존합니다. 더 작은 호스트에 포함하면 고정된 컨트롤 너비 때문에 쓸 만한 미리보기 너비가 남지 않을 수 있습니다. 컨트롤 패널을 더 좁게 설정하거나, 세로 배치로 바꾸거나, 해당 호스트의 최소 크기를 명시하세요.
대신 사용자 지정 레이아웃을 써야 할 때는 언제인가요?
이 방법은 제한된 창 영역을 사용하고 알려진 가로세로 비율을 적용하는 편집기에 적합합니다. 크기 조정 정책이 명확하며 미리보기 내부 콘텐츠를 측정하는 데 의존하지 않습니다.
재사용 가능한 컴포넌트가 자식 뷰에 따라 선호 크기를 결정해야 한다면 사용자 지정 Layout을 고려하세요. Apple 문서의 계약에서는 sizeThatFits(proposal:subviews:cache:)를 통한 복합 크기 보고와 placeSubviews(in:proposal:subviews:cache:)을 통한 자식 배치를 구분합니다. 하위 뷰 프록시는 측정과 배치 작업을 제공합니다. Apple의 레이아웃 문서.
여기서 다룬 제한된 데스크톱 편집기에는 맞춤 사각형 하나면 충분합니다. 컨트롤용 가로 공간을 확보하고, 사용 가능한 너비와 높이에 미리보기를 맞춘 뒤 그 높이를 컨트롤 뷰포트에 지정하면 됩니다. 이렇게 하면 크기 조정 규칙이 명확해지고 컨트롤 콘텐츠는 자유롭게 스크롤됩니다.
출처
- aspectRatio(_:contentMode:) — Apple 개발자 문서.
- ScrollView — Apple 개발자 문서.
- windowResizability(_:) — Apple 개발자 문서.
- contentMinSize — Apple 개발자 문서.
- GeometryReader — Apple 개발자 문서.
- frame(width:height:alignment:) — Apple 개발자 문서.
- State — Apple 개발자 문서.
- clipped(antialiased:) — Apple 개발자 문서.
- Layout — Apple 개발자 문서.