SwiftUI极简教程19:SwipeCard卡片滑动效果的使用(下)

简介: SwiftUI极简教程19:SwipeCard卡片滑动效果的使用(下)

承接上一章的内容,本章继续完成如何使用Gestures手势和Animations动画实现SwipeCard卡片滑动的效果。


在动态创建新的卡片视图之前,我们必须首先实现Gestures手势特性。拖动卡片视图状态最好的方法是使用Enum枚举,Enum枚举可以将isPresseddragOffset状态合并到一个属性中。


Gestures手势特性


我们声明一个叫做DragState的枚举:


//Gestures手势特性
enum DragState {
    case inactive
    case pressing
    case dragging(translation: CGSize)
var translation: CGSize {
    switch self {
        case .inactive, .pressing:
    return .zero
        case .dragging(let translation):
    return translation
    }
}
var isDragging: Bool {
    switch self {
        case .dragging:
    return true
        case .pressing, .inactive:
    return false
    }
}
var isPressing: Bool {
    switch self {
        case .pressing, .dragging:
    return true
        case .inactive:
    return false
        }
     }
}


Enum枚举有三种状态:不活动,按下和和拖拽。接下来,让我们定义一个@GestureState变量来存储拖动状态,它默认被设置为非活动状态:


@GestureState private var dragState = DragState.inactive


然后我们给CardView加上Gestures手势特性。


.offset(x: self.dragState.translation.width, y: self.dragState.translation.height)
.scaleEffect(self.dragState.isDragging ? 0.95 : 1.0)
.rotationEffect(Angle(degrees: Double( self.dragState.translation.width / 10)))
.animation(.interpolatingSpring(stiffness: 180, damping: 100),value: offset)
.gesture(LongPressGesture(minimumDuration: 0.01)
.sequenced(before: DragGesture())
    .updating(self.$dragState, body: { (value, state, transaction) in
        switch value {
            case .first(true):
                state = .pressing
            case .second(true, let drag):
                state = .dragging(translation: drag?.translation ?? .zero)
            default:
                break
            }
    })
)


image.png

我们顺便给BottomBarMenu底部导航栏加个交互效果,当我们左右滑动CardView卡片视图时,隐藏BottomBarMenu底部导航栏。


BottomBarMenu()
    .opacity(dragState.isDragging ? 0.0 : 1.0)
    .animation(.default, value: offset)


image.png

我们发现,好像最展示了一张CardView卡片视图,其实不是的,而是我们创建的两个视图重叠了,执行一样的动作,所以才看起来只有一张图片卡片。


我们只需要在offset偏移量、 scaleEffect缩放和rotationEffect旋转做修订,判断操作的对象是不是album卡片视图,保证设置的操作只作用在最上面的卡片视图就可以了。


//偏移
.offset(x: self.isTopCard(cardView: album) ? self.dragState.translation.width : 0, y: self.isTopCard(cardView: album) ?self.dragState.translation.height : 0)
//缩放
.scaleEffect(self.dragState.isDragging && self.isTopCard(cardView: album) ? 0.95 : 1.0)
//旋转
.rotationEffect(Angle(degrees: self.isTopCard(cardView: album) ? Double( self.dragState.translation.width / 10) : 0))

image.png

好了,我们完成了卡片的基础的左右滑动了!


展示like/dislike图


下面我们再完善下样式,当我们向右滑动CardVIew卡片的时候,展示一个点赞表示喜欢,当我们左滑动卡片的时候,展示一个点差表示不喜欢

我们在ContentView定义一个拖拽位置dragPosition参数,当我们拖拽CardVIew卡片到一定的位置的时候,就展示喜欢或者不喜欢


private let dragPosition: CGFloat = 80.0


我们可以使用.overlay覆盖修饰符,将我们想要的样式覆盖在CardVIew卡片上,我们把代码放在.zIndex修饰符后面。


我们就实现了向左滑动超过dragPosition参数时,CardVIew卡片展示不喜欢Image图片,向右滑动超过dragPosition参数时,CardVIew卡片展示喜欢Image图片。


//判断喜欢或者不喜欢
.overlay(
    ZStack {
        Image(systemName: "hand.thumbsdown.fill")
            .foregroundColor(.white)
            .font(.system(size: 100))
            .opacity(self.dragState.translation.width < -self.dragPosition && self .isTopCard(cardView: album) ? 1.0 : 0)
        Image(systemName: "hand.thumbsup.fill")
            .foregroundColor(.white)
            .font(.system(size: 100))
            .opacity(self.dragState.translation.width > self.dragPosition && self .isTopCard(cardView: album) ? 1.0 : 0.0) } )

