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
| When | Who requires it | What |
|---|---|---|
| 2026-08-31 — in effect | Google Play | A 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 2026 | Apple | With 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):
- Does the app’s
Info.plisthave theUIApplicationSceneManifestkey with declared configurations? - Does the
AppDelegateimplementapplication(_: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:
- 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. - Free orientation on large screens. On displays with a smallest width of
600dp (tablets, foldables, desktop windowing),
screenOrientation,resizableActivityand 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. - Predictive back on by default.
onBackPressedis no longer called andKEYCODE_BACKis no longer dispatched. If the app intercepts “back”, migrate to the predictive back APIs or temporarily disable it withandroid: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:
-
The
prebuildtemplate still generates the old life cycle. The generated app is born without aUISceneDelegateand does not launch when built with the iOS 27 SDK (expo/expo#46663, expo/expo#46664). Expo has signalled thatprebuildwill start generating theSceneDelegateon its own; until then, the iOS migration above is yours, applied to the generated project. -
expo-modules-jsi57.0.5 and 57.0.6 do not compile with the recent Xcode 26 toolchain. Clang rejectsSWIFT_RETURNS_RETAINEDapplied to a constructor inRuntimeScheduler.h(expo/expo#49214, expo/expo#49426). The workaround confirmed in the issue itself is pinning the exact versions inpackage.json:"overrides": { "expo-modules-core": "57.0.11", "expo-modules-jsi": "57.0.4" }(With Yarn, the same idea under the
resolutionskey.) One important nuance: the defect was introduced in 57.0.5, so “use Xcode 26.2” only helps if you are still onexpo-modules-jsi57.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.