簡単なTodoList Appを作ってみました。
PHPでの開発は3年以上しましたが、Swiftは最近学び始めて、ほぼ知識がない状態でTodoList App tutorialをしてみました。
Youtubeの SwiftUI + MVVMでTodoList Appを作る動画を真似してTodoList Appを作ってみました。
Todo List App with SwiftUI 3.0 | MVVM(https://www.youtube.com/watch?v=1SsRsDoC_eI)
実装した物
Model
, ViewModel
, View
を作りました。
├── App
│ ├── Models
│ │ └── TodoModel.swift
│ ├── TodoListApp.swift
│ ├── ViewModels
│ │ ├── AddTodoViewModel.swift
│ │ └── TodoListViewModel.swift
│ └── Views
│ ├── AddTodoView.swift
│ ├── TodoListRowView.swift
│ └── TodoListView.swift
- TodoModel :
Todo
のModel
(id, title, 完了ステータスを管理)
│ ├── Models
│ │ └── TodoModel.swift
- AddTodoViewModel :
Todo
が登録される時に行う動作を持っているModel
(チェック, 入力したTodo取得) - TodoListViewModel :
Todo
が登録される時に行う動作を持っているModel
(TodoListViewの動作時のロジック管理)
│ ├── ViewModels
│ │ ├── AddTodoViewModel.swift
│ │ └── TodoListViewModel.swift
- TodoListView → TodoListの一覧 (編集ボタン, Todo表示, Todo登録ボタン)
- TodoListRowView → Todo(やること, やったかのチェックマーク)
- AddTodoView → Todo登録画面(text入力フォーム, Saveボタン)
│ └── Views
│ ├── AddTodoView.swift
│ ├── TodoListRowView.swift
│ └── TodoListView.swift
コード的な背景知識(調べた内容 +個人的な考え)
構造体(struct)とは
Swiftにはクラスとよく似た機能として構造体(struct)があります。
構造体(struct)とは、クラスと同様にカプセル化を実現する方法として提供されている機能です。
struct 構造体名 {
var num1:Int;
var num2:Int = 100;
var str:String;
init(value: Int) {
val = 150
}
}
structは継承ができない。
structはデイニシャライザ(クラスのインスタンス破棄)ができない。
classは参照型、structは値渡し。
イニシャライザの定義
initというメソッドでイニシャライザを定義することができる
swiftではクラスの方もinitを書いてイニシャライザを定義できる
//structの定義
struct Area {
var radius: Int // 半径
var pi: Double // 円周率
// イニシャライザ
init(radius: Int, pi: Double) {
self.radius = radius
self.pi = pi
}
}
class のほうが多機能である分複雑で、
Swift の公式ドキュメントでは特に必要がない限り struct を使うことがおすすめされています。
Manageing Model Data
(https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
- ObservedObject iOS 13+
- EnvironmentObject iOS 13+
- StateObject iOS 14+
Modelクラスを定義した時にViewの中でModelを呼ぶためのConnectionを提供してくれるのが上記のObejctの役割
各Objectの差に対してはまだよくわからないです。
Viewの中で定義する場合は StateObject, Classで定義する場合はObservableObjectを使うという認識でした。
$0とはなんでしょう?
$0とはクロージャの第一引数を表す記号
こちらで確認できます。
配列に要素を追加する場合
+= 演算子あるいはappendメソッドを使う
var strArray = ["Google", "Apple", "Facebook"]
strArray += ["Twitter"]
strArray.append("Instagram")
print(strArray) // ["Google", "Apple", "Facebook", "Twitter", "Instagram"]
View的な部分(調べた内容 + 個人的な考え)
ViewControllerとは(今回は使われてない)
VIewControllerは、名前の通り表示されるViewを管理・操作(表示・非表示・配置・アニメーションなど)をする役割を持つクラスです。
ViewControllerでは表示, 操作を管理する役割ができます。
その中で viewDidLoadedは画面がロードされるイベントが発生した時に行う動作を定義するfunctionです。
SwiftUI : Viewの定義
HTMLのタグみたいにViewファイルの内部で text, image, buttonなどを定義するとクライエントの画面で表示する要素を作れる
要素のデザインはメソッドチェイニングみたいに.backgroundColorみたいに定義して変えられる
SwiftUI : Stack
VStack・・・垂直方向にViewを並べる
HStack・・・水平方向にViewを並べる
ZStack・・・Viewを重ねる
SwiftUI : List
Listはデータの一覧表示をするのに適したViewです。
画面に収まらない量の場合はスクロール表示になるなど、UIKitのUITableViewに似ていますが、はるかに簡単に使えます。
ListはForEachとセットで使うケースが多い
動的にリストを生成する時には以下のように
struct ContentView: View {
let fruits = ["りんご", "オレンジ", "バナナ"]
var body: some View {
List {
ForEach(0 ..< fruits.count) { index in
Text(fruits[index])
}
}
}
}
List{
ForEach(HogeVM.hogeList) {item in
HogeView(hoge: item)
}.onDelete{
//リストから削除された時に行う挙動
}.onMove{
// リストから変更された時に行う挙動
}
}
SwiftUI : NavigationBar にボタンや画像を配置する
List {
}.navigationBarTitle(Text("ほげほげ画面"), displayMode: .inline)
.navigationBarItems(
leading: Text("左"),
trailing: Text("右")
)
leadingかtrailingのところにEditButton()を入れるとlistの編集ボタンができます。
SwiftUI : NavigationLink 画面の遷移を設定できる
NavigationLink(destination{押下時遷移されるところ}, label:{Text:リンクを表示するテキスト})
List {
}.navigationTitle("Todo List")
.navigationBarItems(leading: EditButton(),
trailing: NavigationLink(destination: { HogeView() }, label: {Text("Add Hoge")}))
実装したソースコード
TodoModel
初期化時に uuidString
を生成してidがユニクなTodoを作ってくれる
onCompletedToggle()
はTodoの完了チェックボタン押下時の処理, isCompletedフラグを変えてreturn
import Foundation
struct TodoModel: Identifiable {
let id: String
let title: String
let isCompleted: Bool
init(id: String = UUID().uuidString, title: String, isCompleted: Bool) {
self.id = id
self.title = title
self.isCompleted = isCompleted
}
func onCompletedToggle() -> TodoModel{
return TodoModel(id: id, title: title, isCompleted: !isCompleted)
}
}
TodoListViewModel
TodoListのViewで行う処理的な部分をまとめているmodel
- onMove : ソート
- onDelete : 削除
- updateItem : 更新
- onSave : 登録 (Todoのidが被った時の防止策処理が入っているという認証でした。)
import Foundation
class TodoListViewModel: ObservableObject {
@Published var todoList: [TodoModel] = []
init(){
todoList.append(contentsOf: [
TodoModel(title: "Item 1", isCompleted: false),
TodoModel(title: "Item 2", isCompleted: false)
])
}
func onMove(indexSet: IndexSet, to: Int) {
todoList.move(fromOffsets: indexSet, toOffset: to)
}
func onDelete(indexSet: IndexSet) {
todoList.remove(atOffsets: indexSet)
}
func updateItem(item: TodoModel) {
if let index = todoList.firstIndex(where: {$0.id == item.id}) {
todoList[index] = item
}
}
func onSave(item: TodoModel) {
if let index = todoList.firstIndex(where: {$0.id == item.id}) {
todoList[index] = item
return
}
todoList.append(item)
}
}
TodoListView
TodoListの一覧, Editボタン, 登録ボタンを表示するためのViewファイルTodoListApp.swift
で呼ぶと
アプリを開いた時にこちらを表示してくれる
WebだとしたらHTML
的なものでModel
のtodolist
の情報をForeachでListのRowで表示してくれる
また画面のボタンを押下した時にViewModel
のfunc
を使ってどういう動作をするかを定義している
import SwiftUI
struct TodoListView: View {
@StateObject var todoListVM = TodoListViewModel()
var body: some View {
List{
ForEach(todoListVM.todoList) {item in
TodoListRowView(
todo: item,
onCompletedToggle: {
todoListVM.updateItem(item: item.onCompletedToggle())
}
)
}
.onDelete(perform: todoListVM.onDelete)
.onMove(perform: todoListVM.onMove)
}.navigationTitle("Todo List")
.navigationBarItems(leading: EditButton(), trailing: NavigationLink(destination: {
AddTodoView { todo in
todoListVM.onSave(item: todo)
}
}, label: {
Text("Add items")
}))
}
}
struct TodoListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView{
TodoListView()
}
}
}
AddTodoViewModel
登録
する時のView
の動作, 処理を持っているModel
- canSave : 最大件数を確認して登録できるかを判別
- getTodo : 登録する時に登録画面に書いたTodo情報を取得
import Foundation
class AddTodoViewModel: ObservableObject {
@Published var title: String = ""
func canSave () -> Bool {
if title.isEmpty {
return false
}else if title.count < 5 {
return false
}
return true
}
func getTodo(id: String = UUID().uuidString) -> TodoModel {
return TodoModel(id: id, title: title, isCompleted: false)
}
}
AddTodoView
Todo登録画面でテキスト入力フォームと登録ボタンがある
TodoListViewと同様で表示とボタン押下時のイベントを管理しているHTML
みたいな表示テンプレートの役割
import SwiftUI
struct AddTodoView: View {
@Environment(\.presentationMode) var presentationMode
let onSave: (_ todo: TodoModel) -> Void
let id: String = UUID().uuidString
@StateObject var addTodoVM = AddTodoViewModel()
var body: some View {
VStack {
ScrollView {
TextField("Todo", text: $addTodoVM.title)
.padding()
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(12)
.onSubmit {
onSaveClick()
}
}
Button {
if addTodoVM.canSave() {
onSaveClick()
}
} label: {
Text("Save")
.foregroundColor(.white)
.font(.headline)
.frame(height: 56)
.frame(maxWidth: .infinity)
.background(.primary)
}
}.navigationTitle("Add Todo")
}
func onSaveClick() {
let todo: TodoModel = addTodoVM.getTodo(id: id)
onSave(todo)
presentationMode.wrappedValue.dismiss()
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AddTodoView{ todo in
}
}
}
}
実際動かしてみた。
一覧表示
削除
登録
Todo完了
ソート(下から上に変更)
感想
- MVVMアーキテクチャ, SwiftUIでTodoListAPPを作ってみる動画をまねしながら色々勉強しました。
- ViewModelはViewを完全にデザイン的な両駅にしたいという観点, Web開発だったらJS的なポジションだと思いました。
- xcodeの開発する時にcanvasにpreviewがすぐ反映されるのは良かったが、以外にcanvas気になって書くときも見ました。
- TodoListAppのtutorialでDBは使ってなかったですが、CRUD的な機能を味わったから今後DBと一緒に動かせてみたいです。