SwiftUIで色々なレイアウトを作ってみる【iOS】

SwiftUIに少しずつ慣れてきたので今回はアニメーション付きで色々なレイアウトを作ってみます。

1. 開閉可能なメニューの作成

ボタン、テキスト、画像を使用して開閉可能なメニューを作成してみます。

レイアウトの構成は以下のようにしました。

[開くボタン][システム画像]
[ボタン1]
[ボタン2]
[ボタン3]
[ボタン4]


全体はVStackで縦並びにして、開閉ボタンとシステム画像はHStackで横並びにします。

開閉状態を管理する変数isOpenの初期値はfalseとしています。toggle()でtrueに切り替わったときに開く、falseになったときに閉じる動作が行われます。

struct DropDownMenu: View {
    
    @State var isOpen = false
    
    var body: some View {
        VStack {
            
            HStack {
                Text(isOpen ? "閉じる" : "開く").fontWeight(.heavy)
                
                Image(systemName: isOpen ? "chevron.up" : "chevron.down")
            }.onTapGesture {
                self.isOpen.toggle()
            }
            
            if isOpen {
                Button {
                    //
                } label: {
                    Text("Button1").padding(5).foregroundColor(.black)
                }
                
                Button {
                    //
                } label: {
                    Text("Button2").padding(5).foregroundColor(.black)
                }
                
                Button {
                    //
                } label: {
                    Text("Button3").padding(5).foregroundColor(.black)
                }
                
                Button {
                    //
                } label: {
                    Text("Button4").padding(5).foregroundColor(.black)
                }
            }
        }.frame(height: isOpen ? 200 : 30)
            .padding(7)
            .background(LinearGradient(gradient: .init(colors: [.yellow, .green]), startPoint: .top, endPoint: .bottom))
            .cornerRadius(10)
            .animation(.spring())
    }
}


2. カスタムアラートの作成

iOSの標準アラートはシンプルなデザインなのでどんなデザインのアプリでも違和感が少ないですが、アプリ統一感をもたせたい場合、やはりアラートのデザインもカスタムして作りたい場面が有ると思います。

開く、閉じる、メッセージを表示するといったシンプルな機能のカスタムアラートを作ってみます。

2-1. アラート表示用Viewの作成

@Bindingで表示、非表示の状態を管理する変数を追加します。

その後Buttonを追加してシステム画像の×を設定しています。

ボタンに背景を設定して背景の形状を円にすることでよりボタンらしくなります。

最後に大枠のZStackに対してサイズを指定します。この部分がアラート全体の背景部分のサイズとなります。

アラート表示用のCustomAlertが完成しました。

struct CustomAlert: View {
    //表示、非表示状態
    @Binding var isShow: Bool
    
    var body: some View {
        ZStack {
            
            VStack {
                
                HStack {
                    Spacer()
                    
                    Button {
                        //表示、非表示状態の切り替え
                        self.isShow.toggle()
                    } label: {
                        //アラートの閉じるボタン
                        Image(systemName: "xmark").resizable().frame(width: 10, height: 10).padding(8).background(.black)
                            .foregroundColor(.white)
                            .clipShape(Circle())
                            .offset(x: -5, y: 5)
                    }
                }
                
                Spacer()
            }
        }
        //アラート枠のサイズ
        .frame(width: UIScreen.main.bounds.width - 25, height: 200)
        .background(.yellow)
        .cornerRadius(20)
            
    }
}

2-2. アラートの表示、アニメーションの追加

ナビゲーションバー右のOpenボタンをタップ後に先ほど作成したカスタムアラートを表示する処理を追加します。

アラートの表示にはアニメーションを設定、アラート表示時にはアラート以外の部分をぼかす処理を設定しました。

    @State var isShow = false
    
    var body: some View {
        ZStack {
            
            NavigationView {
                
                List(0..<10){ i in
                    
                    Text("テキスト\(i)")
                }.navigationBarTitle("一覧")
                    .navigationBarItems(trailing: Button(action: {
                        //action
                        self.isShow.toggle()
                    }, label: {
                        Text("Open")
                    }))
            }.blur(radius:  isShow ? 30 : 0)
            
            if isShow {
                CustomAlert(isShow: $isShow)
                    
            }
        }
        .edgesIgnoringSafeArea(.all)
        .animation(.spring())
        
    }


3. スライド表示のメニュー

次はスライド表示のメニューを作ってみます。

ナビゲーションバー左上のメニューボタンをタップすると左側からメニューがスライド表示されるイメージで実装していきます。

3-1. メニュー表示のView

メニュー表示の部分のViewを作ります。

右上にメニューを閉じるボタンを配置し、その下にメニューの項目を並べます。メニューの項目は画像とテキストのみで作ったためタップはできません。

左側から表示されるメニューなのでSpacerを使って左寄せでレイアウトを作っていきます。

メニュー部分の完成イメージ


struct SlideMenu: View {
    
    @Binding var menuWidth: CGFloat
    @Binding var isMenuOpen: Bool
    
