Animating Views and Transitions

SwiftUI를 사용 할때, 효과가 있는 곳과 상관없이, 뷰나 뷰의 상태에 대한 변경에 대해 개별적으로 애니메이션화 할 수 있습니다. SwiftUI는 결합, 중첩, 중단시키는 애니메이션들의 복잡한 모든 것을 처리합니다.

이 튜토리얼에서, 사용자가 랜드마크(Landmark) 앱을 사용하는 동안에 하이킹(hikes)을 추적하는 그래프를 포함한 뷰를 애니메이션할 것입니다. animation(_:) 수식어(modifier)를 사용해서, 뷰가 애니메이션을 얼마나 쉽게 하는지 보게될 것입니다.

시작 프로젝트를 다운로드하고 이 튜토리얼을 따라하거나 완성된 프로젝트를 열고 코드를 살펴보세요.

[프로젝트 파일 다운로드]

Section 1. 개별 뷰에 애니메이션 추가하기(Add Animations to Individual Views)

뷰에서 animation(_:) 수식어를 사용할때, SwiftUI는 뷰의 애니메이션 가능한 속성들에 대한 모든 변경 사항을 애니메이션화합니다. 뷰의 색상, 불투명도, 회전, 크기, 다른 속성들은 모두 애니메이션 가능합니다.

1 단계

HikeView.swift안에서, 라이브 미리보기를 켜고 그래프를 보여주고 숨기는 실험을 합니다. 

이 튜토리얼 전체에서 라이브 미리보기를 사용해서 각 단계별 결과를 실험할 수 있습니다.

https://docs-assets.developer.apple.com/published/5f64eb5dc3/_p22_explore-no-animations.mp4

2 단계

animation(.basic())을 추가해서 버튼 회전에 대한 애니메이션을 켭니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .padding()
                        .animation(.basic())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

3 단계

그래프가 보여질때 버튼을 더 크개해서 다른 애니메이션 가능한 변경사항을 추가합니다.

뷰안의 모든 애니메이션 가능한 변경을 적용하기 위해 animation(_:) 수식어를 사용합니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                        .animation(.basic())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

4 단계

애니메이션 타입을 basic()에서 spring()으로 변경합니다.

SwiftUI는 스프링(spring)과 부드러운(fluid) 애니메이션을 포함해서 미리정의 되었거나 사용자정의가 쉬운 기본 애니메이션을 포함합니다. 애니메이션의 속도, 애니메이션 시작 전에 지연 설정하거나 애니메이션 반복 횟수를 지정할 수 있습니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                        .animation(.spring())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

5 단계

scaleEffect 수식 바로 위에 다른 애니메이션 수정자를 추가해서 회전에 대한 애니메이션을 끄세요.

실험(Experiment)
회전에 대한 SwiftUI를 사용합니다. 무엇이 가능한지 보기 위해 다양한 애니메이션 효과를 조합해 보세요.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .animation(nil)
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                        .animation(.spring())
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

6 단계

다음 섹션으로 넘어가기 전에 animation(_:) 수식어를 모두 제거합니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    self.showDetail.toggle()
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))

                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()

                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

Section 2. 상태 변경의 효과 애니메이션하기(Animate the Effects of State Changes)

개별 뷰에 어떻게 애니메이션을 적용하는지 배웠습니다. 이번에는 상태 값이 변경되는 곳에 애니메이션을 추가합니다.

여기에서, 사용자가 버튼을 누르고 showDetail 상태 속성을 토글할때 발생하는 모든 변경사항에 대해 애니메이션을 적용할 것입니다.

1 단계

showDetail.toggle() 호출하기 위해 withAnimation 함수 호출로 감쌉니다.

showDetail 속성에 영향을 받는 뷰 모두(보여진 버튼과 HikeDetail 뷰)는 현재 전환 애니메이션을 가지고 있습니다. 

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

애니메이션 속도를 줄여서 SwiftUI 애니메이션이 중단되는 방법을 확인합니다.

2 단계

Animation 함수에 4초간의 basic 애니메이션을 전달합니다. 

animation(_:) 수식어에 전달된 withAnimation 함수에 동일한 애니메이션을 전달 할 수 있습니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation(.basic(duration: 4)) {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

3 단계

그래프 뷰 중간 애니메이션을 열고 닫는 실험을 해보세요.

https://docs-assets.developer.apple.com/published/da9238c645/_p22_slow-animation.mp4

4 단계

다음 섹션을 진행하기 전에 withAnimation 함수를 호출에서 느린 애니메이션을 제거합니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
            }
        }
    }
}

