Twitterのホーム画面とスライドメニューを作る

Published by @SoNiceInfo at 6/24/2020


Twitterのホーム画面を作っているxcode

SwiftUIでTwitterのホーム画面とスライドメニューを作る方法を紹介します。
gesture, DragGestureを利用して右にスワイプするとスライドメニューが表示されるようにします。
スライドメニューの表示・非表示は、横方向のオフセットの値を変更して実装します。

ホーム画面とスライドメニューを作るための要素

ホーム画面を以下の要素に分解して順番に作り方を紹介します。

メニュー(オレンジ枠)

MenuView.swift

タイトルバーとタブ(青枠)

MainView.swift

タイムライン(緑枠)

TimelineView.swift

組み合わせる

ここでスワイプに関する実装をします。

ContentView.swift
Twitterのホーム画面を要素に分解した画像

メニュー(オレンジ枠) - MenuView.swift

まずはメニューを用意します。ここではViewの表示内容を実装します。
スワイプに関する部分はContentView.swiftで実装します。

//
//  MenuView.swift
//

import SwiftUI

struct MenuView: View {

    var body: some View {
        VStack(alignment: .leading) {
            Image("animal_kuma")
                .resizable()
                .overlay(
                    Circle().stroke(Color.gray, lineWidth: 1))
                .frame(width: 60, height: 60)
                .clipShape(Circle())
            Text("SwiftUIへの道")
                .font(.largeTitle)
            Text("@road2swiftui")
                .font(.caption)
            Divider()
            ScrollView (.vertical, showsIndicators: true) {
                HStack {
                    Image(systemName: "person")
                    Text("Profile")
                }
                HStack {
                    Image(systemName: "list.dash")
                    Text("Lists")
                }
                HStack {
                    Image(systemName: "text.bubble")
                    Text("Topics")
                }
            }
            Divider()
            Text("Settings and privacy")
        }
        .padding(.horizontal, 20)
    }
}


struct MenuView_Previews: PreviewProvider {
    static var previews: some View {
        MenuView()
    }
}
MenuViewを作成した画面

タイトルバーとタブ(青枠) - MainView.swift

NavigationViewTabViewを組み合わせることでタイトルバーとタブを再現します。
タイトルバーのフォントサイズや背景色はSwiftUIでは直接変更できません。
イニシャライザ内でUINavigationBar.appearance(), UITabBar.appearance()で変更します。

//
//  MainView.swift
//

import SwiftUI

struct MainView: View {
    
    init() {
        // タイトルバーのフォントサイズを変更
        UINavigationBar.appearance().titleTextAttributes = [.font: UIFont.systemFont(ofSize: 26)]
        // タイトルバーの背景色を変更
        UINavigationBar.appearance().barTintColor = UIColor.white
        // タイトルバーの裏の背景色を変更
        UINavigationBar.appearance().backgroundColor = UIColor.white
        // タブバーの背景色を変更
        UITabBar.appearance().barTintColor = UIColor.white
        // タブバーの裏の背景色を変更
        UITabBar.appearance().backgroundColor = UIColor.white
    }
    
    var body: some View {
        TabView {
            NavigationView {
                ScrollView (.vertical, showsIndicators: false) {
                    // この部分は次のセクションで実装します。
                    TimelineView(timelines: timelines)
                }
                    // タイトルと左右のアイコンを指定
                    .navigationBarTitle(Text("🐥"), displayMode: .inline)
                    .navigationBarItems(
                        leading: Image("animal_kuma")
                            .resizable()
                            .overlay(
                                Circle().stroke(Color.gray, lineWidth: 1))
                            .frame(width: 30, height: 30)
                            .clipShape(Circle()),
                        trailing: HStack{
                            IconView(systemName: "sparkles")
                        }
                        .padding(.bottom, 10)
                    )
                }
                .tabItem {
                    IconView(systemName: "house")
                }
            IconView(systemName: "magnifyingglass")
                .tabItem {
                    IconView(systemName: "magnifyingglass")
                }
            IconView(systemName: "bell")
                .tabItem {
                    IconView(systemName: "bell")
                }
            IconView(systemName: "envelope")
                .tabItem {
                    IconView(systemName: "envelope")
                }
        }
        // 選択されているアイコンの色を青に変更
        .accentColor(.blue)
    }
}

// Iconの形式をそろえる
struct IconView: View {
    var systemName: String

