WWDC25:Record, replay, and review: UI automation with Xcode

UIテストの自動化について。全然使いこなしていないので、結果の録画機能がここまで充実しているとは知らなかったし、失敗結果をもとに実装の修正案を提案までしてくれるとは優秀過ぎる。


0:00 – Introduction and agenda

  • Xcode の UI automation を使って、ワンクリックで複数のデバイス、言語、設定でアプリを実行
  • 全ての実行の高品質ビデオ録画を取得可能
  • テストフレームワーク:Swift Testing と XCTest の2つがある
  • XCUIAutomation:XCTest インポート時に自動で含まれ、人と同じようにアプリを自動化
  • テストの種類
    • Unit tests:アプリのロジックとモデルをテスト
    • UI automation tests:ユーザー体験、Apple ハードウェア統合、共通ワークフローをテスト

3:58 – UI automation overview

  • UI automation の利点:ジェスチャテスト、アクセシビリティ技術での知覚テスト、多言語対応確認、ハードウェア機能統合テスト、起動パフォーマンステスト
  • 3つのフェーズ:Record(操作録画)→ Replay(複数設定で再生)→ Review(ビデオと結果確認)
  • プラットフォーム対応:iOS、iPadOS、macOS、watchOS、tvOS、visionOS で同じ自動化を実行可能

6:26 – Prepare your app for automation

  • アクセシビリティとの関係:UI automation はアクセシビリティフレームワークを基盤とする
  • コード変更なく導入することが可能だが、以下手順を踏むことでテスト品質を高められる
  • accessibility identifier の追加
    • 要素を一意に識別する最良の方法
    • ローカライズされた文字列や動的コンテンツを持つ要素に追加
    • 良い identifier:アプリ全体で一意、説明的、静的
    • SwiftUI:accessibilityIdentifier modifier
    • UIKit:accessibilityIdentifier プロパティ
  • Accessibility Inspector:アクセシビリティの問題を発見・診断・修正するツール
  • UI Testing Bundle の追加:プロジェクト設定で新しい UI テストターゲットを追加

11:32 – Record your interactions

  • 録画プロセス:サイドバーのボタンで UI 録画開始、操作がコードとして自動記録
  • 録画後の作業
    • 録画されたコードのレビュー
    • XCTest API を使用した検証の追加
    • 他の automation API の探索
  • UI クエリの選択
    • ローカライズされた文字列:テキストそのものでなく、accessibility identifier を選択
    • 深くネストされたビュー:最短のクエリを選択
    • 動的コンテンツ:より汎用的なクエリ(e.g. .firstMatch)や identifier を使用
  • 検証の追加waitForExistencewait(for:toEqual:)XCTAssert などを使用
  • セットアップ最適化:orientation、appearance、location の設定、launchArgumentslaunchEnvironment の使用
    • アクセシビリティ監査の実行:.performAccessiblityAudit()

17:30 – Replay in multiple configurations

  • Test Plan の活用
    • 個別テストの包含・除外
    • システム設定の管理
    • タイムアウト、繰り返し、並列化の設定
  • 複数設定の構成
    • 各ロケールを個別の設定として追加
      • ドイツ語(長い文字列)、アラビア語・ヘブライ語(右から左のレイアウト)など長い言語のための個別構成も可能
    • 実行中ビデオ、スクショをキャプチャするか、実行後も残すか(デフォルトは失敗時のみ残す)
  • Xcode Cloud
    • クラウドでのビルド、テスト実行、App Store アップロード
    • チーム全体での実行履歴とビデオ録画の共有

20:54 – Review videos and results

  • Test Report の機能
    • 失敗したテストへのナビゲーション
    • ビデオ録画と説明の表示
    • 異なる設定での実行間の素早い切り替え
  • ビデオ解析
    • タイムライン上での失敗箇所へ直接ジャンプ
    • 失敗時点での UI 要素のオーバーレイ表示
      • 要素をクリックして、自動化コード内での推奨実装が表示
      • 複数の代替例の表示とコードの直接コピー、該当箇所に直接遷移し、ペーストして修正

参加メモ:A11y Tokyo Meetup オフライン交流会 #7 Beer Bash

最近アクセシビリティについて WWDC のセッション動画を中心に学んでいるが、障害当事者の方々と接する機会がないため、知識のインプットだけに終始している。