Section 3. 뷰 전환 사용자 정의하기(Customize View Transitions)

기본적으로, 뷰를 페이드 인(fading in)과 아웃(fading out)으로 화면 안과 바깥으로 전환합니다. 여러분은 transition(_:) 수식어를 사용해서 전환을 사용자정의 할 수 있습니다.

https://docs-assets.developer.apple.com/published/bb9d5e87d6/customize-view-transitions.mp4

1 단계

HikeView를 보여주는 조건부에 transition(_:) 수식어를 추가합니다.

이제 그래프는 보여지고 슬라이딩 애니메이션으로 사라집니다.

import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(.slide)
            }
        }
    }
}

2 단계

AnyTransition의 정적 속성으로 전환하는 것을 추출합니다.

이렇게 하면 사용자 정의 전환을 확장할때 처럼 코드가 깔끔해집니다. SwiftUI에 포함되어 있던 것처럼 사용자정의 전환에 점 표기법(dot notatino)을 사용할 수 있습니다.

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.slide
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}

3 단계

move(edge:) 전환을 사용하는 것으로 교체하며, 같은 방향으로 슬라이드 들어오고 나가도록 합니다.

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        AnyTransition.move(edge: .trailing)
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}

4 단계

뷰가 나타나고 사라질때 다른 전환을 제공하기 위해 asymmetric(insertion:removal:) 수식어를 사용합니다.

import SwiftUI

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale()
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = false

    var body: some View {
        VStack {
            HStack {
                HikeGraph(data: hike.observations, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button(action: {
                    withAnimation {
                        self.showDetail.toggle()
                    }
                }) {
                    Image(systemName: "chevron.right.circle")
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    .transition(.moveAndFade)
            }
        }
    }
}

Section 4. 복잡한 효과에 대한 애니메이션 구성하기(Compose Animations for Complex Effects)

하단 바의 버튼들을 클릭할때 그래프가 3가지 다른 데이터 세트로 전환됩니다. 이번 섹션에서, 캡슐에 그래프가 동적이고, 물결로 전환되도록 만드는 합성된 애니메이션을 사용할 것입니다.

1 단계

showDetail에 대한 기본 값을 true로 변경하고, 캔버스에 HikeView 미리보기를 고정시킵니다.

이렇게 하면 여러분이 다른 파일에서 애니메이션을 작업하는 동안 상황에 맞게 그래프를 볼수 있습니다.

2 단계

GraphCapsule.swift에서, 새로운 계산된 animation 속성을 추가하고 캡슐 모양에 적용합니다.

import SwiftUI

struct GraphCapsule: View {
    var index: Int
    var height: Length
    var range: Range<Double>
    var overallRange: Range<Double>

    var heightRatio: Length {
        max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
    }

    var offsetRatio: Length {
        Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
    }

    var animation: Animation {
        Animation.default
    }

    var body: some View {
        Capsule()
            .fill(Color.gray)
            .frame(height: height * heightRatio, alignment: .bottom)
            .offset(x: 0, y: height * -offsetRatio)
            .animation(animation)
        )
    }
}

3 단계

바(bar)가 초기 속도로 뛰어오르도록 animation을 스프링 애니메이션으로 교체합니다.

import SwiftUI

struct GraphCapsule: View {
    var index: Int
    var height: Length
    var range: Range<Double>
    var overallRange: Range<Double>

    var heightRatio: Length {
        max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
    }

    var offsetRatio: Length {
        Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
    }

    var animation: Animation {
        Animation.spring(initialVelocity: 5)
    }

    var body: some View {
        Capsule()
            .fill(Color.gray)
            .frame(height: height * heightRatio, alignment: .bottom)
            .offset(x: 0, y: height * -offsetRatio)
            .animation(animation)
        )
    }
}

4 단계

각 바(bar)가 새로운 위치로 이동하는 시간을 줄이기 위해 animation에 속도를 약간 올립니다.

import SwiftUI

struct GraphCapsule: View {
    var index: Int
    var height: Length
    var range: Range<Double>
    var overallRange: Range<Double>

    var heightRatio: Length {
        max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
    }

    var offsetRatio: Length {
        Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
    }

    var animation: Animation {
        Animation.spring(initialVelocity: 5)
            .speed(2)
    }

