【SwiftUI模块】0029、SwiftUI创建具有动画形状的时尚动画滑出菜单

SwiftUI模块系列 - 已更新29篇
SwiftUI项目 - 已更新2个项目
往期Demo源码下载

技术:SwiftUI、SwiftUI3.0、侧滑菜单、个性化侧滑菜单、动画形状侧滑菜单
运行环境:
SwiftUI3.0 + Xcode13.4.1 + MacOS12.5 + iPhone Simulator iPhone 13 Pro Max

概述

使用SwiftUI创建具有动画形状的时尚动画滑出菜单

详细

一、运行效果

请添加图片描述

二、项目结构图

在这里插入图片描述

三、程序实现 - 过程

思路:
1.创建头部模块 进行测试上下滚动拥有放大缩小效果
2.搭建分类模块 固定在头部下面
3.搭建列表模块
4.监听滚动偏移的操作

1.创建一个项目命名为 SideMenuWithCustomShape

在这里插入图片描述
在这里插入图片描述

1.1.引入资源文件和颜色

颜色
BG1 #3B4563
BG2 #202843
Blue #6DA3F5
Red #F86D6D
图片
背景1张
头像1张
素材4张
icon2张

在这里插入图片描述

2. 创建一个虚拟文件New Group 命名为 View

在这里插入图片描述
在这里插入图片描述

3. 创建一个虚拟文件New Group 命名为 Model

在这里插入图片描述
在这里插入图片描述

4. 创建一个文件New File 选择SwiftUI View类型 命名为Home

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5. 创建一个文件New File 选择SwiftUI View类型 命名为Story 删除预览视图 并且继承Identifiable 作为模型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

6. 创建一个文件New File 选择SwiftUI View类型 命名为BlurView - 菜单模板视图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Code

ContentView - 主窗口

主要是展示主窗口Home

//
//  ContentView.swift
//  Shared
//
//  Created by 李宇鸿 on 2022/9/13.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Home()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Home - 主页

思路

  1. 搭建列表故事板UI
  2. 搭建一个菜单背景视图 BlurView
  3. 给菜单视图添加曲线
  4. 搭建菜单视图的菜单按钮
  5. 通过操作 执行菜单的曲线路径动画
//
//  Home.swift
//  SideMenuWithCustomShape (iOS)
//
//  Created by 李宇鸿 on 2022/9/14.
//

import SwiftUI

struct Home: View {
    
    
    
    @State var showMenu : Bool = false
    
    // 动画路径
    @State var animatePath : Bool = false
    @State var animateBG : Bool = false

    
    var body: some View {
        ZStack{
            // 主页
            VStack(spacing:15){
                
                // 导航栏
                HStack{
                    Button  {
                        withAnimation {
                            animateBG.toggle()
                        }
                        
                        withAnimation(.spring()) {
                            showMenu.toggle()
                        }
                        
                        // 动画路径与小延迟…
                        withAnimation(.interactiveSpring(response: 0.4, dampingFraction: 0.3, blendDuration: 0.3
                                                        ).delay(0.2)){
                            animatePath.toggle()
                        }
                        
                    } label: {
                        Image("menu")
                            .resizable()
                            .renderingMode(.template)
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 25, height: 25)
                            .shadow(radius: 1)
                    }
                    Spacer()
                    
                    Button  {
                        
                    } label: {
                        Image("add")
                            .resizable()
                            .renderingMode(.template)
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 25, height: 25)
                            .shadow(radius: 1)
                    }
                    

                }
                .overlay(
                    Text("Stories")
                        .font(.title2.bold())
                )
                .foregroundColor(Color.white.opacity(0.8))
                .padding([.horizontal,.top])
                .padding(.bottom,5)
                
                ScrollView(.vertical,showsIndicators: false)
                {
                    VStack(spacing:25){
                        // 样本卡...
                        ForEach(stories){ story in
                            // 卡片视图
                            CardView(story: story)
                            
                        }
                    }
                    .padding()
                    .padding(.top,10)
                }
                
            }
            .frame(maxWidth:.infinity,maxHeight: .infinity)
            .background(
                // Gradient BG...
                // 渐变色背景
                LinearGradient(colors: [
                    Color("BG1"),
                    Color("BG2"),
                ], startPoint: .top, endPoint: .bottom)
                .ignoresSafeArea()
            )
            
            
            Color.black.opacity(animateBG ?  0.25 : 0)
            
            
            MenuView(showMenu: $showMenu,animatePath: $animatePath,animateBG: $animateBG)
                .offset(x:showMenu ? 0 : -getRect().width)
            
            
            
        }
    }
    
    @ViewBuilder
    func CardView(story : Story )-> some View {
        
        VStack(alignment: .leading, spacing: 12) {
            
            // 获取屏幕宽度
            GeometryReader { proxy in
                
                let size = proxy.size
                Image(story.image)
                    .resizable().aspectRatio(contentMode: .fill).frame(width: size.width, height: size.height)
                    .cornerRadius(15)
            }
            .frame(height:200)
            
            Text(story.title)
                .font(.title2)
                .fontWeight(.semibold)
                .foregroundColor(Color.white.opacity(0.8))
            
            Button  {
                
            } label: {
                Text("Read Now")
                    .font(.caption)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding(.vertical,6)
                    .padding(.horizontal)
                    .background(
                        Capsule()
                            .fill(Color("Red"))
                    )
            }

            
        }
        
    }
}

struct Home_Previews: PreviewProvider {
    static var previews: some View {
        Home()
    }
}


// 菜单视图
struct MenuView: View {
    
    @Binding var showMenu : Bool
    @Binding var animatePath : Bool
    @Binding var animateBG : Bool