    var body: some View {
        
        VStack {
            
            HStack {
                Spacer()
                
                Button {
                    self.isMenuOpen.toggle()
                    self.menuWidth = UIScreen.main.bounds.width / 1.6
                } label: {
                    Image(systemName: "xmark")
                        .resizable()
                        .frame(width: 20, height: 20)
                        .padding(8)
                        .background(.black)
                        .foregroundColor(.white)
                        .clipShape(Circle())
                        .animation(.spring(), value: isMenuOpen)
                }.offset(x: -10)
            }
            
            HStack {
                Image("home").resizable().aspectRatio(contentMode: .fit).frame(width: 20, height: 20)
                
                Text("Home")
                
                Spacer()
            }.padding(.leading, 20)
            
            HStack {
                Image("memo").resizable().aspectRatio(contentMode: .fit).frame(width: 20, height: 20)
                
                Text("Memo")
                
                Spacer()
            }.padding(.leading, 20)
            
            HStack {
                Image("heart").resizable().aspectRatio(contentMode: .fit).frame(width: 20, height: 20)
                
                Text("Like")
                
                Spacer()
            }.padding(.leading, 20)
            
            HStack {
                Image("plus").resizable().aspectRatio(contentMode: .fit).frame(width: 20, height: 20)

3-2. メインViewでの設定

メインViewではスライドメニューの表示の切り替え、アニメーションの設定を行っています。

    @State var menuWidth = UIScreen.main.bounds.width / 1.6
    @State var isMenuOpen = false
    
    var body: some View {
        ZStack {
            NavigationView {
                
                List(0..<10) { i in
                    Text("テキスト")
                }
                
                .navigationBarTitle("ホーム").navigationBarTitleDisplayMode(.inline)
                .navigationBarItems(leading: Button(action: {
                    self.menuWidth = 0
                    self.isMenuOpen.toggle()
                }, label: {
                    Image("menu").resizable().frame(width: 20, height: 20).aspectRatio(contentMode: .fit)
                }))
                
            }
            
            HStack {
                SlideMenu(menuWidth: $menuWidth, isMenuOpen: $isMenuOpen).offset(x: -menuWidth).animation(.spring(), value: isMenuOpen)
                
                Spacer()
            }
        }
    }


4. スワイプで表示・非表示の切り替えができるViewを作る

次は下から上にスワイプで表示、上から下にスワイプで非表示になるViewを作成します。

スワイプ中はアニメーション付きでなめらかに表示されるように実装します。

4-1. カスタムViewの作成

まずはカスタムViewを作ります。

名前はSwipeViewとします。

テキストを3つ追加して背景色はホワイトにしました。

.fontWeightでフォントを指定、.paddingで余白の設定、Spacerで横いっぱいに広げる設定をしたシンプルなレイアウトです。

struct SwipeView: View {
    var body: some View {
        VStack {
            
            VStack {
                Text("メニュー").fontWeight(.heavy).padding([.top, .bottom], 10)
            }
            
            HStack {
                Spacer()
                Text("トップ").padding()
            }
            
            
            
            Spacer()
            
            Text("ボトム").padding()
            
        }.background(.white)
    }
}

4-2. カスタムViewにジェスチャーを設定する

先ほど作成したSwipeViewをメインのViewに追加します。

SwipeViewに.gestureを追加してジェスチャーイベントを受け取れる状態にします。


DragGesuture()ではスワイプしたときのイベントを取得することができます。

.onChengeでは受け取ったジェスチャーに変化があったことを検知して処理をします。

.onEndedではジェスチャーイベントが終わったときにの処理を追加します。


.onChange内ではまずアニメーションフラグを切り替えてtrueにしています。

縦の移動距離が0より大きいの場合(下にスワイプ)を検知した場合、0より小さい(上にスワイプ)した場合で処理を分けています。

上にスワイプした場合は初期位置から移動距離がマイナスされることでスワイプを離さない限りスワイプ位置と連動して表示されます。


.onEnded内ではスワイプを離したときの処理が入っています。

縦の移動距離が0より大きい(下にスワイプ)かつ移動距離が200より大き場合は初期位置に戻ります。

メニューが表示されていて下にスワイプした時この処理が実行されます。

200より小さい時は初期位置から下にスワイプした場合、表示状態から下スワイプが少なかった場合です。

初期位置の見えている範囲の縦の長さは120しか無いため、この場合は自動的に表示状態となるようにしました。(この部分は改善の余地があります)


縦の移動距離が-200よりも小さい場合は、初期位置から上にスワイプした時の処理です。

-200以上スワイプすると表示の位置に切り替わります。

そうでない場合はスワイプの移動距離が足りない場合の処理です。

移動が足りない場合、初期位置に戻ります。

    @State var size: CGFloat = UIScreen.main.bounds.height - 120
    
    @State var isAnimation = false
    
    var body: some View {
        ZStack {
            
            Color.yellow
            
            SwipeView().cornerRadius(20).padding().offset(y: size)
                .gesture(DragGesture()
                    .onChanged({ value in
                        
                        self.isAnimation.toggle()
                        
                        if value.translation.height > 0 {
                            //self.size = value.translation.height
                        }
                        else {
                            //スワイプ中の位置に移動
                            let temp = UIScreen.main.bounds.height - 120
                            self.size  = temp + value.translation.height
                        }
                    })
                    .onEnded({ value in
                        
                        self.isAnimation.toggle()
                          //下スワイプ
                        if value.translation.height > 0 {
                            //表示のの状態から下に200以上

5. まとめ

いかがでしたでしょうか。

今回はSwiftUIでポップアップで表示されるアラートやスライド表示のメニュー画面など、覚えておくとデザインの幅が広がりそうなものを中心に作成しました。

参考になる部分がありましたら幸いです。