iOS で background ( 端末画面が非表示のとき ) 処理を定期実行させることは鬼門のようです。Appleの情報、ネット記事、有識者の情報 どれも幾分違っていて真実が分かりません。iOSではバッテリ節約が最優先され、background 処理時間に制限が厳しいと言われているようです。 ’24年7月に実装した結果を報告します。
1) 条件
OS | iPadOS 17.6 |
端末 | iPad 6th |
Xcode | 実機TEST 15.1 、シュミレータTEST 14.2 |
言語 | Swift UI |
2) background 検証時の注意
基本 background は「アプリが前面に表示されていないが動作しているとき」ですが、完全なbackground状態にするには以下の配慮が必要なようです。
- iOS系は 画面スリープ無し に設定できますが、これは有効にしないこと。
- USBで電源供給したままにしない。
- Xcodeと接続しない。
またプログラムに、
- トレースログが取得できる仕組みを作成しておく。
3) 定周期でbackground処理
取り掛かりとしてApp の onChange(of:scenePhase) イベント で background と foreground の切替りを検出してました。そのbackground のifスコープにで タイマーをつっこみました。(以下赤字部) これが一番シンプルかなと…カンで組んでみました。
struct TestApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
// 中略
}
.onChange(of:scenePhase){ phase in
if phase == .background{
//------------- Background ---------------------
BackTimer = Timer.scheduledTimer(
withTimeInterval:intarvalSec, repeats: true){ _ in
// 各種業務処理
}
}
else if phase == .inactive{
//------------- パージ状態 ---------------------
}
else {
//------------- Forground ---------------------
}
}
}
一見動作しているように見えたのですが、途中でお亡くなりになるようです。background に移行してから動作可能な時間制限があるとのことですが、正確にどのくらいか色々試してもよく分かりませんでした。 UIApplication.shared.beginBackgroundTask ~endBackgroundTask を使って時間制限を検出しリカバリする方法も試しましたが、なんだか効果なしです。BGTaskScheduler というのもありますが有識者によると、「定周期background 処理は基本的ニ出来ない」とのことで試さずじまいです。
4) リージョン出入り検出でbackground処理
iOSで background 処理を定期的に行う手法として、GPSによる中心座標からの半径範囲(リージョン)への出入りを検出する方法が、いろいろと紹介されています。これは XamarinのiOS Background処理の説明ページにも記載されています。概念図は以下の通りです。
もちろんこれが適用されるのは、端末がある程度の時間経過とともに移動するシステムに限ります。その検出部のコードは以下のような感じです。
class TestLocation: NSObject, ObservableObject, CLLocationManagerDelegate {
// 中略
private var myLocMgr: CLLocationManager // iOS Location管理
private var myRegion = CLCircularRegion // カレントRegion
init() {
myLocMgr = CLLocationManager() // super.init()より必ず先
super.init( )
myLocMgr.delegate = self
myLocMgr.requestWhenInUseAuthorization()
myLocMgr.requestAlwaysAuthorization() // BG中も座標取得する場合、要call
myLocMgr.desiredAccuracy = kCLLocationAccuracyBest // 位置精度=最高
myLocMgr.allowsBackgroundLocationUpdates = true // BG中も座標取得する場合true
// バッテリ消費に対してtrueが推奨だが、BG中に止められる場合ありとのこと。
myLocMgr.pausesLocationUpdatesAutomatically = false
myRegion = CLCircularRegion(center: coordinate, // 中心座標
radius: 10 , // 半径
identifier: "test01" //リージョン名、何でもよい )
myRegion.notifyOnEntry = true // 脱出検知あり
myRegion.notifyOnExit = true // 入り検知あり
myLocMgr.startMonitoring(for: myRegion) // Regionを登録し検出開始。
}
// Region出た検出イベント
private func locationManager(_
manager: CLLocationManager,
didExitRegion region: CLRegion
) {
// background 定期処理
:
:
}
// Region出た検出イベント
func locationManager(_
manager: CLLocationManager,
didEnterRegion region: CLRegion
) {
// background 定期処理
:
:
}
ポイントとしては、
- 移動しないとBackground実行できません。
- 定周期でBackground実行する場合は、移動量と時間のCONVERTが必要です。
- 半径5mに指定しても、イベント発生させるには50~100mくらい移動を要します。
- UIApplicationDelegateAdaptor を Appクラスで絡めるコード例もありますが、シンプルに CLLocationManager だけで構築できます。
5) 単純移動検知でbackground処理
「リージョン出入り検出」は、秒、分単位の定周期処理とすると不向きのようです。通常、リアルタイム緯度経度の検出で使用しているCLLocationManager のイベントも、よく観察するとBackground時でも動作していることに気付きました。リアルタイム緯度経度検出 と Background処理を一緒に行えば、もっと早い周期でBackground処理が可能となります。その検出部のコードは以下のような感じです。
class TestLocation: NSObject, ObservableObject, CLLocationManagerDelegate {
// 中略
private var myLocMgr: CLLocationManager // iOS Location管理
public var NowLatitude = 0.0 // 現在緯度
public var NowLongitude = 0.0 // 現在経度
init() {
myLocMgr = CLLocationManager() // super.init()より必ず先
super.init( )
myLocMgr.delegate = self
myLocMgr.requestWhenInUseAuthorization()
myLocMgr.requestAlwaysAuthorization() // BG中も座標取得する場合、要call
myLocMgr.desiredAccuracy = kCLLocationAccuracyBest // 位置精度=最高
myLocMgr.allowsBackgroundLocationUpdates = true // BG中も座標取得する場合true
// バッテリ消費に対してtrueが推奨だが、BG中に止められる場合ありとのこと。
myLocMgr.pausesLocationUpdatesAutomatically = false
myLocMgr.distanceFilter = Double( 5.0 ) // 検出移動距離 単位m
myLocMgr.startUpdatingLocation() // 移動検出開始
}
// 移動検出イベント
func locationManager(_
manager: CLLocationManager, // iOS Location管理
didUpdateLocations locations: [CLLocation] // 移動した位置情報
) {
if locations.count > 0 { // Location情報あり
// 現在座標の更新
NowLatitude = locations[0].coordinate.latitude
NowLongitude = locations[0].coordinate.longitude
}
// background 定期処理
:
:
}
ポイントとしては、
- 移動しないとBackground実行できません。
- 定周期でBackground実行する場合は、移動量と時間のCONVERTが必要です。
- 移動距離をもっと小さくすると、秒周期でBackground内でイベント発生させることも可能。(iPadではGセンサも併用している模様)
- 日中での電池の持ち、iPadなら電池の持ちは心配ない感じ。iPhoneは厳しい感じ。
- 移動イベント中に、新しい移動イベントが発生した場合、新しい移動イベントは保留される。(保留最大数は未知)
- リージョン出入り検出と同居可能。
6) backgroundで動ける時間
Background内である程度の周期で動作できるようになったら、何秒くらい動作可能か情報が必要です。これも色々な説があるようです。以下のBackground処理ダミーコードで調べてみました。下記コード中の printLog はローカルtxtファイルへの出力付きの print 関数の自作ラッパーです。5秒置きにlogを残すことでどこまで動けるか確認しました。
// 移動検出イベント
func locationManager(_
manager: CLLocationManager, // iOS Location管理
didUpdateLocations locations: [CLLocation] // 移動した位置情報
) {
if locations.count > 0 { // Location情報あり
// 現在座標の更新
NowLatitude = locations[0].coordinate.latitude
NowLongitude = locations[0].coordinate.longitude
}
BgUserpProc() // background 定期処理
}
public func BgUserpProc( ) {
Task {
let sttime = Date()
var lastPassTime: Int = 0
while( true ) {
let passTimeDbl = sttime.timeIntervalSinceNow /*単位秒、負値になになるる*/
let passTimeInt = Int(passTimeDbl) * -1
if lastPassTime != passTimeInt && passTimeInt % 5 == 0 {
printLog( "<< BgUserpProc() pass time=" + passTimeInt.description + ">>")
}
lastPassTime = passTimeInt
if passTimeInt /*単位秒*/ > 300 /* 最大待ち秒 */ {
break
}
}
printLog( "<< BgUserpProc() out.>>")
}
}
結果としては、
- 最小で45秒までは動けてはいた。公称値が30秒とのウワサ。
- 最大120秒程度まで伸びる場合あり。CPU使用時間が影響しているのかもしれない。
- 許容時間超過後、TASKは休止状態となり、Foregroud移行後に動き出す。
- 製品コードでのBackground処理はScokect通信です。これはうまくいきました。