19
36

【SwiftUI】Todoアプリを作ってSwiftUIを理解していく

Last updated at Posted at 2021-02-17

はじめに

今回初めてSwiftUIのアプリを作ってみました。
アウトプットとして記事をまとめていきます。

また、作成前に勉強として、SwiftUI Tutorialsをやってから作成しました。

こうしたほうがいいとか、あと単純に感想とかでもコメントもらえると嬉しいです。

アプリ概要

諸々書く前にまずは作ったアプリを見てもらいましょう。
ezgif.com-gif-maker.gif

単純ですが、以下のような画面、機能の構成になります。

  • ログイン画面
  • Taskの一覧画面
    • ステータスでソートする機能
  • Task詳細画面
    • Taskの更新機能
    • Taskの新規登録機能

また、今回は誰かに使ってもらうというよりかは、画面や機能を製造することに重きを置いていて作ったため、DB保存や認証処理等の機能は実装していません。

製造で意識した点

Taskの更新、新規登録で各Viewの情報を更新する

ここは主にSwiftUI Tutorialsを参考に製造しました。

  • モデルの作成
  • Viewを超えて参照するタスク一覧の生成
TodoModel.swift
final class TodoModel: ObservableObject {
    @Published var taskList: [Task] = createTaskList()
}
  • 最上位のエントリポイントで環境変数にセット
TodoApp.swift
@main
struct TodoApp: App {
    @StateObject private var todoModel = TodoModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(todoModel)
        }
    }
}
  • TodoListの表示 (モディファイア等の記載は省略)
    • 環境変数から読み込んだListを画面のステータスタブに合わせて表示する。
TodoList.swift
struct TodoList: View {
    @EnvironmentObject var todoModel: TodoModel
    @State var selectedTab: TaskStatus
    
    // 画面のステータスタブに合わせてFilter
    var filteredTaskList: [Task] {
        todoModel.taskList.filter {
            $0.status == selectedTab
        }
    }

var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(filteredTaskList) { task in
                        NavigationLink(destination: TaskDetail(task: task)){
                            HStack {
                                Text(task.name)                                
                                Text(task.description)
                            }
                        }
                    }
                }

// (中略)

}
  • Task詳細(登録・更新)画面
    • 環境変数としてtodoModelを読み込む
    • 画面からの入力内容を変数taskに入れる
    • 登録もしくは更新ボタンタップで入力されたtaskの内容を環境変数todoModelに反映させる
      • 登録時→todoModel.taskListを渡し、配列に追加(append)する。
      • 更新時→該当データのIndexを取得して、そのデータを更新する。
TaskDetail.swift
struct TaskDetail: View {
    @EnvironmentObject var todoModel: TodoModel
    @State var task: Task
    var taskIndex: Int {
        if let taskIndex = todoModel.taskList.firstIndex(where: { $0.id == task.id }) {
            return taskIndex
        } else {
            return todoModel.taskList.count + 1
        }
    }

    var body: some View {
        VStack {
            HStack {
                Spacer()
                
                if taskIndex > todoModel.taskList.count {
                    // 登録
                    RegistTaskButton(todoList: $todoModel.taskList, task: task)
                } else {
                    // 更新
                    UpdateTaskButton(beforeValueTask: $todoModel.taskList[taskIndex], afterValueTask: task)
                }
                
            }
// (中略)
    }
}

TodoListステータスタブのアニメーション

このアニメーションですね。

ezgif.com-gif-maker (1).gif

ここは@hoshi005 さんの記事「matchedGeometryEffect と @Namespaceを使ったアニメーション」を参考に製造しました。

Viewの作りとしては以下の通りです。

  • StatusTabButton

    • 各ステータスごとのボタン生成時に汎用的に使用するView

    Preview画像としては以下の画像です。
    スクリーンショット 2021-02-17 11.07.15.png

  • TaskStatusTabBar

    • StatusTabButtonをステータスごとに生成し、ステータスバーとしてViewを返却する。

    Preview画像としては以下の画像です。
    スクリーンショット 2021-02-17 11.07.29.png

ボタン側の実装

  • 選択されているボタンのステータスを保持(selectedButton)

  • このボタンはどのステータスのボタンなのかを保持(selfButtonStatus)

  • アニメーション(matchedGeometryEffect)で使用する名前空間ID(namespace)

  • 自分のボタンが選択された時(if selectedButton == selfButtonStatus)、matchedGeometryEffectを使用し、ZstackでCapsuleViewをButtonViewに被せる。

    • 引数idは同じアニメーションをさせるボタン間で揃っていれば任意のものでOK
StatusTabButton.swift
//(一部モディファイアは省略)
struct StatusTabButton: View {
    
    @Binding var selectedButton: TaskStatus
    let selfButtonStatus: TaskStatus
    var namespace: Namespace.ID

    var title = // selfButtonStatusから設定
    var body: some View {
        
        ZStack {
            if selectedButton == selfButtonStatus {
                Capsule()
                    .fill(Color("TabSelected"))
                    .matchedGeometryEffect(id: "CustomButton", in: namespace)
                
            }
            Button(action: {
                withAnimation{
                    selectedButton = selfButtonStatus
                }
            }){
                Text(title)
                    .foregroundColor(selectedButton == selfButtonStatus ? .white : .gray)
                
            }
        }
    }
}

ステータスバー側の実装

  • 選択されているボタンのステータスを保持(selectedButton)
  • @Namespaceプロパティラッパーを使用して変数を定義(namespace)
  • あとはStatusTabButtonを生成しているだけ
TaskStatusTabBar.swift
//(一部モディファイアは省略)
struct TaskStatusTabBar: View {
    @Binding var selectedTab: TaskStatus
    @Namespace var namespace
    
    var body: some View {
        HStack {
            ForEach(TaskStatus.allCases, id: \.self) { status in
                StatusTabButton(selectedButton: $selectedTab, selfButtonStatus: status, namespace: namespace)
            }
        }
    }
}

更新画面と新規登録画面を同一Viewで作成

Viewの汎用性をあげるため、今回は更新画面と新規登録画面を同一のViewで作成しました。
SwiftUI TutorialsのFavorite機能を参考に、登録・更新ボタンのみViewを分け、他は同一のViewで実装しました。
ソースは上記の「TaskDetail.swift」のソースを参照してください。

各ButtonViewの生成はtaskIndexがヒットしたかどうかで、更新用のView(UpdateTaskButton)か、登録用のView(RegistTaskButton)かで使い分けてます。

最後に

一覧画面でステータスを変えた時に、Listの挙動が若干変なアニメーションっぽくなるのなんとかならないですかね。。。

全体のソースはここにあります。
https://github.com/skyTiki/TodoAPP

参考

SwiftUI Tutorials
[Swift] SwiftUIのチートシート
【SwiftUI】シートの使い方(sheet)
matchedGeometryEffect と @Namespaceを使ったアニメーション
Dating Login With Firebase
【第1回】日本語版SwiftUIチュートリアル【基本要素を学ぶ】

19
36
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
36