SwiftUI 實現(xiàn)側(cè)滑菜單 Side Menu
效果
代碼
代碼里都有相關(guān)注釋
源碼 github 鏈接:https://gist.github.com/RandyWei/05261947fcf7493d9edd4b58bdb43cbe
//
// ContentView.swift
// SiderMenuDemo01
//
// Created by RandyWei on 2021/9/7.
//
import SwiftUI
struct ContentView: View {
//劃動偏移量
@GestureState var offset:CGFloat = 0
//滑動應(yīng)該停留在某個點
//停留點: 屏幕寬度的3/5
let maxOffset:CGFloat = UIScreen.main.bounds.width * 3 / 5
//滑動展開之后的 offset
@State var expandOffset:CGFloat = 0
//回彈點:最大停留點/2
private var springOffset:CGFloat{
maxOffset / 2
}
//縮放比例熄捍,默認(rèn)是1
@State private var scaleRatio:CGFloat = 1
//最小 可縮放值
let minScale:CGFloat = 0.9
private var dragGesture: some Gesture {
DragGesture()
.updating($offset, body: { value, out, _ in
//判斷是否反向滑動,如果是展開狀態(tài)需要反向滑動
if value.translation.width >= 0 || expandOffset != 0 {
out = value.translation.width
}
})
.onChanged { value in
//為了順暢給縮放增加過渡
if value.translation.width >= 0 {
//對縮放比例進行計算:縮放值 = 劃動比例 * 可縮放值(1-minScale)
//因為是往小了縮喂很,所以是1-縮放值
scaleRatio = 1 - (value.translation.width / maxOffset) * (1 - minScale)
} else {
//反向value.translation.width是負(fù)數(shù) 她紫,所以+maxOffset變?yōu)檎? scaleRatio = 1 - ((maxOffset + value.translation.width) / maxOffset) * (1 - minScale)
}
}
.onEnded { value in
//需要判斷滑動是否超過某個點來決定是重置還是停留
if value.translation.width >= springOffset {
expandOffset = maxOffset
//停止后奈虾,縮小 到0.9
scaleRatio = minScale
} else {
expandOffset = 0
scaleRatio = 1
}
}
}
var body: some View {
ZStack{
//側(cè)邊菜單層
SideMenuView()
//功能區(qū)域
FeatureView()
.offset(x: offset + expandOffset)
.scaleEffect(scaleRatio)
.animation(.easeInOut(duration: 0.05))
.gesture(dragGesture)
}
}
}
struct FeatureView:View {
var body: some View{
GeometryReader{proxy in
VStack{
HStack{
Image(systemName: "list.dash")
.resizable()
.frame(width: 20, height: 20, alignment: .center)
Text("功能區(qū)域")
.font(.title)
Spacer()
}
ScrollView(.vertical, showsIndicators: false, content: {
VStack{
ForEach(0..<50){_ in
HStack{
Image(systemName: "person")
.resizable()
.frame(width: 80, height: 80, alignment: .center)
VStack(alignment: .leading){
Text("titletitletitletitletitle")
.font(.title)
Spacer()
Text("bodybodybodybodybodybody")
.font(.body)
}
}
}.redacted(reason: .placeholder)
}
})
}
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.white)
.cornerRadius(30)
.shadow(radius: 10)
.ignoresSafeArea()
}
}
}
struct SideMenuView:View {
var body: some View{
GeometryReader{proxy in
VStack(alignment:.leading){
//祖?zhèn)黝^像
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipShape(Circle())
Text("韋爵爺")
.font(.title)
Text("這個人很懶,什么都沒留下")
//菜單
HStack{
Image(systemName: "archivebox")
Text("菜單一")
}
.padding(.top)
HStack{
Image(systemName: "note.text")
Text("菜單二")
}
.padding(.top)
HStack{
Image(systemName: "gearshape")
Text("個人設(shè)置")
}
.padding(.top)
Spacer()
HStack{
Image(systemName: "signature")
Text("退出登錄")
}
.padding(.top)
}
.foregroundColor(.white)
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.padding(.bottom, 8 + proxy.safeAreaInsets.bottom)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.orange)
.ignoresSafeArea()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}