[SwiftUI] DragGesture Animation

Junyoung Park·2022년 11월 10일
0

SwiftUI

목록 보기
96/136
post-thumbnail
post-custom-banner

SwiftUI Coffee App Animations | SwiftUI Challenge | Animations | Xcode 14

DragGesture Animation

구현 목표

  • 드래그 제스처를 통한 이미지 효과

구현 태스크

  • 드래그를 통한 이미지 이동 결정
  • 스크롤 정도에 대한 이미지 크기 결정

핵심 코드

...
.gesture(
            DragGesture()
                .onChanged({ value in
                    offsetY = value.translation.height * 0.5
                })
                .onEnded({ value in
                    let translation = value.translation.height
                    
                    withAnimation(.easeInOut) {
                        if translation > 0 {
                            if currentIndex > 0 && translation > 250 {
                                currentIndex -= 1
                            }
                        } else {
                            if currentIndex < CGFloat(viewModel.model.count - 1) && -translation > 250 {
                                currentIndex += 1
                            }
                        }
                        offsetY = .zero
                    }
                })
        )
  • 부모 뷰의 제스처 이벤트를 감지하는 부분
  • 드래그 방향 및 드래그 정도를 통해 인덱스 이동 방향 결정

소스 코드

import SwiftUI

struct HomeView: View {
    private let viewModel = CoffeeViewModel()
    @State private var offsetY: CGFloat = 0
    @State private var currentIndex: CGFloat = 0
    var body: some View {
        GeometryReader { geometry in
            let size = geometry.size
            let cardSize = size.width
            gradientView
            headerView
            VStack(spacing: 0) {
                ForEach(viewModel.model) { coffee in
                    CoffeeView(coffee: coffee, size: size)
                }
            }
            .frame(width: size.width)
            .padding(.top, size.height - cardSize)
            .offset(y: offsetY)
            .offset(y: -currentIndex * cardSize)
        }
        .coordinateSpace(name: "SCROLL")
        .contentShape(Rectangle())
        .gesture(
            DragGesture()
                .onChanged({ value in
                    offsetY = value.translation.height * 0.5
                })
                .onEnded({ value in
                    let translation = value.translation.height
                    
                    withAnimation(.easeInOut) {
                        if translation > 0 {
                            if currentIndex > 0 && translation > 250 {
                                currentIndex -= 1
                            }
                        } else {
                            if currentIndex < CGFloat(viewModel.model.count - 1) && -translation > 250 {
                                currentIndex += 1
                            }
                        }
                        offsetY = .zero
                    }
                })
        )
    }
}
  • 드래그 제스처를 통해 오프셋 결정
  • 드래그 제스처 완료 시 드래그 방향과 드래그 정도를 통해 카드 이동 방향 및 이동 자체를 결정
  • coordinateSpace를 통해 자식 뷰 커피 뷰에서 부모 뷰인 홈 뷰의 스크롤 이벤트를 감지 가능
extension HomeView {
    private var gradientView: some View {
        LinearGradient(colors: [.clear, Color.brown.opacity(0.2), Color.brown.opacity(0.45), Color.brown], startPoint: .top, endPoint: .bottom)
            .frame(height: 300)
            .frame(maxHeight: .infinity, alignment: .bottom)
            .ignoresSafeArea()
    }
    
    private var headerView: some View {
        VStack {
            HStack {
                Button {
                    
                } label: {
                    Image(systemName: "chevron.left")
                        .font(.title2.bold())
                        .foregroundColor(.black)
                }
                Spacer()
                Button {
                    
                } label: {
                    Image(systemName: "cart")
                        .resizable()
                        .renderingMode(.template)
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .foregroundColor(.black)
                }
            }
            
            GeometryReader { geometry in
                let size = geometry.size
                HStack(spacing: 0) {
                    ForEach(viewModel.model) { coffee in
                        VStack(spacing: 15) {
                            Text(coffee.title)
                                .font(.title.bold())
                                .multilineTextAlignment(.center)
                            Text(coffee.price)
                                .font(.title)
                        }
                        .frame(width: size.width)
                    }
                }
                .offset(x: currentIndex * -size.width)
                .animation(.interactiveSpring(response: 0.6, dampingFraction: 0.8, blendDuration: 0.8), value: currentIndex)
            }
            .padding(.top, -5)
        }
        .padding(15)
    }
}
  • 수평 방향의 이미지 뷰 이외의 수직 방향의 헤더 뷰 구현
  • 지오메트리 리더를 통해 커피 뷰와 동일한 로직으로 구성
  • 수평 방향이므로 오프셋은 x 축 기준
  • currentIndex는 이미지 뷰를 위아래로 움직일 때 변경 가능, 이 변경된 값을 통해 HStack으로 쌓인 현재 헤더 뷰의 오프셋을 이동
import SwiftUI

struct CoffeeView: View {
    let coffee: CoffeeModel
    let size: CGSize
    var body: some View {
        let cardSize = size.width * 1
        let maxCardsDisplaySize = size.width * 4
        GeometryReader { geometry in
            let _size = geometry.size
            let offset = geometry.frame(in: .named("SCROLL")).minY - (size.height - cardSize)
            let scale = offset <= 0 ? (offset / maxCardsDisplaySize) : 0
            let reducedScale = 1 + scale
            let currentCardScale = offset / cardSize
            Image(coffee.imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: _size.width, height: _size.height)
                .scaleEffect(reducedScale < 0 ? 0.001 : reducedScale, anchor: .init(x: 0.5, y: 1 - (currentCardScale / 2.4)))
                .scaleEffect(offset > 0 ? 1 + currentCardScale : 1, anchor: .top)
                .offset(y: offset > 0 ? currentCardScale * 200 : 0)
                .offset(y: currentCardScale * -130)
        }
        .frame(height: cardSize)
    }
}
  • 주어진 프레임 사이즈를 지오메트리 리더를 통해 얻어옴
  • 홈 뷰의 스크롤 프레임을 읽어온 뒤 오프셋을 얻어옴
  • 오프셋을 통해 어느 정도로 현재 이미지를 확대할 것인지 결정
  • 카드를 올릴 때 축소, 카드를 내릴 때 확대 효과
import Foundation

class CoffeeViewModel {
    let model: [CoffeeModel]
    
    init() {
        var mockData = [CoffeeModel]()
        for x in 1...6 {
            let imageName = "item_\(x)"
            let title = "Coffee_\(x)"
            if let randomNumber = (100...150).randomElement() {
                let priceNumber = Double(randomNumber) / 10.0
                let priceString = "$" + String(priceNumber)
                let mockItem = CoffeeModel(imageName: imageName, title: title, price: priceString)
                mockData.append(mockItem)
            }
        }
        self.model = mockData
        print(mockData)
    }
}
  • Assets의 이미지 이름을 담고 있는 데아토 배열
import Foundation

struct CoffeeModel: Identifiable {
    let id = UUID().uuidString
    let imageName: String
    let title: String
    let price: String
}
  • Assets에 저장된 이름을 담고 있는 데이터 모델

구현 화면

iOS 환경에서는 낯선 애니메이션이었다. 하지만 애플 프로덕트 페이지 등 뛰어난 시각적 효과를 주는 웹 디자인에서는 그리 낯설지 않은 모습인 듯하다. 무엇보다도 아직까지 이해가 잘 되지 않은 강의 중 하나다. 사실 앱 내 원활한 애니메이션을 적용하기 위해서는 기본적인 수학이 전제되어 있어야 한다는 점에서 공부가 더 필요하다! 열심히 하자!

profile
JUST DO IT
post-custom-banner

0개의 댓글