Apple Watch用メモアプリを作ってみた 

# はじめに
せっかくMac mini M2を買ったのでアプリを作ってみようかと思いました
WatchOS6からWatch単体アプリを作れるようになったということで、Apple Watch単体アプリを作ってみました

# アプリ内容
Apple Watchにはメモ帳アプリがなかったのでメモ帳アプリを作りました
最初は複数のメモを管理できるような(いわゆる普通の？iPhoneのメモアプリ的な)ものを考えたのですが、Apple Watchの小さい画面でたくさんメモ取らないだろうと思い、ブキーボード的なちょっと書いてすぐ消すようなメモ用紙的なアプリを作ることにしました
### 開発環境
- Mac mini M2
- MacOS Sonoma 14.4.1
- Xcode 15.3 (15E204a)
- WatchOS 10

# アプリのコード
今回作成したアプリはGithubに置きました
全体のコードはこちらを参照ください

https://github.com/odo-odo/WatchMemo

# Xcodeプロジェクトの作成
Xcodeを起動し新規プロジェクト作成からWatchOS -> Appを選びます
![20240409-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/943b1108-3155-98e1-638f-14343d0444b7.png)
次の画面でWatch-only Appを選んでプロジェクトを作成します
![20240409-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/03dd2633-5fdf-91fa-d6b5-b426e38a474a.png)

# Widget Extensionの追加
Xcodeのメニュー -> File -> New -> TargetからWatchOSのWidget Extensionを追加します
Product Nameはプロジェクトの中でExtensionとわかりやすい名前をつけば良いかと思います
![20230409-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/ed05de0d-7413-91b4-cfcc-c0b26fad609b.png)

# App Groupsの作成
プロジェクトを選択してTargetからWatchOSアプリを選択します
Signing & Capabilitiesのタブを選択し、+ CapabilityをクリックしApp Groupsを追加します
![20240409-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/0fc3d5ed-31c8-e14a-41e3-89bad8879854.png)
App Groupsのセクションから、「+」をクリックしてconteinerを追加します
この時決めたコンテナーIDを使ってアプリとウィジェット間でデータをやり取りすることになります
![20240409-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/35202191-f0ca-2157-6822-2c86c4ba8740.png)

これと同様の操作を先ほど追加したWidget Extensionでも行います
この時App Groupsを追加した際のコンテナーIDはWatchOSアプリ側で作成したconteinerと同じコンテナーIDにします
![20240409-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481960/e1c743a0-8dbc-d5f3-faa4-28c05fd1e4b0.png)


# メモデータの格納
メモデータの格納はUserDefaultsを使っています
今回は1つだけのメモを管理するので、「memo」というキーでUserDefaultsを作ります
またWidgetKitでウィジェットも作るのでApp GroupsのコンテナーIDを指定するようにしています

```swift:UserDefaultsの定義
import Foundation

class MemoData: ObservableObject {
    @Published var memo: String {
        willSet {
            print("willset")
        }
        didSet {
            UserDefaults(suiteName: "group.com.watch.memo")?.set(memo, forKey: "memo")
            print("didset")
        }
    }

    // 初期化
    init() {
        memo = UserDefaults(suiteName: "group.com.watch.memo")?.string(forKey: "memo") ?? " "
    }
}
```

# メインView
WatchOSではTextEditorが使えないのでメイン画面にTextを配置し、NavigationStackの遷移先にTextFiledをおいてメモを編集する形式にしています
メイン画面にはNavigationStackとListの中にTextとメモ削除用のButtonを配置します
メモを削除する処理もこのViewに書いています

NavigationStack -> NavigationLinkのdestinationに遷移したいView(ここではEditVew)を指定し、渡したい値(value -> Textに表示させている文字列)を引数として指定します
Textの.onAppear()処理でUserDefaultsの値を表示させます
```swift:メインView
NavigationStack {
    List {
        NavigationLink(destination: EditView(value: "\(value)")) {
            Text(value)
                .onAppear() {
                    guard let defaultMemo = UserDefaults(suiteName: "group.com.watch.memo")?.object(forKey: "memo")
                    else{return}
                    // memoがNullの場合、値に空文字を挿入する
                    value = defaultMemo as! String
                    if value == "" {
                        value = " "
                    }
                }
        }

        Button(action: {erase()}) {
            Label("erase", systemImage: "trash")
        }
        // 背景色を透明にする
        .listRowBackground(Color.clear)
    }
}
```

