【Xcode/SwiftUI】ハンバーガーメニュー(サイドメニュー)を作ってみる

実装

今回使用&新規作成するファイル一覧

  • SideMenuRowType.swift
  • MainTabView.swift
  • SideMenuView.swift
  • SideMenu.swift
  • HomeView.swift(デフォルトだとContentView.swift)
  • PostView.swift
  • NewsView.swift
  • SettingView.swift
  • TabItemView.swift

それでは作っていきましょう。

SideMenuRowType (各メニューアイテムの名前、アイコン画像の管理)

import Foundation

enum SideMenuRowType: Int, CaseIterable {
    case home = 0
    case post
    case news
    case setting

    var title: String {
        switch self {
        case .home:
            return "Home"
        case .post:
            return "Post"
        case .news:
            return "News"
        case .setting:
            return "Setting"
        }
    }

    var iconName: String {
        switch self {
        case .home:
            return "house"
        case .post:
            return "camera.viewfinder"
        case .news:
            return "newspaper"
        case .setting:
            return "gear"
        }
    }
}

MainTabView (サイドメニュー全体の管理)

import SwiftUI

struct MainTabView: View {
    @State var presentSideMenu = false
    @State var selectedSideMenuTab = 0

    var body: some View {
        ZStack {
            TabView(selection: $selectedSideMenuTab) {
                HomeView(presentSideMenu: $presentSideMenu)
                    .tag(0)
                PostView(presentSideMenu: $presentSideMenu)
                    .tag(1)
                NewsView(presentSideMenu: $presentSideMenu)
                    .tag(2)
                SettingView(presentSideMenu: $presentSideMenu)
                    .tag(3)
            }
            SideMenu(isShowing: $presentSideMenu, content: AnyView(SideMenuView(selectedSideMenuTab: $selectedSideMenuTab, presentSideMenu: $presentSideMenu)))
        }
    }
}

struct MainTabView_Previews: PreviewProvider {
    static var previews: some View {
        MainTabView()
    }
}

SideMenuView (サイドメニューのUIを構成)

import SwiftUI

struct SideMenuView: View {
    @Binding var selectedSideMenuTab: Int
    @Binding var presentSideMenu: Bool

    var body: some View {
        HStack {
            ZStack {
                Rectangle()
                    .fill(.white)
                    .frame(width: 270)
                    .shadow(color: .blue.opacity(0.3), radius: 5, x: 0, y: 3)
                VStack(alignment: .leading, spacing: 0) {
                    profileImageView()
                        .frame(height: 140)
                        .padding(.bottom, 30)
                    ForEach(SideMenuRowType.allCases, id: \.self) { row in
                        rowView(isSelected: selectedSideMenuTab == row.rawValue, imageName: row.iconName, title: row.title) {
                            selectedSideMenuTab = row.rawValue
                            presentSideMenu.toggle()
                        }
                    }
                    Spacer()
                }
                .padding(.top, 100)
                .frame(width: 270)
                .background(
                    Color.white
                )
            }
            Spacer()
        }
        .background(.clear)
    }

    @ViewBuilder
    private func profileImageView() -> some View {
        VStack(alignment: .center) {
            HStack {
                Spacer()
                Image("<#好きな画像#>")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .overlay(
                        RoundedRectangle(cornerRadius: 50)
                            .stroke(.blue.opacity(0.3), lineWidth: 10)
                    )
                    .cornerRadius(50)
                Spacer()
            }

            Text("<#好きなテキスト#>")
                .font(.system(size: 18, weight: .bold))
                .foregroundColor(.black)

            Text("<#好きなテキスト#>")
                .font(.system(size: 14, weight: .semibold))
                .foregroundColor(.black.opacity(0.5))
        }
    }

    @ViewBuilder
    private func rowView(isSelected: Bool, imageName: String, title: String, hideDivider: Bool = false, action: @escaping (() -> Void)) -> some View {
        Button {
            action()
        } label: {
            VStack(alignment: .leading) {
                HStack(spacing: 20) {
                    Rectangle()
                        .fill(isSelected ? .blue.opacity(0.3) : .white)
                        .frame(width: 5)
                    ZStack {
                        Image(systemName: imageName)
                            .resizable()
                            .renderingMode(.template)
                            .foregroundColor(isSelected ? .black : .gray)
                            .frame(width: 26, height: 26)
                    }
                    .frame(width: 30, height: 30)
                    Text(title)
                        .font(.system(size: 14, weight: .regular))
                        .foregroundColor(isSelected ? .black : .gray)
                    Spacer()
                }
            }
        }
        .frame(height: 50)
        .background(
            LinearGradient(colors: [isSelected ? .blue.opacity(0.3) : .white, .white], startPoint: .leading, endPoint: .trailing)
        )
    }

}


struct SideMenuView_Previews: PreviewProvider {
    static var previews: some View {
        SideMenuView(selectedSideMenuTab: .constant(0), presentSideMenu: .constant(false))
    }
}

SideMenu

import SwiftUI

struct SideMenu: View {
    @Binding var isShowing: Bool
    var content: AnyView
    var edgeTransition: AnyTransition = .move(edge: .leading)
    
    var body: some View {
        ZStack(alignment: .bottom) {
            if isShowing {
                Color.black
                    .opacity(0.3)
                    .ignoresSafeArea()
                    .onTapGesture {
                        isShowing.toggle()
                    }
                content
                    .transition(edgeTransition)
                    .background(
                        Color.clear
                    )
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
        .ignoresSafeArea()
        .animation(.easeInOut, value: isShowing)
    }
}

struct SideMenu_Previews: PreviewProvider {
    static var previews: some View {
        SideMenu(isShowing: .constant(false), content: AnyView(MainTabView()))
    }
}

HomeView ~ SettingView (各メニューアイテムのView)

import SwiftUI

struct <#各Viewの名前#>: View {
    @Binding var presentSideMenu: Bool

    var body: some View {
        TabItemView(presentSideMenu: $presentSideMenu, title: "<#各Viewの名前#>")
            .padding(.horizontal, 24)
    }
}

struct <#各Viewの名前#>_Previews: PreviewProvider {
    static var previews: some View {
        <#各Viewの名前#>(presentSideMenu: .constant(false))
    }
}

TabItemView (各メニューのUIをまとめる)

import SwiftUI

/// 各タブアイテムのView
struct TabItemView: View {
    @Binding var presentSideMenu: Bool
    let title: String

    var body: some View {
        VStack {
            HStack {
                Button {
                    presentSideMenu.toggle()
                } label: {
                    Image(systemName: "list.bullet")
                        .resizable()
                        .frame(width: 32, height: 32)
                        .foregroundColor(.black)
                }
                Spacer()
            }
            Spacer()
            Text(title)
                .font(.system(size: 32, weight: .medium))
            Spacer()
        }
    }
}

(プロジェクト名).swift (初期表示するViewを決めるところ)

import SwiftUI

@main
struct SwiftUI_PlaygroundApp: App {
    @State static var presentSideMenu = false
    var body: some Scene {
        WindowGroup {
            MainTabView()
        }
    }
}