この記事はGMOペパボ エンジニア Advent Calendar 2025の23日目です
きっかけ
iPhoneでアラームを設定しても、ちゃんと起きれたことがほぼありません。 平日は特に何度もスヌーズが続きます。 そこで、WWDC 2025で発表されたAlarmKitを使って、オリジナルのアラームを作ってみようじゃないかという試みです。
どんなアラームか
- 目覚ましが鳴る
- 画面を解除する
- 腕立て伏せのカウント画面が開く
- ○回クリアしたら目覚ましの音が鳴り止む
- そのまま顔を洗うなど1日をはじめる
という、いたってシンプルなアプリです。
使用したフレームワークは以下です。
- Vision(姿勢検出)
- AlarmKit(タイマー / アラーム)
- AppIntents(アラーム解除後のアクション)
デモ
アラームのデモ
腕立て伏せ検知のデモ
まだ、途中ですが、アラームの起動と腕立て伏せのカウントまではできました。 悩み中なのは、アラームを解除してから腕立て伏せの画面を開く動線で、単純に画面遷移を実装しただけではうまくできませんでした。 また画面ロックしていると、解除して終了してまう…まあまだまだ理想の形には遠いです。
Visionとは
Vision は Apple が提供する画像解析フレームワークで、顔検出、テキスト検出、物体検出、人体姿勢検出などを、比較的少ないコードで扱えます。
注意点としては、Info.plistに以下を追加する必要があります。
<key>NSCameraUsageDescription</key>
<string>姿勢検出のためにカメラを使用します</string>
姿勢検出の基本的な流れ
姿勢検出の流れはだいたい次の通りです。
- カメラ映像を
AVCaptureSessionで取得 - 各フレームに対して
VNDetectHumanBodyPoseRequestを実行 (VNDetectAnimalBodyPoseRequest を使えば、動物骨格検知もできます) - 肩・肘・手首などの関節位置を取得
- 腕立ての判定
腕立ての判定はシンプルに考えました。
- 肩・肘・手首の3点から 肘の角度 を計算すればよさそう
- 腕を「伸ばした状態」「曲げた状態」をしきい値で判定すればよさそう
- up → down → up を 1 回としてカウントすればよさそう
という具合にポイントだけ考えて、詳しい計算式はChatGPTに投げました。
チャッピーは正しく実装してくれたみたいですが、僕の考えが間違っていて、見事に不正ができるものになってしまいました…(笑)
private func evaluatePushUpPose(
_ points: [VNHumanBodyPoseObservation.JointName: VNRecognizedPoint]
) {
// 必要な関節(肩・肘・手首)がすべて取れているかチェック
guard let leftShoulder = points[.leftShoulder],
let rightShoulder = points[.rightShoulder],
let leftElbow = points[.leftElbow],
let rightElbow = points[.rightElbow],
let leftWrist = points[.leftWrist],
let rightWrist = points[.rightWrist] else {
// どれか欠けていたら判定できないので終了
return
}
// 検出精度(confidence)が低い点はノイズになりやすいので除外
guard leftShoulder.confidence > 0.2,
rightShoulder.confidence > 0.2,
leftElbow.confidence > 0.2,
rightElbow.confidence > 0.2,
leftWrist.confidence > 0.2,
rightWrist.confidence > 0.2 else {
// 精度が低いフレームは判定しない
return
}
// 左腕の肘角度を計算(肩→肘→手首の3点から角度を出す)
let leftAngle = angleBetween(
CGPoint(x: leftShoulder.x, y: leftShoulder.y), // 左肩の座標
CGPoint(x: leftElbow.x, y: leftElbow.y), // 左肘の座標(角度の頂点)
CGPoint(x: leftWrist.x, y: leftWrist.y) // 左手首の座標
)
// 右腕の肘角度を計算(肩→肘→手首の3点から角度を出す)
let rightAngle = angleBetween(
CGPoint(x: rightShoulder.x, y: rightShoulder.y), // 右肩の座標
CGPoint(x: rightElbow.x, y: rightElbow.y), // 右肘の座標(角度の頂点)
CGPoint(x: rightWrist.x, y: rightWrist.y) // 右手首の座標
)
// 左右の肘角度の平均を取って、片腕のブレをならす
let avgAngle = (leftAngle + rightAngle) / 2
// 腕を曲げた判定(この角度より小さければ「下」扱い)
let downThreshold: CGFloat = 100
// 腕を伸ばした判定(この角度より大きければ「上」扱い)
let upThreshold: CGFloat = 140
// 腕立ての状態遷移(waitingForUp → up → down → up で1回カウント)
switch pushUpState {
case .waitingForUp:
// 最初は「腕が伸びた状態」になるまで待つ(準備フェーズ)
if avgAngle > upThreshold {
pushUpState = .up // 伸びたので「up」状態へ
}
case .up:
// 「up(伸び)」から「down(曲げ)」へ移行したら下降判定
if avgAngle < downThreshold {
pushUpState = .down // 曲がったので「down」状態へ
}
case .down:
// 「down(曲げ)」から「up(伸び)」へ戻ったら1回完了としてカウント
if avgAngle > upThreshold {
pushUpState = .up // 伸びたので「up」状態へ戻す
pushUpCount += 1 // 腕立て1回分を加算
}
}
}
というのも、今回は腕の角度のみで腕立て伏せを判定しているので、腕だけ動かせば判定されてしまいます。 実際に、腕立て伏せの伏せる必要がないことです。
とまあ課題は残りましたが、アラームの実装に取り掛かることにしました。
AlarmKitとは
iOS 26で登場したAlarmKit を使うと、タイマー、アラーム、フルスクリーンのアラームのUIを実装することができます。(お馴染みの画面が割と簡単に作れます)
デバイスの標準システムで動くのがメリットだと思います。
AlarmKitのアラーム機能でカスタマイズできること
今回はアラームに限って触ったので、アラームでどんなカスタマイズができるかを簡単に紹介します。

