发布&选择发布,使用SwiftUI搭建一个新建发布弹窗(下)

3,746 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情

承接上一章节的内容,在上一章节中,我们完成了“新建发布”的入口背景蒙层的搭建,那么本章来进入重点,我们来完成弹窗的交互。

样式预览

1.png

弹窗视图

我们来分析下“新建发布”弹窗的内容,它包括一个提示的下拉条,一排横向布局的发布功能按钮,一个关闭弹窗的按钮。

我们构建一个新的视图,示例:

// 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类型的变量imagetext,分别代表操作按钮中的图标图片和操作按钮名称。

当我们点击按钮的时候,切换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)    
    }
}

2.png

交互动画

弹出关闭

完成了弹窗视图后,我们回到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动画加了一个从下向上展示的过渡动画。

3.gif

向下拖动关闭

新建发布弹窗除了常规的点击关闭按钮关闭弹窗外,点击弹窗向下拖动时关闭弹窗,要实现这个功能,我们回到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变量关闭弹窗。

项目预览

完成全部后,我们整体预览下效果。

4.gif

恭喜你,完成了本章的全部内容!

快来动手试试吧。

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