[Swift] Journaling Suggestions API に関して


# はじめに

iOS 17.2 から`ジャーナル`機能が提供されました！
（なんで、17.2 っていう中途半端なタイミングで...みたいなツッコミもありつつ...）

今回、機能だけでなく開発者向けの API も提供されました。

https://developer.apple.com/videos/play/tech-talks/111384/


これを触ってみたので、ざっくりと紹介していきます。


# Journaling Suggestions API に触れてみる

公式の動画では、以下のステップで実行できるとのこと。

![Screenshot 2023-12-13 at 5.04.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/dde17aa7-3a03-3472-7dbf-b8dd48816cc5.png)

（引用：https://developer.apple.com/videos/play/tech-talks/111384/）


では、実際にやっていきます。

## セットアップ

### 1. 環境の用意

- Xcode 15.1~
- iOS 17.2~ の**実機**（ここ重要）

Xcode 15の Beta にも入っている（入ってないものもあるらしい？）が、実機ビルドを考えると最新のものを入れておくと良いかと思います。

また、後々のコード面でも触れますが、ジャーナルの機能は**実機でしか確認できない**ので、かならず実機を用意してください。

### 2. Capability の追加

Capability に **Journaling Suggestions** が新しく追加されています。

![Screenshot 2023-12-12 at 23.36.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/d1dd453c-b6e8-7d8a-ce08-779270af27c1.png)

これを追加すると、設定の一覧に追加されます。

![Screenshot 2023-12-13 at 1.32.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/1a617044-ac12-1e45-a75d-16f946842046.png)

これで **Journaling Suggestions** の API を使えるようになります。


## 実装

### - ジャーナルの提案画面を表示

Capability を追加していれば、`JournalingSuggestions` を import できます。

```.swift
import JournalingSuggestions
```

あとは、ジャーナルのピッカーを呼び出すだけです。
`JournalingSuggestionsPicker` を使うだけで簡単に実装できます。

```.swift
struct ContentView: View {
    var body: some View {
        JournalingSuggestionsPicker {
            Text("Picker Label")
        } onCompletion: { suggestion in
            // do some actions
        }
    }
}
```

実際の画面はこんな感じです。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/c89ef699-a59e-e528-acb5-2f6853c59bba.png" width=600>

ピッカーを呼び出すラベルを設定し、それをタップすればサジェストが自動で表示されます。

### - 提案画面から得られるデータにアクセスする

ジャーナルの提案一覧から、詳細な情報を閲覧できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/d5dc537c-e0ee-aaa9-1456-5d074b81a313.png" width=600>

この詳細から **「追加」** を押すとデータが取得できます。
また、アイコン（オレンジの部分）をタップした場合は直接データを取得できます。


取得したデータは、`JournalingSuggestionsPicker` の `onCompletion` に入ってきます。実際のデータはこのような形です。（先ほど提示した位置情報のジャーナルデータ）

```.swift
JournalingSuggestions.JournalingSuggestion(
	items: [
		JournalingSuggestions.JournalingSuggestion.ItemContent(
			id: BD17F57B-EF6C-494A-AEAC-FB29DB89441B, 
			representations: [
				JournalingSuggestions.JournalingSuggestion.Location
			], 
			content: JournalingSuggestions.InternalAssetContent(
				providers: [
					JournalingSuggestions.InternalAssetContent.AssetProvider(
						type: JournalingSuggestions.JournalingSuggestion.Location, 
						loader: (Function)
					)
				])
			)
	], 
	title: "訪問（夜間、初台1丁目へ）", 
	date: Optional(2023-12-29 11:39:28 +0000 to 2023-12-29 14:35:55 +0000), 
	suggestionIdentifier: 4FFF438A-C99A-4EE6-8B93-823C58551DAA,
	suggestionHashValue: 10797189376
)
```

`JournalingSuggestionsPicker` のコールバックでは

