iPhone Duo는 커버 디스플레이(5.4인치)와 펼쳤을 때의 메인 폴더블 디스플레이(7.6인치)를 제공하는 듀얼/폴더블 스크린 디바이스입니다.
SwiftUI에서는 화면의 무분별한 하드코딩 대신 Size Class, GeometryReader, Hinge Angle API, Vertical Toolbar, ArrangementView 등 확장된 레이아웃 프레임워크를 활용해 디스플레이 상태에 완벽하게 대응합니다.
iPhone Duo 대응을 위한 핵심 레이아웃 개념
Size Class Adaptive System: 접었을 때(horizontalSizeClass == .compact), 펼쳤을 때(horizontalSizeClass == .regular) 레이아웃 분기.
Hinge Angle Sensing: 힌지의 접힌 각도(0°~180°)에 따라 스탠드 모드, 테이블톱 모드, 완전히 펼친 모드로 앱의 UI 전환.
Safe Area & Reserved Regions: 힌지 영역이나 사이드 상태바(Status Bar) 영역을 회피하는 안전 영역 처리.
Vertical Toolbar & Controls: iOS에서 수직 배치 모디파이어를 제공하여 힌지 측면/우측에 툴바 배치.
Step-by-Step 실습 데모 코드 10선
1. Size Class에 따른 화면 전환 (Basic Responsive View)
커버 스크린과 내장 스크린의 기본 레이아웃 대응 규칙을 배웁니다.
Swift
import SwiftUI
struct Demo01_SizeClassView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
Group {
if horizontalSizeClass == .compact {
// 커버 디스플레이 (단일 컬럼)
VStack(spacing: 20) {
Image(systemName: "iphone")
.font(.system(size: 60))
Text("Cover Display Mode")
.font(.title2)
.bold()
}
} else {
// 펼쳐진 메인 디스플레이 (나란히 배치)
HStack(spacing: 30) {
VStack {
Image(systemName: "rectangle.split.2x1")
.font(.system(size: 60))
Text("Left Screen Region")
}
Divider()
VStack {
Image(systemName: "sidebar.right")
.font(.system(size: 60))
Text("Right Screen Region")
}
}
.padding()
}
}
.animation(.default, value: horizontalSizeClass)
}
}
2. NavigationSplitView를 활용한 마스터-디테일 패턴
접었을 때는 Stack Navigation으로 동작하고, 펼쳤을 때는 2-Column SplitView로 자동 전환되는 표준 패턴입니다.
Swift
import SwiftUI
struct Item: Identifiable, Hashable {
let id = UUID()
let title: String
let description: String
}
struct Demo02_SplitView: View {
@State private var items = (1...10).map { Item(title: "Item \($0)", description: "Detail content for item \($0)") }
@State private var selectedItem: Item?
var body: some View {
NavigationSplitView {
List(items, selection: $selectedItem) { item in
NavigationLink(value: item) {
Text(item.title)
}
}
.navigationTitle("Duo Items")
} detail: {
if let item = selectedItem {
VStack(alignment: .leading, spacing: 16) {
Text(item.title)
.font(.largeTitle)
.bold()
Text(item.description)
.font(.body)
Spacer()
}
.padding()
} else {
Text("Select an item from list")
.foregroundColor(.secondary)
}
}
}
}
3. Hinge Angle 감지와 Pose 감지 (Flex/Tabletop Mode)
기기의 접힘 각도(hingeAngle)를 감지하여 90도 안팎으로 접혔을 때 카메라/플레이어 컨트롤러 형태로 변경합니다.
Swift
import SwiftUI
// iPhone Duo 힌지 각도 Environment Key (가상 정의 구조)
struct HingeAngleKey: EnvironmentKey {
static let defaultValue: Double = 180.0 // 180 = 완전 펼침
}
extension EnvironmentValues {
var hingeAngle: Double {
get { self[HingeAngleKey.self] }
set { self[HingeAngleKey.self] = newValue }
}
}
struct Demo03_HingePoseView: View {
@Environment(\.hingeAngle) var hingeAngle
// 70도~110도 사이면 Flex / Tabletop 모드로 진단
var isTabletopMode: Bool {
return hingeAngle >= 70 && hingeAngle <= 110
}
var body: some View {
VStack {
if isTabletopMode {
// 상단: 미디어 화면
Color.black
.overlay(
Text("Video Player (Top Screen)")
.foregroundColor(.white)
)
.frame(maxHeight: .infinity)
// 하단: 조작 패널
VStack(spacing: 20) {
Text("Controls (Bottom Screen)")
.font(.headline)
HStack(spacing: 40) {
Button(action: {}) { Image(systemName: "backward.fill") }
Button(action: {}) { Image(systemName: "play.fill") }
Button(action: {}) { Image(systemName: "forward.fill") }
}
.font(.title)
}
.frame(maxHeight: .infinity)
.background(Color(.secondarySystemBackground))
} else {
// 일반 풀스크린 레이아웃
VStack {
Text("Full Screen Display")
.font(.title)
Text("Hinge Angle: \(Int(hingeAngle))°")
}
}
}
}
}
4. Side Toolbar (수직 툴바) 레이아웃
iOS 27 및 iPhone Duo 표준에 추가된 수직 툴바 배치(toolbarVerticalEdge)를 적용하는 기법입니다.
Swift
import SwiftUI
struct Demo04_VerticalToolbarView: View {
@State private var text = "iPhone Duo 수직 툴바 테스트"
var body: some View {
NavigationStack {
TextEditor(text: $text)
.padding()
.navigationTitle("Note Editor")
.toolbar {
// 오른쪽 사이드 수직 영역에 배치되는 툴바 항목들
ToolbarItemGroup(placement: .topBarTrailing) {
Button(action: {}) { Image(systemName: "bold") }
Button(action: {}) { Image(systemName: "italic") }
Button(action: {}) { Image(systemName: "underline") }
Button(action: {}) { Image(systemName: "square.and.arrow.up") }
}
}
// iPhone Duo 수직 바 배치 옵션
.toolbarVerticalEdge(.trailing)
}
}
}
5. Dual Screen App Switching (ArrangementView / Screen Splitting)
화면 왼쪽과 오른쪽에 서로 다른 보조 앱 UI/뷰를 할당하는 Layout 패러다임입니다.
Swift
import SwiftUI
struct Demo05_DualWorkspaceView: View {
var body: some View {
GeometryReader { geometry in
let isWideScreen = geometry.size.width > 600
if isWideScreen {
// 듀얼 스크린 각각 독립적인 워크스페이스
HStack(spacing: 0) {
// Left Screen: Document List
NavigationStack {
List(1...5, id: \.self) { index in
Text("Document \(index)")
}
.navigationTitle("Files")
}
.frame(width: geometry.size.width / 2)
Divider()
// Right Screen: Preview Canvas
VStack {
Text("Live Preview Canvas")
.font(.title2)
Spacer()
}
.padding()
.frame(width: geometry.size.width / 2)
}
} else {
// 단일 화면
Text("Please unfold your iPhone Duo to use Dual Workspace.")
.padding()
}
}
}
}
6. 힌지 물리 자원 회피 (Hinge Avoidance Layout)
힌지 영역(Hinge Boundary)을 감지하고, 중요한 UI 요소가 힌지 중앙 선에 잘리지 않도록 안전 패딩을 자동 부여합니다.
Swift
import SwiftUI
struct Demo06_HingeAvoidanceView: View {
// 힌지가 위치한 중앙의 가상 경계 너비 (12pt 예시)
let hingeWidth: CGFloat = 12.0
var body: some View {
GeometryReader { proxy in
let totalWidth = proxy.size.width
let sideWidth = (totalWidth - hingeWidth) / 2
HStack(spacing: 0) {
// 좌측 디스플레이 Safe Content
VStack {
Text("Left Side UI")
.bold()
Spacer()
}
.frame(width: sideWidth)
.background(Color.blue.opacity(0.1))
// 힌지 회피 영역 (마진 처리)
Color.clear
.frame(width: hingeWidth)
// 우측 디스플레이 Safe Content
VStack {
Text("Right Side UI")
.bold()
Spacer()
}
.frame(width: sideWidth)
.background(Color.green.opacity(0.1))
}
}
}
}
7. Duo Preview (커버 스크린 카메라이용 셀피 및 듀얼 프리뷰)
iPhone Duo의 Rear Camera 사용 시 외부 디스플레이에 Live Preview를 동시 출력하는 UI 구조 예시입니다.
Apple
Swift
import SwiftUI
struct Demo07_DuoCameraPreviewView: View {
@State private var isOuterPreviewActive = false
var body: some View {
VStack(spacing: 24) {
// 메인 뷰파인더
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color.gray.opacity(0.3))
Text("Main Camera Viewfinder")
}
.frame(height: 300)
// Duo Preview 활성화 버튼
Toggle("Duo Preview (Outer Screen Active)", isOn: $isOuterPreviewActive)
.padding()
.background(RoundedRectangle(cornerRadius: 12).stroke(Color.gray))
if isOuterPreviewActive {
Label("Outer Screen is displaying live selfie preview for subject.", systemImage: "display.2")
.font(.caption)
.foregroundColor(.blue)
}
}
.padding()
}
}
8. 적응형 그리드 시스템 (Adaptive Grid for Foldable Screens)
디스플레이를 열고 접을 때 열(Column) 개수가 2개에서 4개로 자연스럽게 늘어나는 그리드입니다.
Swift
import SwiftUI
struct Demo08_AdaptiveGridView: View {
@Environment(\.horizontalSizeClass) var sizeClass
// 펼침 여부에 따른 GridItem 동적 생성
var columns: [GridItem] {
if sizeClass == .regular {
return Array(repeating: GridItem(.flexible(), spacing: 16), count: 4)
} else {
return Array(repeating: GridItem(.flexible(), spacing: 12), count: 2)
}
}
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(1...20, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(Color.indigo.opacity(0.6))
.frame(height: 100)
.overlay(
Text("Card \(index)")
.foregroundColor(.white)
.bold()
)
}
}
.padding()
}
}
}
9. Continuum Transition (연속성 애니메이션)
커버 화면에서 보던 내용이 화면을 펼쳤을 때 끊김 없이 정교하게 확장되는 Transition 처리입니다.
Swift
import SwiftUI
struct Demo09_ContinuityView: View {
@Namespace private var animation
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
VStack {
if sizeClass == .compact {
// Compact 모드: 카드 형태
VStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color.orange)
.matchedGeometryEffect(id: "card", in: animation)
.frame(height: 150)
.overlay(Text("Tap or Unfold to Expand").foregroundColor(.white))
}
.padding()
} else {
// Regular (Fold Unfolded) 모드: 풀 대시보드
HStack(spacing: 20) {
RoundedRectangle(cornerRadius: 16)
.fill(Color.orange)
.matchedGeometryEffect(id: "card", in: animation)
.frame(width: 250, height: 300)
.overlay(Text("Expanded View").foregroundColor(.white))
VStack(alignment: .leading, spacing: 12) {
Text("Detailed Analytics").font(.title).bold()
Text("Seamless continuum transition from cover screen to main inner display.")
Spacer()
}
}
.padding()
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: sizeClass)
}
}
10. 종합 예제: iPhone Duo 멀티태스킹 메모 앱
Size Class, Split View, 힌지 대응 및 사이드 바 컨트롤까지 결합된 종합 교육용 데모 프로젝트입니다.
Swift
import SwiftUI
struct Demo10_DuoMasterApp: View {
@Environment(\.horizontalSizeClass) var sizeClass
@State private var notes: [String] = ["수업 준비하기", "SwiftUI Duo API 실습", "아이디어 스케치"]
@State private var selectedNote: String?
var body: some View {
NavigationSplitView {
List(notes, id: \.self, selection: $selectedNote) { note in
Text(note)
}
.navigationTitle("Notes")
.toolbar {
Button(action: { notes.append("새 노트 \(notes.count + 1)") }) {
Image(systemName: "plus")
}
}
} detail: {
if let note = selectedNote {
VStack {
TextEditor(text: .constant("\(note)의 상세 내용입니다."))
.padding()
}
.navigationTitle(note)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button(action: {}) { Image(systemName: "trash") }
Button(action: {}) { Image(systemName: "square.and.arrow.up") }
}
}
.toolbarVerticalEdge(sizeClass == .regular ? .trailing : .automatic)
} else {
Text("노트를 선택해주세요.")
.font(.title3)
.foregroundColor(.secondary)
}
}
}
}
댓글 없음:
댓글 쓰기
참고: 블로그의 회원만 댓글을 작성할 수 있습니다.