You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为已有翻转动画的SwiftUI闪卡视图添加滑动过渡效果?

闪卡左右滑动切换动画实现方案

问题描述

我开发了一个闪卡功能,参考现有代码实现了卡片翻转动画,但目前SwiftUI相关技术能力有限。现在希望为视图中的卡片添加滑动过渡效果:点击前进按钮时卡片从右侧滑至屏幕中央,点击后退按钮时从左侧滑至中央,请问是否可行?

实现方案

当然可行,只需通过以下几步修改现有代码即可实现:

  • 添加方向枚举与状态变量:用来记录当前卡片切换的方向(前进/后退)
  • 为卡片视图添加过渡动画:根据切换方向设置对应的滑入过渡效果
  • 包裹按钮点击的动画逻辑:在切换卡片计数器时,同步更新方向并触发动画
  • 重置卡片翻转状态:切换卡片时重置翻转相关的状态,避免显示异常

修改后的完整代码

import SwiftUI

// 新增:定义滑动方向枚举
enum SlideDirection {
    case left, right, none
}

struct flashCardActivity: View {
    
    @State var flashCardObj: flashCardObject
    
    @State var flipped = false
    @State var animate3d = false
    @State var showButtonSet = false
    
    @State var counter: Int = 0
    // 新增:记录滑动方向
    @State private var slideDirection: SlideDirection = .none
    
    var body: some View {
        
        VStack {
            progressBar(counter: self.$counter, totalCards: flashCardObj.words.count)
                .padding(.top, 175)
                .padding(.bottom, 80)
            
            Spacer()
            
            // 修改:添加过渡动画与动画包裹
            cardView(flipped: self.$flipped, animate3d: self.$animate3d, counter: self.$counter, flashCardObj: self.$flashCardObj)
                .transition(
                    slideDirection == .right ? .move(edge: .trailing) :
                    slideDirection == .left ? .move(edge: .leading) : .identity
                )
            
            rightWrongButtonSet().padding(.top, 20)
            saveToMyListButton().padding(.top, 25)
            
            nextPreviousButtonSet(fcO: self.$flashCardObj, counter: self.$counter, slideDirection: $slideDirection, flipped: $flipped, animate3d: $animate3d)
                .offset(y:50)
            
        }.offset(y:-80)
        
    }
}

struct cardView: View {
    
    @Binding var flipped: Bool
    @Binding var animate3d: Bool
    @Binding var counter: Int
    @Binding var flashCardObj: flashCardObject
    
    var body: some View{
        ZStack() {
            flashCardItal(counter: $counter, fcO: $flashCardObj).opacity(flipped ? 0.0 : 1.0)
            flashCardEng(counter: $counter, fcO: $flashCardObj).opacity(flipped ? 1.0 : 0.0)
        }
        .modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
        .onTapGesture {
            withAnimation(Animation.linear(duration: 0.8)) {
                self.animate3d.toggle()
            }
        }
        
    }
}

struct FlipEffect: GeometryEffect {
    
    var animatableData: Double {
        get { angle }
        set { angle = newValue }
    }
    
    @Binding var flipped: Bool
    var angle: Double
    let axis: (x: CGFloat, y: CGFloat)
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        
        DispatchQueue.main.async {
            self.flipped = self.angle >= 90 && self.angle < 270
        }
        
        let tweakedAngle = flipped ? -180 + angle : angle
        let a = CGFloat(Angle(degrees: tweakedAngle).radians)
        
        var transform3d = CATransform3DIdentity;
        transform3d.m34 = -1/max(size.width, size.height)
        
        transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
        
        let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
        
        return ProjectionTransform(transform3d).concatenating(affineTransform)
    }
}

struct flashCardItal: View {
    
    @Binding var counter: Int
    @Binding var fcO: flashCardObject
    
    var body: some View{
        Text(fcO.words[counter].wordItal)
            .font(Font.custom("Marker Felt", size: 40))
            .foregroundColor(Color.black)
            .frame(width: 325, height: 250)
            .background(Color.teal)
            .cornerRadius(20)
            .shadow(radius: 10)
            .padding(.bottom, 30)
            .padding([.leading, .trailing], 10)
            .zIndex(1)
    }
}

struct flashCardEng: View {
    