    var body: some View{
        
        ZStack{
            // 菜单蒙版视图
            BlurView(style: .systemThinMaterialDark)
            
            //  与蓝色混合…
            Color("BG2")
                .opacity(0.7)
                .blur(radius: 15)
            
            // 内容
            VStack(alignment: .leading, spacing: 25) {
                Button  {
                    
                    // 关闭菜单
                    
                    // 动画路径与小延迟…
                    withAnimation(.interactiveSpring(response: 0.4, dampingFraction: 0.3, blendDuration: 0.3)){
                        animatePath.toggle()
                    }
                    
                    withAnimation {
                        animateBG.toggle()
                    }
                    
                    withAnimation(.spring().delay(0.1)) {
                        showMenu.toggle()
                    }
                    
         
                    
                } label: {
                    Image(systemName: "xmark.circle")
                        .font(.title)
                    
                }
                .foregroundColor(Color.white.opacity(0.8))
                
                
                // 菜单按钮
                MenuButton(title: "Preimum Access", image: "square.grid.2x2", offset: 0)
                MenuButton(title: "Upload Content",image: "square.and.arrow.up.on.square", offset: 10)
                
                MenuButton(title: "My Account",image: "Profile", offset: 30)
                
                MenuButton(title: "Make Money",image: "dollarsign.circle", offset: 10)
                
                MenuButton(title: "Help",image: "questionmark.circle" ,offset: 0)
                
                Spacer(minLength: 10)
                
                MenuButton(title: "LOGOUT", image: "rectangle.portrait.and.arrow.right", offset: 0)


            }
            .padding(.trailing,120)
            .padding()
            .padding(.top,getSafeArea().top)
            .padding(.bottom,getSafeArea().bottom)
            .frame(maxWidth:.infinity,maxHeight: .infinity, alignment: .topLeading)
            
        }
        // 自定义形状……
        .clipShape(MenuShape(value: animatePath ? 150 : 0))
        .background(
            MenuShape(value: animatePath ? 150 : 0)
                .stroke(
                    .linearGradient(.init(colors: [
                        Color("Blue"),
                        Color("Blue").opacity(0.7),
                        Color("Blue").opacity(0.5),
                        Color.clear

                    ]), startPoint: .top, endPoint: .bottom)
                    ,lineWidth: animatePath ?  7 : 0
                )
                .padding(.leading,-50)
               
            
        )
        .ignoresSafeArea()
        
    }
    
    @ViewBuilder
    func MenuButton(title: String,image:String,offset: CGFloat) -> some View{
        Button  {
            
        } label: {
            HStack(spacing:12){
                if image == "Profile" {
                    // Asset Image
                    Image(image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 50, height: 50)
                        .clipShape(Circle())
                    
                }
                else {
                    // 系统图片
                    Image(systemName: image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 25, height: 25)
                        .foregroundColor(.white)
                        
                }
                
                Text(title)
                    .font(.system(size: 17))
                    .fontWeight(title == "LOGOUT" ? .semibold :  .medium)
                // kerning 字间距
                    .kerning(title == "LOGOUT" ? 1.2 : 0.8)
                    .foregroundColor(Color.white.opacity(title == "LOGOUT" ? 0.9 : 0.65))
            }
            .padding(.vertical)
        }
        .offset(x: offset)

    }
    
}

// 形状
struct MenuShape : Shape{
    
    var value : CGFloat
    //动画路径…
    var animatableData: CGFloat {
        get{return value}
        set{value = newValue}
    }
    
    func path(in rect: CGRect) -> Path {
        return Path{ path in
            // 曲线的形状
            let width = rect.width - 100
            let height = rect.height
            path.move(to: CGPoint(x: width, y: height))
            path.addLine(to: CGPoint(x: 0, y: height))
            path.addLine(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: width, y: 0))
            

            // 曲线
            path.move(to: CGPoint(x: width, y: 0))
            
            path.addCurve(to: CGPoint(x: width, y: height + 100), control1: CGPoint(x: width + value, y: height / 3), control2: CGPoint(x: width - value, y: height / 2))

        }
    }
}

//获取SafeArea的扩展视图....
extension View {
    func getSafeArea()-> UIEdgeInsets {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .zero
        }
        
        guard let safeArea = screen.windows.first?.safeAreaInsets
        else {
            return .zero
        }
        
        return safeArea
    }
    
    func getRect() -> CGRect {
        return UIScreen.main.bounds
    }
}
BlurView - 菜单视图蒙版
//
//  BlurView.swift
//  SideMenuWithCustomShape (iOS)
//
//  Created by 李宇鸿 on 2022/9/16.
//

import SwiftUI

// 因为App支持iOS 14…
struct BlurView: UIViewRepresentable {
    
    var style : UIBlurEffect.Style
    
    // UIVisualEffectView UI视觉效果视图
    func makeUIView(context: Context) -> UIVisualEffectView {
        let view = UIVisualEffectView(effect: UIBlurEffect(style: style))
        return view
    }
    func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
    
    }
}
Story - 模型
//
//  Story.swift
//  SideMenuWithCustomShape (iOS)
//
//  Created by 李宇鸿 on 2022/9/15.
//

import SwiftUI

import SwiftUI

// 模型和样本数据....
var stories = [
    
    Story(image: "Pic1",title: "Jack the Persian and the Black Castel"),
    Story(image: "Pic2",title: "The Dreaming Moon"),
    Story(image: "Pic3",title: "Fallen In Love"),
    Story(image: "Pic4",title: "Hounted Ground"),
]

struct Story : Identifiable {
    
    var id = UUID().uuidString
    var image : String
    var title : String
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇夜iOS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值