はじめに
アイスタイル Advent Calendar 2023の13日目を担当させて頂きます、こたちゃんです
私は、iOSのアプリチームで新卒のエンジニア2年目になります👩💻
今回はSwiftUI1でTODOアプリを作ってみました😌
作成するアプリ
仕様・要件
- ToDoリスト画面
- 入力できるTextFieldがある
- 「追加」というボタンがある
- ボタンをクリックすると入力したテキストがリストに反映される
- テキストがなにも入力されていない時は送信されないようにする
- リストにはタスクとチェックボックスが表示される
- チェックボックスはタップするとチェックの切り替えができる
- チェックボックスをtrueにすると打ち消し線も表示される
このアプリ開発で学べること
- Swiftの基礎
- struct
- indices
- ForEach
- 三項演算子
- toggle
- SwiftUIの基礎
- Stack
- Text
- TextField
- Button
- @Status
実装手順
- アプリの雛形作成
- ContentView画面を修正する
- TodoListView画面を修正する
- toDoItemViewに切り出す
- TextFieldの追加
- チェックボックスをタップしたらボタンが切り替わる
- タスクの新規作成
1. アプリの雛形作成
Xcodeを立ち上げると「Xcode」と表示されており、その下に3つの項目があります。
※ 今回はXcode15を使用しております。
新しいiOSアプリのプロジェクトを作成するため、下記を選択してください。
iOSのAppを選択して「Next」ボタンをクリックください。
次にProductNameに「Todo」と入力します。
InterfaceがSwiftUI
か確認した後に「Next」ボタンをクリックしてください。
アプリファイルの保存は、ご自身のPCの好きな場所を選択し「Create」ボタンをクリックしてください。
下記のような画面になりましたら雛形作成が完了です。
さっそく、実装を始めていきます!!
2. ContentView画面を修正する
まずは左側の並んでいるファイルツリーの中からTodoAppを選択して指を2つ添えて右クリックして「New File...」をクリックします。
次にiOSのSwift Fileを選択し「Next」ボタンをクリックします。
ファイル名はTodoListView.swift
と入力して「Create」ボタンをクリックします。
作成したTodoListView
を表示するために
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型で持ちます。
まずは下記のように仮の配列データを作成します。
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
を使用していきます。
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のアイコンは用意されているものを使用します。
下記の赤色で囲んである「+」ボタンをクリックします。
一番右側にある星マークのアイコンをクリックしてみるとたくさんのアイコンが表示されます。
この中から使用したいアイコンを選び、ドラッグ&ドロップでTodoListView.swift
においてください。
私は「checkmark」と検索して以下の画像を使用してます。
ここまででシミュレーターを確認してみると以下のようになっていると正解です。
4. TextFieldの追加
続いて、TextFieldを追加してタスクの新規追加ができるようにします。
まずは全体のコードが以下になります。
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: {})
5. ToDoItemの追加
現在は[(isChecked: Bool, task: String)]
と記述していますが
structに切り出してToDoItemとして使用します。
struct
3は構造体といいます。簡単にまとめると継承のできないクラスのようなものです。
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. チェックボックスをタップしたらボタンが切り替わる
そして次にチェックボックスをタップしたらチェックが切り替わるようにしていきます。
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
をつける必要があります。
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に入力した内容をリストに表示させます。
:
:
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メソッドは配列に要素を追加する処理です。
newTask
をtask
に追加することでタスクを新規作成しています。
また、タスクを追加した際に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
が返され新規作成する処理を実行しています。
Button("追加", action: {
todoLists.append(
ToDoItem(isChecked: false, task: newTask)
)
newTask = ""
})
.padding(.trailing, 20)
.disabled(newTask.isEmpty)
isCheckedがtrueの時、テキストに打ち消し線をいれる
strikethrough()で打ち消し線をいれることができます。
if文でisCheckedがtrueの時、表示されるようにしています。
if todoLists[index].isChecked {
Text(todoLists[index].task)
.strikethrough()
} else {
Text(todoLists[index].task)
}
完成コード
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()
}
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
TodoListView()
}
}
}
#Preview {
ContentView()
}
最後に
今年もSwiftでの記事を書かせていただきました。
SwiftUIは、まだまだ記事も少ないためぜひ参考にしていただけるとうれしいです!❤️
素敵なクリスマスを
参考文献
-
SwiftUIとは、iPhoneだけでなくiPadやMacOSX、Apple Watchなど、Apple製品のプラットフォームすべてに対応しているUIフレームワークです。 ↩
-
クロージャとは、変数をスコープ内に閉じ込めるためのデータ構造のことです。
array[index]
という形式のため今回ですとtodoLists[index]
となります。
idに.selfを渡していますが、
これはForEachの中身を一意に識別するためのIDとして、それぞれの数値自体を使用しています。
配列の一意性を保証したデータコレクションとして渡せます。 ↩ -
structは値型に分類され、値の受け渡しが「変更を共有しないもの」となっています。
データをオブジェクトの内部に隠して、容易に値を変更できなくするための入れ物のようなイメージです。
これで型の指定には[ToDoItem]
と記述できるようになります。 ↩