    var body: some View {
        Image(systemName: systemName)
            .font(.title)
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}
ホーム画面のタイトルバーとタブを再現した結果

タイムライン(緑枠) - TimelineView.swift

モックはstruct Timelineで雛形を作りlet timelines: [Timeline]で定義しています。
ForEachtimelinesをひとつずつ順番に表示する処理をしています。

//
//  TimelineView.swift
//

import SwiftUI

struct Timeline {
    let id: Int
    let name: String
    let image: String
    let post: String
    let post_image: String
}

let timelines: [Timeline] = [
    Timeline(id: 0, name: "Arupaka", image: "animal_arupaka", post: "This is post content", post_image: "ice_1"),
    Timeline(id: 1, name: "Buta", image: "animal_buta", post: "This is post content", post_image: "ice_2"),
    Timeline(id: 2, name: "Hamster", image: "animal_hamster", post: "This is post content", post_image: "flower"),
    Timeline(id: 3, name: "Hiyoko", image: "animal_hiyoko", post: "This is post content", post_image: "moon"),
    Timeline(id: 4, name: "Inu", image: "animal_inu", post: "This is post content", post_image: "animal_inu")
]

struct TimelineView: View {
    let timelines: [Timeline]

    var body: some View {
        VStack() {
            ForEach(self.timelines, id: \.id) { (timeline) in
                VStack(spacing: 5) {
                    HStack(alignment: .top) {
                        Image(timeline.image)
                            .resizable()
                            .clipShape(Circle())
                            .overlay(
                                Circle().stroke(Color.white, lineWidth: 4))
                            .frame(width: 60, height: 60, alignment: .leading)
                        VStack(alignment: .leading) {
                            HStack {
                                Text(timeline.name)
                                    .fontWeight(.bold)
                                Text("@\(timeline.name)")
                                    .foregroundColor(.gray)
                            }
                            Text(timeline.post)
                            Image(timeline.post_image)
                                .resizable()
                                .scaledToFill()
                                .frame(height: 200)
                                .cornerRadius(20)
                        }
                    }
                    .padding(.horizontal, 10)
                    Divider()
                }
            }
        }
    }
}

struct TimelineView_Previews: PreviewProvider {
    static var previews: some View {
        TimelineView(timelines: timelines)
    }
}
TimelineViewを作成した画面

組み合わせる - ContentView.swift

上記で作成したMenuViewMainViewHStackで表示します。
画面の横方向のoffsetを変更することでスライドメニューを表示・非表示をしています。
横方向のスワイプはvalue.translation.widthで取得しています。

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    // xOffset変数で画面の横のオフセットを保持します
    @State private var xOffset = CGFloat.zero
    @State private var defaultOffset = CGFloat.zero
    
    var body: some View {
        // 画面サイズの取得にGeometoryReaderを利用します
        GeometryReader { geometry in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 0) {
                    MenuView()
                        // 横幅は画面サイズの70%にします
                        .frame(width: geometry.size.width * 0.7)
                    Divider()
                    MainView()
                        // 横幅は画面サイズの100%にします
                        .frame(width: geometry.size.width)
                }
                // 最初に画面のオフセットの値をスライドメニュー分マイナスします。
                .onAppear(perform: {
                    self.xOffset = geometry.size.width * -0.7
                    self.defaultOffset = self.xOffset
                })
                .offset(x: self.xOffset)
                // 画面サイズを明示します
                .frame(width: geometry.size.width, alignment: .leading)
                // スライドのアニメーションを設定します
                .animation(.default)
                // ジェスチャーに関するイベントを検知します
                .gesture(
                    // ドラッグ、すなわちスライドやスワイプに関するイベントに関して検知します
                    DragGesture()
                        // スワイプが検知されたときの動きを実装します
                        .onChanged{ value in
                            // スワイプの移動距離が5以上のときにオフセットの値を動的に変化させます
                            // しきい値(ここでは5)を超えるとリアルタイムな動きを与えます
                            if (self.xOffset != .zero && value.translation.width > 5) {
                                self.xOffset = self.defaultOffset + value.translation.width
                            }
                        }
                        // スワイプが終了したときの動きを実装します
                        .onEnded { value in
                            // もし、右方向にスワイプした距離が5以上ならオフセットを0にします
                            // すなわち、メニューを表示します
                            // それ以外はオフセットをスライドメニュー分設定します
                            // すなわちスライドメニューを隠します
                            if (value.translation.width > 5) {
                                self.xOffset = .zero
                            } else {
                                self.xOffset = self.defaultOffset
                            }
                        }
                    )
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

素材

かわいいフリー素材集 いらすとや
Beautiful Free Images & Pictures | Unsplash

    アプリをリリースしました!

    ToDoアプリ

    iCloudを利用したデバイス間データ共有、ダークモードに対応しています。

    リリースしたToDoアプリのスクリーンショット

    IPアドレス履歴保存アプリ

    取得したIPアドレスを確認、位置情報とともに保存できます。

    リリースしたIPアドレス保存アプリのスクリーンショット