0
点赞
收藏
分享

微信扫一扫

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


今日职言:心有所信,方能行远。学有所悟,而后笃行。

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

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

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 =

然后我们给​​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

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

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

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

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

我们发现,好像最展示了一张​​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))

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

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

展示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) } )

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

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

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
}()

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

然后,我们需要声明一个新的变量,来定位​​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​​​卡片视图,这就是滑动移除一张图片显示另一张图片的​​原理​​了。

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

然后,我们还需要更新​​.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()
}
})

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

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

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))
)
}
}

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

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

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

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

然后,将​​.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 =

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

恭喜恭喜!

我们终于完成了整个​​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))
)
}
}

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~

举报

相关推荐

0 条评论