SwiftUI 애니메이션과 전환

3 분 소요

애니메이션(animation): 화면상의 뷰 회전, 확대, 동작 등의 형태
전환(transition): 레이아웃에서 뷰가 추가되거나 제거될 때 뷰가 어떻게 나타나고 사라질지 정의


암묵적 애니메이션

뷰의 외형을 제어하는 프로퍼티(크기, 불투명도, 색상, 회전각도) -> 애니메이션화
1. 암묵적 애니메이션(implicit animation)

  • animation() 수정자 사용
  • 애니메이션 수정자 앞에 있는 모든 수정자에 암묵적으로 애니메이션
struct ContentView: View {

    @State private var rotation: Double = 0
    
    var body: some View {
        Button(action: {
            self.rotation = 
                (self.rotation < 360 ? self.rotation + 60 : 0)
        }) {
            Text("Click to animate")
                .rotationEffect(.degrees(rotation))
        }
    }
}

360도를 완전히 회전하면 반시계 방향으로 360도 회전하지만 너무 빨라서 눈에 보이지 않음
회전 효과의 속도를 줄이고 부드럽게 회전 -> animation() 수정자 추가

var body: some View {
    Button(action: {
        self.rotation = 
            (self.rotation < 360 ? self.rotation + 60 : 0)
    }) {
        Text("Click to animate")
            .rotationEffect(.degrees(rotation))
            .animation(.linear, value: rotation)
    }
}

.animation() 삭제 -> .animation(_:value:) 사용

  • 애니메이션 커브
    애니메이션 타이밍을 제어
    • .linear - 지정된 시간 동안 일정한 속도로 애니메이션 수행
    • .easeOut - 빠른 속도로 애니메이션을 시작, 애니메이션의 끝에 다다를수록 점점 느려짐
    • .easeIn - 느린 속도로 애니메이션을 시작, 애니메이션의 끝에 다다를수록 점점 빨라짐
    • .easeInOut - 느린 속도로 애니메이션을 시작, 점점 속도를 올리다가 애니메이션의 끝에
      다다를수록 다시 느려짐
  • 애니메이션 시간 지정
      .animation(.linear(duration: 1), value: rotation)
    

    1초 동안 애니메이션 재생

  • 애니메이션 하나 이상의 수정자에 적용
      @State private var rotation: Double = 0
      @State private var scale: CGFloat = 1
        
      var body: some View {
          Button(action: {
              self.rotation = (self.rotation < 360 ? self.rotation + 60 : 0)
              self.scale = (self.scale < 2.8 ? self.scale + 0.3 : 1)
          }) {
              Text("Click to animate")
                  .scaleEffect(scale)
                  .rotationEffect(.degrees(rotation))
                  .animation(.linear(duration: 1), value: rotation)
          }
      }
    

    회전하면서 크기가 커짐
    스크린샷 2022-06-10 오후 12 59 26

  • 스프링 효과
      Text("Click to animate")
          .scaleEffect(scale)
          .rotationEffect(.degrees(rotation))
          .animation(.spring(response: 1, dampingFraction: 0.2, blendDuration: 0), 
                      value: rotation)
    

애니메이션 반복하기

.animation(.linear(duration: 1).repeatCount(10), value: rotation)

10번 반복

  • 뷰의 원래 모양을 즉시 되돌려야 하는 경우
    .animation(.linear(duration: 1).repeatCount(10, autoreverses: false), value: rotation)
    
  • 애니메이션을 무한 반복
    .animation(.linear(duration: 1).repeatForever(autoreverses: true), value: rotation)
    

명시적 애니메이션

2. 명시적 애니메이션(explicit animation)

  • withAnimation() 클로저 사용
  • animation()은 withAnimation()과 달리 해당뷰에만 애니메이션 효과를 부여
  • withAnimation은 변화된 값에 영향을 받는 모든 뷰에 영향
