1.はじめに
2021/5/28にリリースされたみんなの銀行というサービスはご存知でしょうか。
こちらは国内初のデジタルバンクであり、銀行口座の開設から運用まで全てスマホで完結するというサービスです。
公式サイトはこちら
私も実際に口座を開設して使ってみましたが、
UIがいい意味で銀行らしくなく、とても現代的なデザインで素敵でした。
ということで今回はみんなの銀行のUIをSwiftUIで再現できるかという記事になります。
※私はSwiftUI初めて1ヶ月程度の初心者ですので実装についてもっとこうした方がいいなどあればご指摘いただけると幸いです。
最後にコード全て載せているので実際にコピペして使ってみてください!
2. みんな銀行のUI
本家のUIです。
3.作ったもの
ベースとなるTabViewとNavigationView、上にスワイプでモーダルメニュー画面が表示されるところまで再現してみました。
画像は特に関係のないフリー素材をデモ用として使っています。
4. 設計
大体の設計はこんな感じです。作成するのはTabViewで切り替える3画面とスワイプしたときに表示する1画面となります。
画面でみるとこんな感じです。ポイントとなる①〜④についてコードを記載していきたいと思います。
5. 実装
① NavigationView
まずはベースのNavigationViewについてです、こちらは基本的な内容ですね。
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を記載していく
}
)
}
3点ほど作成するにあたってつまずきました。
- NavigationBarの色を変える
下記のようにすると変えられるようです。
UINavigationBar.appearance().barTintColor = UIColor(.white)
- NavigationBarとViewの境目線を消す
shadowImageに空のイメージを入れると消えるようです。
UINavigationBar.appearance().shadowImage = UIImage()
- NavigationBarのタイトルをでっかくする
はじめnavigationTitleで指定しようとしてfontが思うように反映されず手こずりました。
navigationVarItemにTextで突っ込んでしまえば解決です。
leading: Text("タイトル".font(.largeTitle).fontWeight(.black)
②TabView(PageTabViewStyle)を使ってViewを切り替え
メイン画面についてはTabViewをつかっています。
こちらはタブでの画面切り替えと画面のスワイプでの切り替え両方を実現するように考えました。
調べましたがうまく実装してくれるのがなさそうでしたので、
スワイプでの画面切り替えをTabViewのPageTabViewStyleで実現し、
タブでの画面切り替えは自作しました。(④)
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())を指定してあげるとスワイプでタブ切り替えを実現してくれます。
また、下記のように設定すると初期の表示時とタブ切り替え時にスライドアニメーションしてくれます。
.animation(.easeInOut)
.transition(.slide)
③ 上にスワイプするとメニュー画面をモーダル表示
ここの実装が一番悩みました。。
SwiftUIでこれ!といったものが見つけられず。(もしあったら教えてください)
今回の作り方としてはテキストViewで「_____」と表示してそこを上にスワイプしたときにメニュー画面を表示するとしました。
”上にスワイプしたとき”というのは下記の記事のコードを参考にさせていただきました。
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にします。
.fullScreenCover(isPresented: $isMenuShowing) {
ModalMenuView(isMenuShowing: self.$isMenuShowing)
}
④ 自作でタブ切り替え
画面下部にあるタブ切り替え部分は自前で実装します。
自前と言っても下記をほぼそのまま使わせていただきました。ありがとうございます。
SwiftUIのTabViewの問題に自作TabView(SwiftUI製)で対処する
6. おわりに
いかがでしたでしょうか。
まだまだ本家の完成度には及びませんが、実際のUIを真似してみるのはとてもいい勉強になりますね。
おまけにコードを全て載せているので興味のある方はご自身の環境で実行してみてください!
おまけ
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()
}
}
}
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()
}
}
import SwiftUI
struct BankingView: View {
var body: some View {
VStack {
Image("IMG03")
.resizable()
.scaledToFit()
.padding(50)
Spacer()
}
}
}
import SwiftUI
struct RecordView: View {
var body: some View {
VStack {
Image("IMG02")
.resizable()
.scaledToFit()
.padding(50)
Spacer()
}
}
}