    @Binding var counter: Int
    @Binding var fcO: flashCardObject
    
    var body: some View{
        VStack{
            Text(fcO.words[counter].wordEng)
                .font(Font.custom("Marker Felt", size: 40))
                .foregroundColor(Color.black)
                .padding(.bottom, 30)
                .padding([.leading, .trailing], 10)
            
            Text(fcO.words[counter].gender.rawValue)
                .font(Font.custom("Marker Felt", size: 30))
                .foregroundColor(Color.black)
                .padding(.top, 2)
                .padding([.leading, .trailing], 10)
            
        } .frame(width: 325, height: 250)
            .background(Color.teal)
            .cornerRadius(20)
            .shadow(radius: 10)
    }
}

struct rightWrongButtonSet: View{
    var body: some View{
        
        HStack{
            
            Button(action: {
                
            }, label:
                    {Image("cancel")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 65, height: 65)
            }).padding(.leading, 80)
            
            Spacer()
            
            Button(action: {
                
            }, label:
                    {Image("checked")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 65, height: 65)
            }).padding(.trailing, 80)
            
        }
        
    }
}

struct saveToMyListButton: View{
    var body: some View{
        Button(action: {}, label: {Text("Save to My List")})
    }
}

// 修改:更新按钮视图,添加方向绑定与状态重置
struct nextPreviousButtonSet: View{
    
    @Binding var fcO: flashCardObject
    @Binding var counter: Int
    @Binding var slideDirection: SlideDirection
    @Binding var flipped: Bool
    @Binding var animate3d: Bool
    
    var body: some View{
        HStack{
            Button(action: {
                if counter > 0 {
                    withAnimation(.easeInOut(duration: 0.5)) {
                        slideDirection = .left
                        counter -= 1
                        // 重置翻转状态
                        flipped = false
                        animate3d = false
                    }
                }
            }, label:
                    {Image(systemName: "arrow.backward").resizable()
                    .bold()
                    .scaledToFit()
                    .frame(width: 65, height: 65)
                    .foregroundColor(Color.black)
            }).padding(.leading, 90)
            
            Spacer()
            
            Button(action: {
                if counter < fcO.words.count - 1 {
                    withAnimation(.easeInOut(duration: 0.5)) {
                        slideDirection = .right
                        counter += 1
                        // 重置翻转状态
                        flipped = false
                        animate3d = false
                    }
                }
            }, label:
                    {Image(systemName: "arrow.forward").resizable()
                    .bold()
                    .scaledToFit()
                    .frame(width: 65, height: 65)
                    .foregroundColor(Color.black)
            }).padding(.trailing, 90)
        }
    }
}

struct progressBar: View {
    
    @Binding var counter: Int
    let totalCards: Int
    
    var body: some View {
        VStack {
            
            Text(String(counter) + "/" + String(totalCards)).offset(y:20)
            
            ProgressView("", value: Double(counter), total: Double(totalCards))
                .frame(width: 300).cornerRadius(10)
                .scaleEffect(x: 1, y: 4)
            
        }
    }
}

// 补充原代码缺失的定义,保证可编译
enum Gender: String {
    case masculine, feminine, neuter
}

struct Word {
    let wordItal: String
    let wordEng: String
    let gender: Gender
}

struct flashCardObject {
    let words: [Word]
    
    static let Food = flashCardObject(words: [
        Word(wordItal: "Pizza", wordEng: "Pizza", gender: .feminine),
        Word(wordItal: "Pasta", wordEng: "Pasta", gender: .feminine)
    ])
}

struct flashCardActivity_Previews: PreviewProvider {
    static var previews: some View {
        flashCardActivity(flashCardObj: flashCardObject.Food)
    }
}

关键修改说明

  • 新增SlideDirection枚举,标记卡片滑入的方向
  • 在主视图中添加slideDirection状态变量,控制过渡动画的触发方向
  • 为卡片视图绑定过渡效果,根据方向选择从左侧或右侧滑入
  • 修改按钮点击逻辑,用withAnimation包裹状态更新,确保动画流畅执行,同时重置卡片翻转状态,保证新卡片以正面显示
  • 补充了原代码缺失的flashCardObject相关定义,让代码可以直接编译运行

内容的提问来源于stack exchange,提问作者mzm0227

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 14:15:00