RORK LABEN
PLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっていますPLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっています
記事一覧/開発ツール
開発ツール/2026-05-28中級

Rork アプリで iOS の BGTaskScheduler.submit が Error Code=1 で失敗するときの切り分け手順

BGTaskScheduler.submit が Error Code=1 を返すとき、原因は 6 種類に収まります。Expo 構成に固有の identifier の落とし穴と、getPendingTaskRequests による自己確認まで、実際に踏んだ順番で整理しました。

Rork547iOS109BGTaskScheduler3バックグラウンド更新2Expo193React Native234

バックグラウンド更新を入れたつもりが、リリース後しばらく経って Crashlytics に BGTaskSchedulerErrorDomain Code=1 が静かに溜まっているのを見つける、というのは個人開発でいちばん起こりやすい事故のひとつだと感じています。クラッシュしないので落ちたことにも気づけません。発火していないと気づくのが半年遅れれば、そのぶん DAU が静かに削られていきます。

Code=1 は仕様上 BGTaskSchedulerErrorCodeUnavailable と定義されており、ドキュメントには「Background App Refresh が無効、もしくは現在の環境でタスクをスケジュールできない状態」と一行だけ書かれています。実際に詰まる原因は 6 種類くらいに分かれていて、上から潰していけば必ず原因にたどり着けます。本稿は Rork で生成したアプリに後から BGTaskScheduler を組み込んだ前提で、その 6 パターンを順番に整理します。

Code=1 が厄介なのは、エラーの文面が原因を一つも教えてくれないところです。裏を返せば、原因の母数さえ確定させてしまえば、あとは上から潰していくだけの作業になります。実際に踏んだ順番のまま並べます。

まず「どの状態で Code=1 になっているか」を特定する

Code=1 はひとつのエラーコードに 4〜5 種類の原因が押し込まれているため、最初に「いつ起きているか」を切り分けるだけで体感作業時間が半分になります。次のように submit の周囲をログで包んで、リリースビルドの実機・シミュレーター・TestFlight それぞれで挙動を確認します。

// AppDelegate.swift もしくは Expo モジュール内
import BackgroundTasks
import os
 
let log = Logger(subsystem: "net.rorklab.app", category: "bgtask")
 
func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "net.rorklab.app.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
 
    do {
        try BGTaskScheduler.shared.submit(request)
        log.info("bgtask submitted: id=\(request.identifier)")
    } catch let error as NSError {
        log.error("bgtask submit failed: domain=\(error.domain) code=\(error.code) info=\(error.userInfo)")
        // userInfo の NSLocalizedFailureReason をそのまま Crashlytics などに送る
    }
}

ここで userInfo を捨ててしまうと、後段の切り分けでもう一周ハマります。私は必ず userInfo ごと送って Crashlytics の Custom Key に乗せるようにしています。

Expo / Rork 構成では identifier が「自分のもの」ではない

先に、Swift で書いた場合には絶対に起きない Rork 固有の落とし穴を潰しておきます。Rork が生成するのは React Native(Expo)のアプリなので、Swift を直接触らずに expo-background-task 経由でバックグラウンド処理を組む方が多いはずです。

このとき内部で submit される identifier は、自分で決めた文字列ではなく Expo モジュールが持つ com.expo.modules.backgroundtask.processing です。Info.plistBGTaskSchedulerPermittedIdentifiers にこの文字列が入っていなければ、JS 側のタスク定義がどれだけ正しくても iOS は走らせません。

{
  "expo": {
    "ios": {
      "infoPlist": {
        "BGTaskSchedulerPermittedIdentifiers": [
          "com.expo.modules.backgroundtask.processing"
        ]
      }
    }
  }
}

CNG(Continuous Native Generation)で prebuild を通す構成なら、UIBackgroundModes とこの identifier は prebuild が自動で入れます。手を出さなければ動く、ということです。問題は、自分のネイティブ側タスクを足したくなって ios.infoPlist.BGTaskSchedulerPermittedIdentifiers を自分で書いたときに起きます。自前の identifier だけを列挙すると Expo の分が配列から抜け、JS 側のタスクだけが黙って止まります。

「Swift の submit は成功しているのに BackgroundTask が発火しない」という一見矛盾した状態は、たいていこれです。両方使うなら両方書きます。

"BGTaskSchedulerPermittedIdentifiers": [
  "com.expo.modules.backgroundtask.processing",
  "net.rorklab.app.refresh"
]

この要件が公式ドキュメントに書かれていない期間が長く、expo/expo の Issue #40440 で「ドキュメントに記載がない」と報告されています。公式を読んでも見つからないときは、先に Issue を検索したほうが早いことがあります。

もう一点、prebuild を繰り返すと config plugin が同じキーを重ねて書き込み、Info.plist の配列に同じ値が並ぶ挙動も報告されています。値そのものは残るので Code=1 の直接原因にはなりませんが、diff を読むときのノイズになります。配列が膨らんできたら prebuild --clean を一度通します。

原因 1:Info.plist の BGTaskSchedulerPermittedIdentifiers が未登録

