Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
Tags
- PushNotification
- protocol
- list
- ScrollView
- viewlifecycle
- Observer
- segue
- SWIFT
- http
- SWIFTUI
- calendar
- apns
- 글또
- error
- NotificationCenter
- Refresh
- singleton
- self
- struct
- Switch
- array
- Animation
- Git
- mvvm
- escaping
- IOS
- uikit
- 고차함수
- 화면전환
- class
Archives
- Today
- Total
seong_hye, the developer
SwiftUI) Transition, Animation에 대해 알아보기 본문
📘 Transition
뷰의 존재 유무의 변화를 애니메이션
뷰를 이동할 때의 개념보다는 뷰를 보여지게 하거나 사라지게 하는 애니메이션의 개념
.transition(_:)으로 선언하여 사용 가능
들어가는 인수는 AnyTransition
트랜지션이 실행되려면 상태 변경을 애니메이션으로 감싸야 함
withAnimation { ... } 또는 .animation
🔍 AnyTransition
.opacity | 페이드 인 / 아웃 |
.scale | 확대 / 축소로 등장 / 퇴장 |
.move(edge: .top/.bottom/.leading/.trailing) | 가장자리에서 슬라이드 |
.slide | 부모의 leading -> trailing로 슬라이드 (언어 방향성 영향) |
.identity | 트랜지션 없음 (디버그, 조건부 조합에 사용) |
🔹결합 및 비대칭 기능도 존재
// 결합: 이동 + 페이드
.transition(.move(edge: .bottom).combined(with: .opacity))
// 비대칭: 등장/퇴장 서로 다르게
.transition(.asymmetric(
insertion: .move(edge: .bottom).combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
🔹커스텀 트랜지션 (Modifier 기반)
extension AnyTransition {
static var blurScale: AnyTransition {
.modifier(
active: BlueScale(blur: 8, scale: 0.85, opacity: 0),
identity: BlurScale(blue: 0, scale: 1.0, opacity: 1)
)
}
}
struct BlurScale: ViewModifier {
let blur: CGFloat
let scale: CGFloat
let opacity: Double
func body(content: Content) -> some View {
content
.blur(radius: blur)
.scaleEffect(scale)
.opacity(opacity)
}
}
if show {
ContentBox().transition(.blurScale)
}
Button("Change") {
withAnimation(.spring()) { show.toggle() }
}
🔹예시 모음
✅ 아래에서 등장, 작아지며 사라짐
VStack {
ZStack {
if show {
Rectangle()
.foregroundColor(.blue)
.cornerRadius(20)
.frame(width: 200, height: 300)
.transition(.asymmetric(
insertion: .move(edge: .bottom).combined(with: .opacity),
removal: .scale(scale: 0.9).combined(with: .opacity)
))
}
}
Button ("change") {
withAnimation(.snappy) { show.toggle()}
}
}
}
✅ 토스트 / 배너
ZStack {
if showToast {
Text("저장 완료!")
.padding().background(.black.opacity(0.8)).foregroundColor(.white)
.clipShape(Capsule())
.transition(.move(edge: .top).combined(with: .opacity))
.zIndex(1)
}
VStack {
Button ("change") {
trigger.toggle()
}
}
.onChange(of: trigger) { _ in
withAnimation(.snappy) {
showToast = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.snappy) { showToast = false }
}
}
}
✅ 클릭 시 확대되는 기능
@Namespace private var ns
@State private var showDetail = false
ZStack(alignment: .topLeading) {
if !showDetail {
HStack(spacing: 16) {
RoundedRectangle(cornerRadius: 16)
.fill(.pink)
.matchedGeometryEffect(id: "card", in: ns)
.frame(width: 120, height: 80)
Text("Preview")
.frame(width: 100, alignment: .leading)
.matchedGeometryEffect(id: "title", in: ns)
}
.padding()
.onTapGesture {
withAnimation(.bouncy) { showDetail = true }
}
.zIndex(0)
} else {
VStack(alignment: .leading, spacing: 12) {
RoundedRectangle(cornerRadius: 24)
.fill(.pink)
.matchedGeometryEffect(id: "card", in: ns)
.frame(height: 260)
Text("Preview")
.frame(width: 140, alignment: .leading)
.font(.title.bold())
.matchedGeometryEffect(id: "title", in: ns)
Text("상세 설명 ...")
.foregroundStyle(.secondary)
}
.padding()
.transition(.opacity) // 주변 요소는 페이드
.onTapGesture {
withAnimation(.bouncy) { showDetail = false }
}
.zIndex(1) // 상세가 위에
}
}
.ignoresSafeArea(edges: .bottom)
📘 Animation
이미 화면에 존재하는 뷰의 속성 변화를 부드럽게 만드는 것
🔹 명시적(Explicit) - withAnimation
상태를 바꾸는 순간에만 애니메이션이 적용됨
@State private var on = false
var body: some View {
VStack(spacing: 24) {
Circle()
.fill(on ? .mint : .pint)
.frame(width: on ? 180 : 80, height: on ? 180 : 80)
.offset(y: on ? -80 : 0)
Button("Change") {
withAnimation(.spring(response: 0.35), dampingFraction: 0.85)) {
on.toggle()
}
}
}
}
🔹암시적(Implicit) - .animation(_:value:) (iOS 15+ 권장)
어떤 상태가 변할 때 애니메이션할지 value: 로 명시함
@State private var on = false
var body: some View {
VStack(spacing: 24) {
Circle()
.fill(on ? .mint : .pint)
.frame(width: on ? 180 : 80, height: on ? 180 : 80)
.offset(y: on ? -80 : 0)
.animation(.spring(), value: on) //on일 때만 애니메이션
}
}
🔹대표 애니메이션
.animation(.linear(duration: 0.3), value: x)
.animation(.easeInOut(duration: 0.35), value: x)
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: x) // 스프링
.animation(.bouncy, value: x) // iOS 17+
.animation(.snappy, value: x) // iOS 17+
.animation(.smooth, value: x) // iOS 17+
// 반복, 지연, 속ㄷ
.animation(.easeInOut(duration: 1).delay(0.2), value: x)
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: spinning)
.animation(.easeInOut(duration: 1).repeatCount(3, autoreverses: true), value: pulsate)
.animation(.easeInOut(duration: 0.6).speed(1.5), value: x)
🔹애니메이션이 가능한 경우
숫자 / 각도 / 색 / 오파시티 / 프레임 / 오프셋 / 회전 / 스케일 / 블러 / 그레이디언트 등 대부분의 수치 기반 modifier
텍스트 문자열, 이미지 교체 자체는 직접 애니메이션 불가 (대신 opacity로 페이드 등 조합)
🔹예시 모음
✅ 개별 움직임
@State private var sizeOn = false
@State private var colorOn = false
var body: some View {
let base = RoundedRectangle(cornerRadius: 20)
.frame(width: sizeOn ? 200 : 120, height: sizeOn ? 120 : 80)
.foregroundStyle(colorOn ? .orange : .blue)
VStack(spacing: 20) {
base
.animation(.spring(), value: sizeOn) // 크기 변화만 스프링
.animation(.easeInOut(duration: 0.25), value: colorOn) // 색 변화는 빠르게
HStack {
Button("Size") { withAnimation { sizeOn.toggle() } }
Button("Color") { withAnimation { colorOn.toggle() } }
}
}
}
✅ 반복 / 자율 애니메이션 패턴
@State private var spin = false
@State private var pulse = false
var body: some View {
VStack(spacing: 24) {
Image(systemName: "arrow.2.circlepath")
.rotationEffect(.degrees(spin ? 360 : 0))
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: spin)
.onAppear { spin = true }
Circle()
.scaleEffect(pulse ? 1.1 : 0.95)
.animation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: pulse)
.onAppear { pulse = true }
}
}
✅ 제스처를 통한 애니메이션
@GestureState private var drag = CGSize.zero
@State private var pos = CGSize.zero
var body: some View {
let dragGesture = DragGesture()
.updating($drag) { value, state, _ in
state = value.translation
}
.onEnded { _ in
withAnimation(.spring()) { pos = .zero }
}
Circle()
.frame(width: 80, height: 80)
.offset(x: pos.width + drag.width, y: pos.height + drag.height)
.gesture(dragGesture)
}
✅ List / 레이아웃 변화 애니메이션
@State private var items = [1,2,3,4,5]
@State private var reversed = false
var body: some View {
VStack {
Button("Toggle Order") { reversed.toggle() }
List(reversed ? items.reversed() : items, id: \.self) { i in
Text("Row \(i)")
}
.animation(.easeInOut, value: reversed) // 순서 뒤집기 애니메이션
}
}
➡️ 차이 정리
항목 | animation | transition |
무엇을 | 이미 존재하는 뷰의 속성 변화 | 뷰의 삽입/제거 자체를 애니메이트 |
언제 동작 | 상태 값이 바뀌어 modifier 값이 달라질 때 | if/else, switch, ForEach로 뷰가 생기거나 사라질 때 |
적용 방법 | withAnimation{}, .animation(.spring(), value: state) |
.transition(.opacity), .transition(.move(edge: .bottom)) |
애니메이션 지정 | 곡선/스프링/반복 | 보통 withAnimation{} 또는 .animation(..)로 언제 적용 |
자주 하는 실수 | .animation(_:)만 붙이고 value: 안 주는 실수 | 항상 뷰가 존재하면 transition 안 먹음 |
➡️ 사용하는 경우
뷰가 이미 화면에 있고 속성 값만 바뀜 -> animation
뷰를 보여주거나 (삽입) 숨긴다 (제거) -> transition
함께 사용하는 경우 ~ 토글 시점에 withAnimation으로 전환 실행 + 해당 뷰에 적절한 transition 지정
⚠️ 주의 사항
- 트랜지션이 안 먹는 경우
뷰가 진짜로 삽입 / 제거되는 경우인지 확인 (if / switch) ~> opacity(0)로 숨기는 경우는 안됨
- 깜빡이거나 튀는 경우
각 뷰에 상반된 transition을 지정하고 zIndex로 레이어 순서를 명확히 해야 함
동일 영역을 겹치는 동안 앞 / 뒤가 확실해야 깔끔함
- iOS 15+에서 .animation(_:) 경고/ 무시
value: 버전 사용 또는 withAnimation으로 트리거 시점에만 명시
'IOS > SwiftUI' 카테고리의 다른 글
SwiftUI) 이미지(사진) 권한 설정에 대해 알아보기 (0) | 2025.07.15 |
---|---|
SwiftUI) matchedGeometryEffect에 대해 알아보기 (0) | 2025.07.08 |
SwiftUI) 어플 자동 업데이트 기능에 대해 정리하기 (0) | 2024.03.27 |
Swift) List 내용 삭제될 경우 기본 선택 값 변경되도록 제작하기 (0) | 2023.10.31 |
SwiftUI) List에 CheckBox 연동하기 (0) | 2023.09.19 |
Comments