[iOS] CMPedometerで歩数取得

[iOS] CMPedometerで歩数取得

2025.08.31

こんにちは。きんくまです。

CMPedometerで歩数取得してみました。

歩数取得は大きく分けて2つあります

  • CoreMotionのCMPedometerによる取得
  • HealthKitによる取得

今回はCMPedometerによる取得です。
HelathKitはリアルタイムで歩数取得ができないのですが、CMPedometerはリアルタイムによる歩数取得が可能です。

つくったもの

  • 計測開始ボタンをタップすると歩数測定開始
  • バックグラウンド状態でも歩数を計測できているか確認するため、歩数イベントが入ってきたときにローカルプッシュ通知を表示
    • 何もしないとバックグラウンド状態では動かせなかった
    • 追記。位置情報と組み合わせることでバックグラウンドでもローカルプッシュ通知を継続的に出すことができた

250831_pedometer2

実装

リポジトリ
https://github.com/cm-tsmaeda/WalkingStepSample/tree/only-pedometer

権限関連

Info.plistに権限を追加します

    <key>NSMotionUsageDescription</key>
    <string>歩数を読み取ります</string>

測定開始するときに権限ダイアログが表示されます

250831_pedometer1

測定開始

測定開始は以下のメソッドで行います

pedometer.startUpdates(from:withHandler:)

歩数が計測されると、特定のタイミング(数秒おき)にコールバックが実行されます

測定終了は以下のメソッドで行います

pedometer.stopUpdates()

以下は実装のコードです。ローカル通知は別クラスで定義しているので、GitHubリポジトリを参考にしてください

class ViewController: UIViewController {

    @IBOutlet private weak var logLabel: UILabel!
    @IBOutlet private weak var statusLabal: UILabel!
    private var pedometer: CMPedometer?

    override func viewDidLoad() {
        super.viewDidLoad()
        statusLabal.text = ""
        NotificationManager().requestPermission()
        // Do any additional setup after loading the view.
    }

    @IBAction func didTapStartButton() {
        print("start")
        statusLabal.text = "測定開始しました"
        if (pedometer == nil) {
            pedometer = CMPedometer()
        } else {
            print("dbg already started")
            return
        }

        if (CMPedometer.isStepCountingAvailable()) {
            print("dbg isStepCountingAvailable")
            pedometer?.startUpdates(from: Date()) { [weak self] (data, error) in
                print("dbg update!")
                guard let self else {
                    return
                }
                if (error != nil) {
                    print("dbg \(error!.localizedDescription)")
                    return
                }
                guard let data else {
                    print("dbg data is nil")
                    return
                }
                let steps = data.numberOfSteps
                let logText = "歩数: \(steps)"
                print("dbg steps \(steps)")
                NotificationManager().sendNotification(title: "歩数更新", body: logText)
                Task { @MainActor in
                    self.updateLogLabelText(text: logText)
                }
            }
        }
    }

    @IBAction func didTapStopButton() {
        print("stop")
        if (pedometer == nil) {
            return
        }

        pedometer?.stopUpdates()
        pedometer = nil
        statusLabal.text = "測定終了しました"
    }

    func updateLogLabelText(text: String?) {
        logLabel.text = text
    }
}

バックグラウンド状態については、何もしないとバックグラウンド状態では動かせなかったです。
Xcodeからデバッグで起動した時は、イベント発火時にローカル通知を表示していました。ただし、Xcodeと切り離すとバックグラウドでは動作しなくなりました。
-> 追記。位置情報と組み合わせたらバックグラウンドでもローカル通知を定期的に出すことができました。

下のスクショはXcodeとつないでいたときのものです。

250831_pedometer3

バックグラウンド状態でもローカルプッシュ通知を出し続ける

位置情報と組み合わせたらバックグラウンドでもローカル通知を定期的に出すことができました。

リポジトリ
https://github.com/cm-tsmaeda/WalkingStepSample

バックグラウンド状態でも位置情報を定期に取得するのに同僚の加藤さんのブログを参考にしました
https://dev.classmethod.jp/articles/ios-live-activity-with-location/

位置情報のバックグラウンドモードの設定

250831_pedometer4

LocationManagerの一部

        // バックグラウンドでも位置情報更新をONにする
        locationManger.allowsBackgroundLocationUpdates = true
        locationManger.pausesLocationUpdatesAutomatically = false

Info.plist

初期のInfo.plistからの追加分

	<key>UIBackgroundModes</key>
	<array>
		<string>location</string>
	</array>
    <key>NSMotionUsageDescription</key>
    <string>歩数を読み取ります</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>Reason why app needs location for iOS 11+</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Reason why app needs location when in use</string>

各ファイル

LocationManager