- `let items: [JournalingSuggestion.ItemContent]`
- `let title: String`
- `let date: DateInterval?`

の情報にアクセスできます。例えば、先ほどのデータではこうなります。

```.swift
JournalingSuggestionsPicker {
    Text("Picker Label")
} onCompletion: { suggestion in
    print(suggestion.items) // [JournalingSuggestions.JournalingSuggestion.ItemContent(...)]
    print(suggestion.title) // "訪問（夜間、初台1丁目へ）"
    print(suggestion.date) // 2023-12-29 11:39:28 +0000 to 2023-12-29 14:35:55 +0000
}
```

`items` はサジェストされたジャーナルデータの情報を個別に持っています。
先ほどの例では１つでしたが、データが複数の場合はこのような配列で取得されます。

```.swift
JournalingSuggestions.JournalingSuggestion(
	items: [
		JournalingSuggestions.JournalingSuggestion.ItemContent(
			representations: [
				JournalingSuggestions.JournalingSuggestion.MotionActivity
			], 
			..
		), 
		JournalingSuggestions.JournalingSuggestion.ItemContent(
			representations: [
				JournalingSuggestions.JournalingSuggestion.Location
			], 
			..
		), 
		JournalingSuggestions.JournalingSuggestion.ItemContent(
			representations: [
				JournalingSuggestions.JournalingSuggestion.LivePhoto,
				UIImage, 
				SwiftUI.Image
			], 
			..
		),
.
.
```

写真、位置情報、歩数など、`ItemContent` として個別のデータで格納されます。

### - データの詳細にアクセスする

取得できる `items`(`ItemContent` の配列)から、詳細なデータにアクセスできます。

```.swift
public struct ItemContent : Identifiable {
    public var id: UUID
    public var representations: [JournalingSuggestionAsset.Type]
    public func hasContent<Content>(ofType content: Content.Type) -> Bool where Content : JournalingSuggestionAsset
    public func content<Content>(forType content: Content.Type) async throws -> Content? where Content : JournalingSuggestionAsset
    public typealias ID = UUID
}
```

この `ItemContent` が持つ `representations` プロパティにはそれぞれの型情報があるため、この型を元にどの情報を取り出すかを `func content` で行います。

（`representations` の中身は、少し前に記載あるのでそちらを参照してください。）

今回は、`Location` タイプを指定して、そのデータを取り出してみます。

```.swift
JournalingSuggestionsPicker {
    Text("Picker Label")
} onCompletion: { suggestion in
    for item in suggestion.items {
        // バリデーション
        guard item.hasContent(ofType: JournalingSuggestion.Location.self) else {
            return
        }
        // 値の取得
        do {
            Task {
                let location = try await item.content(forType: JournalingSuggestion.Location.self)
                print("place", location?.place)
                print("city", location?.city)
                print("location", location?.location)
                print("date", location?.date)
            }
        }
    }
}
```

例えば、このようなデータが取得できます。

```
place: Optional("新宿3丁目")
city: Optional("新宿区")
location: Optional(<+35.69109060,+139.70486950> +/- 113.60m (speed -1.00 mps / course -1.00) @ 1/01/01, 9:18:59 GMT+09:18:59)
date: Optional(2024-01-17 10:38:57 +0000)
```

また、データが存在するかどうか、`func hasContent` を使ってあらかじめ確認できます。

個別のアイテムからデータを取り出しましたが、大元の `suggestion` 自体から値を型を指定して値を取得することもできます。

```.swift
JournalingSuggestionsPicker {
    Text("Picker Label")
} onCompletion: { suggestion in
    do {
        Task {
            let locations = try await suggestion.content(forType: JournalingSuggestion.Location.self)
            print("locations:", locations)
        }
    }
}
```

以下は取得データ例です。

