이번에 실무에서 아래와 같은 뷰를 만들어 달라는 요청을 받았습니다. 탭을 할 때마다 녹색, 노란색, 빨간색 원이 차례로 선택됩니다. 선택된 원은 다른 원 보다 알파값이 높고 크기도 조금 더 큽니다. 그리고 선택지가 바뀔 때 마다 각각의 원이 작아졌다가 다시 커지는 애니메이션을 구현해야 합니다.
이 뷰를 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로 돌아오는 간단한 뷰입니다. 하지만 위 코드를 실행시킨 결과는 아래와 같습니다. 애니메이션이 적용되지 않는 것을 볼 수 있습니다.
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
}
}
}
}
}