携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情。
承接上一章节的内容,在上一章节中,我们完成了“新建发布”的入口和背景蒙层的搭建,那么本章来进入重点,我们来完成弹窗的交互。
样式预览
弹窗视图
我们来分析下“新建发布”弹窗的内容,它包括一个提示的下拉条,一排横向布局的发布功能按钮,一个关闭弹窗的按钮。
我们构建一个新的视图,示例:
// MARK: 底部弹窗
struct SlideOutMenu: View {
@Binding var showMaskView: Bool
var body: some View {
VStack {
Spacer()
//构建弹窗视图元素
}
}
}
上述代码中,我们创建了一个新视图SlideOutMenu
。以及还使用@Binding
声明了一个变量,方便我们在ContentView
视图中做双向绑定。由于弹窗在底部,我们使用VStack
纵向布局,然后使用Spacer
将弹窗撑开到底部。
完成后,我们来完成弹窗的样式部分。
下拉条
我们一块一块内容完成它,首先是下拉条,示例:
// 下拉条
func pullDownBtnView() -> some View {
Rectangle()
.foregroundColor(Color(.systemGray4))
.cornerRadius(30)
.frame(width: 50, height: 5)
}
上述代码中,我们构建了一个Rectangle
矩形,并赋予了颜色systemGray4
灰色和圆角,尺寸我们使用规定的长宽。
发布功能按钮
发布功能按钮部分,由于具备相同的样式,我们可以使用结构体的方式构建基础样式,再在视图中调用,也可以使用创建视图的方法构建基础样式再调用。两种方法都可以使用,示例:
// 操作功能
func operateBtnView(image: String, text: String) -> some View {
Button(action: {
self.showMaskView = false
}) {
VStack(spacing: 15) {
Image(systemName: image)
.font(.system(size: 30))
.foregroundColor(.black)
.frame(width: 80, height: 80)
.background(Color(.systemGray6))
.cornerRadius(8)
Text(text)
.font(.system(size: 17))
.foregroundColor(.black)
}
}
}
上述代码中,我们构建了一个框架视图operateBtnView
,传入两个String
类型的变量image
、text
,分别代表操作按钮中的图标图片和操作按钮名称。
当我们点击按钮的时候,切换showMaskView
状态关闭蒙层。
关闭按钮
关闭按钮样式也比较简单,我们依旧单独构建样式部分,示例:
// 关闭按钮
func colseBtnView() -> some View {
Button(action: {
self.showMaskView = false
}) {
Image(systemName: "xmark")
.font(.system(size: 24))
.foregroundColor(.gray)
.padding(.bottom, 20)
}
}
上述代码中,我们单独构建按样式视图colseBtnView
。当我们点击关闭按钮的时候,也调用切换showMaskView
状态关闭蒙层。
样式组合
完成上述3个视图后,我们在SlideOutMenu
视图中组合样式视图内容,示例:
// MARK: 底部弹窗
struct SlideOutMenu: View {
@Binding var showMaskView: Bool
var body: some View {
VStack {
Spacer()
//构建弹窗视图元素
VStack {
// 下拉条
pullDownBtnView()
Spacer()
// 操作按钮
HStack(spacing: 20) {
operateBtnView(image: "magazine.fill", text: "写文章")
operateBtnView(image: "doc.plaintext.fill", text: "发沸点")
operateBtnView(image: "book.fill", text: "提问题")
operateBtnView(image: "paperplane.fill", text: "传资源")
}
Spacer()
// 关闭按钮
colseBtnView()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: 320)
.background(Color.white)
.cornerRadius(10, antialiased: true)
}.edgesIgnoringSafeArea(.bottom)
}
}
交互动画
弹出关闭
完成了弹窗视图后,我们回到ContentView
视图中,将弹窗视图附上,示例:
var body: some View {
ZStack {
VStack {
topBarMenu()
Spacer()
}
if showMaskView {
MaskView(showMaskView: $showMaskView)
SlideOutMenu(showMaskView: $showMaskView)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0))
}
}
}
上述代码中,我们根据showMaskView
变量状态决定是否展示背景蒙层视图和弹窗视图,然后在展示SlideOutMenu
新建发布弹窗时,使用transition
过渡和animation
动画加了一个从下向上展示的过渡动画。
向下拖动关闭
新建发布弹窗除了常规的点击关闭按钮关闭弹窗外,点击弹窗向下拖动时关闭弹窗,要实现这个功能,我们回到SlideOutMenu
弹窗视图中,首要声明2个变量,示例:
@State private var offsetY = CGSize.zero
@State var isAllowToDrag: Bool = false
上述代码中,offsetY
变量存储拖动时弹窗Y轴的位置,用来判断用户在向上拖动还是向下拖动,也为了确定向下拖动Y轴到某一位置的,触发关闭弹窗交互。
变量isAllowToDrag
是承接offsetY
变量,当我们判断向上拖动时,禁用弹窗拖动,防止弹窗向上拖动,保证只能向下拖动。
然后在SlideOutMenu
主要内容中使用拖动修饰符,示例:
// MARK: 底部弹窗
struct SlideOutMenu: View {
@Binding var showMaskView: Bool
var body: some View {
VStack {
//隐藏了弹窗视图代码
}
.padding()
.frame(maxWidth: .infinity, maxHeight: 320)
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isAllowToDrag ? offsetY.height : 0)
.gesture(
DragGesture()
.onChanged { gesture in
// 如果向下拖动
if gesture.translation.height > 0 {
self.isAllowToDrag = true
self.offsetY = gesture.translation
}
}
.onEnded { _ in
// 如果拖动位置大于100
if (self.offsetY.height) > 100 {
self.showMaskView = false
} else {
self.offsetY = .zero
}
}
)
}.edgesIgnoringSafeArea(.bottom)
}
}
上述代码中,我们给弹窗内容加了offset
偏移量修饰符,拖动时,如果isAllowToDrag
允许拖动,则拖动位置为offsetY.height
偏移的Y轴位置,否则就是0。
然后使用gesture
手势修饰符,使用DragGesture
拖动手势,当onChanged
拖动改变时,先判断是不是向下拖动,如果是则启用isAllowToDrag
变量,然后拖动后让视图回到原来的位置。
当弹窗视图onEnded
拖动结束时,判断拖动的Y轴的位置offsetY.height
是不是大于100
,也就是弹窗宽度的大约1/3
的位置,如果时则修改showMaskView
变量关闭弹窗。
项目预览
完成全部后,我们整体预览下效果。
恭喜你,完成了本章的全部内容!
快来动手试试吧。
如果本专栏对你有帮助,不妨点赞、评论、关注~