いちばん多いのがこれです。submit する identifierInfo.plistBGTaskSchedulerPermittedIdentifiers 配列に列挙されていない場合、iOS は Code=1 を返します。Expo / Rork 系の構成では app.json (もしくは app.config.ts) の ios.infoPlist から注入する形になります。

{
  "expo": {
    "ios": {
      "infoPlist": {
        "BGTaskSchedulerPermittedIdentifiers": [
          "net.rorklab.app.refresh",
          "net.rorklab.app.processing"
        ],
        "UIBackgroundModes": ["fetch", "processing"]
      }
    }
  }
}

注意点としては、Bundle ID と prefix が一致している必要はないものの、実機での submit 時に Info.plist 配列に文字通り一致する文字列が存在しないと弾かれますprebuild 後に Xcode 上でも Info.plist を開いて値が反映されているか必ず確認します。EAS Build を挟む構成だと、prebuild をスキップして古い Info.plist のままビルドが回ってしまうことがあるので、eas build --clear-cache を 1 度走らせるのも有効です。

原因 2:Background App Refresh が OS 設定で OFF

実機で再現するけれどシミュレーターでは起きない、というときはほぼこれです。「設定 > 一般 > Background App Refresh」が OFF になっている、もしくは「省電力モード」が ON になっていると、ユーザーごとに submit が Code=1 で弾かれます。

これはアプリ側で復旧できる問題ではないので、UX 上は次の 2 点で対応します。

  • 初回起動時に UIApplication.shared.backgroundRefreshStatus を確認し、.denied または .restricted のときは「最新情報を閉じている間も取りに行くには Background App Refresh を ON にしてください」と一段だけ案内を出す
  • Crashlytics には backgroundRefreshStatus を Custom Key として常時送っておき、Code=1 の発生数と OFF ユーザー数の相関を後から確認できるようにする

これを入れただけで、私が運用しているアプリの 1 つでは「Code=1 のうち 7 割は OS 設定 OFF が原因」だったことが分かり、無駄な再現調査をしなくて済むようになりました。

原因 3:シミュレーターで実行している

地味に時間を奪うのがこれです。BGTaskScheduler はシミュレーターでも submit 自体は通る場合があるのですが、iOS 17 以降は条件によって Code=1 を返すようになっています。シミュレーターでバックグラウンド処理を検証するときは、submit の成功・失敗ではなく、デバッガから直接タスクを発火させる方を主軸にします。

LLDB で次のコマンドを叩くと、登録済みのタスクを強制的に発火できます。

(lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"net.rorklab.app.refresh"]

これで handler が即座に呼ばれるので、Code=1 自体は気にせず、handler 側のロジックの検証に集中できます。実機での submit 成功率の確認は TestFlight 配信後に Crashlytics の Code=1 件数で見るのが現実的です。

原因 4:UIBackgroundModes の指定漏れ

BGAppRefreshTaskRequestsubmit するなら UIBackgroundModesfetch が必要、BGProcessingTaskRequest を使うなら processing も必要、というのは公式ドキュメントどおりなのですが、Expo / Rork の構成では prebuild 時に上書きされて消えるケースがあります。

{
  "ios": {
    "infoPlist": {
      "UIBackgroundModes": ["fetch", "processing"]
    }
  }
}

prebuild 後に ios/{AppName}/Info.plistUIBackgroundModes を必ず目視確認します。fetch だけでは BGProcessingTaskRequest で Code=1 を返されるので、両方使うアプリは必ず 2 種類書きます。

原因 5:earliestBeginDate を過去 / 直近すぎる時刻にしている

earliestBeginDateDate() のような直近の時刻にすると、iOS のスケジューラの内部状態次第で Code=1 を返してくることがあります。最低でも 15 分先に設定するのが安全です。

request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)

過去日時を入れた場合は Code=1 ではなく Code=3 (TooManyPendingTaskRequests) 相当の挙動になることもあり、エラーコードだけ見て切り分けると遠回りします。earliestBeginDate を nil もしくは 15 分以上先に固定するのが安定挙動でした。

原因 6:register と submit の identifier 不一致

BGTaskScheduler.shared.register で登録した identifier と submit した identifier がわずかでも違うと Code=1 になります。Rork の生成コードを後から手動で編集すると、文字列がコピペでずれることがあるので、定数化して両者で参照する形に統一しておくと安全です。

enum BackgroundTaskID {
    static let appRefresh = "net.rorklab.app.refresh"
    static let processing = "net.rorklab.app.processing"
}
 
// AppDelegate.swift
BGTaskScheduler.shared.register(forTaskWithIdentifier: BackgroundTaskID.appRefresh, using: nil) { task in
    handleAppRefresh(task: task as! BGAppRefreshTask)
}

私の場合、6 本並行運用している壁紙アプリのうち 1 本で「app.refreshappRefresh の表記揺れ」を見落として 2 週間 Code=1 を出し続けたことがあり、それ以来必ず enum で固めるようにしています。

submit が「通ったつもり」になっていないか確かめる

submit が例外を投げなかったことと、iOS がその登録を保持していることは別の話です。登録が本当に残っているかは getPendingTaskRequests で読み出せます。