// アラーム解除時に実行するIntent
let stopIntent = OpenCameraIntent()
stopIntent.alarmID = alarmID.uuidString
let stopButton = AlarmButton(
text: "さあ、腕立てだ!",
textColor: .white,
systemImageName: "checkmark.circle"
)
let repeteButton = AlarmButton(
text: "リピート",
textColor: .white,
systemImageName: "repeat.circle"
)
let alertPresentation = AlarmPresentation.Alert(
title: "おはよう!🏋️",
stopButton: stopButton,
secondaryButton: repeteButton,
secondaryButtonBehavior: .countdown
)
let attributes = AlarmAttributes<SampleAlarmMetadata>(
presentation: AlarmPresentation(
alert: alertPresentation
),
tintColor: .blue
)
let configuration =
AlarmManager.AlarmConfiguration<SampleAlarmMetadata>.timer(
duration: timerMinutes * 10,
attributes: attributes,
stopIntent: stopIntent,
sound: .default
)
上記のような記述で、アラームの見た目や時間を制御できることがわかりました。 SoundのカスタマイズやDynamic Islandでの表示もいろいろ変更できるみたいです。
AppIntentsでアラーム解除後にカメラ画面を開く
AlarmKit のアラームでは、解除時に実行されるクロージャを設定できます。 最初はこのクロージャの中で腕立て伏せカウント画面へ遷移させようとしたけど、うまくいきませんでした。 そこで、WWDCの動画を見返したところ、AppIntentsを使用していたので参考にしました。
アラーム解除後にこのIntentを実行し、
struct OpenCameraIntent: AppIntent {
func perform() async throws -> some IntentResult {
AppState.shared.shouldShowCamera = true
return .result()
}
}
状態を変更して、navigationDestinationで遷移させます。
.navigationDestination(isPresented: $appState.shouldShowCamera) {
CameraView()
}
こうすことによって、アラーム画面を解除したのちに、腕立て伏せを判定する画面を開らけるようになりました。
Passと組み合わせられるかも?
去年のアドベントカレンダーで【iOS】WalletのPassを作成してみたを紹介をしました。 例えば、飛行機の搭乗が近づいているときに、1時間前にはアラームを鳴らすなど、組み合わせて活用できないかなあとおもいました。
まずば、年末年始ちゃんとの起きるためにアプリ完成させないとですね(笑)