普段プロダクト開発をしながら、自社サービスが十分にアクセシブルでないことは明白。だがユーザーからのそうしたフィードバックは決して多くはない。それは使う以前に見捨てられている、という状況を示している可能性もあるし、より俯瞰してみるとそもそも競合他社含め、(携わっている)業界自体がアクセシブルに機会提供できず、諦めとして認識されているのではないか?という危機感もあった。

そう考えていた時に、元同僚が運営メンバーにいる A11yTokyo Meetup がビアバッシュイベントを開催すると知り、何かきっかけを得られればと思い参加を決めた。

イベントページ:https://a11ytyo.connpass.com/event/364520/

会場テーブルとラガービール

会場は港区のドイツビール・ドイツ料理屋。

参加メンバーは、聴覚、視覚障害をお持ちの方、車椅子利用者の方など障害当事者の参加があった。アクセシビリティへの関わり方についても、業務で主導している方だけでなく、自分のような情報収集をしている段階の方、博士課程で研究されている方と様々だった。また、ホワイトボードを介しての筆談や、手話で会話を取り交わし合う場面もあり、日常では中々意識する機会がないコミュニケーションのあり方に気づかされた。

学生時代ノートテイカーをされていた方、博士課程で弱視の方のスポーツ補助を研究されている方の話を聞けたのも良かった。

細かいことだが、手話にも方言があることを知れたのが驚き。同じ言葉もその表現は地方によって異なり、手話も言語のひとつといえば当たり前のことなのだろうが、そんなことも知らなかった。

ちなみに、筆者も片耳難聴を患っており、たまたま同じテーブルで聴覚障害の方とご一緒でき、日頃の不便を共感しあったりした。その方は北欧から仕事で来日されており、アメリカやヨーロッパにおけるアクセシビリティ対応の現状についても知ることができた。

この会はビアバッシュということでいわゆる飲み会中心の交流会だが、ミートアップ自体は隔月でLT会のようなこともされているとのことで、継続的に参加したいと思った。また、9月には千葉でアクセシビリティカンファレンスが開催されるとのことで、こちらもキャッチアップしたい。

アクセシビリティカンファレンスCHIBA2025

Core Spotlight:セマンティック検索を試みる – その2 (CSUserQuery)

前回の続き:Core Spotlight:セマンティック検索を試みる – その1

Core Spotlight で自前でインデクシングしたコンテンツを検索する場合、CSSearchQuery を使って検索クエリを作成する場合、専用のクエリ文字列を複雑なルールに従って構築する必要がある。

/// Using CSSearchQuery

// Build a query with keywords
func buildQuery(with keywords: [String]) -> String {
    // refer to: 
    // Searching for information in your app | Apple Developer Documentation
    // https://developer.apple.com/documentation/corespotlight/searching-for-information-in-your-app
    let perWordQueries: [String] = keywords.map { word in
        let v = "\"*\(word)*\"c" //大文字小文字を無視
        return "(title == \(v)) || (textContent == \(v)))"
    }
    return perWordQueries.count > 1
        ? "(" + perWordClauses.joined(separator: ") && (") + ")"
        : perWordClauses.first ?? ""
}

