元宇宙来了,准备好你的电子名片了吗?(六)
前提回顾在上一章节中,我们学习了Model-View-ViewModel架构模式,并实现了添加、删除方法。其中删除方法使用的是最简单的,利用SwiftUI自带的contextMenu上下文菜单按钮控件,实现长按唤起删除操作,点击删除操作调用ViewModel视图模型中的删除方法删除指定ID的数据项。
而在卡片式列表应用中,常用的删除方法是横向滑动唤起删除的方法,网上也有很多使用SwiftUI自带的EditButton唤起横向删除的方法,但总有这样那样的原因导致自带的滑动删除并不好用。
在本章中,我们将学习一种使用gesture手势修饰符实现滑动删除的交互。
交互动作:向左滑动唤起删除操作在SwiftUI提供的手势操作中,有onTapGesture点击手势、LongPressGesture长按手势、DragGesture拖拽手势三种主要操作。而要使用这三种手势,需要使用到gesture手势修饰符,如下代码所示:
//拖拽手势
.gesture(
DragGesture()
.onChanged{valuein
//拖动时的操作
}
.onEnded{valuein
//拖动结束时操作
}
)
使用DragGesture拖拽手势前,需要使用@GestureState属性包装器定义一个拖拽位置参数viewState,用来记录我们的拖拽前的初始位置CGSize.zero,也用来监听和更新UI,如下代码所示:
@StatevarviewState=CGSize.zero
因为我们要拖动的是单张身份卡片,因此需要给身份卡片构件CardView添加拖动位置的偏移量修饰符,如下代码所示:
//设置只能从右往左拖动
.offset(x:self.viewState.width0?self.viewState.width:0)
上述代码中,我们给CardView的视图内容添加了offset偏移量修饰符,设置只能沿X轴拖动,并添加条件如果拖拽前的初始位置viewState小于0,则可被沿X轴横向拖拽,如果大于等于0,即向右边拖拽时,则维持位置为0,保证CardView的视图内容只能从右往左拖动。
紧接着,我们给DragGesture手势增加更新方法,如下代码所示:
//拖拽手势
.gesture(
DragGesture()
.onChanged{valuein
//拖动时的操作
self.viewState=value.translation
}
.onEnded{valuein
//拖动结束时操作
self.viewState=.zero
}
)
上述代码中,我们在onChanged拖动时,让身份卡的位置viewState等于拖动的位置translation,如此便实现了卡片拖动动作。而在onEnded拖动结束时,我们让身份卡片回到初始的位置zero。
完成之后,我们可以尝试拖动下卡片,如下图所示:
为了突出当前正在进行拖拽删除的操作,我们可以给身份卡片CardView在拖拽时增加样式,比如在向左拖拽到左边一定位置的时候,让卡片填充一个背景颜色。
那么首先我们先声明拖动到某个位置是操作删除的位置,并还需再声明一个变量告知系统当前是否在执行删除操作,如下代码所示:
@StatevarvalueToBeDeleted:CGFloat=-75
@StatevarreadyToBeDeleted:Bool=false
下一步我们可以在拖动时添加判断当前拖动位置是否达到准备删除的位置,如下代码所示:
self.readyToBeDeleted=self.viewState.widthself.valueToBeDeleted?true:false
并且在删除时,给身份卡片修改背景色,如下代码所示:
.background(self.readyToBeDeleted?Color(.systemRed):.white)
当然,拖动结束后,还需要更新readyToBeDeleted是否操作更新的状态为false,如下代码所示:
self.readyToBeDeleted=false
样式完成后,我们来实现删除逻辑,我们在ViewModel视图模型中创建了删除方法deleteItem,deleteItem方法需要基于卡片ID进行指定删除,而在CardView卡片构件中,并没有ID,而数据集和其ID是在ContentView视图中存在。
要想获得UUID,我们还需要在ViewModel视图模型中创建一个获得数据UUID的方法,如下代码所示:
//获得数据项的UUID
funcgetItemById(itemId:UUID)-Model?{
returnmodels.first(where:{$0.id==itemId})??nil
}
上述代码中,我们创建了一个获得数据项的ID的方法,通过传入点击项的UUID,然后返回对应数据项在数据集中的UUID,告知系统当前操作的数据项是models数组中的哪一个数据。
然后我们再回到ContentView视图中,在CardView身份卡片视图中声明相关的变量,如下代码所示:
varviewModel:ViewModel
varitemId:UUID
varitem:Model?{
returnviewModel.getItemById(itemId:itemId)
}
上述代码中,我们使用全局变量引用ViewModel模型视图,并且声明了两个参数itemId和item。itemId为UUID格式,作为数据项的唯一标识符,item为符合Model模型的数据项,用于通过ID找到Model模型的数据。
由于CardView声明了变量,因此在引用CardView的地方需要绑定相关的参数,如下代码所示:
//卡片视图
CardView(platformIcon:item.platformIcon,title:item.title,platformName:item.platformName,indexURL:item.indexURL,viewModel:viewModel,itemId:item.id)
下一步,我们就可以拖动结束时,判断身份卡是否拖动到删除的位置,如果是,则调用ViewModel视图模型的deleteItem删除数据项方法,删除指定ID的数据项,如下代码所示:
ifself.viewState.widthself.valueToBeDeleted{
self.viewModel.deleteItem(itemId:itemId)
}
上面我们实现了滑动删除的操作,但操作太直接了,可能存在用户误操作的情况。为了避免用户误操作,我们可以增加一层判断机制。当用户唤起删除操作时提醒用户当前正在执行删除操作,请求用户的二次确认,用户确认后方可执行删除。
这种强提醒的用户场景下,我们可以使用警告弹窗告知用户。警告弹窗和模态弹窗的使用方式类型,需要提前声明一个是否打开警告弹窗的变量,如下代码所示:
@StatevarshowDeleteAlert:Bool=false
对于删除使用的警告弹窗,我们可以单独构建警告弹窗视图,然后再调用,如下代码所示:
//删除弹窗
privatevardeleteAlert:Alert{
letalert=Alert(title:Text(""),message:Text("确定要删除吗?"),primaryButton:.destructive(Text("确认")){
},secondaryButton:.cancel(Text("取消")))
returnalert
}
上述代码中,我们创建了一个警告弹窗deleteAlert,视图类型为Alert弹窗。在deleteAlert弹窗中,我们设置了Alert的标题、副标题、主要按钮、取消按钮,最终返回这个Alert样式给到deleteAlert。
要使用Alert弹窗的方式也比较简单,可以使用alert弹窗修饰符,如下代码所示:
//打开删除确认弹窗
.alert(isPresented:$showDeleteAlert,content:{deleteAlert})
上述代码中,我们给整个卡片视图添加了alert警告弹窗修饰符,并绑定打开弹窗的参数showDeleteAlert,警告弹窗的内容为我们单独构建的deleteAlert删除弹窗视图。
然后我们可以再拖动判断删除的时候触发打开删除弹窗,在删除弹窗中点击确定时,调用删除方法,如下代码所示:
self.showDeleteAlert.toggle()
实现滑动删除动作后,总感觉好像少了点东西。操作几次后发现,当用户拖动卡片向左滑动时,确实可以通过颜色表示当前用户正在执行操作,而且确实有警告弹窗进行二次提示,但对于小白用户来说,也确实在警告弹窗出来之前是不知道正在操作删除的。
这存在学习成本,我们可以加一点点小细节,当用户向左滑动卡片时,在卡片背后出现提示文字,如下代码所示:
//提示文字
HStack{
Spacer()
Text("左滑删除")
.padding()
.foregroundColor(Color(.systemGray))
}
上述代码中,我们在List列表中遍历数据项时,在ZStack堆栈视图包裹中的NavigationLink导航链接、CardView身份卡片中增加了一个“提示文字”视图,使用Spacer空间垫片将Text文字撑到右边。
由于ZStack堆栈视图的层级关系和代码的前后顺序有关,Text文字在CardView身份卡片前,那么常规情况下文字会被遮挡。而当CardView身份卡片向左滑动时,就出现了Text文字了。
除了文字外,我们还可以再触发删除操作的时候添加震动反馈,进一步提升用户体验。 SwiftUI提供了反馈生成器 UIFeedbackGenerator供开发者调用iOS系统的线性马达形成震动效果。我们可以创建一个新的Swift文件专门管理震动反馈内容。
创建一个新的文件夹,命名为SupportFile,并创建一个新的Swift文件,命名为Haptics,如下图所示:
然后我们引入SwiftUI,并创建一个类来管理震动反馈的内容,如下代码所示:
importFoundation
importSwiftUI
structHaptics{
staticfunchapticSuccess(){
letgenerator=UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
staticfunchapticWarning(){
letgenerator=UINotificationFeedbackGenerator()
generator.notificationOccurred(.warning)
}
}
上述代码中,我们创建了一个结构体Haptics,声明了2个方法hapticSuccess成功时的震动动效、hapticWarning警告时的震动动效,并调用UINotificationFeedbackGenerator震动反馈生成器组件,赋予不同的震动反馈效果:success或者warning。
紧接着我们回到ContentView视图中,在CardView视图中拖动身份卡片操作时,我们调用Haptics中的震动反馈方法,如下代码所示:
Haptics.hapticWarning()
如此,在用户向左滑动身份卡执行删除操作时,系统就会基于用户一个震动反馈,告知用户这是一个“值得谨慎的操作”,进一步地提供用户的体验。
很多时候,加一点点小细节,整个应用会上一个台阶。
项目小结在本章中,我们实现了自定义滑动删除的交互操作,这比起直接使用List自带的滑动删除,或者使用简单的控件实现更加“高级一些”,当然这也增加了一些学习成本。
对于很多时候SwiftUI自带控件无法满足开发需要时,我们要具备各种自定义实现控件或者操作能力,这是区分只会用框架的“搬砖员”和真正的程序员的重要特征。
另外我们还在该项目中增加了“一点点细节”,让应用的交互性和用户体验更好一些,这也是作为一个创作者的追求。其实也想表达一个观念,如果一个程序员只会使用框架而加入自己的思考和理解,那么25岁和30岁也只是打字快慢的区别罢了。
接下来的章节,我们想要做的还有很多,我也会把每一步的实现细节和操作流程都分享出来,请保持期待吧~
版权声明本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线