25
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アイスタイルAdvent Calendar 2023

Day 13

SwiftUIでtodoアプリをつくってみた

Last updated at Posted at 2023-12-12

はじめに

アイスタイル Advent Calendar 2023の13日目を担当させて頂きます、こたちゃんです:heart_eyes:
私は、iOSのアプリチームで新卒のエンジニア2年目になります👩‍💻
今回はSwiftUI1でTODOアプリを作ってみました😌

作成するアプリ

ファイル名

仕様・要件

  • ToDoリスト画面
    • 入力できるTextFieldがある
    • 「追加」というボタンがある
    • ボタンをクリックすると入力したテキストがリストに反映される
    • テキストがなにも入力されていない時は送信されないようにする
    • リストにはタスクとチェックボックスが表示される
    • チェックボックスはタップするとチェックの切り替えができる
    • チェックボックスをtrueにすると打ち消し線も表示される

このアプリ開発で学べること

  • Swiftの基礎
    • struct
    • indices
    • ForEach
    • 三項演算子
    • toggle
  • SwiftUIの基礎
    • Stack
    • Text
    • TextField
    • Button
    • @Status

実装手順

  1. アプリの雛形作成
  2. ContentView画面を修正する
  3. TodoListView画面を修正する
  4. toDoItemViewに切り出す
  5. TextFieldの追加
  6. チェックボックスをタップしたらボタンが切り替わる
  7. タスクの新規作成

1. アプリの雛形作成

Xcodeを立ち上げると「Xcode」と表示されており、その下に3つの項目があります。
※ 今回はXcode15を使用しております。

新しいiOSアプリのプロジェクトを作成するため、下記を選択してください。

スクリーンショット 2023-11-30 15.39.32.png

iOSのAppを選択して「Next」ボタンをクリックください。

スクリーンショット 2022-12-15 19.03.33.png

次にProductNameに「Todo」と入力します。
InterfaceがSwiftUIか確認した後に「Next」ボタンをクリックしてください。

qq.png

アプリファイルの保存は、ご自身のPCの好きな場所を選択し「Create」ボタンをクリックしてください。
下記のような画面になりましたら雛形作成が完了です。

スクリーンショット 2023-12-04 16.13.06.png

さっそく、実装を始めていきます!!

2. ContentView画面を修正する

まずは左側の並んでいるファイルツリーの中からTodoAppを選択して指を2つ添えて右クリックして「New File...」をクリックします。

スクリーンショット 2023-12-04 15.34.53.png

次にiOSのSwift Fileを選択し「Next」ボタンをクリックします。

スクリーンショット 2023-11-30 16.01.27.png

ファイル名はTodoListView.swiftと入力して「Create」ボタンをクリックします。

スクリーンショット 2023-12-04 15.37.09.png

作成したTodoListViewを表示するために
ContentView.swiftを開き、下記のように修正します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
-           Image(systemName: "globe")
-               .imageScale(.large)
-               .foregroundStyle(.tint)
-           Text("Hello, world!")
+           TodoListView()
        }
-       .padding()
    }
}

#Preview {
    ContentView()
}

3. TodoListView画面でリスト部分を作成しよう

それではTodoListViewに実装していきます。
ToDoリストを作成する上で必要なデータはタスクの内容完了したかどうかの有無です。
タスクの内容taskとしてString型
完了したかどうかの情報isCheckedとしてBool型で持ちます。

まずは下記のように仮の配列データを作成します。

TodoListView.swift
import SwiftUI

struct TodoListView: View {
+   var todoLists: [(isChecked: Bool, task: String)] = [
+       (isChecked: false, task: "読書する"),
+       (isChecked: true, task: "掃除する"),
+       (isChecked: false, task: "散歩する")
+   ]
    
