概要
- SwiftUIを使って、CoreMotionをバックグラウンドで起動させたかった。
- CoreMotion単体では、フォアグラウンド限定で、バックグラウンドでは動かせないと知る。
- 色々記事を見ていると、こことかにCoreLocationで位置情報を常に許可したら、バックグラウンドでも使えるということも知る。
- 参考になる記事はあれど、iOS8だったので、新しい情報に巡り会えなかったので、ここに記録する。
環境
- iOS: 16.1
- macOS: 13.0
- XCode: 14.1
ソースコード全文
-
位置情報の設定(忘れてたので追記)
-
TARGETS -> Signing & Capabilities -> + Capability -> Background Modes
"Location updates" をチェック -
Info.plist に以下を追加し、適宜値を記入
- Privacy - Location Always and When In Use Usage Description
- Privacy - Location When In Use Usage Description
-
-
以下は、CoreLocationとCoreMotionを組み合わせた部分。ここまで融合させなくてもいけるのでしょうか。
start()
のメソッドで位置情報とモーションの取得を開始させて、STOPで両方切ってます。.allowsBackgroundLocationUpdates
の設定とやらでバックグラウンドになっても常に位置情報を取っているので、一緒にモーションも取ってくれているという算段。LocationManager.swiftimport CoreLocation import CoreMotion import UserNotifications class LocationAndMotionManager: NSObject, ObservableObject, CLLocationManagerDelegate { // モーションセンサーの変数 @Published var isStarted = false @Published var xStr = "0.0" @Published var yStr = "0.0" @Published var zStr = "0.0" // 位置情報の変数 @Published var authorizationStatus: CLAuthorizationStatus // 位置情報許可の状態 @Published var lastSeenLocation: CLLocation? // 最新の位置情報 // モジュールインスタンス化 private let motionManager = CMMotionManager() // モーションセンサー private let locationManager = CLLocationManager() // 位置情報 // 位置情報のデリゲートなどの初期値設定 override init() { authorizationStatus = locationManager.authorizationStatus super.init() locationManager.delegate = self // 自身のクラスをデリゲートに設定 locationManager.desiredAccuracy = kCLLocationAccuracyBest // 最高精度の位置情報 locationManager.allowsBackgroundLocationUpdates = true // バックグラウンド実行中も座標取得する場合、trueにする locationManager.pausesLocationUpdatesAutomatically = false // 途中で位置情報のアプデを自動で止めるか } // モーションセンサーと位置情報の記録開始 func start() { if motionManager.isDeviceMotionAvailable { locationManager.startUpdatingLocation() // 位置情報取得開始 motionManager.deviceMotionUpdateInterval = 0.1 // モーションセンサーの記録を0.1秒ごとに motionManager.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler: {(motion:CMDeviceMotion?, error:Error?) in self.updateMotionData(deviceMotion: motion!) // モーションセンサーの記録開始 }) } isStarted = true } // モーションセンサーと位置情報の記録終了 func stop() { isStarted = false locationManager.stopUpdatingLocation() // 位置情報取得終了 motionManager.stopDeviceMotionUpdates() // モーションセンサー取得終了 } // モーションセンサーを取得する関数(とりあえず加速度) private func updateMotionData(deviceMotion:CMDeviceMotion) { let xDouble = deviceMotion.userAcceleration.x let yDouble = deviceMotion.userAcceleration.y let zDouble = deviceMotion.userAcceleration.z xStr = String(xDouble) yStr = String(yDouble) zStr = String(zDouble) print("xAccel: " + xStr) // バックグラウンドでも動き続けることを確認する } func requestPermission() { locationManager.requestAlwaysAuthorization() // バックグラウンド実行中も座標取得する } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { authorizationStatus = manager.authorizationStatus } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { lastSeenLocation = locations.first } }
-
以下は、SwifuUIのフロントエンド部分です。ちなみに
@EnvironmentObject
にこだわりはありません@StateObject
でも@ObservedObject
でも動くことを確認しました。ContentView.swiftimport SwiftUI import MapKit struct ContentView: View { @EnvironmentObject var locationViewModel: LocationAndMotionManager var coordinate: CLLocationCoordinate2D? { locationViewModel.lastSeenLocation?.coordinate } var body: some View { VStack{ // 位置情報の許可ステータスに合わせてSwitch switch locationViewModel.authorizationStatus { case .notDetermined: Button { locationViewModel.requestPermission() } label: { Text("位置情報の使用を許可する") } case .restricted: Text("位置情報の使用が制限されています。") case .denied: Text("位置情報を使用できません。") case .authorizedAlways, .authorizedWhenInUse: Text("位置情報許可OK!") VStack { Text("<現在地>") .fontWeight(.heavy) .padding() Text("経度:" + String(coordinate?.longitude ?? 0)) Text("緯度:" + String(coordinate?.latitude ?? 0)) } VStack { Text("<加速度>") .fontWeight(.heavy) .padding() Text("X:" + locationViewModel.xStr) Text("Y:" + locationViewModel.yStr) Text("Z:" + locationViewModel.zStr) Button { locationViewModel.isStarted ? locationViewModel.stop() : locationViewModel.start() } label: { locationViewModel.isStarted ? Text("STOP") : Text("START") } .frame(width: 100.0, height: 30.0) .background(.blue) .foregroundColor(.white) .padding() } default: Text("Unexpected status") } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .environmentObject(LocationAndMotionManager()) } }
-
@EnvironmentObject
にした場合、Appのところは.environmentObject(~~)
を忘れずに。MotionSensorBackgroundApp.swiftimport SwiftUI @main struct MotionSensorBackgroundApp: App { var body: some Scene { WindowGroup { ContentView() .environmentObject(LocationAndMotionManager()) } } }
結果画像
-
画面。これはエミュレータのスクショですが、モーションセンサとかやるので、デベロッパモードでiPhoneでやらないと動作確認できませんね。
-
アプリを起動して、STARTを押す。試しにホームに戻ってバックグラウンドにしてみても、動いているっぽい。放置してみたら、180分以上いけたのでSTOPしない限り動くでしょう。
参考URL
-
位置情報関係で参考になった記事
-
モーションセンサーで参考になった記事
-
CoreMotionとCoreLocationを組み合わせるといいことを書いてた記事