var body: some View {
            
    Button(action: { withAnimation(.linear (duration: 2)) {
        self.rotation = (self.rotation < 360 ? self.rotation + 60 : 0)
        }
        self.scale = (self.scale < 2.8 ? self.scale + 0.3 : 1)
    }) {
        Text("Click to Animate")
            .rotationEffect(.degrees(rotation))
            .scaleEffect(scale)
          //.animation(.linear(duration: 1))                
    }

책에서는 클로저 내에서 변경된 프로퍼티인 회전 애니메이션만 실행될 것이라 했지만 둘 다 실행됨
클로저 내에서 변경된 프로퍼티만 애니메이션됨


애니메이션과 상태 바인딩

상태 프로퍼티 바인딩 에 애니메이션을 적용 -> 상태 값이 변경되면 애니메이션 작동

struct ContentView : View {
    
    @State private var visibility = false
    
    var body: some View {
        VStack {
            Toggle(isOn: $visibility.animation(.linear(duration: 5))) {
                Text("Toggle Text Views")
            }
            .padding()
            
            if visibility {
                Text("Hello World")
                    .font(.largeTitle)
            }
            
            if !visibility {
                Text("Goodbye World")
                    .font(.largeTitle)
            }
        }
    }
}

상태 프로퍼티 바인딩시 $ 붙임
Text 뷰들이 즉시 사라짐 -> 천천히 사라짐


자동으로 애니메이션 시작하기

사용자 인터랙션 없이 애니메이션 시작 애니메이션이 가능한 뷰의 프로퍼티가 변경될 때마다 애니메이션이 트리거됨

var body: some View {
        
    ZStack {
        Circle()
            .stroke(lineWidth: 2)
            .foregroundColor(Color.blue)
            .frame(width: 360, height: 360)
            
        Image(systemName: "forward.fill")
            .font(.largeTitle)
            .offset(y: -180)
            .rotationEffect(.degrees(360))
            .animation(Animation.linear(duration: 5)
                        .repeatForever(autoreverses: false), value: 360)
    }
}

스크린샷 2022-06-11 오후 6 14 40
움직이지 않음: 애니메이션이 가능한 프로퍼티를 변경하는 작업이 없기 때문

👉 onAppear() 수정자 사용

@State private var isSpinning: Bool = true // 애니메이션 가능한 프로퍼티
    
var body: some View {
        
    ZStack {
        Circle()
            .stroke(lineWidth: 2)
            .foregroundColor(Color.blue)
            .frame(width: 360, height: 360)
            
        Image(systemName: "forward.fill")
            .font(.largeTitle)
            .offset(y: -180)
            .rotationEffect(.degrees(isSpinning ? 0 : 360))
            .animation(Animation.linear(duration: 5)
                        .repeatForever(autoreverses: false), value: isSpinning)
    }
    .onAppear() {
        self.isSpinning.toggle()
    }
}

뷰가 나타나면 onAppear() 수정자가 isSpinning 프로퍼티를 false 로 전환 -> 회전각도 = 360
스크린샷 2022-06-11 오후 6 15 31


SwiftUI 전환

전환(transition)

  • 사용자에게 뷰가 표시되거나 사라질 때마다 발생
  • 하나 또는 여러 개의 애니메이션 효과를 조합 가능
struct ContentView : View {
    
    @State private var isButtonVisible: Bool = true
    
    var body: some View {
        VStack {
            Toggle(isOn: $isButtonVisible.animation(
                    .linear(duration: 2))) {
                Text("Show/Hide Button")
            }
            .padding()
            
            if isButtonVisible {
                Button(action: {}) {
                    Text("Example Button")
                }
                .font(.largeTitle)
            }
        }
    }
}

토글 전환시 Button 뷰가 페이드 인/페이드 아웃(디폴트)

<전환 옵션>

  • .scale - 뷰가 커지면서 인 작아지면서 아웃
  • .move(edge: edge) - 지정된 방향으로 뷰가 이동
  • .opacity - 디폴트 전환
  • .slide - 뷰가 슬라이딩하여 인/아웃
      if isButtonVisible {
          Button(action: {}) {
              Text("Example Button")
          }
          .font(.largeTitle)
          .transition(.slide)
      }
    

전환 결합하기

AnyTransition 의 인스턴스를 combined(with:) 메서드와 함께 사용

  • 페이딩(fading) 효과 + 무빙(moving)
      .transition(AnyTransition.opacity.combined(with: .move(edge: .top)))    
    
  • 익스텐션 구현 복잡함 제거, 재사용성 높임
      extension AnyTransition {
          static var fadeAndMove: AnyTransition {
              AnyTransition.opacity.combined(with: .move(edge: .top))
          }
      }
    
      .transition(.fadeAndMove)    
    

비대칭 전환

뷰 인/아웃시 서로 다른 전환 지정 -> 전환이 비대칭적 이도록 선언

.transition(.asymmetric(insertion: .scale, removal: .slide))

인: scale 전환, 아웃: slide 전환

카테고리:

업데이트: