【SwiftUI】みんなの銀行のUIを再現してみた

# 1.はじめに
2021/5/28にリリースされたみんなの銀行というサービスはご存知でしょうか。
こちらは国内初のデジタルバンクであり、銀行口座の開設から運用まで全てスマホで完結するというサービスです。

公式サイトは[こちら](https://corporate.minna-no-ginko.com/information/corporate/2021/05/28/32/)


私も実際に口座を開設して使ってみましたが、
UIがいい意味で銀行らしくなく、とても現代的なデザインで素敵でした。
ということで今回はみんなの銀行のUIをSwiftUIで再現できるかという記事になります。
※私はSwiftUI初めて１ヶ月程度の初心者ですので実装についてもっとこうした方がいいなどあればご指摘いただけると幸いです。

最後にコード全て載せているので実際にコピペして使ってみてください！


<br><br><br>
# 2. みんな銀行のUI
本家のUIです。

![Videotogif(1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/26eec7c3-1676-48cc-ef94-29d3ea36a23e.gif)




<br><br><br>
# 3.作ったもの

実際に作ったものがこちらです。
![みんなの銀行_再現.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/ae59789a-d19c-ce57-91c0-c54e04c74772.gif)

ベースとなるTabViewとNavigationView、上にスワイプでモーダルメニュー画面が表示されるところまで再現してみました。
画像は特に関係のないフリー素材をデモ用として使っています。

<br><br><br>
# 4. 設計
　大体の設計はこんな感じです。作成するのはTabViewで切り替える３画面とスワイプしたときに表示する１画面となります。

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/7fbe2bb4-eb61-4496-e613-f33ac3442988.png)

　画面でみるとこんな感じです。ポイントとなる①〜④についてコードを記載していきたいと思います。
![図３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/640e93d3-4140-3a70-95d8-50821ecd5c51.png)

<br><br><br>
# 5. 実装
## ① NavigationView

まずはベースのNavigationViewについてです、こちらは基本的な内容ですね。


```swift:ContentView.swift
struct ContentView: View {
    
    init() {
        UINavigationBar.appearance().shadowImage = UIImage()
        UINavigationBar.appearance().barTintColor = UIColor(.white)
    }
    
    var body: some View {
        NavigationView {
            // TabViewを記載していく
        }
　　　　　.navigationBarItems(
                    leading: Text("タイトル".font(.largeTitle).fontWeight(.black)
                    ,
                    trailing:
                        HStack {
                            // NavigationBarに表示するButtonを記載していく
                        }
                )
    }
```

３点ほど作成するにあたってつまずきました。

- NavigationBarの色を変える<br>
下記のようにすると変えられるようです。

```swift
UINavigationBar.appearance().barTintColor = UIColor(.white)
```

  
- NavigationBarとViewの境目線を消す<br>
shadowImageに空のイメージを入れると消えるようです。

```swift
UINavigationBar.appearance().shadowImage = UIImage()
```

 - NavigationBarのタイトルをでっかくする
 はじめnavigationTitleで指定しようとしてfontが思うように反映されず手こずりました。
　navigationVarItemにTextで突っ込んでしまえば解決です。

```swift
leading: Text("タイトル".font(.largeTitle).fontWeight(.black)
```

<br><br>
## ②TabView（PageTabViewStyle)を使ってViewを切り替え
　メイン画面についてはTabViewをつかっています。
　こちらはタブでの画面切り替えと画面のスワイプでの切り替え両方を実現するように考えました。

　調べましたがうまく実装してくれるのがなさそうでしたので、
　スワイプでの画面切り替えをTabViewのPageTabViewStyleで実現し、
　タブでの画面切り替えは自作しました。（④）


```swift
struct ContentView: View {
    @State private var selection = 0
・・・

    VStack {
                // *** ページタブビュー ***
                TabView(selection: $selection) {
                    WalletView()
                        .tag(0)
                    BankingView()
                        .tag(1)
                    RecordView()
                        .tag(2)
                }
                .accentColor(.white)
                .tabViewStyle(PageTabViewStyle())
                .animation(.easeInOut)
                .transition(.slide)
    }
・・・
}

```

tabViewStyle(PageTabViewStyle())を指定してあげるとスワイプでタブ切り替えを実現してくれます。





また、下記のように設定すると初期の表示時とタブ切り替え時にスライドアニメーションしてくれます。

```swift
                .animation(.easeInOut)
                .transition(.slide)
```



<br><br>
## ③ 上にスワイプするとメニュー画面をモーダル表示

ここの実装が一番悩みました。。
SwiftUIでこれ！といったものが見つけられず。（もしあったら教えてください）

今回の作り方としてはテキストViewで「_____」と表示してそこを上にスワイプしたときにメニュー画面を表示するとしました。
”上にスワイプしたとき”というのは下記の記事のコードを参考にさせていただきました。

[/[SwiftUI] Swipeの検知と実装](https://software.small-desk.com/development/2020/07/30/swiftui-swipe/)


```swift
Text("________")
                .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height / 100 * 7, alignment: .bottom)
                .gesture(DragGesture()
                            .onEnded({ value in
                                if (abs(value.translation.height) < 10) { return } // too small movement, ignore note: 10 is default value for minimumDistance
                                if (value.translation.height < 0 ) {
                                    
                                    print("swipe to up")
                                    self.isMenuShowing = true
                                } else if (value.translation.height > 0 ) {
                                    print("swipe to down")
                                    self.isMenuShowing = false                                }
                            })
                )
                .fullScreenCover(isPresented: $isMenuShowing) {
                    ModalMenuView(isMenuShowing: self.$isMenuShowing)
                }
```


こちらがメニュー画面を表示する処理になります。
状態変数isMenuShowingがtrueの時に表示されます。
こちらは.gestureで上スワイプをした時にtrueにします。

```swift
               .fullScreenCover(isPresented: $isMenuShowing) {
                    ModalMenuView(isMenuShowing: self.$isMenuShowing)
                }
```

<br><br>
## ④ 自作でタブ切り替え
画面下部にあるタブ切り替え部分は自前で実装します。
自前と言っても下記をほぼそのまま使わせていただきました。ありがとうございます。

[SwiftUIのTabViewの問題に自作TabView(SwiftUI製)で対処する](https://qiita.com/tsuzuki817/items/ce9685397cdf061381c7)

<br><br><br>
# 6. おわりに
いかがでしたでしょうか。
まだまだ本家の完成度には及びませんが、実際のUIを真似してみるのはとてもいい勉強になりますね。

おまけにコードを全て載せているので興味のある方はご自身の環境で実行してみてください！
<br><br><br>
# おまけ

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var selection = 0
    @State private var nvBarTitle : [String] = ["Wallet", "Banking", "Record"]
    private let width = UIScreen.main.bounds.width
    private let heigth = UIScreen.main.bounds.height / 100 * 9
    
    
    init() {
        UITabBar.appearance().barTintColor = UIColor(.black)
        UITabBar.appearance().backgroundColor = UIColor(.white)
        UITabBar.appearance().unselectedItemTintColor = UIColor(.gray)
        
        UINavigationBar.appearance().shadowImage = UIImage()
        UINavigationBar.appearance().barTintColor = UIColor(.white)
        
    }
    
    var body: some View {
        NavigationView {
            VStack {
                // *** ページタブビュー ***
                TabView(selection: $selection) {
                    WalletView()
                        .tag(0)
                    BankingView()
                        .tag(1)
                    RecordView()
                        .tag(2)
                }
                .accentColor(.white)
                .tabViewStyle(PageTabViewStyle())
                .animation(.easeInOut)
                .transition(.slide)
                .navigationBarItems(
                    leading: Text(nvBarTitle[self.selection]).font(.largeTitle).fontWeight(.black)
                    ,
                    trailing:
                        HStack {
                            if self.selection == 0 {
                                Button(action: {}) {
                                    Image(systemName: "trash")
                                        .foregroundColor(.black)
                                }
                                Button(action: {}) {
                                    Image(systemName: "trash")
                                        .foregroundColor(.black)
                                    
                                }
                            }else if self.selection == 1 {
                                Button(action: {}) {
                                    Image(systemName: "trash")
                                        .foregroundColor(.black)
                                }
                            }
                        }
                )
                
                // *** 自作ナビゲーションタブ ***
                ZStack {
                    Rectangle()
                        .foregroundColor(Color.black)
                        .frame(width: self.width, height: self.heigth)
                    
                    HStack(spacing: self.heigth / 2) {
                        Spacer()
                        
                        Button(action: {
                            self.selection = 0
                        }) {
                            VStack {
                                Image(systemName: "doc")
                                    .foregroundColor(self.selection != 0 ? Color.gray : Color.white)
                                    .font(.system(size: self.heigth / 3, design: .rounded))
                            }
                        }
                        .padding(.bottom, 30)
                        
                        Spacer()
                        
                        Button(action: {
                            self.selection = 1
                        }) {
                            VStack {
                                Image(systemName: "waveform.path.ecg")
                                    .foregroundColor(self.selection != 1 ? Color.gray : Color.white)
                                    .font(.system(size: self.heigth / 3, design: .rounded))
                            }
                        }.padding(.bottom, 30)
                        
                        Spacer()
                        
                        Button(action: {
                            self.selection = 2
                        }) {
                            VStack {
                                Image(systemName: "folder.fill.badge.plus")
                                    .foregroundColor(self.selection != 2 ? Color.gray : Color.white)
                                    .font(.system(size: self.heigth / 3, design: .rounded))
                            }
                        }.padding(.bottom, 30)
                        
                        Spacer()
                    }
                }
            }.edgesIgnoringSafeArea(.bottom)
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
}
```

```swift:WalletView.swift

import SwiftUI
import SlideOverCard

struct WalletView: View {
    @State private var position = CardPosition.middle
    @State private var background = BackgroundStyle.solid
    @State private var isMenuShowing = false
    
    var body: some View {
        VStack(alignment: .leading) {
            Image("IMG01")
                .resizable()
                .scaledToFit()
                .padding(50)
                .border(Color.black)
            
            VStack(alignment: .leading) {
                Text("普通預金")
                Text("¥********").font(.largeTitle).fontWeight(.black)
                Text("残高を表示")
                
            }
            .padding(30)
            
            Text("＊＊＊お知らせ＊＊＊").font(.title3)
                .padding(30)
            
            Spacer()
            Text("________")
                .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height / 100 * 7, alignment: .bottom)
                .gesture(DragGesture()
                            .onEnded({ value in
                                if (abs(value.translation.height) < 10) { return } // too small movement, ignore note: 10 is default value for minimumDistance
                                if (value.translation.height < 0 ) {
                                    // swiped to left
                                    print("swipe to up")
                                    self.isMenuShowing = true
                                } else if (value.translation.height > 0 ) {
                                    // swiped to down
                                    print("swipe to down")
                                    self.isMenuShowing = false
                                }
                            })
                )
                .fullScreenCover(isPresented: $isMenuShowing) {
                    ModalMenuView(isMenuShowing: self.$isMenuShowing)
                }
        }
        
        
    }
}

struct WalletView_Previews: PreviewProvider {
    static var previews: some View {
        WalletView()
    }
}
```

```Swift:BankingView.swift
import SwiftUI

struct BankingView: View {
    var body: some View {
        VStack {
            Image("IMG03")
                .resizable()
                .scaledToFit()
                .padding(50)

            Spacer()
        }
    }
}
```

```swift:RecordView.swift
import SwiftUI

struct RecordView: View {
    var body: some View {
        
        VStack {
            Image("IMG02")
                .resizable()
                .scaledToFit()
                .padding(50)

            Spacer()
        }
        
    }
}

```