LoginSignup
16
10

More than 3 years have passed since last update.

[Swift] SwiftUIで作るハンバーガーメニュー

Last updated at Posted at 2020-04-15

作ったもの

ボタンをポチと押すと画面左からメニューがニュッと出てくるハンバーガーメニューを作りました。
hamburger_menu.gif

どうやって実装するの?

最初は画面外に居るハンバーガーメニュー

まずハンバーガーメニューがどういう描画をされ、どういう動きをしているのかを理解するところから始めましょう。

ハンバーガーメニューはアプリがロードされたタイミングで既に画面上に描画をされています。
しかし、画面の外にいるのでユーザは見えていないだけです。
そして、何かイベントが発火したタイミングでx座標を変化させながらユーザが見える範囲の画面に登場、という流れになっています。

図にすると以下のようになります。
スクリーンショット 2020-04-15 21.43.54.png

ハンバーガーメニューの位置を計算

ハンバーガーメニューをうまくコントロールするに当たって、ハンバーガーメニューViewの初期位置と終着位置のそれぞれ2つのX座標を定める必要があります。
これは当然デバイスによって画面の幅というのは異なるので、GeometryReaderなどを用いて都度変化させる必要があります。

初期のX座標を計算

理想としては、ハンバーガーメニューの右端が親Viewの左端と重なるくらいが良いですね。

スクリーンショット 2020-04-15 21.50.29.png

私がいつもシミュレータで使っているiPhone11 ProMaxの画面幅414.0を例に計算します。
また、ハンバーガーメニューの大きさは好みですが、私は今回親Viewのちょうど半分(親Viewの幅 * 0.5)にしました。

言葉で説明すると大変なので図にしましたw
親Viewの左端にあたるX座標から更にハンバーガーメニューのViewの幅の半分をマイナス方向に引きずり込んであげるイメージです。
スクリーンショット 2020-04-15 22.44.43.png

コードに起こすとこんな感じです

@State public var currentOffset = CGFloat //動的に変化するハンバーガーメニューのX座標
@State public var closeOffset = CGFloat   //閉じた状態のX座標

/**
 * MenuViewの初期位置・出現時のx軸を定める
 * @param CGFloat viewWidth 親Viewの幅
 */ 
public func setInitPosition(viewWidth: CGFloat) {
    self.currentOffset = (viewWidth/2) * -1 + ((viewWidth * 0.5) / 2) * -1
    self.closeOffset = self.currentOffset
}

終着のX座標を計算

次にハンバーガーメニューがどこまで出しゃばるかを決めます。
例えば画面の半分までハンバーガーメニューが出現するようにします。
初期位置の場合は親Viewの左端よりも更にマイナス方向に押し込みましたが、今度はプラスの方向に押し出すイメージです。
スクリーンショット 2020-04-15 22.44.52.png

先ほどのコードに開いた状態のStateを追加して数値を入れてみましょう。

@State public var currentOffset = CGFloat //動的に変化するハンバーガーメニューのX座標
@State public var closeOffset = CGFloat   //閉じた状態のX座標
@State public var openOffset = CGFloat    //開いた状態のX座標

/**
 * MenuViewの初期位置・出現時のx軸を定める
 * @param CGFloat viewWidth 親Viewの幅
 */ 
public func setInitPosition(viewWidth: CGFloat) {
    self.currentOffset = (viewWidth/2) * -1 + ((viewWidth * 0.5) / 2) * -1
    self.closeOffset = self.currentOffset
     self.openOffset = ((viewWidth / 2) * -1)+((viewWidth * 0.5) / 2)
}

ボタンによる出現

座標が計算できたので、実際にメニューViewを作りボタンから発火するようにしましょう。