    var body: some View {
        Capsule()
            .fill(Color.gray)
            .frame(height: height * heightRatio, alignment: .bottom)
            .offset(x: 0, y: height * -offsetRatio)
            .animation(animation)
        )
    }
}

5 단계

그래프의 캡슐 위치를 기반으로한 각 animation에 지연시간(delay)를 추가합니다.

import SwiftUI

struct GraphCapsule: View {
    var index: Int
    var height: Length
    var range: Range<Double>
    var overallRange: Range<Double>

    var heightRatio: Length {
        max(Length(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
    }

    var offsetRatio: Length {
        Length((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
    }

    var animation: Animation {
        Animation.spring(initialVelocity: 5)
            .speed(2)
            .delay(0.03 * Double(index))
    }

    var body: some View {
        Capsule()
            .fill(Color.gray)
            .frame(height: height * heightRatio, alignment: .bottom)
            .offset(x: 0, y: height * -offsetRatio)
            .animation(animation)
        )
    }
}

6 단계

그래프간 전환할때 물결효과를 제공하는 사용자정의 애니메이션을 관찰합니다.

 

이해하는지 확인하기(Check Your Understanding)

퀴즈 1. 일련의 수식어들에서 특정 수식어로 애니메이션 적용되는 것을 어떻게 막을 수 있나요? 바꿔말하면, 다음 예제에서 애니메이션되는 회전 효과를 어떻게 막을수 있나요?

Image(systemName: "chevron.right.circle")
    .imageScale(.large)
    .rotationEffect(.degrees(showDetail ? 90 : 0))
    .scaleEffect(showDetail ? 1.5 : 1)
    .padding()
    .animation(.spring())

1. animation(_:) 수식어에 nil을 전달하기

Image(systemName: "chevron.right.circle")
    .imageScale(.large)
    .rotationEffect(.degrees(showDetail ? 90 : 0))
    .animation(nil)
    .scaleEffect(showDetail ? 1.5 : 1)
    .padding()
    .animation(.spring())

 

2. rotationEffect(_:) 수식어는 애니메이션할 수 없기에, 애니메이션을 막기 위해 코드를 변경할 필요가 없습니다.

3. withoutAnimation(_:) 수식어를 적용해서 애니메이션 되는것을 보호해줍니다.

Image(systemName: "chevron.right.circle")
    .imageScale(.large)
    .withoutAnimation {
        $0.rotationEffect(.degrees(showDetail ? 90 : 0))
    }
    .scaleEffect(showDetail ? 1.5 : 1)
    .padding()
    .animation(.spring())

[정답 : 1]
rotationEffect(_:) 수식어를 사용해서 생성한 애니메이션 회전을 사용할 수 있습니다. 

퀴즈 2. 애니메이션을 개발하고 개선할때 왜 캔버스에 미리보기를 고정시킬까요?

  1. 애니메이션의 현재 프레임을 고정시키기 위해서
  2. 여러 기기 환경 설정에서 개발하는 애니메이션을 미리보기하기 위해서
  3. Xcode에서 다른 파일로 교체하는 동안에 특정 미리보기를 열어두기 위해서

[정답 : 3]
미리보기를 고정하지 않으면, 캔버스는 방금 열었던 파일에 대한 미리보기로 교체됩니다.

퀴즈 3. 상태가 변경되는 것과 같은 애니메이션을 중단하는 테스트를하는 빠른 방법은 무엇인가요?

  1. animation(_:) 수식어가 포함된 줄마다 중단점을 추가해서 프레임별로 애니메이션 단계를 수행합니다.
  2. 세부사항을 충분히 관찰할수 있도록 애니메이션의 수행시간을 조정합니다.
  3. 애니메이션 속도를 줄이기 위해 sleep(100) 을 반복해서 호출합니다.

[정답 : 2]
애니메이션을 길게 수행하도록 만드는 것이 애니메이션을 반복하는데 효과적이며 빠르고 쉽게 할 수 있는 변경입니다.

'SwiftUI > Tutorials' 카테고리의 다른 글

Working with UI Controls  (0) 2019.08.13
Composing Complex Interfaces  (0) 2019.08.13
Animating Views and Transitions  (0) 2019.08.13
Drawing Paths and Shapes  (0) 2019.08.13
Handling User Input  (0) 2019.08.13
Building Lists and Navigation  (0) 2019.07.23
Posted by 까칠코더