image.png

下面我们继续完成交互样式。

moveCard移除卡片视图


我们拖拽CardView卡片再放手时,CardVIew卡片会回到原来的位置。还记得之前的章节的内容吗?我们在List列表删除一条数据,系统会自动加回来的问题。

是的,我们用的是同一个方法。我们用@State标记albums图片数组,这样我们就可以更新它的值并刷新页面。

//图片数组
@State var albums: [Album] = {
    var views = [Album]()
    for index in 0 ..< 2 {
        views.append(Album(name: album[index].name, image: album[index].image))
    }
    return views
}()

image.png

然后,我们需要声明一个新的变量,来定位CardVIew卡片最后一张。当albums图片数组初始化的时候,我们显示存储在albums图片数组中的前两个图片,最后一个图片的索引值设置为1


//最后一张图片索引值
@State private var lastIndex = 1


紧接着,我们实现滑动卡片移除上面一张图片,再显示下一张图片的moveCard方法。


//移除卡片显示下一张卡片
func moveCard() {
    albums.removeFirst()
    self.lastIndex += 1
    let cards = album[lastIndex % album.count]
    let newCardView = Album(name: cards.name, image: cards.image)
    albums.append(newCardView)
}


moveCard方法首先从albums图片数组中移除最上面的卡片。然后实例化一个新的CardView卡片视图用来显示后面的Image图片。由于albums图片数组被定义为一个状态属性,因此一旦albums图片数组的值发生变化,SwiftUI将再次呈现CardView卡片视图,这就是滑动移除一张图片显示另一张图片的原理了。

image.png

然后,我们还需要更新.gesture修饰符并插入.onEnded函数,实现拖拽到dragPosition位置时调用用moveCard移除卡片方法。


//拖拽移除卡片
.onEnded({ (value) in
    guard case .second(true, let drag?) = value else {
        return
    }
    if drag.translation.width < -self.dragPosition || drag.translation.width > self.dragPosition {
        self.moveCard()
    }
})


image.png

恭喜你!又离成功近了一步!


Transition转场动画


如果我们功能做完了,但是交互效果还是差一点,为了App实际运行时的动画效果,我们可以尝试附加转场动画修饰符,并将一个不对称的转场动画应用到CardView卡片视图。

我们使用非对称转场动画是为了达到下面的效果:


1、CardVIew卡片视图被移除时动画化有转换动画,有种卡片被扔出去的感觉

2、当一个新的CardVIew卡片视图展示在上面的时候没有动画。

我们可以创建一个叫做AnyTransition的扩展,并定义两个转场动画效果:


//转场动画效果
extension AnyTransition {
    static var trailingBottom: AnyTransition {
        AnyTransition.asymmetric(
            insertion: .identity, removal: AnyTransition.move(edge: .trailing).combined(with: .move(edge : .bottom))
        )
    }
    static var leadingBottom: AnyTransition {
        AnyTransition.asymmetric(
            insertion: .identity, removal: AnyTransition.move(edge: .leading).combined(with: .move(edge: .bottom))
        )
    }
}

image.png


trailingBottom转场效果在CardView卡片视图被扔到到屏幕右侧时使用,而我们在CardView卡片视图被丢弃到屏幕左侧时应用leadingBottom转场效果。

接下来,声明一个保存转换类型的@state属性,默认为trailingBottom


//转场类型动画
@State private var removalTransition = AnyTransition.trailingBottom


然后,将.transition修饰符附加到CardView卡片视图中的.animation修饰符之后。


//转场动画
.transition(self.removalTransition)


最后,用onChanged函数更新.gesture修饰符的代码,通过设置removalTransition让转换类型根据滑动方向更新样式。


//拖动时添加转场效果
.onChanged({ (value) in
    guard case .second(true, let drag?) = value else {
        return
    }
    if drag.translation.width < -self.dragPosition {
        self.removalTransition = .leadingBottom
    }
    if drag.translation.width > self.dragPosition {
        self.removalTransition = .trailingBottom
    }
})


image.png

恭喜恭喜!

我们终于完成了整个SwipeCard卡片滑动效果的学习。

由于写的内容太多了,一边写一边改,有些地方能解释说明的尽量解释说明,但还是有很多地方没有办法一一详细说明清楚。

这两章关于SwipeCard卡片滑动效果的使用,基本涉及到了前面的所有章节的内容,因此有些方法就粗略带过了。

源代码也会分享给大家,供大家参考学习,算是一个实战小项目吧。

完整代码如下:


