폴더블·듀얼 화면 iPhone Duo를 대상으로, SwiftUI 수업에서 순서대로 사용할 수 있는 데모 10개를 구성했습니다. 각 데모는 학습 목표 → 코드 → 단계별 설명 → 확인 과제 순서입니다.
Apple 공식 안내에 따르면 Duo의 전체 화면 경험을 활용하려면 Xcode 27.1과 iOS 27.1 SDK를 사용합니다. Device Hub의 iPhone Duo 시뮬레이터에서 열기·닫기·회전·접힘을 시험할 수 있습니다. 아래 코드는 공식 API를 바탕으로 작성했으며, 이 환경에서 Xcode 빌드나 기기 실행 검증은 수행하지 못했습니다. Apple Developer
먼저 학생들에게 설명할 핵심 원칙
Duo 대응에서는 다음 세 가지 정보를 구분해야 합니다.
| 정보 | 의미 | SwiftUI에서 사용 |
|---|---|---|
| Size Class | 현재 공간에 적합한 UI의 큰 분류 | horizontalSizeClass, verticalSizeClass |
| 뷰의 크기 | 해당 뷰에 실제로 주어진 너비와 높이 | GeometryReader |
| 접힘·예약 영역 | 힌지나 카메라가 레이아웃에 영향을 주는 영역 | ArrangementView, reservedRegions |
기기 이름이나 화면 방향만으로 레이아웃을 결정하지 않습니다. 펼친 Duo에서도 멀티태스킹으로 앱이 좁아질 수 있습니다. Apple은 화면 방향 대신 Size Class를 사용하고, UIScreen.main보다 현재 뷰나 Scene의 공간을 기준으로 구성하도록 안내합니다. Safe Area도 좌우가 대칭이라고 가정하면 안 됩니다. Apple Developer
또한 화면이 넓다고 반드시 펼친 상태인 것은 아닙니다. 레이아웃 판단과 물리적인 힌지 상태 판단은 별개입니다.
공통 실행 방법
- Xcode에서 iOS → App → Interface: SwiftUI 프로젝트를 만듭니다.
- 아래 예제들을 Swift 파일에 추가합니다. 각 예제는 필요한 보조 뷰까지 포함했습니다.
- 생성된 App 파일의
WindowGroup에서 실행할 데모를 선택합니다.
import SwiftUI
@main
struct DuoClassApp: App {
var body: some Scene {
WindowGroup {
Demo01View()
}
}
}프로젝트에는 @main이 하나만 있어야 합니다. 위 코드를 추가하기보다 기존 App 파일을 수정하세요. 다음 실습에서는 Demo01View()를 Demo02View()처럼 변경합니다.
1~8번은 기본적으로 iOS 16 이상에서 사용하는 API로 작성했습니다. 9~10번은 Xcode 27.1 SDK가 필요한 심화 예제입니다. 오래된 SDK에서는 #available만으로 존재하지 않는 API를 컴파일할 수 없습니다.
데모 1. Size Class 관찰하기
학습 목표: 화면 변화가 SwiftUI 환경값에 어떻게 전달되는지 확인합니다.
import SwiftUI
struct Demo01View: View {
@Environment(\.horizontalSizeClass) private var horizontal
@Environment(\.verticalSizeClass) private var vertical
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("현재 레이아웃 환경")
.font(.title.bold())
Text("가로: \(name(horizontal))")
Text("세로: \(name(vertical))")
if horizontal == .regular {
Label("넓은 공간용 UI", systemImage: "rectangle.split.2x1")
} else {
Label("좁은 공간용 UI", systemImage: "rectangle")
}
Text("기기를 열고 닫거나 앱의 크기를 바꿔 보세요.")
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: .topLeading)
}
private func name(_ value: UserInterfaceSizeClass?) -> String {
switch value {
case .compact: return "Compact"
case .regular: return "Regular"
case nil: return "미지정"
@unknown default: return "알 수 없음"
}
}
}Step by Step
@Environment는 현재 뷰가 속한 환경에서 값을 읽습니다.horizontalSizeClass는 가로 공간에 적합한 UI 분류입니다. 정확한 너비 숫자는 아닙니다.- 값이 바뀌면 SwiftUI가
body를 다시 평가합니다. .regular에서는 넓은 공간용 안내를, 나머지 경우에는 좁은 공간용 안내를 표시합니다.nil과 미래의 새로운 값도 처리하여 분기를 완성합니다.
확인 과제: 외부 화면, 내부 화면, 내부 화면의 멀티태스킹 상태에서 값을 기록합니다. Apple은 외부 화면의 가로 Size Class를 compact, 내부 화면의 가로·세로를 regular로 설명하지만, 앱의 실제 실행 공간 변화도 함께 시험해야 합니다. Apple Developer
데모 2. 현재 뷰의 너비로 레이아웃 결정하기
학습 목표: 기기의 전체 화면 너비 대신 부모가 제공한 공간을 사용합니다.
import SwiftUI
struct Demo02View: View {
var body: some View {
GeometryReader { proxy in
let wide = proxy.size.width >= 600
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("사용 가능한 너비: \(Int(proxy.size.width))pt")
.font(.headline)
if wide {
HStack(alignment: .top, spacing: 16) {
panel("학습 내용", color: .blue)
panel("실습 결과", color: .green)
}
} else {
VStack(spacing: 16) {
panel("학습 내용", color: .blue)
panel("실습 결과", color: .green)
}
}
}
.padding()
}
}
}
private func panel(_ title: String, color: Color) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(title).font(.title2.bold())
Text("이 영역은 부모 뷰가 제공한 공간에 맞춰 배치됩니다.")
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(color.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}Step by Step
GeometryReader의proxy.size로 현재 컨테이너 크기를 읽습니다.- 너비가 600pt 이상이면 패널을 좌우로 배치합니다.
- 좁아지면 패널을 위아래로 배치합니다.
- 높이가 부족할 때 내용에 접근할 수 있도록
ScrollView를 사용합니다. maxWidth: .infinity는 현재 부모가 제공한 공간 안에서 패널을 확장합니다.
수업 포인트: 600은 이 예제의 디자인 기준입니다. Duo의 펼침 여부를 감지하는 공식 기준이 아닙니다. 이 데모는 기본적인 크기 대응이며, 힌지 영역 회피는 9번에서 다룹니다.
확인 과제: 기준을 500pt와 700pt로 바꾸고, 어떤 경우에 두 패널의 내용이 읽기 편한지 비교합니다.
데모 3. ViewThatFits로 버튼 줄 자동 전환하기
학습 목표: 너비 기준을 직접 정하지 않고 콘텐츠가 들어가는 배치를 선택합니다.
ViewThatFits는 지정한 축에서 공간에 들어가는 첫 번째 후보 뷰를 선택합니다. 따라서 가로 배치를 먼저, 세로 배치를 나중에 둡니다. Apple Developer Documentation
import SwiftUI
struct Demo03View: View {
@State private var message = "버튼을 선택하세요."
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
Text("학습 자료 관리")
.font(.title.bold())
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
actions
}
.fixedSize(horizontal: true, vertical: false)
VStack(alignment: .leading, spacing: 12) {
actions
}
}
.buttonStyle(.borderedProminent)
Text(message)
.foregroundStyle(.secondary)
}
.padding()
}
}
@ViewBuilder
private var actions: some View {
Button("학습 자료 다운로드") {
message = "다운로드 버튼 선택"
}
Button("실습 결과 제출") {
message = "제출 버튼 선택"
}
Button("선생님께 질문") {
message = "질문 버튼 선택"
}
}
}Step by Step
- 세 버튼을
actions로 추출하여 같은 내용을 두 배치에서 사용합니다. - 첫 후보인
HStack은 버튼들을 가로로 배치합니다. .fixedSize(horizontal: true, ...)로 가로 후보의 자연스러운 너비를 유지합니다. 버튼이 압축된 채 억지로 들어가는 것을 줄입니다.- 가로 후보가 들어가지 않으면
VStack을 선택합니다. - 선택 결과는 상위 뷰의
@State에 저장합니다.
수업 포인트: 후보마다 별도의 하위 뷰가 만들어집니다. 입력값이나 선택 상태를 공유해야 한다면 이 예제처럼 상태를 상위에 두는 편이 안전합니다.
확인 과제: 시스템 글자 크기를 크게 설정합니다. 화면이 넓어도 글자가 커지면 세로 배치가 필요한지 확인합니다.
데모 4. AnyLayout으로 상태를 유지하면서 배치 바꾸기
학습 목표: 동일한 하위 뷰의 정체성을 유지하면서 가로·세로 배치를 전환합니다.
Apple의 레이아웃 예제는 AnyLayout을 레이아웃 종류 간 전환에 사용합니다. GitHub
import SwiftUI
struct Demo04View: View {
@Environment(\.horizontalSizeClass) private var horizontal
@Environment(\.dynamicTypeSize) private var textSize
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var useHorizontal: Bool {
horizontal == .regular && !textSize.isAccessibilitySize
}
private var layout: AnyLayout {
if useHorizontal {
return AnyLayout(HStackLayout(spacing: 20))
} else {
return AnyLayout(VStackLayout(spacing: 20))
}
}
var body: some View {
ScrollView {
layout {
Demo04Counter(title: "Swift")
Demo04Counter(title: "SwiftUI")
}
.padding()
.animation(
reduceMotion ? nil : .easeInOut(duration: 0.25),
value: useHorizontal
)
}
}
}
private struct Demo04Counter: View {
let title: String
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text(title).font(.title2.bold())
Text("\(count)").font(.largeTitle)
Button("학습 횟수 +1") {
count += 1
}
.buttonStyle(.bordered)
}
.frame(maxWidth: .infinity)
.padding()
.background(.blue.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}Step by Step
- 각 카운터는 자체
@State를 가집니다. - 충분한 공간이 있고 접근성용 큰 글자가 아니면
HStackLayout을 선택합니다. - 그 외에는
VStackLayout을 선택합니다. - 두 카운터는 항상 같은
layout호출 안에 같은 순서로 존재합니다. - 배치 전환 시 카운터 값이 유지되는지 관찰합니다.
- 사용자가 동작 줄이기를 설정했다면 전환 애니메이션을 생략합니다.
수업 포인트: AnyLayout은 레이아웃 전환에 도움을 주지만, 상위 뷰를 제거하거나 .id(...)를 바꾸면 상태가 초기화될 수 있습니다.
확인 과제: 두 카운터를 각각 올린 후 기기를 열고 닫습니다. 배치는 바뀌되 값이 유지되어야 합니다.
데모 5. Adaptive Grid로 카드 열 수 조정하기
학습 목표: 좁은 화면에서는 적은 열, 넓은 화면에서는 많은 열을 표시합니다.
import SwiftUI
struct Demo05View: View {
@Environment(\.dynamicTypeSize) private var textSize
private var columns: [GridItem] {
[
GridItem(
.adaptive(
minimum: textSize.isAccessibilitySize ? 280 : 160
),
spacing: 16
)
]
}
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(1...12, id: \.self) { number in
VStack(alignment: .leading, spacing: 12) {
Image(systemName: "book.fill")
.font(.largeTitle)
.foregroundStyle(.blue)
Text("SwiftUI 실습 \(number)")
.font(.headline)
Text("화면 크기에 맞춰 카드의 열 수가 바뀝니다.")
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity,
minHeight: 150,
alignment: .topLeading)
.padding()
.background(.blue.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding()
}
}
}Step by Step
.adaptive(minimum:)로 카드의 목표 최소 너비를 지정합니다.- 그리드는 현재 너비에 들어갈 수 있는 열 수를 계산합니다.
- 너비가 늘어나면 카드 수를 바꾸지 않고 열 수가 늘어납니다.
- 접근성용 큰 글자에서는 최소 너비를 늘려 열 수를 줄입니다.
minHeight만 지정하므로 글자가 많아지면 카드 높이가 늘어날 수 있습니다.
수업 포인트: 일반 adaptive grid만으로 모든 힌지 회피 요구가 해결되지는 않습니다. 반쯤 접었을 때 카드의 핵심 내용이나 버튼이 접힘 부분에 걸리는지도 시험하세요.
확인 과제: 최소 너비를 120pt로 낮춥니다. 열 수 증가가 항상 읽기 편한 UI로 이어지는지 토론합니다.
데모 6. NavigationSplitView로 목록과 상세 구성하기
학습 목표: 좁을 때는 순차 탐색, 넓을 때는 목록·상세 탐색을 제공합니다.
NavigationSplitView는 앞쪽 열의 선택으로 뒤쪽 열의 내용을 결정하는 탐색 컨테이너입니다. Apple Developer Documentation
import SwiftUI
struct Demo06View: View {
@State private var selected: String?
private let lessons = [
"State와 Binding",
"Adaptive Layout",
"Navigation"
]
var body: some View {
NavigationSplitView {
List(lessons, id: \.self, selection: $selected) { lesson in
NavigationLink(value: lesson) {
Label(lesson, systemImage: "book")
}
}
.navigationTitle("강의 목록")
} detail: {
if let selected {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text(selected)
.font(.largeTitle.bold())
Text("선택한 강의의 설명입니다.")
Text("기기를 열고 닫은 뒤 선택 상태를 확인하세요.")
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle(selected)
} else {
VStack(spacing: 16) {
Image(systemName: "sidebar.left")
.font(.largeTitle)
Text("강의를 선택하세요.")
}
}
}
}
}Step by Step
selected에 현재 선택한 강의의 ID를 저장합니다.- 목록의
selection을 $selected와 연결합니다. NavigationLink(value:)에 목록과 동일한 ID를 전달합니다.- 상세 열은
selected에 맞춰 내용을 표시합니다. - 목록을 선택한 후 앱 크기를 바꿔 탐색 구조와 선택 상태를 관찰합니다.
Duo에서는 닫힌 상태에서 열들이 단일 스택으로 축소되고, 열린 상태에서는 공간에 따라 나란히 또는 오버레이 형태로 나타납니다. 항상 두 열이 동시에 보인다고 가정하지 마세요. Apple Developer
확인 과제: 강의를 선택한 채 닫았다가 다시 엽니다. 학생이 보던 강의가 유지되는지 확인합니다.
데모 7. 비대칭 Safe Area와 표준 Toolbar 사용하기
학습 목표: 배경은 화면 끝까지, 내용과 조작 버튼은 안전한 영역에 배치합니다.
import SwiftUI
struct Demo07View: View {
@State private var count = 0
var body: some View {
NavigationStack {
ZStack {
LinearGradient(
colors: [.blue.opacity(0.3), .purple.opacity(0.2)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea() // 배경에만 적용
ScrollView {
VStack(alignment: .leading, spacing: 24) {
Text("Safe Area 실습")
.font(.largeTitle.bold())
Text("배경은 화면 끝까지 확장합니다.")
Text("본문과 버튼은 시스템 UI를 피하도록 둡니다.")
Text("저장 횟수: \(count)")
.font(.title2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
.navigationTitle("학습 노트")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
count += 1
} label: {
Label("저장", systemImage: "square.and.arrow.down")
}
}
}
.safeAreaInset(edge: .bottom, spacing: 0) {
Text("기기를 회전하고 멀티태스킹으로 이동해 보세요.")
.font(.footnote)
.frame(maxWidth: .infinity)
.padding()
.background(.regularMaterial)
}
}
}
}Step by Step
- 배경에만
.ignoresSafeArea()를 적용합니다. - 본문은 기본 Safe Area 안에 배치합니다.
- 저장 버튼은 직접 좌표를 지정하지 않고 표준 Toolbar에 넣습니다.
- 하단 안내는
safeAreaInset으로 공간을 확보합니다. - 화면의 다른 가장자리에 시스템 UI가 나타나도 버튼이 안전한지 확인합니다.
수업 포인트: 루트 전체에 ignoresSafeArea()를 적용하면 조작 요소까지 시스템 UI와 겹칠 수 있습니다. 좌우 여백을 같은 숫자로 계산하는 방식도 피합니다.
확인 과제: 내부 화면의 멀티태스킹에서 앱을 왼쪽과 오른쪽에 각각 배치합니다.
데모 8. 편집 Sheet와 상태 보존하기
학습 목표: 크기가 변해도 작성 중인 내용을 유지하고, 편집 결과를 저장하거나 취소합니다.
import SwiftUI
struct Demo08View: View {
@State private var note = "오늘의 SwiftUI 수업"
@State private var draft = ""
@State private var isEditing = false
var body: some View {
NavigationStack {
ScrollView {
Text(note.isEmpty ? "내용 없음" : note)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.navigationTitle("노트")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("편집") {
draft = note
isEditing = true
}
}
}
.sheet(isPresented: $isEditing) {
NavigationStack {
TextEditor(text: $draft)
.padding()
.navigationTitle("노트 편집")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") {
isEditing = false
}
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") {
note = draft
isEditing = false
}
}
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}
}
}Step by Step
note는 저장된 내용,draft는 편집 중인 내용입니다.- 편집을 시작할 때 원본을 초안으로 복사합니다.
TextEditor는 $draft를 통해 입력값을 수정합니다.- 저장하면 원본을 바꾸고 Sheet를 닫습니다.
- 취소하거나 아래로 내려 닫으면 원본은 유지됩니다.
- 초안은 Sheet보다 상위 뷰에 있으므로 Sheet 내부의 배치 변화와 분리됩니다.
수업 포인트: @State는 앱 종료 후까지 보존하는 저장소가 아닙니다. 영구 저장이 필요하면 SwiftData나 파일 저장 등을 추가해야 합니다.
확인 과제: 글을 입력하다가 기기를 접거나 펼친 뒤 입력값을 확인합니다. 키보드가 표시된 상태도 시험하세요.
데모 9. ArrangementView와 접힘 예약 영역 관찰하기
학습 목표: 단순한 화면 너비 분기를 넘어 Duo의 접힘에 대응하는 시스템 배치를 사용합니다.
ArrangementView는 주 뷰와 보조 뷰를 배치하며, Size Class·종횡비·접힘 영역 등을 반영합니다. reservedRegions(kind: .division)은 현재 뷰에 영향을 주는 활성 분할 영역을 조회합니다. 이 API들은 iOS 27.1 심화 실습에 사용합니다. Apple Developer
import SwiftUI
@available(iOS 27.1, *)
struct Demo09View: View {
@State private var isPlaying = false
var body: some View {
NavigationStack {
ArrangementView {
player
} secondary: {
transcript
}
.arrangementViewStyle(.split)
.navigationTitle("강의 플레이어")
}
}
private var player: some View {
GeometryReader { proxy in
let divisions = proxy.reservedRegions(kind: .division)
VStack(spacing: 20) {
Image(systemName: "play.rectangle.fill")
.font(.system(size: 60))
.foregroundStyle(.blue)
Text("SwiftUI 강의")
.font(.title2.bold())
Text("활성 분할 영역: \(divisions.count)")
.font(.caption)
.foregroundStyle(.secondary)
Button(isPlaying ? "일시 정지" : "재생") {
isPlaying.toggle()
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var transcript: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("강의 자막")
.font(.title2.bold())
ForEach(1...15, id: \.self) { number in
Text("\(number). 현재 공간에 맞게 콘텐츠를 배치합니다.")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
}App 파일에서는 다음처럼 실행합니다.
WindowGroup {
if #available(iOS 27.1, *) {
Demo09View()
} else {
Text("이 실습은 iOS 27.1 이상이 필요합니다.")
}
}Step by Step
- 플레이어를 주 뷰, 자막을 보조 뷰로 지정합니다.
.arrangementViewStyle(.split)으로 분할 배치를 선택합니다.- 열기·회전·부분 접힘 상태에서 두 뷰의 배치를 관찰합니다.
- 플레이어 내부
GeometryReader에서 활성 분할 영역 수를 표시합니다. - 이 숫자는 진단용입니다. 실제 배치 대응은
ArrangementView가 담당합니다. - 재생 상태는 상위에 두어 배치 변화 중에도 유지되도록 구성합니다.
주의: 영역 조회는 해당 뷰의 좌표 공간을 기준으로 합니다. 표시된 숫자를 기기 전체의 힌지 개수라고 해석하면 안 됩니다.
확인 과제: .split을 .split.axes(.horizontal)로 바꿉니다. 세로로 긴 공간에서 보조 뷰가 계속 표시되는지 비교합니다. 축을 제한하면 공간 조건에 따라 주 뷰만 표시될 수 있습니다. Apple Developer
데모 10. 힌지 각도로 시각 효과 만들기
학습 목표: 물리적인 접힘 각도를 앱의 상호작용에 연결합니다.
onHingeChange는 이전·현재 힌지 정보를 제공합니다. 힌지가 없는 기기에서는 context.hinge가 nil입니다. Apple은 힌지 데이터를 효과나 상호작용에 사용하고, 레이아웃은 Arrangement·Reserved Region API로 처리하도록 안내합니다. Apple Developer
import SwiftUI
@available(iOS 27.1, *)
struct Demo10View: View {
@State private var bend = 0.0
@State private var angleDegrees: Double?
@State private var statusText = "힌지 정보를 기다리는 중"
var body: some View {
ScrollView {
VStack(spacing: 28) {
Text("힌지 인터랙션")
.font(.title.bold())
Image(systemName: "sparkles")
.font(.system(size: 80))
.foregroundStyle(.purple)
.scaleEffect(1 + bend * 0.5)
.rotationEffect(.degrees(bend * 30))
.frame(height: 160)
.accessibilityLabel("접힘에 따라 변하는 시각 효과")
Text(statusText)
if let angleDegrees {
Text("각도: \(angleDegrees, specifier: "%.1f")°")
.monospacedDigit()
}
ProgressView(value: bend)
.accessibilityLabel("접힘 효과 강도")
Text("부분적으로 접으면 효과의 크기와 회전이 바뀝니다.")
.foregroundStyle(.secondary)
}
.padding()
}
.onHingeChange { _, context in
guard let hinge = context.hinge else {
angleDegrees = nil
bend = 0
statusText = "힌지가 없는 기기"
return
}
angleDegrees = hinge.angle.degrees
if hinge.status == .partiallyOpen {
statusText = "부분적으로 열린 상태"
// 수업용 효과 매핑: 결과를 0...1로 제한
let normalized = (180 - hinge.angle.degrees) / 180
bend = min(max(normalized, 0), 1)
} else {
statusText = "부분 접힘 상태 아님"
bend = 0
}
}
}
}실행할 때는 9번의 가용성 분기에서 Demo09View()를 Demo10View()로 바꿉니다.
Step by Step
- 힌지 콜백에서
context.hinge의 존재를 확인합니다. - 힌지가 없으면 각도 표시와 효과를 초기화합니다.
- 힌지가 있으면
Angle.degrees로 각도를 읽습니다. .partiallyOpen인 동안 각도를 효과 강도로 변환합니다.min(max(...))로 결과를 0~1 범위에 제한합니다.- 값으로 이미지의 크기와 회전을 바꿉니다.
- 부분 접힘 상태를 벗어나면 효과를 원래대로 돌립니다.
수업 포인트: 위 각도 공식은 시각 효과를 위한 예제입니다. 각도를 임의의 기준으로 나눠 화면 배치를 결정하거나, 두 화면을 동시에 제어하는 코드로 해석하지 않습니다.
확인 과제: Device Hub에서 접힘 각도를 천천히 변경합니다. 일반 iPhone에서도 실행하여 힌지가 없는 경우를 확인합니다.
댓글 없음:
댓글 쓰기
참고: 블로그의 회원만 댓글을 작성할 수 있습니다.