let searchQuery = buildQuery(with: keywords)
let context = CSSearchQueryContext()
let query = CSSearchQuery(
    queryString: searchQuery,
    queryContext: context
)
// Event handling
query.foundItemsHandler = { items in ...
query.completionHandler = { error in ...

// Start searching
query.start() 

一方、ユーザーが入力した文字列を元にセマンティックに検索したいばあいは、CSUserQuery が便利。入力された文字列をそのまま、userQueryString としてクエリのイニシャライザに渡すことができる。

/// Using CSUserQuery

let context = CSUserQueryContext()
let query = CSUserQuery(
    userQueryString: searchQuery,
    userQueryContext: context
)
for try await element in query.responses {
    switch(element) {
    case .item(let item): 
        // 検索結果に対する処理
    case .suggestion(let suggestion):
        //サジェスト候補に対する処理
    @unknown default: break
    }
}

しかし、検索結果はとてもセマンティックとは言い難く、部分一致的な挙動しかしてくれない。


Core Spotlight に関する素晴らしい記事を発見し(しかも投稿が2週間前)、これを参考にしている。

Core Spotlight integration for Spotlight and internal app search


追記(2025/09/09)

Core Spotlight のセマンティック検索が部分一致的な挙動しかしないのは、実装の問題ではなく、Siri の性能限界であると Apple の方が教えてくださった。セマンティック検索のセッション(WWDC24)で実演され、また言及もされているデモアプリのサンプルコードについても、いくら探しても見つからなかったが、これもそもそも公開されていないということだった。一件落着(?)。

聴講メモ:実例から学ぶ!モバイルアプリ開発における「AIの使いどころ」

Claude、Devin をはじめとしたコーディングエージェントの利用方法、操り方について実践を経て得られた知見について、五者五様の発表。モノリポ管理であったり、コード調査であったり、実装タスクの向き不向き、デザイン連携、MCP の利用まで多岐にわたっていた。こうした業務利用から生まれたノウハウは、業務生産性向上もそうだが、無駄なトークン消費をいかに抑えるかであったり、そのための見切りポイントの参考にもなるので嬉しい。

イベントページ:https://findy.connpass.com/event/363471/

今回はオンライン聴講で、いつもであればメモを記事にするのだが、今回は移動しながら聴いていてできていない。が、どの発表もAIの業務利用に取り入れたい知見で溢れていたので、備忘録として発表スライドを並べておく。(ページ読み込みが激しく遅くなったので改ページした。)

Core Spotlight:セマンティック検索を試みる – その1

前回のつづき:Core Spotlight にコンテンツをインデックスする

コンテンツのインデックスはできたので、WWDC24:Support semantic search with Core Spotlight に紹介されているセマンティック検索を試みようとした。これによると「セマンティック検索はプロセスに必要な学習モデルのダウンロードが必要」で、CSUserQuery.prepare() を事前に呼んでおく必要があるとのこと。

そのとおりにしたところ、以下のようなエラーが出力されてしまった。SpotlightResources 配下の Info.plist に権限がなくアクセスできないらしい。

Error loading asset properties: Error Domain=NSCocoaErrorDomain Code=257 "The file “Info.plist” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/private/var/MobileAsset/AssetsV2/com_apple_MobileAsset_SpotlightResources/xxx.asset/Info.plist, NSURL=file:///private/var/MobileAsset/AssetsV2/com_apple_MobileAsset_SpotlightResources/xxx.asset/Info.plist, NSUnderlyingError=0x11b4419b0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}

原因がわからない。

WWDC25:Make your Mac app more accessible to everyone

Mac アプリのアクセシビリティ。WWDC25:Evaluate your app for Accessibility Nutrition Labels でも実演されていたように、VoiceOver 操作を実演しながら、実装による変化を説明してくれるのでとてもわかりやすい。ホバー操作のアクセシビリティは盲点だった。


0:00 – Welcome

  • アクセシビリティは誰もがアプリを体験し愛することを可能にする
  • Mac アプリ特有の特徴:キーボード・マウスインタラクション中心、密度の高い UI 、強力なマルチタスキング
  • Mac 固有の品質が重要なアクセシビリティ考慮事項をもたらす
  • 主要な要素:レイアウトの表現、ナビゲーションの加速、インタラクションのアクセシビリティ向上

0:44 – Layout

  • アクセシビリティ要素の基本概念
    • Mac アプリは多くのスペースでコントロールとコンテンツを表示
    • 視覚的なレイアウトと同様に、アクセシビリティ技術への伝達方法も重要
    • SwiftUI は個々のビューをアクセシビリティ要素として伝達、アクセシビリティ技術がアプリを理解・操作するために必要な情報
  • VoiceOver によるテスト
    • VoiceOver は様々な視覚レベルの人々がアプリを使用できるスクリーンリーダー、インターフェースを音声として聞いたり、点字として感じることが可能
    • VoiceOver でのテストはアプリのアクセシビリティをテストする優れた方法
    • Voice Control や Switch Control など他のアクセシビリティ技術との良好な体験への道
  • コンテナによるナビゲーション最適化
    • Mac では VoiceOver は主にキーボードショートカットで制御
    • 画面上の次・前の要素への移動ショートカット
    • 一度に一つの要素ずつ VoiceOver フォーカスを移動して説明を聞く
    • SwiftUI は関連アクセシビリティ要素をコンテナアクセシビリティ要素にグループ化可能
  • Mac 特有の階層構造
    • Mac では VoiceOver はデフォルトで関連要素をコンテナにグループ化、コンテナごとを移動しナビゲーションを高速化
    • Mac のアクセシビリティは iPhone や iPad とは異なり、コンテナがネストされたコンテナを含むことが多く、ツリー構造を生成
    • 入れ子のコンテナレベルが多くなりすぎないよう注意
  • .accessibilityElement(children:) modifier の活用
    • .contain: ビューをアクセシビリティコンテナとして表現、サブビューがその中のアクセシビリティ要素
    • .combine: ビューとサブビューを一つのアクセシビリティ要素として表現、すべての属性とアクションをマージ
    • .ignore: ビューを一つのアクセシビリティ要素として表現、サブビューを完全に無視
  • 要素の順序調整
    • .accessibilitySortPriority(_:) modifier を使用してアクセシビリティ要素の順序を変更
    • デフォルトのソート優先度は 0、同じ優先度のビューは視覚的位置に基づいてソート
    • 例:Book Title を Book Author より先に読み上げるため、高い優先度を設定

7:48 – Navigation

  • Rotor によるナビゲーション加速
    • VoiceOver ユーザーはすべてのページをナビゲートする必要がある
    • Rotor によりビューやテキスト範囲のコレクションを定義し、それらの間を迅速に移動可能にする
    • .accessibilityRouter(_:) modifier
  • .accessibilityDefaultFocus modifier
    • macOS と iOS 26 で、VoiceOver などのアクセシビリティ技術に初期フォーカスを提案可能
    • 新しいシーンが表示されるとき、SwiftUI はこのモディファイアを持つビューにフォーカスするよう提案

9:52 – Interaction

  • ホバーインタラクションの課題
    • ホバーによって出現する操作トリガー(ボタン)
      • VoiceOver ユーザーはポインターを動かさないためボタンにアクセス不可
    • ポインターのホバーやトラックパッドジェスチャーを必要とするインタラクションは全員にアクセシブルではない
    • .accessibilityAction(named:) modifier
      • ビューにアクセシビリティアクションを追加、VoiceOver がアクションメニューを読み上げて選択可能
    • Switch Control や Voice Control などの他のアクセシビリティ技術もこれらのアクションに依存
  • キーボードショートカットの重要性
    • アプリの一般的なタスクにキーボードショートカットを追加
    • パワーユーザー機能であるだけでなく、アクセシビリティにも大きく貢献
    • マウスを使用できない人にとって特に重要
  • カスタムコントロールの考慮事項
    • 独自のカスタムコントロールを作成する場合、他のコントロールが組み込みで持つアクセシビリティ情報を持たない可能性
    • SwiftUI Accessibility: Beyond the basics(WWDC 2021)を参照

Core Spotlight にコンテンツをインデックスする

日記エントリを Spotlight にインデックスさせたい。
(経緯:WWDC24:Support semantic search with Core Spotlight – long forgotten now…

// インデックス作成
func searchableItem(from entry: DiaryEntry) -> CSSearchableItem {
    let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.content)
    attributeSet.contentType = UTType.text.identifier
    attributeSet.title = entry.title
    attributeSet.textContent = entry.content
    attributeSet.recordingDate = entry.date
    
    let item = CSSearchableItem(
        uniqueIdentifier: "\(entry.date.timeIntervalSince1970)",
        domainIdentifier: domainIdentifier,
        attributeSet: attributeSet
    )
    return item
}

let index = CSSearchableIndex(name: "example")
let items = diaryEntries.map { searchableItem(from: $0) }
do {
    try await index.indexSearchableItems(items)
} catch {
    // error handling
}
// 検索
func buildQuery(with keywords: [String]) -> String {
    // refer to: 
    // Searching for information in your app | Apple Developer Documentation
    // https://developer.apple.com/documentation/corespotlight/searching-for-information-in-your-app
    let perWordQueries: [String] = keywords.map { word in
        let v = "\"*\(word)*\"c" //大文字小文字を無視
        return "(title == \(v)) || (textContent == \(v)))"
    }
    return perWordQueries.count > 1
        ? "(" + perWordClauses.joined(separator: ") && (") + ")"
        : perWordClauses.first ?? ""
}

let searchQuery = buildQuery(with: keywords)
let context = CSSearchQueryContext()
context.fetchAttributes = ["title", "textContent"]
let query = CSSearchQuery(
    queryString: searchQuery,
    queryContext: context
)
var results: [CSSearchableItem] = []
query.foundItemsHandler = { items in
    // `items` is hit results
    results += items
}
query.completionHandler = { error in
    // completion handling
    print(results)
}

これでアプリ内からインデクスしたコンテンツに対して Spotlight 検索し、ヒットすることが確認できた。

参考
Adding your app’s content to Spotlight indexes | Apple Developer Documentation
Searching for information in your app | Apple Developer Documentation

Foundation Models 最新情報(2025/08)

Apple 武石さんが、Foundation Models に関する情報を盛んに共有してくださり興味深々。まさにこれまでの試行錯誤の過程でも課題の中心にいる、トークンウィンドウの上限について tips を共有されていたのが目を引いた。

RAG実装における注意にも言及されており、筆者はありものでラクしようと Core Spotlight を活用しようとしているのだが、ここではトークンウィンドウの制限を正攻法的?にクリアしようというアプローチについて言及されている。

  1. データソースを分割
  2. NLContextualEmbeddingMLTensor
  3. 質問も同様に処理
  4. ベクトルの近いものをピックアップし、FMプロンプトに渡す

NLContextualEmbedding
A model that computes sequences of embedding vectors for natural language utterances.(自然言語の文に対して埋め込みベクトルのシーケンスを計算するモデル。)
NLContextualEmbedding | Apple Developer Documentation

MLTensor
A multi-dimensional array of numerical or Boolean scalars tailored to ML use cases, containing methods to perform transformations and mathematical operations efficiently using a ML compute device.(機械学習(ML)のユースケースに特化した数値またはブール型のスカラーからなる多次元配列で、ML計算デバイスを使用して変換や数学的演算を効率的に実行するためのメソッドを含む。)
MLTensor | Apple Developer Documentation

質問ベクトルと近い上位のチャンクに絞って Foundation Models に渡すということっぽい? RAG をきちんと理解する上でこの辺の知識は抑えてみたい。

このあたりの記事がおおいに参考になりそう。


ちなみに、これも武石さんのポストで知ったのだが、Apple Japan にて Foundation Models に関する対面式のセッションがあるらしい(2025/09/09-11)。これも申し込んでみたので、参加資格を得られたらこのあたりの相談をしてみたい。

WWDC25:Explore concurrency in SwiftUI

並行処理の基本から実践的なガイダンスまでてんこもり。@MainActor 隔離や Sendable の扱いについて、いまだにコンパイルエラーに振り回されている状況で、理解し使いこなせている感じがしないのだが、何のためにそうしたアノテーションが存在するのか、理解に近づいた気がした。


0:00 – Introduction

  • SwiftUI アプリ開発における並行処理について
  • データレースバグ(予期しないアプリ状態、ちらつくアニメーション、永続的なデータ損失)からの安全性を確保
  • SwiftUI がコードを様々な方法で並行実行する仕組みと、API の並行処理アノテーションによる識別方法を学習
  • Swift 6.2 の新しい言語モードでは、モジュール内のすべての型に @MainActor アノテーションが暗黙的に付与

2:13 – Main-actor Meadows

  • @MainActor の基本概念
    • SwiftUI の View プロトコルは @MainActor 隔離を宣言
    • e.g. ColorExtractorView が View に準拠するため、@MainActor 隔離される
      • コンパイル時に暗黙的に適用されるが実際のコードには含まれない
  • 型レベルでの隔離効果
    • 型全体が @MainActor で隔離されると、そのすべてのメンバーも暗黙的に隔離される
      • body プロパティ、@State 変数なども含む
    • 共有された @MainActor 隔離により、これらのアクセスはコンパイラによって安全であることが保証
  • コンパイル時のデフォルト
    • @MainActor は SwiftUI のコンパイル時デフォルト
    • ほとんどの場合、アプリ機能の構築に集中でき、 並行処理について考える必要がない、並行処理目的のためのコードアノテーションは不要で、自動的に安全
  • データモデルとの連携
    • データモデル型には @MainActor アノテーションが不要
    • ビュー宣言内でモデルをインスタンス化すると、Swift がモデルインスタンスの適切な隔離を保証
  • 非同期コンテキストでの利用
    • body@MainActor 隔離されているため、例では .onTapGesture で実行している Task { } クロージャもメインスレッドで実行
  • AppKit/UIKit との相互運用性
    • AppKit と UIKit の API は排他的に @MainActor 隔離
      • SwiftUI はこれらのフレームワークとシームレスに相互運用
      • UIViewRepresentable プロトコルは View プロトコルを継承し、@MainActor 隔離
      • UILabel のイニシャライザも @MainActor 隔離を要求 → @MainActor 隔離の makeUIView で機能
  • ランタイムセマンティクスの表現
    • SwiftUI の並行処理アノテーションはランタイムセマンティクスを表現
    • アノテーションはフレームワークの意図されたランタイムセマンティクスの下流

7:17 – Concurrency Cliffs

  • パフォーマンス最適化の必要性
    • アプリ機能の追加により、メインスレッドに負荷がかかりフレーム落ちやちらつきが発生
    • Task と構造化 Concurrency を使用してメインスレッドから計算処理を切り離し
  • SwiftUI の組み込みアニメーション最適化
    • 組み込みアニメーションはバックグラウンドスレッドで中間状態を計算
      • scaleEffect アニメーションの例:1から1.5の間で異なるスケール値をフレームごとに計算
      • 複雑な数学計算を含むアニメーション値の計算は高コストなため、バックグラウンドスレッドで実行
  • 宣言的特性の活用
    • SwiftUI は宣言的で、View プロトコルに準拠する構造体はメモリ内の固定位置を占有しない
    • ランタイムで SwiftUI がビュー個別の表現を作成
    • この表現により多くの最適化の機会を提供、重要なのはバックグラウンドスレッドでのビュー表現の一部評価
  • バックグラウンド実行の対象 API
    • Shape プロトコルpath メソッドがバックグラウンドスレッドから呼び出される可能性
    • visualEffect:視覚効果が高コストなため、クロージャがバックグラウンドスレッドから呼び出される可能性
    • Layout プロトコル:要求メソッドがメインスレッド外で呼び出される可能性
    • onGeometryChange:最初の引数のクロージャがバックグラウンドスレッドから呼び出される可能性
  • Sendable アノテーション
    • SwiftUI は Sendable アノテーションでコンパイラと開発者にランタイム動作を表現
    • Sendable@MainActor からデータを共有する際の潜在的なデータレースが生じる可能性があることを思い出させるもの(危険立ち入り禁止の標識のようなもの)
    • Swift はコードの潜在的なレース条件を確実に発見し、コンパイラエラーで通知
  • データレース回避戦略
    • データ競合を回避する最良の戦略は、並行タスク間でデータをまったく共有しないこと
    • SwiftUI API が sendable 関数を要求する場合、フレームワークが必要な変数の大部分を関数引数として提供
      • e.g. sizeThatFits(…) が提供する subviews パラメタ:外部変数を利用せず高度な計算が可能
  • 外部変数へのアクセス
    • sendable 関数で外部変数にアクセスする場合の制約
    • @MainActor 隔離変数を sendable クロージャで共有する一般的なシナリオ
    • self が Main actor から Background thread のコード領域に境界を越える必要がある
      • 「変数 self を バックグラウンドスレッドに送信する(Send self from main actor)」
      • この「送信」には self の型が Sendable である必要(非隔離領域での参照)
      • self のプロパティにバックグラウンドからアクセスするには、そのプロパティがどのアクターにも隔離されないことが必要(properties must be nonisolated)→ 解決方法へ
  • 問題解決方法
    • View への参照を通じたプロパティ読み取りを避ける:クロージャのキャプチャリストで変数のコピーを作成し送信 e.g. { [pulse] in ...
      • コピーを参照することで self をクロージャに送信することを回避
      • Bool などの単純な値型は sendable のため、コピーが可能
      • 関数スコープ内でのみ存在するコピーへのアクセスはデータレース問題を引き起こさない
    • 参照すべてのプロパティを非隔離状態(nonisolated)にする

16:53 – Code Camp

  • 同期 API の設計理念
    • ほとんどの SwiftUI API(Button のアクションコールバックなど)は同期的
    • 並行コードを呼び出すには、まず Task で非同期コンテキストに切り替える必要
    • Button が非同期クロージャを受け取らない理由:同期的な更新が良好なユーザー体験において重要(特に長時間のタスクの前には)
  • 長時間実行タスクでの UI 更新
    • 長時間実行タスクがあり、結果を待つ必要がある場合に特に重要
    • 非同期関数で長時間実行タスクを開始する前に、進行中であることを示すための UI 更新が重要
    • この更新は同期的であるべき、特に時間に敏感なアニメーションをトリガーする場合
  • 実例:言語モデルによる色抽出
    • withAnimation を使用して同期的に様々なローディング状態をトリガー
    • タスク完了時に別の同期状態変更でローディング状態を反転
  • フレームレートの制約
    • UI フレームワークとして、SwiftUI は滑らかなインタラクションのためにデバイスの画面リフレッシュレートの現実に対処する必要
    • スクロールなどの連続ジェスチャーに反応する際の重要なコンテキスト
  • 非同期処理とアニメーションのタイミング
    • onScrollVisibilityChange でスクロール可視性の変化を検出
    • 状態変数を true に設定してアニメーションをトリガー
    • 非同期作業前にアニメーション変更を追加する場合のタイミング問題
  • 一時停止ポイント(suspension point) の影響
    • await で非同期関数を呼ぶと一時停止ポイントが作成される
    • Task は非同期関数を引数として受け取る
    • コンパイラが await を見ると、非同期関数を2つの部分に分割
      • 最初の部分を実行後、Swift ランタイムは関数を一時停止し、CPU で他の作業を実行可能
      • この中断により、タスククロージャがデバイスのリフレッシュ期限を過ぎるまで再開されない可能性
        • → 非同期関数では不足、同期コールバックが必要
  • 同期コールバックの利点
    • SwiftUI はデフォルトで同期コールバックを提供
    • 非同期コードの意図しない中断を回避
    • 同期アクションクロージャ内での UI 更新は正しく実行しやすい
    • Task を使用して非同期コンテキストにオプトインする選択肢は常に存在
    • 同期コードが多くのアプリにとっての優れた出発点・終着点
  • UI と非 UI コードの境界分離
    • アプリが多くの並行作業を行う場合、UI コードと非 UI コードの境界を見つける
    • 非同期処理のロジックをビューロジックから分離することがベスト
      • 状態をブリッジとして使用:状態が UI コードと非同期コードを分離
    • UI ロジックはほとんど同期的に保つ
    • 非同期コードのテストが UI ロジックから独立するため、より簡単になる
  • 構造的な改善
    • 時間に敏感な変更を多く必要とする UI コードと長時間実行非同期ロジックの境界を見つけることが重要
    • ビューを同期的かつレスポンシブに保つのに役立つ
    • 非 UI コードも適切に整理することが重要

23:47 – Next steps

  • Swift 6.2 の活用
    • 優れたデフォルトアクター分離設定を提供
    • 既存アプリでの試用を推奨、ほとんどの @MainActor アノテーションを削除可能
  • Mutex の活用
    • クラスを sendable にするための重要なツール
    • 公式ドキュメントで学習方法を確認
  • 単体テストの挑戦
    • アプリの非同期コードに対する単体テストの作成
    • SwiftUI をインポートせずに実行できるかチャレンジ
  • まとめ
    • SwiftUI が Swift  Concurrency を活用して高速でデータレースフリーなアプリ構築を支援
    • SwiftUI における並行処理の確固としたメンタルモデルの獲得を目標

WWDC25:Track workouts with HealthKit on iOS and iPadOS

ワークアウトは Apple Watch で毎日使っているので、iOS / iPadOS でどういうことが可能なのか確認してみた。


0:00 – Introduction

  • 数百のヘルス・フィットネスアプリが HealthKit の暗号化されたデータベースと強力な API を活用
  • ワークアウト API は HealthKit が提供する最も強力な API の一つ
  • Apple Watch で既にワークアウトアプリを運用している場合、最小限の変更で同じコードを iPhone と iPad で使用可能

0:56 – Run a workout session

  • ワークアウトセッションの基本手順
    1. セットアップHKWorkoutConfiguration を作成し、アクティビティタイプを設定(例:ランニング、屋外)
    2. セッション作成:設定を使って HKWorkoutSession を作成
    3. ビルダー取得:ワークアウトセッションから関連する builder を取得し、データソース(HKLiveWorkoutDataSource)をアタッチ
    4. 準備session.prepare を呼び出し、3秒のカウントダウンを表示
      • オンデバイスセンサーの起動や外部心拍モニターの接続時間を確保
      • ワークアウト開始時にメトリクスがすぐに利用可能になることを保証
    5. 開始:カウントダウン完了後、セッションで startActivity、関連する workout builder で beginCollection を呼び出し
  • UI 更新の簡素化
    1. anchored object query を使用する必要なし
    2. workout builder の delegate が新しいデータ収集時に便利な更新を提供
    3. ワークアウト保存時のメトリクス同期を自動処理
  • 終了処理
    1. session.stopActivity を呼び出し、最終メトリクスの収集を許可
    2. セッションが停止状態に遷移後、builder で endCollection を呼び出し
    3. builder 完了後、セッションで end を呼び出し、ワークアウトサマリーを表示

2:50 – Get session metrics

  • 利用可能センサーの違い
    • iPhone と iPad には心拍センサーが内蔵されていない
    • 心拍 GAT プロファイル対応デバイス(ウェアラブル心拍モニター、Powerbeats Pro 2 など)とのペアリングが可能
    • ペアリング後、HealthKit が心拍データを自動取得し、Health Store にサンプルとして保存
  • データタイプの分類
    • Generated types:システムがワークアウト中に生成するデータタイプ(カロリー、距離など)
    • Collected types:ライブで観察し、ワークアウトサンプルに追加したいメトリクス
    • 例:ワークアウト中の水分摂取量を収集する場合、アプリがサンプルをヘルスデータベースに追加する必要
  • データソースの設定
    • 初期化時、データソースの types to collect プロパティには現在のアクティビティで収集可能な全サンプルタイプが含まれる
    • 外部心拍センサーがない場合でも心拍数が含まれる
    • システム生成またはアプリ保存のサンプルを観察し、live builder に渡す
  • コレクションタイプのカスタマイズ
    • enable / disable collection for type メソッドでタイプの追加・削除が可能
    • 例:ワークアウト中の水分摂取量収集時、Enable Collection を呼び出し、測定値をサンプルとしてヘルスデータベースに追加
  • 保存後のワークアウトメトリクス読み取り
    • ワークアウトオブジェクトの statistics を使用してサマリーを表示
    • 期間中のメトリクスをチャート化する場合は statistics collection query を使用
    • 細かいデータが必要な場合、HKQuantitySeriesSampleQuery を使用してより詳細なデータにアクセス
  • デバイスロック時の対応
    • iPhone はワークアウト中にロックされる可能性が高い
    • 初回ワークアウトセッション開始時、デバイスロック中でもワークアウトデータが利用可能になる旨のシステムプロンプトを表示
    • Live Activity でロックスクリーン上に重要なメトリクスを表示可能
    • プライバシー配慮:データアクセス不可時はメトリクスを省略し、ワークアウト時間のみ表示
  • Siri 対応
    • ロックスクリーンからワークアウトの開始、一時停止、再開、キャンセルが可能
    • アプリ内で Intent Handler(INExtension) を定義し、StartWorkoutIntent などの各インテントを処理
    • アプリデリゲートで Intent に応答する設定が必要

8:35 – Recover from a crash

  • クラッシュ復旧の仕組み
    • システムがクラッシュ時にアプリを自動再起動
    • ワークアウトセッションと Builder が以前の状態で復元
    • ライブデータソースの再設定が必要
  • iPhone と iPad 向けの新機能
    • 進行中のワークアウト復旧用の新しい scene delegate を追加
    • Siri インテント用に作成したアプリデリゲートにシーンデリゲートを追加してクラッシュ復旧を処理

9:34 – Best practices

  • Watch アプリ優先
    • Watch アプリがある場合、そこでワークアウトを開始して全利用可能メトリクスを取得
    • Health Store から Start Watch App を呼び出し
    • ワークアウトを iPhone にミラーリング
  • 認証リクエストの最適化
    • 必要なデータタイプのみ認証をリクエスト
    • アプリの焦点と無関係に見えるデータタイプの認証理由をユーザーが疑問に思わないよう配慮
  • Workout Builder API の使用
    • ワークアウトの作成と保存には常に workout builder API を使用
    • アクティビティリングが適切に更新されることを保証
  • 次のステップ
    • セッション添付のデモアプリ
    • 既存の iPhone/iPad アプリがある場合は Workout Builder API にアップグレード
    • Apple Watch アプリがある場合、同じ API のマルチプラットフォームサポートにより Apple Watch を持たないユーザー向けに提供可能