はじめに
今回初めてSwiftUIのアプリを作ってみました。
アウトプットとして記事をまとめていきます。
また、作成前に勉強として、SwiftUI Tutorialsをやってから作成しました。
こうしたほうがいいとか、あと単純に感想とかでもコメントもらえると嬉しいです。
アプリ概要
単純ですが、以下のような画面、機能の構成になります。
- ログイン画面
- Taskの一覧画面
- ステータスでソートする機能
- Task詳細画面
- Taskの更新機能
- Taskの新規登録機能
また、今回は誰かに使ってもらうというよりかは、画面や機能を製造することに重きを置いていて作ったため、DB保存や認証処理等の機能は実装していません。
製造で意識した点
Taskの更新、新規登録で各Viewの情報を更新する
ここは主にSwiftUI Tutorialsを参考に製造しました。
- モデルの作成
- Viewを超えて参照するタスク一覧の生成
final class TodoModel: ObservableObject {
@Published var taskList: [Task] = createTaskList()
}
- 最上位のエントリポイントで環境変数にセット
- @StateObjectを付与し、変数の監視を行う。
@main
struct TodoApp: App {
@StateObject private var todoModel = TodoModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(todoModel)
}
}
}
- TodoListの表示 (モディファイア等の記載は省略)
- 環境変数から読み込んだListを画面のステータスタブに合わせて表示する。
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を取得して、そのデータを更新する。
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ステータスタブのアニメーション
このアニメーションですね。
ここは@hoshi005 さんの記事「matchedGeometryEffect と @Namespaceを使ったアニメーション」を参考に製造しました。
Viewの作りとしては以下の通りです。
-
StatusTabButton
- 各ステータスごとのボタン生成時に汎用的に使用するView
-
TaskStatusTabBar
- StatusTabButtonをステータスごとに生成し、ステータスバーとしてViewを返却する。
ボタン側の実装
-
選択されているボタンのステータスを保持(selectedButton)
-
このボタンはどのステータスのボタンなのかを保持(selfButtonStatus)
-
アニメーション(matchedGeometryEffect)で使用する名前空間ID(namespace)
-
自分のボタンが選択された時(if selectedButton == selfButtonStatus)、matchedGeometryEffectを使用し、ZstackでCapsuleViewをButtonViewに被せる。
- 引数idは同じアニメーションをさせるボタン間で揃っていれば任意のものでOK
//(一部モディファイアは省略)
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を生成しているだけ
//(一部モディファイアは省略)
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チュートリアル【基本要素を学ぶ】