作ったもの
ボタンをポチと押すと画面左からメニューがニュッと出てくるハンバーガーメニューを作りました。
どうやって実装するの?
最初は画面外に居るハンバーガーメニュー
まずハンバーガーメニューがどういう描画をされ、どういう動きをしているのかを理解するところから始めましょう。
ハンバーガーメニューはアプリがロードされたタイミングで既に画面上に描画をされています。
しかし、画面の外にいるのでユーザは見えていないだけです。
そして、何かイベントが発火したタイミングでx座標を変化させながらユーザが見える範囲の画面に登場、という流れになっています。
ハンバーガーメニューの位置を計算
ハンバーガーメニューをうまくコントロールするに当たって、ハンバーガーメニューViewの初期位置と終着位置のそれぞれ2つのX座標を定める必要があります。
これは当然デバイスによって画面の幅というのは異なるので、GeometryReaderなどを用いて都度変化させる必要があります。
初期のX座標を計算
理想としては、ハンバーガーメニューの右端が親Viewの左端と重なるくらいが良いですね。
私がいつもシミュレータで使っているiPhone11 ProMaxの画面幅414.0を例に計算します。
また、ハンバーガーメニューの大きさは好みですが、私は今回親Viewのちょうど半分**(親Viewの幅 * 0.5)**にしました。
言葉で説明すると大変なので図にしましたw
親Viewの左端にあたるX座標から更にハンバーガーメニューのViewの幅の半分をマイナス方向に引きずり込んであげるイメージです。
コードに起こすとこんな感じです
@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の左端よりも更にマイナス方向に押し込みましたが、今度はプラスの方向に押し出すイメージです。
先ほどのコードに開いた状態の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を作りボタンから発火するようにしましょう。
メニューはとりあえず適当に(ぁ
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も作ります。
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からコンポーネントとして切り出してあるので、ちょっと特殊です。
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です。
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の関係は以下のようになっています。
実際に動かしてみる
参考にさせて頂いたWebサイト
SwiftUIへの道
SwiftUIの肝となるGeometryReaderについて理解を深める
【SwiftUI】SwiftUIで@Bindingを触ってみました。