1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Todo List App with SwiftUI 3.0 (簡単なTodoList Appを作ってみました。)

Posted at

簡単な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 : TodoModel(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的なものでModeltodolistの情報をForeachでListのRowで表示してくれる
また画面のボタンを押下した時にViewModelfuncを使ってどういう動作をするかを定義している


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
                
            }
        }
    }
}

実際動かしてみた。

一覧表示

스크린샷 2022-06-07 09.34.36.png

削除

스크린샷 2022-06-07 09.34.46.png

登録

스크린샷 2022-06-07 09.35.45.png

Todo完了

스크린샷 2022-06-07 09.35.55.png

ソート(下から上に変更)

스크린샷 2022-06-07 09.36.08.png


感想

  • MVVMアーキテクチャ, SwiftUIでTodoListAPPを作ってみる動画をまねしながら色々勉強しました。
  • ViewModelはViewを完全にデザイン的な両駅にしたいという観点, Web開発だったらJS的なポジションだと思いました。
  • xcodeの開発する時にcanvasにpreviewがすぐ反映されるのは良かったが、以外にcanvas気になって書くときも見ました。
  • TodoListAppのtutorialでDBは使ってなかったですが、CRUD的な機能を味わったから今後DBと一緒に動かせてみたいです。
1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?