```
[
    JournalingSuggestions.JournalingSuggestion.Location(
        place: Optional("新宿3丁目"), 
        city: Optional("新宿区"), 
        location: Optional(<+35.69109060,+139.70486950> +/- 113.60m (speed -1.00 mps / course -1.00) @ 1/01/01, 9:18:59 GMT+09:18:59), 
        date: Optional(2024-01-17 10:38:57 +0000)
    )
]
```

今回は１つしかありませんが、全部のアイテムから、全ての指定した型のデータを取得できます。


### - 取得できるデータに関して


取得できるデータは `JournalingSuggestionAsset` プロトコルに準拠しているものです。

```.swift
@available(iOS 17.2, *)
public protocol JournalingSuggestionAsset {
    associatedtype JournalingSuggestionContent : JournalingSuggestionAsset = Self
}
```

`JournalingSuggestionAsset` は、いくつかの `Struct` 型があります。
中身はデータによって変わり、現状は１１の型があります。

| 型 | 情報 | 関連 | 
|:---|:---|:---|
|Workout|ワークアウトの情報<br><br>-> アクティビティの種類、消費エネルギー、距離、<br>心拍数、時間、緯度経度、ワークアウトのアイコン| HealthKit |
|WorkoutGroup|ワークアウトの情報<br>（ワークアウトが複数ある場合に配列で取得）| HealthKit |
|Contact|連絡先の情報<br><br>-> 名前、連絡先のアイコン| CallKit |
|Location|位置情報<br><br>-> 場所、都市名、緯度経度、日付||
|LocationGroup|位置情報<br>（位置情報が複数ある場合に配列で取得）||
|Song|ライブラリからの音楽情報<br><br>-> 曲名、アーティスト名、アルバム名、アートワーク画像、<br>再生日||
|Podcast|聴いたポッドキャストの情報<br><br>-> エピソード名、番組名、アートワーク画像、再生日||
|Photo|ライブラリからの写真情報<br><br>-> 画像、撮影日||
|Video|ライブラリからの動画情報<br><br>-> 動画、撮影日||
|LivePhoto|ライブラリからのライブフォト情報<br><br>-> 動画、撮影日||
|MotionActivity|モーション活動の情報<br><br>-> 歩数、活動時間、アクティビティのアイコン||

ジャーナルとして扱うこれらのデータは、`SiriKit`、 `CallKit`、 `HealthKit` からもサジェストされるため、多様な種類の型にマッピングできるようにが存在しているように思われます。（今後も増える気がする..🤔）

また、`UIImage` と `Image` も `JournalingSuggestionAsset` に準拠しており、データとして扱えるようになっています。（取得できるデータに画像の UI コンポーネントが返ってくることがある。）

### - 実装における注意

`JournalingSuggestions` をインポートして何かを実装しようとした時
現状は Simulator では動かないためエラーが出ます。

![Screenshot 2023-12-13 at 2.11.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/a68d5031-7c80-5dc9-9a87-5f7779860f13.png)

エラーの通り`canImport`を使用して


```.swift
#if canImport(JournalingSuggestions)
import JournalingSuggestions
#endif
```

とすることが推奨されます。

例えば、以下のような書き方です。

```.swift
#if canImport(JournalingSuggestions)
import JournalingSuggestions
#endif

struct ContentView: View {
    var body: some View {
        #if canImport(JournalingSuggestions)
        JournalingSuggestionsPicker {
            Text("Picker Label")
        } onCompletion: { suggestion in
            // do some actions
        }
        #else
        Text("can not import JournalingSuggestions on Simulator")
        #endif
    }
}
```

（結局 Simlator でビルドできないだけなので、煩雑になるのが嫌な方はそのままでも...笑）

# 終わりに

データを提供してくれるのみで、こちらから書き込んだり追加したりできない点が、少し不憫だなというのが印象です。

かなり限定的な機能なため、この API 自体を他のアプリに入れ込むのも、使いどころが難しいなと思ったり🧐

何か間違いあればコメントいただけますとmm