SwiftUI: 아핀 변환 + 애니메이션

SteadySlower·2023년 3월 1일
0

SwiftUI

목록 보기
39/64
post-custom-banner

만들고자 하는 View

이번에 실무에서 아래와 같은 뷰를 만들어 달라는 요청을 받았습니다. 탭을 할 때마다 녹색, 노란색, 빨간색 원이 차례로 선택됩니다. 선택된 원은 다른 원 보다 알파값이 높고 크기도 조금 더 큽니다. 그리고 선택지가 바뀔 때 마다 각각의 원이 작아졌다가 다시 커지는 애니메이션을 구현해야 합니다.

.transformEffect로 만들기

이 뷰를 SwiftUI로 만들어야 합니다. 제가 가장 먼저 떠올린 생각은 아핀 변환을 사용하는 것입니다. (아핀 변환에 대한 설명은 이 포스팅을 참고하세요.) UIKit이었다면 분명히 아핀 변환을 활용했을 것입니다. 아핀변환을 SwiftUI에 적용하는 방법은 .transformEffect를 활용하는 것입니다.

import SwiftUI

//🚫 안되는 예시
struct ExampleAni: View {
    
    @State var transfrom: CGAffineTransform = .init(scaleX: 0, y: 0)
    
    var body: some View {
        VStack {
            Circle()
                .foregroundColor(.blue)
                .transformEffect(transfrom)
            Button("커져라") {
                withAnimation(.linear(duration: 0.5)) {
                    transfrom = .identity
                }
            }
        }
    }
}

그래서 코드를 위와 같이 만들어 보았습니다. @State 변수로 크기를 0으로 만드는 아핀 변환을 가지고 있고 버튼을 누르면 아핀 변환이 .identity로 돌아오는 간단한 뷰입니다. 하지만 위 코드를 실행시킨 결과는 아래와 같습니다. 애니메이션이 적용되지 않는 것을 볼 수 있습니다.

.scaleEffect 활용하기

SwiftUI에서 우리가 원하는 애니메이션을 구현하기 위해서는 .scaleEffect를 활용해야 합니다. 이 메소드의 설명에는 아핀 변환이라는 말이 나와있지는 않지만 구현되는 것은 아핀변환과 동일합니다.

그리고 scaleEffect 이외에도 아핀변환과 마찬가지로 offset, rotationEffect 뿐만 아니라 rotation3DEffect 역시 각각의 메소드를 통해서 구현할 수 있습니다.

import SwiftUI

//✅ 되는 예시
struct Example2Ani: View {
    @State var scale = 0.0
    
    var body: some View {
        VStack {
            Circle()
                .foregroundColor(.blue)
                .scaleEffect(scale)
            Button("커져라") {
                withAnimation(.linear(duration: 0.5)) {
                    scale = 1.0
                }
            }
        }
    }
}


1%85%AC%E1%84%82%E1%85%B3%E1%86%AB_%E1%84%8B%E1%85%A8%E1%84%89%E1%85%B5.gif)

만들고자 하는 뷰의 코드

위와 같이 .scaleEffect를 활용해서 맨 처음에 보여드린 신호등 버튼을 구현했습니다.

import SwiftUI

enum Signal: Int {
    case go = 0, wait, stop

    var color: Color {
        switch self {
        case .go: return .green
        case .wait: return .yellow
        case .stop: return .red
        }
    }
}

struct AffineAni: View {

    @State var signal: Signal = .stop
    @State var isAnimating: Bool = false

    var body: some View {
        HStack(alignment: .center) {
            Light(type: .go, isOn: signal == .go, scale: scale(.go))
            Light(type: .wait, isOn: signal == .wait, scale: scale(.wait))
            Light(type: .stop, isOn: signal == .stop, scale: scale(.stop))
        }
        .onTapGesture { tapped() }
    }

    func tapped() {
        isAnimating = true
        withAnimation(.linear(duration: 0.5)) {
            isAnimating = false
            let nextSignalRawValue = (signal.rawValue + 1) % 3
            signal = Signal(rawValue: nextSignalRawValue) ?? .stop
        }
    }
    
    func scale(_ of: Signal) -> Double {
        if isAnimating {
            return 0.0
        } else {
            return signal == of ? 1 : 0.8
        }
    }
}

struct Light: View {

    let type: Signal
    let isOn: Bool
    let scale: Double
    
    var body: some View {
        Circle()
            .foregroundColor(type.color.opacity(isOn ? 1: 0.5))
            .scaleEffect(scale)
    }
}

참고

위에서 소개한 scaleEffect, offset, rotationEffect, rotation3DEffect을 모두 적용해서 만든 애니메이션 예시입니다.

import SwiftUI

struct Example2Ani: View {
    @State var scale = 0.1
    @State var offset = 50.0
    @State var degree = 90.0
    @State var degree3D = 180.0
    
    var body: some View {
        VStack {
            Rectangle()
                .foregroundColor(.blue)
                .scaleEffect(scale)
                .offset(x: offset, y: offset)
                .rotationEffect(.degrees(degree))
                .rotation3DEffect(
                    Angle(degrees: degree3D),
                    axis: (x: 1, y: 2, z: 3)
                )
            Button("커져라") {
                withAnimation(.linear(duration: 0.5)) {
                    scale = 1.0
                    offset = 0.0
                    degree = 0
                    degree3D = 0
                }
            }
        }
    }
}

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.
post-custom-banner

0개의 댓글