[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(๋กœ์ปฌ/local)
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