import SwiftUI
// 创建Album定义变量
struct Album: Identifiable {
    var id = UUID()
    var name: String
    var image: String
}
// 创建演示数据
var album = [
    Album(name: "图片01", image: "image01"),
    Album(name: "图片02", image: "image02"),
    Album(name: "图片03", image: "image03"),
    Album(name: "图片04", image: "image04"),
    Album(name: "图片05", image: "image05"),
    Album(name: "图片06", image: "image06"),
    Album(name: "图片07", image: "image07"),
    Album(name: "图片08", image: "image08"),
    Album(name: "图片09", image: "image09"),
]
// 创建2个卡片视图
var albums: [Album] = {
    var views = [Album]()
    for index in 0 ..< 2 {
        views.append(Album(name: album[index].name, image: album[index].image))
    }
    return views
}()
//主视图
struct ContentView: View {
    @GestureState private var dragState = DragState.inactive
    @State private var offset: CGFloat = .zero
    private let dragPosition: CGFloat = 80.0
    //图片数组
    @State var albums: [Album] = {
        var views = [Album]()
        for index in 0 ..< 2 {
            views.append(Album(name: album[index].name, image: album[index].image))
        }
        return views
    }()
    //最后一张图片索引值
    @State private var lastIndex = 1
    //转场类型动画
    @State private var removalTransition = AnyTransition.trailingBottom
    var body: some View {
        VStack {
            TopBarMenu()
            // 卡片视图
            ZStack {
                ForEach(albums) { album in
                    CardView(name: album.name, image: album.image)
                        .zIndex(self.isTopCard(cardView: album) ? 1 : 0)
                        //判断喜欢或者不喜欢
                        .overlay(
                            ZStack {
                                Image(systemName: "hand.thumbsdown.fill")
                                    .foregroundColor(.white)
                                    .font(.system(size: 100))
                                    .opacity(self.dragState.translation.width < -self.dragPosition && self .isTopCard(cardView: album) ? 1.0 : 0)
                                Image(systemName: "hand.thumbsup.fill")
                                    .foregroundColor(.white)
                                    .font(.system(size: 100))
                                    .opacity(self.dragState.translation.width > self.dragPosition && self .isTopCard(cardView: album) ? 1.0 : 0.0)
                            }
                        )
                        //偏移
                        .offset(x: self.isTopCard(cardView: album) ? self.dragState.translation.width : 0, y: self.isTopCard(cardView: album) ? self.dragState.translation.height : 0)
                        //缩放
                        .scaleEffect(self.dragState.isDragging && self.isTopCard(cardView: album) ? 0.95 : 1.0)
                        //旋转
                        .rotationEffect(Angle(degrees: self.isTopCard(cardView: album) ? Double( self.dragState.translation.width / 10) : 0))
                        //动画
                        .animation(.interpolatingSpring(stiffness: 180, damping: 100), value: offset)
                        //转场动画
                        .transition(self.removalTransition)
                        //手势
                        .gesture(LongPressGesture(minimumDuration: 0.01)
                            .sequenced(before: DragGesture())
                            .updating(self.$dragState, body: { value, state, _ in
                                switch value {
                                case .first(true):
                                    state = .pressing
                                case .second(true, let drag):
                                    state = .dragging(translation: drag?.translation ?? .zero)
                                default:
                                    break
                                }
                            })
                                 //拖动时添加转场效果
                                .onChanged({ (value) in
                                    guard case .second(true, let drag?) = value else {
                                        return
                                    }
                                    if drag.translation.width < -self.dragPosition {
                                        self.removalTransition = .leadingBottom
                                    }
                                    if drag.translation.width > self.dragPosition {
                                        self.removalTransition = .trailingBottom
                                    }
                                })
                                 //拖拽移除卡片
                                .onEnded({ (value) in
                                    guard case .second(true, let drag?) = value else {
                                        return
                                    }
                                    if drag.translation.width < -self.dragPosition || drag.translation.width > self.dragPosition {
                                        self.moveCard()
                                    }
                                })
                    )
                }
            }
            Spacer(minLength: 20)
            BottomBarMenu()
                .opacity(dragState.isDragging ? 0.0 : 1.0)
                .animation(.default, value: offset)
        }
    }
    // 获得图片zIndex值
    func isTopCard(cardView: Album) -> Bool {
        guard let index = albums.firstIndex(where: { $0.id == cardView.id }) else {
            return false
        }
        return index == 0
    }
    //移除卡片显示下一张卡片
    func moveCard() {
        albums.removeFirst()
        self.lastIndex += 1
        let cards = album[lastIndex % album.count]
        let newCardView = Album(name: cards.name, image: cards.image)
        albums.append(newCardView)
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
// 顶部导航栏
struct TopBarMenu: View {
    var body: some View {
        HStack {
            Image(systemName: "ellipsis.circle")
                .font(.system(size: 30))
            Spacer()
            Image(systemName: "heart.circle")
                .font(.system(size: 30))
        }.padding()
    }
}
// 卡片视图
struct CardView: View {
    let name: String
    let image: String
    var body: some View {
        Image(image)
            .resizable()
            .frame(minWidth: 0, maxWidth: .infinity)
            .cornerRadius(10)
            .padding(.horizontal, 15)
            .overlay(
                VStack {
                    Text(name)
                        .font(.system(.headline, design: .rounded)).fontWeight(.bold)
                        .padding(.horizontal, 30)
                        .padding(.vertical, 10)
                        .background(Color.white)
                        .cornerRadius(5)
                }
                .padding([.bottom], 20), alignment: .bottom
            )
    }
}
// 底部导航栏
struct BottomBarMenu: View {
    var body: some View {
        HStack {
            Image(systemName: "xmark")
                .font(.system(size: 30))
                .foregroundColor(.black)
            Button(action: {
            }) {
                Text("立即选择")
                    .font(.system(.subheadline, design: .rounded)).bold()
                    .foregroundColor(.white)
                    .padding(.horizontal, 35)
                    .padding(.vertical, 15)
                    .background(Color.black)
                    .cornerRadius(10)
            }.padding(.horizontal, 20)
            Image(systemName: "heart")
                .font(.system(size: 30))
                .foregroundColor(.black)
        }
    }
}
// Gestures手势特性
enum DragState {
    case inactive
    case pressing
    case dragging(translation: CGSize)
    var translation: CGSize {
        switch self {
        case .inactive, .pressing:
            return .zero
        case let .dragging(translation):
            return translation
        }
    }
    var isDragging: Bool {
        switch self {
        case .dragging:
            return true
        case .pressing, .inactive:
            return false
        }
    }
    var isPressing: Bool {
        switch self {
        case .pressing, .dragging:
            return true
        case .inactive:
            return false
        }
    }
}
//转场动画效果
extension AnyTransition {
    static var trailingBottom: AnyTransition {
        AnyTransition.asymmetric(
            insertion: .identity, removal: AnyTransition.move(edge: .trailing).combined(with: .move(edge : .bottom))
        )
    }
    static var leadingBottom: AnyTransition {
        AnyTransition.asymmetric(
            insertion: .identity, removal: AnyTransition.move(edge: .leading).combined(with: .move(edge: .bottom))
        )
    }
}

快来动手试试吧!




相关文章
|
存储 iOS开发
SwiftUI极简教程17:Gestures手势的使用
SwiftUI极简教程17:Gestures手势的使用
997 0
SwiftUI极简教程17:Gestures手势的使用
|
3月前
|
测试技术
Axure 环形进度条
Axure 环形进度条
36 0
WPF-布局样式练习-Day02-聊天气泡
WPF-布局样式练习-Day02-聊天气泡
236 1
|
6月前
|
前端开发
Flutter笔记:光影动画按钮、滚动图标卡片组等
Flutter笔记:光影动画按钮、滚动图标卡片组等
80 0
|
程序员 索引
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
1054 0
SwiftUI极简教程18:SwipeCard卡片滑动效果的使用(上)
【Axure教程】鼠标滚动上下翻页效果
【Axure教程】鼠标滚动上下翻页效果
|
JSON Android开发 数据格式
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
原生app开发技巧——底部导航栏动画效果按钮制作方法之采用photoshop制作gif动画-过渡动画关键帧
SwiftUI极简教程28:TextEditor多行文本框的使用
SwiftUI极简教程28:TextEditor多行文本框的使用
1181 0
SwiftUI极简教程28:TextEditor多行文本框的使用
|
存储 索引
SwiftUI极简教程42:使用MatchedGeometryEffect构建一个导航菜单
在本章中,你将学会使用MatchedGeometryEffect构建一个导航菜单。 在构建SwiftUI应用过程中,我们常常会使用TabView构建底部菜单,但更多的时候会由于我们定制化的需求,需要我们自己绘制底部菜单。 那么本章中,我们就来试试构建一个底部导航菜单。
452 0
SwiftUI极简教程42:使用MatchedGeometryEffect构建一个导航菜单
|
存储
SwiftUI极简教程40:构建SearchBar搜索栏和TabView底部导航
在本章中,你将学会构建Search搜索进行列表搜索和TabView底部导航。 在上一章节中,我们完成了一个简单的ColourAtla色卡App,接下来我们继续完善App的相关内容。
756 0
SwiftUI极简教程40:构建SearchBar搜索栏和TabView底部导航