[SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう！

# はじめに
この記事は、[[SwiftUI] 2.カウンターアプリの機能を増やそう！](https://qiita.com/rinngo0302/items/d2f864959e84cb920af5)の続きになります。まだ読んでいないという人はそちらを先に読むことをおすすめしましす。

それでは今回もがんばりましょう！

# 環境
- Mac mini 2020
- **チップ**: Apple M1
- **メモリ**: 16GB
- **OS**: macOS Sequoia
- **ツール**: Xcode v16.3(バージョンアップ)

# つくるもの
Tapボタンで数字を上げる

![スクリーンショット 2025-04-08 13.00.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/56da76e8-6f8f-4322-82f1-f1178cef4bfc.png)

タスクキルをする

![スクリーンショット 2025-04-08 13.01.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/59abed13-f4f1-4c96-867b-f094aee9a679.png)

もう一度立ち上げると状態が保存されている

![スクリーンショット 2025-04-08 13.00.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/56da76e8-6f8f-4322-82f1-f1178cef4bfc.png)

そして、アンインストールすると状態が初期化される

このように、タスクキルしても状態が保存されるようなアプリをつくっていきましょう！

https://github.com/rinngo0302/rinngo_practice/tree/task3

# TODO
- [ ] 色を変えるロジックを切り出す
- [ ] `@AppStorage`を指定してデータを保存する

# 開発
それでは、TODOに沿って開発をしていきましょう！

## 色を変えるロジックを切り出す
それでは現状のソースコードを見てみましょう。
```Swift
import SwiftUI

struct ContentView: View {
    @State var count: Int = 0
    @State var textColor: Color = .black
    var body: some View {
        VStack {
            Text(String(count))
                .font(.largeTitle)
                .fontWeight(.regular)
                .foregroundColor(textColor)
            HStack {
                Button("Reset") {
                    count = 0
                    
                    textColor = .black
                }

                Button("Tap") {
                    count += 1
                    
                    if (count % 3 == 0) {
                        textColor = .red
                    } else {
                        textColor = .black
                    }
                }
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}
```

これを見て、ちょっと修正したいなと感じるところがあると思います。
そう、`textColor`を変更するところがちょっとごちゃっとしてるんですよね。なぜなら、`count`を計算するところと`textColor`を計算するところが同じで、かつ`Button("Reset")`にも`textColor`のロジックが記述されているため、全体的に一貫性がないように感じるからです。

直し方としては、**countに応じて適切な色を返す計算プロパティ**をつくってそれを使うことにしましょう。

```Swift
var colorForMultipleOf3: Color {
    if (count % 3 == 0) {
        return .red
    } else {
        return .black
    }
}
```

単純に考えればこのような実装になると思うのですが、このままではcountが0のときにもテキストが赤になってしまって元の挙動と変わってしまうので、 **countが0ではない** という条件も付け足しましょう。

```Swift
var colorForMultipleOf3: Color {
    if (count % 3 == 0 && count != 0) {
        return .red
    } else {
        return .black
    }
}
```

これで動きはするんですが、この程度の処理なら三項演算子を使えばもっと簡単に記述できそうですね。

```Swift
var colorForMultipleOf3: Color {
    return (count % 3 == 0 && count != 0) ? .red : .black
}
```

そして、最後にSwiftでは`return`を省略できるという文法があるため

```Swift
var colorForMultipleOf3: Color {
    (count % 3 == 0 && count != 0) ? .red : .black
}
```

こんな感じで書くとスッキリしますね。

では、これをプログラムに組み込みましょうか。

この計算プロパティは、`.foregroundColor`に直接指定してやればよさそうですね。
```Swift
Text(String(count))
    .font(.largeTitle)
    .fontWeight(.regular)
    .foregroundColor(colorForMultipleOf3)
```
こんな感じで大丈夫です。

それでは無駄な部分を消して実装しましょう。

```Swift
import SwiftUI

struct ContentView: View {
    @State var count: Int = 0
    @State var textColor: Color = .black
    var body: some View {
        VStack {
            Text(String(count))
                .font(.largeTitle)
                .fontWeight(.regular)
                .foregroundColor(colorForMultipleOf3)
            HStack {
                Button("Reset") {
                    count = 0
                }

                Button("Tap") {
                    count += 1
                }
            }
        }
        .padding()
    }
    
    var colorForMultipleOf3: Color {
        (count % 3 == 0 && count != 0) ? .red : .black
    }
}

#Preview {
    ContentView()
}
```

これでソースコードがスッキリしましたね。

## `@AppStorage`を指定してデータを保存する
それではここからが本番です！

今の状態では、`count`を上げてもタスクキルしてしまうと`count`が0になってしまいます。
ですが、今回はタスクキルしても`count`を保存できるようにします。

それを実現するために`UserDefaults`という仕組みを使います。
UserDefaultsはAppleの公式ドキュメントで
> An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.

と説明されています。
これを翻訳すると
> ユーザーのデフォルト・データベースへのインターフェイスで、アプリの起動に渡ってキーと値のペアを永続的に保存します。

となります。
簡単に説明すると、**データベースにキーと値のペアを保存できるもの** です。
そして、この仕組みはアプリを削除すると保存したデータも削除されるため今回に適しているといえるでしょう。

:::note warn
これは永続的にデータを保存できるものの、アプリを削除してしまうとデータが消えるため**重要なデータを保存しないようにしましょう。**
:::

https://developer.apple.com/documentation/swiftui/appstorage

そして、SwiftUIには`@AppStorage`という便利な仕組みがあり、これを使えばUserDefaultsを簡単に扱えます。

https://developer.apple.com/documentation/swiftui/appstorage

使い方は
```Swift
@AppStorage("キー") var 変数名: 型
```
となります。
ここでの`"キー"`は値を識別するためのキーになります。できるだけ何の値なのかわかりやすい名前を付けておきましょう。

ここで注意なのですが、`@AppStorage`を使えるのは
- Bool
- String
- Int
- Double
- Float
- Data
- URL

基本的にこれらなので、これ以外を保存したいときはUserDefaults(まだ保存できる型が多いため)を使うか、どうにか工夫して使ってください。

では実装していきましょうか。

今回は、`count`を保存していけばいいので
```Swift
@AppStorage("CountKey") var count: Int = 0
```
こんな感じでいいと思います。
キーは適当につけちゃったんですが、無難に`count`とかでもよかった気がします笑

では実際のコードに実装しましょうか。
```Swift
import SwiftUI

struct ContentView: View {
    @AppStorage("CountKey") var count: Int = 0
    @State var textColor: Color = .black
    var body: some View {
        VStack {
            Text(String(count))
                .font(.largeTitle)
                .fontWeight(.regular)
                .foregroundColor(colorForMultipleOf3)
            HStack {
                Button("Reset") {
                    count = 0
                }

                Button("Tap") {
                    count += 1
                }
            }
        }
        .padding()
    }
    
    var colorForMultipleOf3: Color {
        (count % 3 == 0 && count != 0) ? .red : .black
    }
}

#Preview {
    ContentView()
}
```
こんな感じですね。

ここで、鋭い人は気がついたでしょう。
「`@State`を消してもUIに反映されるのか」

そうです。本来は`@State`をつけなければUIには反映されないはずです。
しかし、**SwiftUIには`@AppStorage`の値が変更されるとViewが再描画されるという仕組みがあります。**

つまり、`@State`も一緒にしてくれるっていうことですね。
(いやーべんりべんり)

というわけで、これで完成です:v:

# UserDefaultsについて
ちょっとUserDefaultsの使い方も書いておこうと思います。

UserDefaultsを使ったデータの保存と取り出し方を紹介します

- 保存
```Swift
UserDefaults.standard.set(値, forKey: "キー")
```
- 取り出す
```Swift
UserDefaults.standard.object(forKey: "キー")
UserDefaults.standard.string(forKey: "キー")
UserDefaults.standard.array(forKey: "キー")
UserDefaults.standard.dictionary(forKey: "キー")
UserDefaults.standard.data(forKey: "キー")
UserDefaults.standard.stringArray(forKey: "キー")
UserDefaults.standard.integer(forKey: "キー")
UserDefaults.standard.float(forKey: "キー")
UserDefaults.standard.double(forKey: "キー")
UserDefaults.standard.bool(forKey: "キー")
UserDefaults.standard.url(forKey: "キー")
```
:::note info
取り出すには型にあった関数を使う
:::
- 削除
```Swift
UserDefaults.standard.removeObject(forKey: キー)
```
- 全削除
```Swift
let appDomain = Bundle.main.bundleIdentifier
UserDefaults.standard.removePersistentDomain(forName: appDomain!)
```

ここで注意なのですが、値を取り出すときに**オプショナル型で返ってくるものと非オプショナル型で返ってくるもの**があります。
| メソッド名                                 | 返り値の型        | オプショナル？ | 説明                              |
|------------------------------------------|-------------------|----------------|-----------------------------------|
| `string(forKey:)`                         | `String?`         | ✅ Yes         | 文字列が存在しないと `nil`         |
| `array(forKey:)`                          | `[Any]?`          | ✅ Yes         | 配列が存在しないと `nil`          |
| `dictionary(forKey:)`                     | `[String: Any]?`  | ✅ Yes         | 辞書が存在しないと `nil`          |
| `data(forKey:)`                           | `Data?`           | ✅ Yes         | データが存在しないと `nil`        |
| `object(forKey:)`                         | `Any?`            | ✅ Yes         | 値が存在しないと `nil`            |
| `url(forKey:)`                            | `URL?`            | ✅ Yes         | URLが存在しないと `nil`           |
| `stringArray(forKey:)`                    | `[String]?`       | ✅ Yes         | 文字列配列が存在しないと `nil`     |
| `bool(forKey:)`                           | `Bool`            | ❌ No          | 値が存在しない場合は `false` を返す |
| `integer(forKey:)`                        | `Int`             | ❌ No          | 値が存在しない場合は `0` を返す     |
| `float(forKey:)`                          | `Float`           | ❌ No          | 値が存在しない場合は `0.0` を返す   |
| `double(forKey:)`                         | `Double`          | ❌ No          | 値が存在しない場合は `0.0` を返す   |

こんな感じで、型によって違うので気をつけてください。

そして、今回のソースコードを`UserDefualts`を使ったやり方で書いてみようと思います。
```Swift
import SwiftUI

struct ContentView: View {
    @State private var count: Int = UserDefaults.standard.integer(forKey: "CountKey")
    @State var textColor: Color = .black
    var body: some View {
        VStack {
            Text(String(count))
                .font(.largeTitle)
                .fontWeight(.regular)
                .foregroundColor(colorForMultipleOf3)
            HStack {
                Button("Reset") {
                    count = 0
                    UserDefaults.standard.set(0, forKey: "CountKey")
                }

                Button("Tap") {
                    count += 1
                    UserDefaults.standard.set(count, forKey: "CountKey")
                }
            }
        }
        .padding()
    }
    
    var colorForMultipleOf3: Color {
        (count % 3 == 0 && count != 0) ? .red : .black
    }
}

#Preview {
    ContentView()
}
```

少し適当になってしまった気もするのですが、こんな感じで実装できます。
実行したらわかるのですが同じ挙動をすると思います。

# おわりに
今回は、`@AppStorage`と`UserDefaults`を使ってタスクキルをしても情報を保存するプログラムを書きました。
`UserDefaults`も簡単に扱えますが、`@AppStorage`はもっと簡単に扱えてとても便利ですね！

では次回もがんばっていきましょうー！お疲れ様でしたー！

次回: [[SwiftUI入門] 4.NavigationStackでページの遷移をしよう！](https://qiita.com/rinngo0302/items/0e2345909ce1c1d93482)
前回: [[SwiftUI] 2.カウンターアプリの機能を増やそう！](https://qiita.com/rinngo0302/items/d2f864959e84cb920af5)