この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
概要
大阪オフィスの山田です。趣味プログラミングでSwiftUIでサイドメニューを作ってみたので、その実装紹介記事です。
開発環境
- Xcode 11.6
- macOS 10.15.4
作るもの
画像のようなサイドメニューを作ってみたいと思います。横にスライドするのではなく、上に被せる形のメニューです。 選択したメニューのタイトルがメイン画面に表示されます。完成形はこちら。
メイン画面
まずメインの画面の左上に、サイドメニューを開くボタンを配置します。
navigationBarItem
として配置します。
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.navigationBarTitle("メイン画面")
.navigationBarItems(leading: (
Button(action: {
// Todo: サイドメニューを開く
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}))
}
}
}
サイドメニュー
背景部分
サイドメニューの背景部分の実装です。
struct SideMenuView: View {
@Binding var isOpen: Bool
var body: some View {
ZStack {
// 背景部分
GeometryReader { geometry in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isOpen ? 1.0 : 0.0)
.opacity(1.0)
.animation(.easeIn(duration: 0.25))
.onTapGesture {
self.isOpen = false
}
// Todo: ここにリスト部分を実装する
}
}
}
サイドメニューが開いているかどうかをisOpen
フラグで表現します。
GeometryReader
とEmptyView
を組み合わせて背景部分を作ります。透過ありのグレー色を指定しています。この背景部分をタップした時に、isOpen
フラグをfalseにして、opacity
が0になり、見えない状態になります。
ここまで出来たら一度、メイン画面と繋ぎこみます。
struct ContentView: View {
@State var isOpenSideMenu: Bool = false
var body: some View {
ZStack{
NavigationView {
Text("Hello, World!")
.navigationBarTitle("メイン画面")
.navigationBarItems(leading: (
Button(action: {
self.isOpenSideMenu.toggle()
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}))
}
SideMenuView(isOpen: $isOpenSideMenu)
.edgesIgnoringSafeArea(.all)
}
}
}
メイン画面側では、以下の実装を入れます。
- サイドメニューを開いているかどうかのフラグを用意する
- ボタンタップで、フラグをtoggleする
- SideMenuViewにフラグをバインドして渡す
サイドメニューは、SafeAreaまで表示させたいので、.edgesIgnoringSafeArea(.all)
を指定します。
これで、左上のボタンをタップしたらSideMenuView
が表示されます。SideMenuView
側でisOpen
フラグをバインドしているので、SideMenuView
で値が変更されると、ContentView
のisOpenSideMenu
フラグに反映されます。
メニュー部分
HStack
とVStack
を使って実装していきます。List
を使っても良いかと思いましたが、今回はStack
を使いました。
struct SideMenuView: View {
@Binding var isOpen: Bool
let width: CGFloat = 270
var body: some View {
ZStack {
// ...背景部分...
// リスト部分
HStack {
VStack() {
SideMenuContentView(topPadding: 100, systemName: "person", text: "Profile")
SideMenuContentView(systemName: "bookmark", text: "Bookmark")
SideMenuContentView(systemName: "gear", text: "Setting")
Spacer()
}
.frame(width: width)
.background(Color(UIColor.systemGray6))
.offset(x: self.isOpen ? 0 : -self.width)
.animation(.easeIn(duration: 0.25))
Spacer()
}
}
}
}
// セルのビュー
struct SideMenuContentView: View {
let topPadding: CGFloat
let systemName: String
let text: String
init(topPadding: CGFloat = 30, systemName: String, text: String) {
self.topPadding = topPadding
self.systemName = systemName
self.text = text
}
var body: some View {
HStack {
Image(systemName: systemName)
.foregroundColor(.gray)
.imageScale(.large)
.frame(width: 32.0)
Text(text)
.foregroundColor(.gray)
.font(.headline)
Spacer()
}
.padding(.top, topPadding)
.padding(.leading, 32)
}
}
リスト部分はHStack
を使って、リスト部分(VStack
)とスペース部分(Spacer
)を並べます。リスト部分は、指定した幅で表示されますが、isOpen
フラグがfalse
の時は、offsetを幅の分だけマイナスし、隠れるようにします。また表示非表示のアニメーションを指定しています。
メニューの各セルはSideMenuContentView
として切り出しています。上のPaddingは、外部から指定できるようにしています。一番上のセルだけ100を指定して、多めに余白を取れるようにしています。
各セルは画像のsystemName
と、表示するテキストを受け取ることができるようになっています。
選択した文字をメイン画面に反映
いくつか方法はあると思いますが、ここではContentView
でState
を作成し、それを愚直にBinding
で渡していきます。
まずContentView
です。
struct ContentView: View {
@State var isOpenSideMenu: Bool = false
@State var text = "Hello, World!"
var body: some View {
ZStack{
NavigationView {
Text(text)
.navigationBarTitle("メイン画面")
.navigationBarItems(leading: (
Button(action: {
self.isOpenSideMenu.toggle()
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}))
}
SideMenuView(isOpen: $isOpenSideMenu, text: $text)
.edgesIgnoringSafeArea(.all)
}
}
}
State
でtext
という変数を宣言し、SideMenuView
に渡しています。
次は、SideMenuView
です。
struct SideMenuView: View {
@Binding var isOpen: Bool
@Binding var text: String
let width: CGFloat = 270
var body: some View {
ZStack {
GeometryReader { geometry in
EmptyView()
}
.background(Color.gray.opacity(0.3))
.opacity(self.isOpen ? 1.0 : 0.0)
.opacity(1.0)
.animation(.easeIn(duration: 0.25))
.onTapGesture {
self.isOpen = false
}
HStack {
VStack() {
SideMenuContentView(topPadding: 100, systemName: "person", text: "Profile", bindText: $text, isOpen: $isOpen)
SideMenuContentView(systemName: "bookmark", text: "Bookmark", bindText: $text, isOpen: $isOpen)
SideMenuContentView(systemName: "gear", text: "Setting", bindText: $text, isOpen: $isOpen)
Spacer()
}
.frame(width: width)
.background(Color(UIColor.systemGray6))
.offset(x: self.isOpen ? 0 : -self.width)
.animation(.easeIn(duration: 0.25))
Spacer()
}
}
}
}
@Binding
で変数を宣言し、それをSideMenuContentView
に渡しています。選択と同時にサイドメニューを閉じたいので、isOpen
フラグも一緒に渡しています。
最後にSideMenuContentView
です。
struct SideMenuContentView: View {
let topPadding: CGFloat
let systemName: String
let text: String
@Binding var bindText: String
@Binding var isOpen: Bool
init(topPadding: CGFloat = 30, systemName: String, text: String, bindText: Binding<String>, isOpen: Binding<Bool>) {
self.topPadding = topPadding
self.systemName = systemName
self._bindText = bindText
self._isOpen = isOpen
self.text = text
}
var body: some View {
HStack {
Image(systemName: systemName)
.foregroundColor(.gray)
.imageScale(.large)
.frame(width: 32.0)
Text(text)
.foregroundColor(.gray)
.font(.headline)
Spacer()
}
.padding(.top, topPadding)
.padding(.leading, 32)
.onTapGesture {
self.bindText = self.text
self.isOpen = false
}
}
}
bindText
とisOpen
を@Binding
で宣言して、イニシャライザで値をセットしています。そして、タップされた時(onTapGesture
)でisOpen
フラグとbindText
を更新しています。更新された値は、ContentView
にも伝わることで、メニューが閉じられ、選択された文字列が表示されます。
ここまでの実装をGistに載せておきます。
終わりに
サイドメニューって無くなりそうでなかなか無くならないニクイやつ