![[iOS] CMPedometerで歩数取得](https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-b5492b1fe35dc4566d77121a77965902/1b8d77fb46d73fe754facff73d74c026/eyecatch-swift.png)
[iOS] CMPedometerで歩数取得
こんにちは。きんくまです。
CMPedometerで歩数取得してみました。
歩数取得は大きく分けて2つあります
- CoreMotionのCMPedometerによる取得
- HealthKitによる取得
今回はCMPedometerによる取得です。
HelathKitはリアルタイムで歩数取得ができないのですが、CMPedometerはリアルタイムによる歩数取得が可能です。
つくったもの
- 計測開始ボタンをタップすると歩数測定開始
- バックグラウンド状態でも歩数を計測できているか確認するため、歩数イベントが入ってきたときにローカルプッシュ通知を表示
- 何もしないとバックグラウンド状態では動かせなかった
- 追記。位置情報と組み合わせることでバックグラウンドでもローカルプッシュ通知を継続的に出すことができた
実装
リポジトリ
権限関連
Info.plistに権限を追加します
<key>NSMotionUsageDescription</key>
<string>歩数を読み取ります</string>
測定開始するときに権限ダイアログが表示されます
測定開始
測定開始は以下のメソッドで行います
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とつないでいたときのものです。
バックグラウンド状態でもローカルプッシュ通知を出し続ける
位置情報と組み合わせたらバックグラウンドでもローカル通知を定期的に出すことができました。
リポジトリ
バックグラウンド状態でも位置情報を定期に取得するのに同僚の加藤さんのブログを参考にしました
位置情報のバックグラウンドモードの設定
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を実装する