メニューはとりあえず適当に(ぁ

HamburgerMenu.swift
import SwiftUI

struct HamburgerMenu: View {
    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .leading) {
                Text("ハンバーガーメニュー")
                    .font(.system(size: 24))
                Divider()
                ScrollView(.vertical, showsIndicators: true) {
                    Text("あ")
                        .font(.system(size: 18))
                    Text("い")
                        .font(.system(size: 18))
                    Text("う")
                        .font(.system(size: 18))
                }
            }
            .padding(.horizontal, 20)
        }
    }
}

イベントを発火させるボタンを配置したNavigationBarも作ります。

NavigationBar.swift
import SwiftUI

struct NavigationBar: View {

    @Binding public var currentOffset: CGFloat
    @Binding public var openOffset: CGFloat
    @Binding public var closeOffset: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color(red: 107/255, green: 142/255, blue: 35/255).edgesIgnoringSafeArea(.all)
                VStack {
                    Button(action: {
                        self.toggleHamburgerMenu()
                    }) {
                        Image("Hamburger") //予め用意したハンバーガーメニュー用のImage
                            .renderingMode(.original)
                            .resizable()
                            .frame(width: 50.0, height: 50.0, alignment: .leading)
                    }
                }.padding()
            }
        }
    }

    // ハンバーガーメニューのX座標を変化させる処理は今回メソッドに切り出しました
    public func toggleHamburgerMenu() {
        if (self.currentOffset == self.openOffset) {
            self.currentOffset = self.closeOffset
        } else {
            self.currentOffset = self.openOffset
        }
    }
}

そして、ハンバーガーメニューを重ねて表示させるメインのViewを作ります。
今回私が作ったアプリはこのViewもContentViewからコンポーネントとして切り出してあるので、ちょっと特殊です。

MainContent.swift
import UIKit
import SwiftUI
import QGrid
import Combine

struct DelayList: View {

    // HamburgerMenuのx座標をこのViewとNavigationBarで共有
    @Binding public var currentOffset: CGFloat
    @Binding public var openOffset: CGFloat
    @Binding public var closeOffset: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                VStack {
                    Text("さんぷる")
                }
                HamburgerMenu()
                    .background(Color.gray)
                    .frame(width: geometry.size.width * 0.5)
                    .onAppear(perform: {
                        self.setInitPosition(viewWidth: geometry.size.width)
                    })
                    .offset(x: self.currentOffset)
                    .animation(.default)
            }
        }
    }

    // MenuViewの初期位置・出現時のx軸を定める
    public func setInitPosition(viewWidth: CGFloat) {
        self.currentOffset = (viewWidth/2) * -1 + (viewWidth*0.5 / 2) * -1
        //self.currentOffset = viewWidth * -1
        self.closeOffset = self.currentOffset
        self.openOffset = ((viewWidth / 2) * -1)+((viewWidth * 0.5) / 2)
    }
}

そしてNavigationBarとMainContentの2つのViewを包括しているContentViewです。

ContentView.swift
import SwiftUI

struct ContentView: View {

    // ハンバーガーメニューの表示/非表示を管理するための変数
    @State public var currentOffset = CGFloat.zero
    @State public var openOffset = CGFloat.zero
    @State public var closeOffset = CGFloat.zero

    var body: some View {
        GeometryReader { geometry in
            VStack {
                NavigationBar(currentOffset: self.$currentOffset, openOffset: self.$openOffset, closeOffset: self.$closeOffset)
                    .frame(width: geometry.size.width, height: geometry.size.height/10)
                MainContent(currentOffset: self.$currentOffset, openOffset: self.$openOffset, closeOffset: self.$closeOffset)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

今挙げた3つのViewの関係は以下のようになっています。

スクリーンショット 2020-04-15 23.26.40.png

実際に動かしてみる

サイドメニュー適当でごめんなさいw
demo.gif

参考にさせて頂いたWebサイト

SwiftUIへの道
SwiftUIの肝となるGeometryReaderについて理解を深める
【SwiftUI】SwiftUIで@Bindingを触ってみました。

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10