import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate {
    private var locationManger = CLLocationManager()

    private var _lastNumberOfSteps: NSNumber = 0

    static var shared = LocationManager()
    private override init() {

    }

    func setup() {
        locationManger.delegate = self
        // バックグラウンドでも位置情報更新をONにする
        locationManger.allowsBackgroundLocationUpdates = true
        locationManger.pausesLocationUpdatesAutomatically = false

        let status = locationManger.authorizationStatus
        switch status {
        case .authorizedAlways:
            print("authorizedAlways")
        case .authorizedWhenInUse:
            print("authorizedWhenInUse")
        case .denied:
            print("denied")
        case .restricted:
            print("restricted")
        case .notDetermined:
            requestAuthorize()
        default:
            break
        }
    }

    func requestAuthorize() {
        locationManger.requestWhenInUseAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        print("didChangeAuthorization status=\(status)")
        switch status {
        case .authorizedAlways:
            print("authorizedAlways")
        case .authorizedWhenInUse:
            print("authorizedWhenInUse")
            locationManger.requestAlwaysAuthorization()
        case .denied,
             .restricted,
             .notDetermined:
            break
        default:
            break
        }
    }

    func startUpdateLocation() {
        _lastNumberOfSteps = PedometerManager.shared.numberOfSteps
        locationManger.startUpdatingLocation()
    }

    func stopUpdateLocation() {
        locationManger.stopUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        print("didUpdateLocations locations=\(locations)")
        var log = ""
        locations.forEach { location in
            let longitude =  location.coordinate.longitude
            let latitude = location.coordinate.latitude
            log += "long \(longitude), lat \(latitude)"
        }
        let steps = PedometerManager.shared.numberOfSteps
        // 歩数が違うときだけプッシュ通知
        if steps != _lastNumberOfSteps {
            _lastNumberOfSteps = steps
            NotificationManager.shared.sendNotification(title: "歩数: \(steps), 位置情報", body: log)
        }
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("didFailWithError error=\(error.localizedDescription)")
    }
}

NotificationManager

import Foundation
import UserNotifications

// オリジナル
// https://note.com/u_chanmaru/n/n831992bd2732

final class NotificationManager {

    static var shared = NotificationManager()
    private init() {

    }

    private var lastSendDate: Date?

   // 権限リクエスト
   func requestPermission() {
       UNUserNotificationCenter.current()
           .requestAuthorization(options: [.alert, .sound, .badge]) { (granted, _) in
               print("Permission granted: \(granted)")
           }
   }

   // notificationの登録
    func sendNotification(title: String?, body: String?) {
        let now = Date()
        // 10秒以内にメソッドを呼ばれた場合はreturn
        if let lastSendDate = lastSendDate, abs(now.timeIntervalSince(lastSendDate)) < 10 {
            return
        }
        lastSendDate = now

       let content = UNMutableNotificationContent()
       content.title = title ?? "デフォルトタイトル"
       content.body = body ?? "デフォルト本文"

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
        let request = UNNotificationRequest(identifier: "com.pedometer.sample", content: content, trigger: trigger)

       UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
   }
}

PedometerManager

import UIKit
import CoreMotion

protocol PedometerManagerDelegate: AnyObject {
    func pedometerManager(_ manager: PedometerManager, didUpdateNumberOfSteps steps: NSNumber)
}

class PedometerManager {

    private var pedometer: CMPedometer?
    weak var delegate: PedometerManagerDelegate?
    var numberOfSteps: NSNumber = 0

    static var shared = PedometerManager()
    private init() {

    }

    func startUpdates() {
        if (pedometer == nil) {
            pedometer = CMPedometer()
        } else {
            print("dbg already started")
            return
        }

        if (CMPedometer.isStepCountingAvailable()) {
            print("dbg isStepCountingAvailable")
            numberOfSteps = 0

            pedometer?.startUpdates(from: Date()) { [weak self] (data, error) in
                print("dbg update!")
                guard let self else {
                    return
                }
                if (error != nil) {
                    print("dbg \(error!.localizedDescription)")
                    return
                }
                guard let data else {
                    print("dbg data is nil")
                    return
                }
                let steps = data.numberOfSteps
                numberOfSteps = steps
                self.delegate?.pedometerManager(self, didUpdateNumberOfSteps: steps)
            }
        }
    }

    func stopUpdates() {
        if (pedometer == nil) {
            return
        }

        pedometer?.stopUpdates()
        pedometer = nil
    }
}

ViewController

class ViewController: UIViewController {

    @IBOutlet private weak var logLabel: UILabel!
    @IBOutlet private weak var statusLabal: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        statusLabal.text = ""
        NotificationManager.shared.requestPermission()
        PedometerManager.shared.delegate = self
        LocationManager.shared.setup()
    }

    @IBAction func didTapStartButton() {
        print("start")
        statusLabal.text = "測定開始しました"
        PedometerManager.shared.startUpdates()
        LocationManager.shared.startUpdateLocation()
    }

    @IBAction func didTapStopButton() {
        print("stop")
        PedometerManager.shared.stopUpdates()
        LocationManager.shared.stopUpdateLocation()
        statusLabal.text = "測定終了しました"
    }

    func updateLogLabelText(text: String?) {
        logLabel.text = text
    }
}

extension ViewController: PedometerManagerDelegate {
    func pedometerManager(_ manager: PedometerManager, didUpdateNumberOfSteps steps: NSNumber) {
        let logText = "歩数: \(steps)"
        print("dbg steps \(steps)")
        //NotificationManager.shared.sendNotification(title: "歩数更新", body: logText)
        Task { @MainActor in
            self.updateLogLabelText(text: logText)
        }
    }
}

参考サイト

[iPhone] CMPedometer で万歩計を作る
[iOS] SwiftUIでLocal Notificationを実装する

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.