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

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