func dumpPendingTasks() {
    BGTaskScheduler.shared.getPendingTaskRequests { requests in
        guard !requests.isEmpty else {
            log.error("no pending bgtask requests — submit did not persist")
            return
        }
        for request in requests {
            log.info("pending: id=\(request.identifier) earliest=\(String(describing: request.earliestBeginDate))")
        }
    }
}

使い方は、アプリを一度バックグラウンドへ送ってから前面に戻し、この関数を呼ぶだけです。読み方はこう分かれます。

  • submit の直後から空 → identifier か Info.plist 側の問題(原因 1・4・6)
  • submit 直後は入っているのに復帰後に空 → 端末側の状態に寄っている(原因 2・原因 5)

ログを一往復増やすだけで、原因が「自分で直せる側」と「ユーザーの OS 設定側」のどちらに寄っているかが先に分かります。私はこの判定を入れてから、再現しない不具合を追いかける時間がはっきり減りました。

観測できた症状から原因へ戻る

上から順に潰す代わりに、手元で見えている症状から逆引きしたいこともあります。実際に踏んだものを対応表にすると次のようになります。

観測できる症状疑う原因最初に見る場所
シミュレーターでのみ Code=1、実機は成功原因 3LLDB で直接発火させ handler 側だけを検証
実機でのみ Code=1、しかも特定の端末だけ原因 2設定 > 一般 > Background App Refresh と省電力モード
全端末・全ビルドで必ず Code=1原因 1 / 原因 4prebuild 後の Info.plist の 2 つの配列
app refresh は動くが processing だけ Code=1原因 4UIBackgroundModesprocessing があるか
Swift 側は成功するが JS のタスクだけ発火しないExpo の identifier 欠落許可配列に com.expo.modules.backgroundtask.processing があるか
submit は成功するのに保留リストが空原因 5earliestBeginDate が直近すぎないか
ある日を境に急に Code=1 が増えた原因 1 / 原因 4直近の prebuild で Info.plist が上書きされていないか

最後の行が、個人開発でいちばん見落としやすいところです。コードを一行も触っていないのにエラーが増えたときは、まず prebuild の前後で Info.plist を diff します。原因がコードの外にあると分かるだけで、探す場所が変わります。

切り分けが終わったあとの確認順序

ここまでで原因が一巡したので、最後に「次に何をするか」を明確にしておきます。

  1. AppDelegate / Expo モジュールに submituserInfo 込みログを仕込む(数分)
  2. Info.plist の BGTaskSchedulerPermittedIdentifiersUIBackgroundModes を Xcode で目視確認する(5 分)
  3. 実機で「設定 > 一般 > Background App Refresh」を ON にして再現するか確認する
  4. register / submit の identifier を enum で固める(10 分)
  5. TestFlight 配信して 24 時間 Crashlytics で Code=1 の件数推移を見る

この 5 番目で、そもそも TestFlight まで届かないことがあります。私自身、Code=1 を追っていたつもりが App Store Connect の「処理中」で止まっていただけ、という回り道をしたことがありました。そちらで足踏みしているなら Rorkで書き出したアプリがApp Store Connectで「処理中」のまま止まる に切り分けをまとめています。

ここまでやれば、Code=1 の大半は原因が特定でき、残りは OS 設定や省電力モード由来でアプリ側ではどうしようもない領域として切り分けられます。発火したあとの処理そのものを本番品質で組む段になったら、Rorkアプリで「閉じてる間も最新」を本番品質で実装する に Silent Push と Android WorkManager まで含めた設計を書いています。BGTaskScheduler はエラーコードがシンプルすぎる代わりに、原因の母数が決まっているので、いったんチェックリスト化してしまえば再発しても 30 分以内に原因にたどり着けます。

この順序に落ち着いたのは、何度か遠回りをした末に「これ以上短くできない」と確信できたからです。同じ症状で止まっている方の半日が、これで戻ってきたら嬉しく思います。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-05-07
Rorkアプリで「閉じてる間も最新」を本番品質で実装する — iOS BGTaskScheduler × Silent Push × Android WorkManager
Silent Pushが届かない、BGTaskSchedulerが発火しない。Rorkで作ったアプリのバックグラウンド更新を、iOSとAndroidそれぞれの実行モデルに合わせて本番品質へ詰めていく過程と、成功率を計測してから直した記録です。
開発ツール2026-07-27
最低サポート iOS をいつ上げるか — 利用率ではなく、コードに残った分岐の数で決める
最低サポート OS の引き上げを利用率で判断すると、毎年同じ結論になって先送りが続きます。OS 分岐に注記を付けて棚卸しし、引き上げ先ごとに消せる分岐を数えるスクリプトと、引き上げ後に見る指標をまとめました。
開発ツール2026-06-14
アプリを開かなくても更新される、を Expo で実際に届ける — Background Task の現実的な設計
Rork が生成する Expo アプリで『毎朝コンテンツが更新される』体験を作ろうとすると、iOS のバックグラウンド実行は思ったほど律儀に動きません。expo-background-task の最小実装と、実行されない前提で破綻しない設計をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →