SwiftUI サイドメニューの実装(目の保養付き)

<div align="center">
<img width="1000" alt="swiftui_layout_log" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/5bf532b4-e677-4168-f34e-793cc021b411.png">
</div>

# はじめに
サイドメニューを実装してみたので備忘録兼ねて残します。
今回はモチベーションアップ、目の保養の為にイメージ画像は綺麗なお姉ちゃんを使いました。

# イメージ

<img width="300" alt="side_menu_image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184792/8b6ceb22-7374-75b2-cd70-0ee07c18adda.gif">

# 実装内容

## まずメニュー項目のstructを作成

ForEachで使いたいのでIdentifiableに適合させておきます。
今回、画像はSFシンボル名で設定してImage(systemName:)で呼び出すようにしておきます。
メニュータップ時の遷移は実装していないので、こんな感じで終わりにします。

```swift
struct Menu: Identifiable {
    enum MenuType: String {
        case myAccount = "My Account"
        case setting = "Setting"
        case favorite = "Favorite"
        case faq = "FAQ"
        case signOut = "SignOut"
        
        var title: String { self.rawValue }
        
        var imageName: String {
            switch self {
            case .myAccount: return "person.crop.circle"
            case .setting: return "wrench"
            case .favorite: return "star.circle"
            case .faq: return "questionmark.circle"
            case .signOut: return "arrow.turn.up.left"
            }
        }
    }
    
    var id = UUID()
    var type: MenuType
}

```


## MenuRowを作る

サイドメニューのリストに表示するRowの構造体を定義します。
`Image(systemName:)`で表示する画像はデフォルトでは、少々小さいので`imageScale(.large)`にしたほうが良いようです。

```swift
struct MenuRow: View {
    
    var image = ""
    var text = ""
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: image)
                    .imageScale(.large)
                    .frame(width: 32, height: 32)
                Text(text)
                    .font(.headline)
                Spacer()
            }
        }
    }
}
```

## MenuViewを作る
実際にサイドメニューに表示させるViewを作ります。
名前とフォロー、フォロワーあたりは雑な作りなので、いつか綺麗にします。
先に以下、MenuViewの全体象を載せます。

```swift
struct MenuView: View {
    
    var menu = [Menu(type: .myAccount),
                Menu(type: .setting),
                Menu(type: .favorite),
                Menu(type: .faq),
                Menu(type: .signOut),]
    
    @Binding var show: Bool
    
    var body: some View {
        HStack {
            VStack(alignment: .center) {
                Image("female")
                    .resizable()
                    .scaleEffect(2)
                    .offset(x: 0, y: 20)
                    .aspectRatio(5/7, contentMode: .fit)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .overlay(
                        Circle().stroke(Color.white, lineWidth: 4))
                    .shadow(radius: 10)
                
                Text("Miranda Jeen")
                
                HStack {
                    VStack {
                        Text("follow")
                        Text("211")
                    }
                    .padding()
                    
                    VStack {
                        Text("follower")
                        Text("324")
                    }
                    .padding()
                }
                .padding(.vertical)
                
                VStack(alignment: .leading) {
                    ForEach(menu) { item in
                        MenuRow(image: item.type.imageName, text: item.type.title)
                    }
                }
                Spacer()
            }
            .padding(.top, 20)
            .padding(30)
            .frame(width: ScreenSize.width/1.5)
            .background(Color.blue.opacity(0.4))
            .cornerRadius(20)
            .animation(.default)
            .offset(x: show ? 0 : -ScreenSize.width)
            .onTapGesture {
                self.show.toggle()
            }
            Spacer()
        }
        .padding(.top, statusBarHeight)
    }
}
```
## 少し解説

### 表示制御
showは外部からも制御するので@Bindingをつけて定義しておきます。

```swift
@Binding var show: Bool
```

### Imageのレイアウト調節
```swift
               Image("female")
                    .resizable()
                    .scaleEffect(2)
                    .offset(x: 0, y: 20)
                    .aspectRatio(5/7, contentMode: .fit)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .overlay(
                        Circle().stroke(Color.white, lineWidth: 4))
                    .shadow(radius: 10)
```
イメージをリサイズする場合は、`resizable()`をつけます。
`scaleEffect`でスケールを少し拡大して、`offset`で画像自体の表示位置を微調節します。
`aspectRatio(5/7, contentMode: .fit)`でアスペクト比を固定した上で`frame(width: 100, height: 100)`でイメージサイズを指定しています。

### イメージに枠線を付ける
AppleのLandmarkのチュートリアルにあるような枠線で良かったので、そのまま使用しています。

```swift
                    .clipShape(Circle())
                    .overlay(
                        Circle().stroke(Color.white, lineWidth: 4))
                    .shadow(radius: 10)
```

### ForEachでMenuRowを生成する
Identifiableに適合させたMenuをForEachで回して、MenuRowを生成します。

```swift
               VStack(alignment: .leading) {
                    ForEach(menu) { item in
                        MenuRow(image: item.type.imageName, text: item.type.title)
                    }
                }
```

### サイドメニューの動作とMenuViewのレイアウトを調節する

最後にMenuView直下のHStackとその直下のVStackにレイアウトとサイドメニュー動作を追加します。

```swift
     var body: some View {
        HStack {
            VStack(alignment: .center) {
         
            }
            ....
            .padding(.top, 20)
            .padding(30)
            .frame(width: ScreenSize.width/1.5)
            .background(Color.blue.opacity(0.4))
            .cornerRadius(20)
            .animation(.default)
            .offset(x: show ? 0 : -ScreenSize.width)
            .onTapGesture {
                self.show.toggle()
            }
            Spacer()
        }
        .padding(.top, statusBarHeight)
```

`.offset(x: show ? 0 : -ScreenSize.width)`でサイドメニューが非表示状態になっている場合は、画面外の位置に待機させておくようにしています。
こうすることで出しわけることができます。 

最後にHStackに対して`.padding(.top, statusBarHeight)`で余白をつけて、MenuViewは完成です。

## 親のViewで呼び出す

これでボタンをタップしたらサイドメニューが表示されるようになりました。

```swift
struct SideMenuView: View {
    @State var show = false
    
    var body: some View {
        ZStack {
            Group {
                Button(action: {
                    self.show = true
                }) {
                    Text("show side menu")
                }
            }
            .blur(radius: show ? 20 : 0)
            MenuView(show: $show)
        }
        .edgesIgnoringSafeArea(.all)
    }
}
```

サイドメニューを表示している時にメインのViewを濁したいのでblurを使っていますが、今回は、ボタンしかない真っ白名画面なので濁りませんが、つけています。

```swift
.blur(radius: show ? 20 : 0)
```

## ソースコード
SwiftUIのレイアウト学習用のレポジトリです。
申し訳程度のものですが、ちょっとずつあげていきます。

https://github.com/kazy-dev/SwiftUI-Layout-Practice

## さいごに
UIKitで実装するよりもシンプルな実装で作れました。
サイドメニューを表示するたびに綺麗なお姉さんが表示されて、癒されながら学習することができました。
こういうちょっとした目の保養って大事だなと改めて感じましたw

良かったら使ってみてください。

もっといい案あったり、ご指摘、アドバイスあればお待ちしております。
それでは最後までお読みいただきありがとうございました。