→ 기존에 슬라이딩 메뉴를 만들 때, 보통 위아래 화면 뷰를 겹쳐놓고 윗 하면을 옆으로 치우는 방식으로 만들곤 했다. 이번에는 메인 뷰와 사이드 뷰를 HStack으로 일렬로 배치하고 드래그와 버튼으로 열리도록 만들어 보았다.
드래그는 편의성을 위해 조금만 드래그해도 펼쳐 지도록 빌드했다. (처음엔 사이드바의 중앙을 넘으면 펼쳐지도록 했는데 실제 사용해보니 매우 불편했음)
UIBezierPath를 이용해 커스텀 쉐잎을 그리고 .clipShape으로 모서리를 덧씌운다.
import SwiftUI
//Custom Corner Shapes
struct CustomCorners: Shape {
var corners: UIRectCorner
var radius: CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
//CustomCorners 사용
.clipShape(CustomCorners(corners: [.topRight, .bottomRight], radius: 12))
상위 뷰
@Namespace var namespace
var body: some View {
ZStack(alignment: .leading) {
Color.black.ignoresSafeArea()
VStack {
Text("SideView")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.white)
.padding(22)
Spacer()
//SideTapButtom
VStack(spacing: 15) {
TapButton(image: "house", title: "Main", selectedTitle: $selectedTitle, namespace: namespace)
TapButton(image: "square.and.pencil", title: "Memo", selectedTitle: $selectedTitle, namespace: namespace)
TapButton(image: "trash", title: "Delete", selectedTitle: $selectedTitle, namespace: namespace)
}
Spacer()
}
.ignoresSafeArea(edges: .bottom)
}
.frame(width: 240)
}
하위 뷰
struct TapButton: View {
var image: String
var title: String
@Binding var selectedTitle: String
var namespace: Namespace.ID
var body: some View {
Button(action: {
//선택된 타이틀은 selectedTab 값이 된다
//namespace 애니메이션을 넣기 위해서 애니메이션이 있어야한다.
withAnimation(.spring()) {
selectedTitle = title
}
}) {
HStack(spacing: 20){
Image(systemName: image)
.frame(width: 10)
.font(.system(size: 22, weight: .regular))
Text(title)
.fontWeight(.semibold)
}
}
.foregroundColor(.white)
.padding(.vertical, 10)
.padding(.leading, 20)
.frame(maxWidth: 180, alignment: .leading)
.background(
ZStack {
if selectedTitle == title {
Color(hue: 1.0, saturation: 0.0, brightness: 0.046)
//선택된 뷰에 배경
.opacity(selectedTitle == title ? 1 : 0)
//CustomCorners
.clipShape(CustomCorners(corners: [.topRight, .bottomRight], radius: 12))
//id별 궤적 생성 애니메이션
.matchedGeometryEffect(id: "TapEffect", in: namespace)
}
}
)
}
}
struct HomeView: View {
@Binding var selectedTitle: String
var body: some View {
TabView(selection: $selectedTitle ) {
MainView()
.tag("Main")
MemoView()
.tag("Memo")
DeleteView()
.tag("Delete")
}
.frame(width: getRect().width)
}
}
버튼
Button(action: {
//선택된 타이틀은 selectedTab 값이 된다
//namespace 애니메이션을 넣기 위해서 애니메이션이 있어야한다.
withAnimation(.spring()) {
selectedTitle = title
}
})
tabBar 제거 : TabView를 사용한 HomeView가 들어가는 상위 뷰에 넣어줘야함.
//TabView를 사용하면(스크롤 미사용시) 보이지 않는 페이지버튼이 하단에 TabBar로 생성된다. 이것을 제거하는 초기화 코드
init() {
UITabBar.appearance().isHidden = true
}
//사이드뷰 버튼용 변수
@State var showSide = false
//Sliding을 위한 변수
@State var translation: CGSize = .zero
@State var offsetX: CGFloat = -120
//홈과 사이드 메뉴가 움직이도록 하는 오프셋
.offset(x: (translation.width + offsetX) > -120 ? ((translation.width + offsetX) < 120 ? translation.width + offsetX : 120 ) : -120)
//사이드메뉴 버튼으로 showSide값에 변화가 있을 때, 사이드 메뉴 열기
.onChange(of: showSide) {_ in
withAnimation(.spring()) {
if showSide && offsetX == -120 {
offsetX = 120
// print("showSide ON")
}
if !showSide && offsetX == 120 {
offsetX = -120
// print("showSide OFF")
}
}
}
//드래그로 사이드 메뉴 열기
.gesture(
DragGesture()
.onChanged { value in
translation = value.translation
}
.onEnded {_ in
withAnimation(.spring()) {
let dragOffset = translation.width + offsetX
if dragOffset > -100 && offsetX == -120 {
offsetX = 120
showSide = true
} else if dragOffset < 100 && offsetX == 120 {
offsetX = -120
showSide = false
}
translation = .zero
}
}
)
}
}
전체코드 : https://github.com/dduruge/SwiftUI_Test/tree/master/Swiftui/SlidingMenuNTabView