[Swift]Drawer Menu

👉🏻 Drawer Menu를 swift에서 구현하는 방법입니다
How to implement a Drawer Menu in Swift

👉🏻 Drawer Menu의 핵심은 애니메이션 속성을 사용해서 메뉴 위치를 이동하는 것입니다.
The key to the Drawer Menu is using animation properties to move the menu position.

✔️ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var showMenu = false
    @State private var selectedMenu = "Home"
    @State private var hideStatusBar = false

    var body: some View {
        ZStack {
            // Main content
            NavigationView {
                VStack {
                    switch selectedMenu {
                    case "Home":
                        HomeScreen()
                       // HomeScreen2()
                    case "Search":
                        SearchScreen()
                       // SearchScreen2()
                    case "Settings":
                        SettingsScreen()
                       // SettingsScreen2()
                    default:
                        HomeScreen()
                    }
                }
                .navigationBarTitle(Text(selectedMenu), displayMode: .inline)
                .navigationBarItems(leading: Button(action: {
                    withAnimation(.easeInOut(duration: 0.3)) {
                        showMenu.toggle()
                    }
                }) {
                    Image(systemName: "line.3.horizontal")
                        .imageScale(.large)
                })
            }
            .disabled(showMenu)
            
            // 반투명 배경 (항상 존재 + opacity로 제어)
            // Semi-transparent background (always present + controlled by opacity)
            Color.black
                .opacity(showMenu ? 0.4 : 0.0)
                .animation(.easeInOut(duration: 0.3), value: showMenu)
                .edgesIgnoringSafeArea(.all)
                .ignoresSafeArea() //  Status Bar 포함
                .edgesIgnoringSafeArea(.all) // Status Bar 포함(ios15이전 용)
                .onTapGesture {
                    if showMenu {
                        withAnimation(.easeInOut(duration: 0.3)) {
                            showMenu = false
                        }
                    }
                }
            
            // Drawer (offset으로 슬라이드 인/아웃)
            // Drawer (slides in/out with offset)
            
            // Drawer는 Dynamick Island는 가릴 수 없음(시계 우측 타원)
            // Drawer cannot cover Dynamick Island (oval on the right side of the clock)
            HStack {
                
                // 화면의정보 계산할때 GeometryReader사용
                // Use GeometryReader when calculating screen information
                GeometryReader { geo in
                    VStack(alignment: .leading) {
                        Text("Menu Title")
                            .font(.title)
                            .padding(.top, 50)
                            .padding(.bottom, 20)
                        
                        // 각 메뉴 / Each Menu
                        DrawerMenuItem(title: "Home", selectedMenu: $selectedMenu, showMenu: $showMenu)
                        DrawerMenuItem(title: "Search", selectedMenu: $selectedMenu, showMenu: $showMenu)
                        DrawerMenuItem(title: "Settings", selectedMenu: $selectedMenu, showMenu: $showMenu)
                        Spacer() // 나머지 공간 차지 / Takes up the rest of the space

                    } // VStack
                    //.frame(width: 250, height: UIScreen.main.bounds.height) // 화면전체 높이로 설정 / Set to full screen height(deprecated)
                    .frame(width:250, height:geo.size.height) // 드로어를 전체 높이로설정,geometry reader사용 / Set the drawer to full height, using the geometry reader
                    .padding(.horizontal, 16)
                    .background(Color(UIColor.systemBackground))
                }

                
                Spacer()
            } // HStack(Drawer)
            .offset(x: showMenu ? 0 : -260) // 스택위치 0은 화면 안 -260화면 밖 / Stack position 0 is inside the screen -260 is outside the screen
            .animation(.easeInOut(duration: 0.3), value: showMenu) // 에니메이션적용 / Apply animation
            
            /*
                 animation을 지정하지않고 offset만 지정해도 ios에서는 암묵적으로 애니메이션 처리함
                 animation을 있으면 더 자세한 설정이 가능함.
                 Even if you only specify an offset without specifying animation, iOS implicitly handles animation.
                 Animation allows for more detailed settings.
             */
            
            
        }
        .statusBar(hidden: hideStatusBar) // Drawer열릴 경우 Statusbar숨김 / Hide Statusbar when Drawer is opened
        .onChange(of: showMenu) { oldValue, newValue in
            hideStatusBar = newValue
        }

            
        //zstack
    } // body
} //ContentView

// MARK: - DrawerMenuItem
struct DrawerMenuItem: View {
    let title: String //Home,Search,Settings
    @Binding var selectedMenu: String // Default value "Home"
    @Binding var showMenu: Bool // true,false
    /**
           @Binding :  부모뷰의 상태(@State)를 자식뷰가  수정 할 수 있게 연결해주는 참조 ,c++의 포인터와 같은 개념
     */

    var body: some View {
        Button(action: {
            withAnimation(.easeInOut(duration: 0.3)) {
                // 메뉴타이틀 변경(파라메터로 입력된 값을 대입) / Change menu title (substitute the value entered as a parameter)
                selectedMenu = title
                showMenu = false // Close Drawer
            }
        }) {
            HStack {
                Text(title) // Home,Search,Settings
                    .font(.headline)
                    .foregroundColor(selectedMenu == title ? .blue : .primary)
                Spacer()
            }
            .padding(.vertical, 10)
        }
    }
}

// MARK: - Screens(로컬)
struct HomeScreen: View {
    var body: some View {
    Text("로컬화면/Local screen.")
    Text("홈 화면입니다./Home Screen").font(.title).padding()
    }
}
struct SearchScreen: View {
    var body: some View {
        Text("로컬화면./ Local Screen")
        Text("검색 화면입니다. / Search Screen").font(.title).padding()
    }
}
struct SettingsScreen: View {
    var body: some View {
        Text("로컬화면. / Local Screen")
        Text("설정 화면입니다. / Settings Screen").font(.title).padding()
    }
}

// MARK: - Preview
#Preview {
    ContentView()
}

✔️ScreenShot

Leave a Reply