```swift:メモの削除処理
private func erase() {
    let eraseMemo = " "
    self.memoData.memo = eraseMemo
    self.value = eraseMemo
    // Widgetを更新
    WidgetCenter.shared.reloadAllTimelines()
}
```

# 編集View
メモの編集はメインViewから遷移させたViewで行うようにしました
メモの編集 -> UserDefaultsのアップデートを行なっています
```swift: UserDefaultsの定義
func setMemoData(){
        guard let memoData = UserDefaults(suiteName: "group.com.watch.memo")?.object(forKey: "memo")
        else{return}
        self.value = memoData as! String
}
```

```swift:編集用View
var body: some View {
    TextField("Input Text", text: $value)
        .onAppear {
            if $value.wrappedValue == " " {
                value.self = ""
            }
        }
        .onSubmit {
            update(memo: value)
        }
}
```

```swift: メモの編集処理
    private func update(memo: String) {
        self.memoData.memo = memo
        setMemoData()
        // Widgetを更新
        WidgetCenter.shared.reloadAllTimelines()
    }
```

# WidgetExtensionの定義
WidgetExtensionは追加されたテンプレートを割と素直に使っています
ただApple Watchに設定するウォッチフェイスによって表示されるWidgetが異なるので分岐を作って切り替えられるようにしました

```swift:Widget分岐
struct WatchMemoWidgetEntryView : View {
    @Environment(\.widgetFamily) var widgetFamily
    @State var entry: Provider.Entry

    var body: some View {
        switch widgetFamily {
            case .accessoryCorner:
                CornerComplication(memo: entry.memo)
            case .accessoryCircular:
                CircularComplication(memo: entry.memo)
            case .accessoryInline:
                InlineComplication(memo: entry.memo)
            case .accessoryRectangular:
                RectangularComplication(memo: entry.memo)
            @unknown default:
                Text("No　Complication")
        }
    }
}
```
```swift:コンプリケーションに応じたView
struct InlineComplication : View {
    @State var memo: String
    var body: some View {
        HStack {
            Image(systemName:"note.text")
            Text(": \(memo)")
                .lineLimit(1)
        }
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

struct CircularComplication : View {
    @Environment(\.showsWidgetLabel) var showsWidgetLabel
    @State var memo: String
    // 表示される場所によってアイコンイメージ的なものが一緒に表示される時と
    // メモの内容のみが表示される時を分けています
    var body: some View {
        if showsWidgetLabel {
            Image(systemName:"note.text")
                .widgetLabel {
                    Text(memo)
                }
                .containerBackground(.fill.tertiary, for: .widget)
        } else {
            Text(memo)
                .lineLimit(1)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}

struct CornerComplication : View {
    @State var memo: String
    var body: some View {
        Image(systemName:"note.text")
            .widgetLabel {
                Text(memo)
                    .lineLimit(1)
                    .containerBackground(.fill.tertiary, for: .widget)
            }
    }
}

struct RectangularComplication : View {
    @State var memo: String
    var body: some View {
        VStack {
            HStack {
                Image(systemName:"note.text")
                    .font(.system(size: 10.0))
                Spacer()
            }
            Text(memo)
                .lineLimit(2)
                .widgetAccentable()
        }
        .containerBackground(.fill.tertiary, for: .widget)
    }
}
```

# おわりに
現状Apple Watchで使えるSwiftUIではTextEditorが使えないので改行を入力することができないようです
Apple Watchはそもそも画面が小さいので複雑な画面構成は向いていないのでいかにシンプルに作るかが重要ですね
Watch OnlyアプリがiPadのSwiftPlaygoundで作れるようになるといいなと思いました