はじめに
iOS 17.2 からジャーナル
機能が提供されました!
(なんで、17.2 っていう中途半端なタイミングで...みたいなツッコミもありつつ...)
今回、機能だけでなく開発者向けの API も提供されました。
これを触ってみたので、ざっくりと紹介していきます。
Journaling Suggestions API に触れてみる
公式の動画では、以下のステップで実行できるとのこと。
(引用:https://developer.apple.com/videos/play/tech-talks/111384/)
では、実際にやっていきます。
セットアップ
1. 環境の用意
- Xcode 15.1~
- iOS 17.2~ の実機(ここ重要)
Xcode 15の Beta にも入っている(入ってないものもあるらしい?)が、実機ビルドを考えると最新のものを入れておくと良いかと思います。
また、後々のコード面でも触れますが、ジャーナルの機能は実機でしか確認できないので、かならず実機を用意してください。
2. Capability の追加
Capability に Journaling Suggestions が新しく追加されています。
これを追加すると、設定の一覧に追加されます。
これで Journaling Suggestions の API を使えるようになります。
実装
- ジャーナルの提案画面を表示
Capability を追加していれば、JournalingSuggestions
を import できます。
import JournalingSuggestions
あとは、ジャーナルのピッカーを呼び出すだけです。
JournalingSuggestionsPicker
を使うだけで簡単に実装できます。
struct ContentView: View {
var body: some View {
JournalingSuggestionsPicker {
Text("Picker Label")
} onCompletion: { suggestion in
// do some actions
}
}
}
実際の画面はこんな感じです。

ピッカーを呼び出すラベルを設定し、それをタップすればサジェストが自動で表示されます。
- 提案画面から得られるデータにアクセスする
ジャーナルの提案一覧から、詳細な情報を閲覧できます。

この詳細から 「追加」 を押すとデータが取得できます。
また、アイコン(オレンジの部分)をタップした場合は直接データを取得できます。
取得したデータは、JournalingSuggestionsPicker
の onCompletion
に入ってきます。実際のデータはこのような形です。(先ほど提示した位置情報のジャーナルデータ)
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?
の情報にアクセスできます。例えば、先ほどのデータではこうなります。
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
はサジェストされたジャーナルデータの情報を個別に持っています。
先ほどの例では1つでしたが、データが複数の場合はこのような配列で取得されます。
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
の配列)から、詳細なデータにアクセスできます。
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
タイプを指定して、そのデータを取り出してみます。
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
自体から値を型を指定して値を取得することもできます。
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)
)
]
今回は1つしかありませんが、全部のアイテムから、全ての指定した型のデータを取得できます。
- 取得できるデータに関して
取得できるデータは JournalingSuggestionAsset
プロトコルに準拠しているものです。
@available(iOS 17.2, *)
public protocol JournalingSuggestionAsset {
associatedtype JournalingSuggestionContent : JournalingSuggestionAsset = Self
}
JournalingSuggestionAsset
は、いくつかの Struct
型があります。
中身はデータによって変わり、現状は11の型があります。
型 | 情報 | 関連 |
---|---|---|
Workout | ワークアウトの情報 -> アクティビティの種類、消費エネルギー、距離、 心拍数、時間、緯度経度、ワークアウトのアイコン |
HealthKit |
WorkoutGroup | ワークアウトの情報 (ワークアウトが複数ある場合に配列で取得) |
HealthKit |
Contact | 連絡先の情報 -> 名前、連絡先のアイコン |
CallKit |
Location | 位置情報 -> 場所、都市名、緯度経度、日付 |
|
LocationGroup | 位置情報 (位置情報が複数ある場合に配列で取得) |
|
Song | ライブラリからの音楽情報 -> 曲名、アーティスト名、アルバム名、アートワーク画像、 再生日 |
|
Podcast | 聴いたポッドキャストの情報 -> エピソード名、番組名、アートワーク画像、再生日 |
|
Photo | ライブラリからの写真情報 -> 画像、撮影日 |
|
Video | ライブラリからの動画情報 -> 動画、撮影日 |
|
LivePhoto | ライブラリからのライブフォト情報 -> 動画、撮影日 |
|
MotionActivity | モーション活動の情報 -> 歩数、活動時間、アクティビティのアイコン |
ジャーナルとして扱うこれらのデータは、SiriKit
、 CallKit
、 HealthKit
からもサジェストされるため、多様な種類の型にマッピングできるようにが存在しているように思われます。(今後も増える気がする..🤔)
また、UIImage
と Image
も JournalingSuggestionAsset
に準拠しており、データとして扱えるようになっています。(取得できるデータに画像の UI コンポーネントが返ってくることがある。)
- 実装における注意
JournalingSuggestions
をインポートして何かを実装しようとした時
現状は Simulator では動かないためエラーが出ます。
エラーの通りcanImport
を使用して
#if canImport(JournalingSuggestions)
import JournalingSuggestions
#endif
とすることが推奨されます。
例えば、以下のような書き方です。
#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