PushMesh
Sign in Create account
Open section navigation

Index

Platform migration 2026

iOS 27 makes UISceneDelegate mandatory and Google Play now requires targetSdk 36 — what changes in your app, what does not change at all in the SDK, and the step-by-step with code.

Platform migration 2026

Two external requirements reach your app in 2026, both with a set date and both decided outside PushMesh. This page tells you what changes, how to tell whether you are affected, and the migration step by step — including what does not change, which is the part that usually causes unfounded worry.

The good news first: neither change requires touching your PushMesh integration. The work belongs to your app, and this page is its map.


What changes and when

WhenWho requires itWhat
2026-08-31 — in effectGoogle PlayA new app or an update is only accepted in the store targeting targetSdk 36 (Android 16). Extension until 2026-11-01, requested through Play Console
September 2026AppleWith the final iOS 27 SDK, an app that has not adopted the scene-based life cycle (UISceneDelegate) does not launch

The last sentence deserves attention. An app that does not launch gets no APNs token, does not register and receives no push at all — and the gate is the SDK you build with, not the OS version on the user’s device. In other words: rebuilding your same old app with the iOS 27 Xcode, without migrating, is enough for it to not open. Devices that already have the app installed keep running it.


How to tell if you are affected

iOS — two questions

This is Apple’s own official test (technical note TN3187):

  1. Does the app’s Info.plist have the UIApplicationSceneManifest key with declared configurations?
  2. Does the AppDelegate implement application(_:configurationForConnecting:options:)?

If the answer to both is “no”, the app must migrate. Most UIKit apps written before 2025 are in this situation — including every app generated by expo prebuild today (see the Expo section).

Android — one question

grep -rn "targetSdk" android/app/build.gradle android/build.gradle

If 36 (or higher) does not show up, your next submission to the Play Store will not be accepted.


iOS 27 — the migration

Four steps. The Swift code below comes from our own lab app, already migrated: it compiled, launched in the simulator and registered with a real APNs token.

1. Create the SceneDelegate. The scene creates the window now, with UIWindow(windowScene:) instead of the UIWindow(frame:) the AppDelegate used:

import UIKit

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
             options: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else { return }
    let newWindow = UIWindow(windowScene: windowScene)
    newWindow.rootViewController = YourRootViewController()
    newWindow.makeKeyAndVisible()
    window = newWindow
  }
}

2. Wire the scene in the AppDelegate with application(_:configurationForConnecting:options:):

func application(_ app: UIApplication,
                 configurationForConnecting session: UISceneSession,
                 options: UIScene.ConnectionOptions) -> UISceneConfiguration {
  let config = UISceneConfiguration(name: "Default", sessionRole: session.role)
  config.delegateClass = SceneDelegate.self
  return config
}

3. Declare the scene manifest in Info.plist:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

If the app loads its interface from a storyboard, add the UISceneStoryboardFile key with its name in the same dictionary — the system builds the window for you, and step 4 does not apply to you.

4. Move to the scene what belonged to the window. Everything your didFinishLaunchingWithOptions did for the interface — creating the UIWindow, setting the rootViewController — moves into scene(_:willConnectTo:options:). The rest of the method, which is where the initializations live, stays where it is.

The full walkthrough, including multiple-scene and CarPlay scenarios, is Apple’s technical note TN3187.


What does NOT change

This is the reassuring part:

  • Push callbacks stay in the UIApplicationDelegate. Token registration (didRegisterForRemoteNotificationsWithDeviceToken) and the notification callbacks do not move with the migration — and that is exactly where our SDK hangs, through swizzling. Once the life cycle is migrated, PushMesh keeps working with no integration change at all.
  • None of our SDKs reads launchOptions. That is the code path the scene migration usually breaks in apps that handle push “by hand”. Our migrated lab app registered and confirmed delivery (recebidos = 1) with no change to the SDK.
  • Initialization is the same. Same methods, same parameters, same delivery promise with a receipt.

One single caveat is yours, not ours: if your app reads launchOptions[.remoteNotification] to react to the tap that opened it, that piece must migrate to the scene along with the window.


Android — targetSdk 36

In your app’s android/app/build.gradle:

android {
    defaultConfig {
        targetSdkVersion 36
    }
}

Our SDK’s Android module follows the host app’s targetSdk — there is nothing to change in it. What changes is the behavior Android 16 turns on once the target goes up.

What changes when targeting 36

The three Android 16 changes that most often hit a real app, in order of likelihood:

  1. Edge-to-edge with no way out. On Android 15 you could opt out with windowOptOutEdgeToEdgeEnforcement; targeting 36, the attribute is ignored and the app draws all the way to the edges. If your layout touches the status bar or the navigation bar, handle the insets.
  2. Free orientation on large screens. On displays with a smallest width of 600dp (tablets, foldables, desktop windowing), screenOrientation, resizableActivity and the aspect-ratio locks are ignored — an app locked to portrait will rotate. There is a temporary escape hatch per activity or for the whole app (PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY): use it to buy time, not as a destination.
  3. Predictive back on by default. onBackPressed is no longer called and KEYCODE_BACK is no longer dispatched. If the app intercepts “back”, migrate to the predictive back APIs or temporarily disable it with android:enableOnBackInvokedCallback="false" in the manifest.

The full reference is the official Android 16 behavior changes page.

What catches apps coming from older targets

Not new in 36, but it hits whoever has been skipping versions: since Android 14 (API 34), the full-screen notification (full-screen intent, incoming-call style) requires a user grant — the install-time default became denied, except for calling and alarm apps. If your app uses setFullScreenIntent, check NotificationManager.canUseFullScreenIntent() at runtime instead of assuming the permission.

What does not come from our SDK

Two requirements you can cross off your list on our account:

  • Notification trampoline — Android 12 banned notifications that open an activity through an intermediate broadcast. Taps on our notifications go straight through PendingIntent.getActivity (PushMeshMessageRouter.kt:329): correct by construction, nothing to do.
  • 16 KB memory pages — the requirement hits anyone shipping a native library (.so). Our Android module is pure Kotlin, with no native code of its own.

Expo — two known issues, neither of them ours

Full honesty first: today, on the latest published versions, an Expo app trips twice on the new Xcode. Both defects are Expo’s, not PushMesh’s — and both have a workaround:

  1. The prebuild template still generates the old life cycle. The generated app is born without a UISceneDelegate and does not launch when built with the iOS 27 SDK (expo/expo#46663, expo/expo#46664). Expo has signalled that prebuild will start generating the SceneDelegate on its own; until then, the iOS migration above is yours, applied to the generated project.

  2. expo-modules-jsi 57.0.5 and 57.0.6 do not compile with the recent Xcode 26 toolchain. Clang rejects SWIFT_RETURNS_RETAINED applied to a constructor in RuntimeScheduler.h (expo/expo#49214, expo/expo#49426). The workaround confirmed in the issue itself is pinning the exact versions in package.json:

    "overrides": {
      "expo-modules-core": "57.0.11",
      "expo-modules-jsi": "57.0.4"
    }

    (With Yarn, the same idea under the resolutions key.) One important nuance: the defect was introduced in 57.0.5, so “use Xcode 26.2” only helps if you are still on expo-modules-jsi 57.0.4 or earlier — with 57.0.5 or 57.0.6, the build breaks on 26.2 as well, and the pin is the way.


If a step does not work

Then this page needs fixing, and we want to know: contato@pushmesh.io. Tell us the step, your Xcode or Android Gradle Plugin version and the exact error — “I followed the guide and it did not launch” with those three details becomes a documentation fix, not trial and error.