watchOS について
watchOSアプリ開発向けのガイダンススキルです。Watchアプリ、拡張機能、コンプリケーションの構築を対象に、SwiftUIのUI、デバイス間のデータ転送、WidgetKit、HealthKit、アクセシビリティを扱います。
主な機能
- 01Watch優先のUIとナビゲーションの指針
- 02Watch Connectivityによるデータ転送
- 03ClockKitとWidgetKitのコンプリケーション
- 04HealthKitのワークアウトとバックグラウンド実行
- 05watchOSのアクセシビリティ設計
- 06パフォーマンスとバッテリーの指針
活用シーン
- 01ひと目で把握できるWatch UIを設計
- 02iPhoneとWatchアプリ間でデータを同期
- 03文字盤コンプリケーションとSmart Stackウィジェットを作成
- 04ワークアウト追跡とアクセシブルな操作を実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rshankras/ claude-code-apple-skills --skill watchosインストール後、Claude Code で/watchosを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
complications.mdhealth-fitness.mdwatch-connectivity.mdwidgets-for-watch.md詳細
watchOS 開発
SwiftUI、Watch Connectivity、コンプリケーションを使った watchOS アプリ開発の包括的なガイダンスです。
このスキルが有効になる場面
次のような場合に、このスキルを使用します。
- watchOS アプリまたは Watch 拡張機能を開発している
- Watch Connectivity(iPhone ↔ Watch の同期)について質問している
- コンプリケーションや ClockKit についてサポートが必要
- watchOS 固有の UI パターンを実装したい
- WidgetKit コンプリケーションや、ClockKit から WidgetKit への移行について質問している
- 文字盤のコンプリケーション(accessoryCircular、accessoryRectangular、accessoryCorner、accessoryInline)を作成したい
- watchOS の HealthKit、ワークアウトセッション、心拍数、フィットネストラッキングについて質問している
- バックグラウンドでワークアウトを追跡するための延長ランタイムセッションが必要
- watchOS ウィジェットまたは Smart Stack ウィジェットを作成したい
- ウィジェットの関連性、Smart Stack の並べ替え、ウィジェットの提案について質問している
- iOS と watchOS の間でウィジェットをクロスプラットフォームで共有する必要がある
- watchOS のアクセシビリティ(VoiceOver、AssistiveTouch、Watch での Dynamic Type)について質問している
重要な原則
1. Watch を最優先した設計
- 一目で確認できるコンテンツ — ユーザーが見るのは数秒間であり、数分間ではありません
- すばやく操作できること — 2 秒以内
- 必要不可欠な情報のみ — 長いテキストをスクロールさせない
- 大きなタッチ領域 — 高さは最低 38pt
2. 独立型とコンパニオン型
- 可能であれば、独立型の Watch アプリを優先します
- Watch Connectivity はデータ同期に使用し、依存関係にはしません
- オフラインでアクセスできるよう、データをローカルにキャッシュします
- 接続エラーには適切に対処します
3. パフォーマンス
- バックグラウンド処理を最小限に抑えます(バッテリー消費を抑えるため)
- コンプリケーションの更新は控えめにします
- ライブ更新より、タイムラインベースのコンテンツを優先します
- ビューを軽量に保ちます
watchOS のデザインルール(WWDC20/23)
10 秒テスト
およそ 10 秒間の注意を引くことを想定して設計します。「誰かの注意を 10 秒間引けるとしたら、どの情報を表示しますか?」その詳細画面に直接移動させます。表示する内容は、場所、新しさ、頻度に基づいて選び、タイトルがなくても明確に伝わるようにします。
3 つの基本的なレイアウト
| レイアウト | 用途 | 備考 |
|---|---|---|
| Dial | 一目で確認できる詳細なステータス | 四隅に最大 4 つのコントロールを配置できます。ベゼルに合わせるには .scenePadding(.horizontal) を使用します |
| Infographic | グラフとメトリクス | 1 つのグラフと、それを補足する数値 |
| List | スクロールして項目を探す | ユーザーが項目を見つける必要がある場合 |
ナビゲーションモデル
- Digital Crown による垂直ページ送りを優先します。目的が明確な、画面の高さに収まるページ間を移動させます。水平ページ送りは「ナビゲーションがより難しい」方法です。
NavigationSplitViewを使った 2 階層の Source List パターンを優先します。選択項目を必ず初期化し、アプリが詳細画面から直接起動するようにします。また、Source List にタイトルは付けません。- どちらのパターンにも当てはまらない場合に限り、
NavigationStackを使用します。階層型ナビゲーションでは、前回の起動時に表示していた移動先を記憶する必要があります。 - Digital Crown はナビゲーション、スクロール、精密な入力の基点となりますが、必ずタッチ操作でも行えるようにします。
// Source List: launch to detail, not the list
NavigationSplitView {
List(rooms, selection: $selectedRoom) { room in // source list stays untitled
Text(room.name)
}
} detail: {
RoomView(room: selectedRoom)
}
// Initialize selectedRoom (last used / most relevant) so launch lands on detail
背景とマテリアル
- 背景には、太陽を追跡するソーラーグラデーションや、黒からオレンジへの状態変化など、認識や情報に役立つ機能を持たせます。単なる装飾にしてはいけません。
- 4 種類の鮮やかな全画面マテリアル(Ultra Thin → Thick)は、Primary から Quaternary までの鮮やかな前景スタイルや、鮮やかなセマンティックカラーと組み合わせることで、どのような背景でもコンテンツを読みやすくします。
ツールバーとアクションボタン
- ツールバーの配置位置は
.topBarLeading、.topBarTrailing(時刻が中央に移動します)、.bottomBarです。 - 詳細画面の下部にアクションボタンを配置するのが、最も見つけやすいパターンです。赤いラベルは破壊的な操作を示します。データを復元できない場合は、確認を追加します。
- More ボタン(円形のコンテナに入った省略記号。白色、不透明度 85%、黒色の外側の光彩は 1pt、不透明度 50%)には、二次アクションのみを配置します。主要なアクションは配置しません。
- ツールバーから表示するボタンは、スクロール可能なビューにのみ配置します。スクロールによってボタンが見つけやすくなるためです。
watchOS のアクセシビリティ(WWDC21 10223)
Watch の Dynamic Type
- watchOS には 11 種類のテキストスタイルがあります。固定の
.font(.system(size: 24))では拡大縮小されないため、.font(.title3)などを使います。 - テキストを折り返すようにします。
lineLimit(1)はアクセシビリティサイズでテキストを切り詰めるため、実際にサポートする最大行数(.lineLimit(3))を設定するか、制限をなくします。 - Watch の設定では、テキストサイズの初期値はペアリングされた iPhone の設定に最も近いサイズになります。アクセシビリティサイズを使うユーザーが実際にいることを想定してください(WWDC21 10223)。
- 折り返しでレイアウトが窮屈になる場合は切り替えます。
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
if sizeCategory < .extraExtraLarge {
PlantViewHorizontal(plant: $plant) // default layout
} else {
PlantViewVertical(plant: $plant) // stacked layout for large sizes
}
}
VoiceOver
NavigationLinkは子要素のアクセシビリティを自動的にまとめます。その内部で追加のグループ化をしないでください。行全体が単一の要素になります(WWDC21 10223)。- アイコンとテキストを含む行には、要素の羅列ではなく意味が伝わるラベルを付けます。たとえば、「Drop, image. Five days.」ではなく
.accessibilityLabel("Watering in five days")を使います。アイコンのみのボタンにもラベルを付けます。.accessibilityLabel("Log \(task.name)")→「水やりを記録、ボタン」。 - ステッパー/カウンターでは、[マイナス、値、プラス]を 1 つの調整可能な要素にまとめます。変化する数値を値に設定します。値は変更のたびに再度読み上げられ、ラベルは移動時にのみ読み上げられます。
CustomCounter(value: value, increment: increment, decrement: decrement)
.accessibilityElement() // drops the +/- buttons as separate stops
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: increment() // swipe up
case .decrement: decrement() // swipe down
default: break
}
}
.accessibilityLabel("\(task.name) frequency")
.accessibilityValue("\(value) days")
- コンプリケーションや動的通知にも同じ対応が必要です。これらはアプリ外からコンテンツにアクセスする追加の経路です。略語は展開し(「Wednesday Mar 9」ではなく「Wednesday, March 9th」)、画像を使ったコンプリケーションにはラベルを付けます。そうしないと、VoiceOver がアセット名を読み上げます(WWDC21 10223)。
AssistiveTouch
ハンドジェスチャーを使うと、画面に触れずに Watch を操作できます。握る=タップ、2 回握る=アクションメニュー、ピンチ=次の要素、2 回ピンチ=前の要素(WWDC21 10223)。カーソルがフォーカスするのは、Button、Toggle、NavigationLink、タップジェスチャーのあるビュー、アクセシビリティアクション、またはアクション可能な特性を持つインタラクティブ要素のみです。静的なテキストと無効な要素はスキップされます。
// ✅ static text whose parent owns the tap gesture — make it a cursor stop
FreeDrinkInfoView()
.accessibilityRespondsToUserInteraction(true)
// ✅ cursor frame == tappable area; enlarge tiny hit targets
NavigationLink(destination: EditView()) {
Image(systemName: "ellipsis").symbolVariant(.circle)
}
.contentShape(Circle().scale(1.5))
VoiceOver のカスタムアクションは、AssistiveTouch のアクションメニューに自動的に表示されます。.accessibilityAction { } label: { Label("Edit", systemImage: "ellipsis.circle") } の Label 形式で適切なアイコンを指定してください。指定しない場合、メニューにはアクション名の先頭文字が表示されます(WWDC21 10223)。
アーキテクチャパターン
アプリの構造
@main
struct MyWatchApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
ナビゲーション
// Use NavigationStack (watchOS 9+)
NavigationStack {
List {
NavigationLink("Item 1", value: Item.one)
NavigationLink("Item 2", value: Item.two)
}
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
// TabView for main sections
TabView {
HomeView()
ActivityView()
SettingsView()
}
.tabViewStyle(.verticalPage)
リストの設計
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete(perform: delete)
}
.listStyle(.carousel) // For focused content
.listStyle(.elliptical) // For browsing
Watch Connectivity
セッションの設定
import WatchConnectivity
@Observable
final class WatchConnectivityManager: NSObject, WCSessionDelegate {
static let shared = WatchConnectivityManager()
private(set) var isReachable = false
override init() {
super.init()
if WCSession.isSupported() {
WCSession.default.delegate = self
WCSession.default.activate()
}
}
// Required delegate methods
func session(_ session: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) {
isReachable = session.isReachable
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
#endif
}
データ転送方法
| 方法 | 用途 | 配信 |
|---|---|---|
updateApplicationContext | 最新の状態(設定) | 前の状態を上書き |
sendMessage | リアルタイム通信、両方のアプリがアクティブ | 即時 |
transferUserInfo | キューに入れたデータ | 順番どおりに確実に配信 |
transferFile | 大容量データ | バックグラウンド転送 |
// Application Context (most common)
func updateContext(_ data: [String: Any]) throws {
try WCSession.default.updateApplicationContext(data)
}
// Real-time messaging
func sendMessage(_ message: [String: Any]) {
guard WCSession.default.isReachable else { return }
WCSession.default.sendMessage(message, replyHandler: nil)
}
// Receiving data
func session(_ session: WCSession, didReceiveApplicationContext context: [String: Any]) {
Task { @MainActor in
// Update UI with received data
}
}
コンプリケーション
タイムラインプロバイダー
import ClockKit
struct ComplicationController: CLKComplicationDataSource {
func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
let descriptor = CLKComplicationDescriptor(
identifier: "myComplication",
displayName: "My App",
supportedFamilies: [.circularSmall, .modularSmall, .graphicCircular]
)
handler([descriptor])
}
func getCurrentTimelineEntry(
for complication: CLKComplication,
withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void
) {
let template = makeTemplate(for: complication.family)
let entry = CLKComplicationTimelineEntry(date: .now, complicationTemplate: template)
handler(entry)
}
}
WidgetKit コンプリケーション(watchOS 9 以降)
import WidgetKit
import SwiftUI
struct MyComplication: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "MyComplication",
provider: ComplicationProvider()
) { entry in
ComplicationView(entry: entry)
}
.configurationDisplayName("My Complication")
.supportedFamilies([
.accessoryCircular,
.accessoryRectangular,
.accessoryCorner,
.accessoryInline
])
}
}
UI コンポーネント
Digital Crown
@State private var crownValue = 0.0
ScrollView {
// Content
}
.focusable()
.digitalCrownRotation($crownValue)
触覚フィードバック
WKInterfaceDevice.current().play(.click)
WKInterfaceDevice.current().play(.success)
WKInterfaceDevice.current().play(.failure)
Now Playing
import WatchKit
NowPlayingView() // Built-in now playing controls
ワークアウトアプリ
import HealthKit
@Observable
class WorkoutManager {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
func startWorkout(type: HKWorkoutActivityType) async throws {
let config = HKWorkoutConfiguration()
config.activityType = type
config.locationType = .outdoor
session = try HKWorkoutSession(healthStore: healthStore, configuration: config)
builder = session?.associatedWorkoutBuilder()
session?.startActivity(with: .now)
try await builder?.beginCollection(at: .now)
}
}
ベストプラクティス
パフォーマンス
ObservableObjectより@Observableを使います(watchOS 10 以降)。- バックグラウンド更新を制限します。
- 画像をローカルにキャッシュします。
- リストには遅延読み込みを使います。
バッテリー
- 位置情報の更新を最小限にします。
- スケジュールされたバックグラウンドタスクを使います。
- 頻繁な更新よりコンプリケーションを優先します。
- ネットワークリクエストをまとめます。
ユーザーエクスペリエンス
- 常に読み込み状態を表示します。
- 触覚フィードバックを提供します。
- キーボード入力に対応します。
- わかりやすいアイコンを使います。
テスト
シミュレーター
- さまざまな Watch サイズでテストします。
- すべてのファミリーでコンプリケーションを確認します。
- ペアリングした iPhone シミュレーターで Watch Connectivity をテストします。
実機
- バッテリーへの影響をテストします。
- 触覚フィードバックが適切に感じられるか確認します。
- さまざまな照明条件でテストします。
判断ツリー
ユーザーのニーズに応じて、適切なリファレンスファイルを選択します。
What are you building?
|
+- iPhone <-> Watch data sync
| -> watch-connectivity.md
| +- Session management, application context, real-time messaging
| +- File transfers, offline caching, complication push updates
|
+- Watch face complications
| -> complications.md
| +- ClockKit (legacy) vs WidgetKit (modern) complications
| +- Migration from ClockKit to WidgetKit
| +- Complication families (circular, rectangular, corner, inline)
| +- Timeline providers, reload strategies, gauges
|
+- Health / fitness / workout tracking
| -> health-fitness.md
| +- HealthKit authorization and data types
| +- HKWorkoutSession and HKLiveWorkoutBuilder
| +- Real-time heart rate, calories, distance
| +- Extended Runtime sessions, route tracking
|
+- watchOS widgets / Smart Stack
| -> widgets-for-watch.md
| +- Smart Stack configuration and relevance
| +- Cross-platform widget sharing (iOS + watchOS)
| +- watchOS-specific design (dark background, small screen)
|
+- General watchOS app development
-> This file (SKILL.md)
+- Design rules: ten-second test, layouts, navigation model, action buttons
+- App structure, navigation, lists
+- Digital Crown, haptics, Now Playing
参考ファイル
| ファイル | 内容 |
|---|---|
| watch-connectivity.md | iPhone <-> Watch の同期、セッション管理、データ転送、オフラインキャッシュ |
| complications.md | ClockKit から WidgetKit への移行、コンプリケーションファミリー、タイムラインプロバイダー、ゲージ |
| health-fitness.md | HealthKit、ワークアウトセッション、心拍数、Extended Runtime、ルート追跡、プライバシー |
| widgets-for-watch.md | Smart Stack ウィジェット、関連性、クロスプラットフォーム共有、watchOS のデザイン |