    var body: some View {
    
    }
:
:

続いて、作成したデータを表示するためにForEachを使用していきます。

TodoListView.swift
import SwiftUI

struct TodoListView: View {
    var todoLists: [(isChecked: Bool, task: String)] = [
        (isChecked: false, task: "読書する"),
        (isChecked: true, task: "掃除する"),
        (isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
+       VStack {
+           Text("ToDoList")
+               .font(.system(size: 30, weight: .bold, design: .default))
+           ForEach(todoLists.indices, id: \.self) { index in
+               HStack {
+                   Image(systemName:
+                       todoLists[index].isChecked ? "checkmark.square" : "square"
+                   )
+                   Text(todoLists[index].task)
+               }
+               .padding(.top, 1)
+               .padding(.leading, 20)
+               .frame(maxWidth: .infinity, alignment: .leading)
+           }
+       }

+       Spacer()
    }
    
:
:

レイアウトを整えるためのコード説明(padding,frameなど)は省略しています。
気になる方はぜひ調べてみてください。

細かく説明していきます。

VStack

一番大きな枠がVStackで囲まれていますがこれは垂直方向に並んで配置されます。
つまり、要素の下に配置されていくようなイメージです。

Text

Textの()内に文字列や数字を指定すると画面に表示されます。
文字列で "ToDoList"と指定していますがこれはタイトルに当たります。

Text("ToDoList")

ForEach

ForEach文は繰り返し処理の1つです。
ForEach文ではbreakやreturnなどで処理を抜けることができません。

todoLists.indicesですが「配列名.indices」とすると、
クロージャ2の引数として配列のindexを取得できます。

ForEach(todoLists.indices, id: \.self) { index in
    
}

HStack ・ 三項演算子

今回は2つの要素を横に並べて表示したいためHStackで囲んでいます。

そしてImage()ではタスクの完了の有無によって画像を出し分けたいため三項演算子を使用しています。
isCheckedがtureの場合 → "checkmark.square"
isCheckedがfalseの場合 → "square" が表示されるように実装しました。

Text()にはtodoLists[index].taskを表示するように指定しています。
そのため、今の段階ではtodoListsに指定してある3つの要素が表示されています。

HStack {
   Image(systemName:
       todoLists[index].isChecked ? "checkmark.square" : "square"
   )
   Text(todoLists[index].task)
}

表示するImageのアイコンは用意されているものを使用します。
下記の赤色で囲んである「+」ボタンをクリックします。

スクリーンショット 2023-12-04 17.08.23.png

一番右側にある星マークのアイコンをクリックしてみるとたくさんのアイコンが表示されます。
この中から使用したいアイコンを選び、ドラッグ&ドロップでTodoListView.swiftにおいてください。
スクリーンショット 2023-11-30 16.44.56.png

私は「checkmark」と検索して以下の画像を使用してます。

スクリーンショット 2023-12-04 18.33.40.png

ここまででシミュレーターを確認してみると以下のようになっていると正解です。

スクリーンショット 2023-12-04 17.08.23.png

4. TextFieldの追加

続いて、TextFieldを追加してタスクの新規追加ができるようにします。
まずは全体のコードが以下になります。

TodoListView.swift
struct TodoListView: View {
+   @State var newTask: String = ""
    var todoLists: [(isChecked: Bool, task: String)] = [
        (isChecked: false, task: "読書する"),
        (isChecked: true, task: "掃除する"),
        (isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
+           HStack {
+               TextField("タスクを入力してください", text: $newTask)
+                   .textFieldStyle(.roundedBorder)
+                   .padding(EdgeInsets(
+                       top: 10,
+                       leading: 20,
+                       bottom: 10,
+                       trailing: 15
+                   ))
+               Button("追加", action: {})
+                   .padding(.trailing, 20)
+           }
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
                    Image(systemName:
                            todoLists[index].isChecked ? "checkmark.square" : "square"
                    )
                    Text(todoLists[index].task)
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        
        Spacer()
    }
}

@State

@Stateは値の更新を監視し、通知が来たら自動でViewを再描画します。
SwiftUIのViewではstruct内に保持するプロパティを変更できませんが
@Stateを使用するとstruct内のプロパティを変更可能にしてくれます。
下記でいうプロパティは、newTaskを指しています。

@Stateで管理するため、TextFieldで入力した内容に変更が可能になっています。

@State var newTask: String = ""

TextField

TextFieldの第一引数にはプレースホルダとして表示されるテキストを設定します。
今回は「タスクを入力してください」としています。

第二引数には変数を指定する際に、頭文字に「$」を付与しています。
「$」をつけることで、その変数の「参照」を渡します。
変数とTextFieldのデータを双方向に紐付けているというイメージです。
このように、「text」に状態変数のプロパティを紐づけることでTextFieldを使用できます。

TextField("タスクを入力してください", text: $newTask)

Button

Buttonの第一引数にはボタンとして表示される文字列を指定できます。
第二引数には、actionを指定できます。
ボタンをクリックした際に行われてほしい処理を記述します。
actionは一旦、{}にしておきます。

Button("追加", action: {})

ここまでで下記のようになっていれば正解です。
スクリーンショット 2023-12-04 18.57.16.png

5. ToDoItemの追加

現在は[(isChecked: Bool, task: String)]と記述していますが
structに切り出してToDoItemとして使用します。
struct3は構造体といいます。簡単にまとめると継承のできないクラスのようなものです。

TodoListView.swift
import SwiftUI

+ struct ToDoItem {
+     var isChecked: Bool
+     var task: String
+ }

struct TodoListView: View {
    @State var newTask: String = ""
-   var todoLists: [(isChecked: Bool, task: String)] = [
-       (isChecked: false, task: "読書する"),
-       (isChecked: true, task: "掃除する"),
-       (isChecked: false, task: "散歩する")
+   var todoLists: [ToDoItem] = [
+       ToDoItem(isChecked: false, task: "読書する"),
+       ToDoItem(isChecked: true, task: "掃除する"),
+       ToDoItem(isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
        VStack {
            Text("ToDoLis

6. チェックボックスをタップしたらボタンが切り替わる

そして次にチェックボックスをタップしたらチェックが切り替わるようにしていきます。

TodoListView.swift
struct TodoListView: View {
    @State var newItem: String = ""
    var todoLists: [ToDoItem] = [
        ToDo(isChecked: false, task: "読書する"),
        ToDo(isChecked: true, task: "掃除する"),
        ToDo(isChecked: false, task: "散歩する")
    ]

    var body: some View {
        VStack {
        
            ... 省略
            
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
+                   Button(action: {
+                       todoLists[index].isChecked.toggle()
+                   }, label: {
                        Image(systemName:
                                todoLists[index].isChecked ? "checkmark.square" : "square"
                        )
                        .imageScale(.large)
                        .foregroundStyle(.pink)
+                   })
                    Text(todoLists[index].task)
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        
        Spacer()
    }
}

Button()のlabelにはタップする要素を記述します。
今回はチェックボックスを画像で表示しているのでlabel内にImage()を置きます。

toggle

bool型の真偽を入れ替えたい変数名の後に、.toggle()を付けます。
そのため.toggle()によって画像が切り替わるようになっています。

ただ、このままでは下記のエラーが出てしまいます。

Cannot use mutating member on immutable value: 'self' is immutable

SwiftUIのViewではstruct内に保持するプロパティを変更できないため
todoListsは、@Stateをつける必要があります。

TodoListView.swift
struct TodoListView: View {
    @State var newItem: String = ""
-   var todoLists: [ToDoItem] = [
+   @State var todoLists: [ToDoItem] = [
        ToDo(isChecked: false, task: "読書する"),
        ToDo(isChecked: true, task: "掃除する"),
        ToDo(isChecked: false, task: "散歩する")
    ]

7. タスクの新規作成

TextFieldに入力した内容をリストに表示させます。

TodoListView.swift
:
:
struct TodoListView: View {
    @State var newTask: String = ""
    @State var todoLists: [ToDoItem] = []
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
            HStack {
                TextField("タスクを入力してください", text: $newTask)
                    .textFieldStyle(.roundedBorder)
                    .padding(EdgeInsets(
                        top: 10,
                        leading: 20,
                        bottom: 10,
                        trailing: 15
                    ))
                Button("追加", action: {
+                   todoLists.append(
+                       ToDoItem(isChecked: false, task: newTask)
+                   )
+                   newTask = ""
                })
                .padding(.trailing, 20)
            }
:
:

append

appendメソッドは配列に要素を追加する処理です。
newTasktaskに追加することでタスクを新規作成しています。
また、タスクを追加した際にTextField内は空にしたいので "" を代入します。

Button("追加", action: {
    todoLists.append(
        ToDoItem(isChecked: false, task: newTask)
    )
    newTask = ""
})
.padding(.trailing, 20)
.disabled(newTask.isEmpty)

これでTodoリストは完成です!

挑戦

TextFieldが空白の時に追加ボタンをクリックしても新規作成されないようにする

disabled()とisEmptyを使用しています。
disabledはボタンを押せなくするという役割があり、trueの場合はボタンが押せなくなります。
isEmptyは空白かどうかのチェックをしてくれるため
空白の場合はtrueが返されボタンが押せません。
空白でない場合はfalseが返され新規作成する処理を実行しています。

TodoListView.swift
Button("追加", action: {
    todoLists.append(
        ToDoItem(isChecked: false, task: newTask)
    )
    newTask = ""
})
.padding(.trailing, 20)
.disabled(newTask.isEmpty)
isCheckedがtrueの時、テキストに打ち消し線をいれる

strikethrough()で打ち消し線をいれることができます。
if文でisCheckedがtrueの時、表示されるようにしています。

TodoListView.swift
if todoLists[index].isChecked {
    Text(todoLists[index].task)
        .strikethrough()
} else {
    Text(todoLists[index].task)
}

完成コード

TodoListView.swift
import SwiftUI

struct ToDoItem {
    var isChecked: Bool
    var task: String
}

struct TodoListView: View {
    @State var newTask: String = ""
    @State var todoLists: [ToDoItem] = []
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
            HStack {
                TextField("タスクを入力してください", text: $newTask)
                    .textFieldStyle(.roundedBorder)
                    .padding(EdgeInsets(
                        top: 10,
                        leading: 20,
                        bottom: 10,
                        trailing: 15
                    ))
                Button("追加", action: {
                    if !newTask.isEmpty {
                        todoLists.append(
                            ToDoItem(isChecked: false, task: newTask)
                        )
                        newTask = ""
                    }
                })
                .padding(.trailing, 20)
            }
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
                    Button(action: {
                        todoLists[index].isChecked.toggle()
                    }, label: {
                        Image(systemName:
                                todoLists[index].isChecked ? "checkmark.square" : "square"
                        )
                        .imageScale(.large)
                        .foregroundStyle(.pink)
                    })
                    if todoLists[index].isChecked {
                        Text(todoLists[index].task)
                            .strikethrough()
                    } else {
                        Text(todoLists[index].task)
                    }
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            
            Spacer()
        }
    }
}

#Preview {
    TodoListView()
}

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            TodoListView()
        }
    }
}

#Preview {
    ContentView()
}

最後に

今年もSwiftでの記事を書かせていただきました。
SwiftUIは、まだまだ記事も少ないためぜひ参考にしていただけるとうれしいです!❤️
素敵なクリスマスを:santa_tone2::snowflake:

参考文献

  1. SwiftUIとは、iPhoneだけでなくiPadやMacOSX、Apple Watchなど、Apple製品のプラットフォームすべてに対応しているUIフレームワークです。

  2. クロージャとは、変数をスコープ内に閉じ込めるためのデータ構造のことです。
    array[index] という形式のため今回ですとtodoLists[index]となります。
    idに.selfを渡していますが、
    これはForEachの中身を一意に識別するためのIDとして、それぞれの数値自体を使用しています。
    配列の一意性を保証したデータコレクションとして渡せます。

  3. structは値型に分類され、値の受け渡しが「変更を共有しないもの」となっています。
    データをオブジェクトの内部に隠して、容易に値を変更できなくするための入れ物のようなイメージです。
    これで型の指定には[ToDoItem]と記述できるようになります。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?