Sanniv Khare 10 min read

iOS 26 SDK to iOS 27.1: what to change before April 2027

iOS 26 SDK to iOS 27.1: what to change before April 2027

The short version: from April 2027, App Store Connect only accepts iOS and iPadOS builds made with the iOS 27 SDK or later. The change that stops an app cold is in the iOS 27 release notes: apps built with the latest SDK must adopt the scene-based life cycle “or they fail to launch”. If your app still sets up its window in application(_:didFinishLaunchingWithOptions:), move that code to a UIWindowSceneDelegate first. On React Native, 0.88 is the first release whose template does this for you.

This guide covers Xcode 26 (iOS 26 SDK) to Xcode 27.1 (Swift 6.4, iOS 27.1 SDK, needs macOS Tahoe 26.6 or later). Checked on 8 Nov 2026 against Apple’s Xcode 27, 27.1 and 27.2 release notes, the iOS 27 release notes and Apple’s 9 Sep 2026 news post. We also diffed the native iOS sources of react-native 0.87.1 and 0.88.1, and of the @react-native-community/template at the same two versions, all pulled with npm pack.

Why we did this: every iOS app we maintain has to make this hop before April. That includes the Beacon-type banking apps on React Native, Peepal and Shopkeeper, and the iOS side of our TV work. We wanted one list we could work through for each project.

The porting path: Xcode 26, then 27.0, then 27.1

The order we recommend: Xcode 26, then 27.0 notes, then 27.1. The 27.1 notes add Mac Catalyst and simulator issues on top of 27.0.

The deadline, in Apple’s words

Apple’s news post of 9 September 2026 (App Store submissions now open for the latest OS releases) says: “Starting April 2027, apps and games uploaded to App Store Connect need to meet the following minimum requirements”. The first item is “iOS and iPadOS apps must be built with the iOS 27 & iPadOS 27 SDK or later”. The same rule applies to tvOS, visionOS and watchOS with their 27 SDKs. No day in April is given.

Two related rules are already in force, per Apple’s upcoming requirements page. Since 28 April 2026, uploads must be built with Xcode 26 or later. Since 9 September 2026, iOS and iPadOS uploads “must target iOS 13 or later”.

Porting steps

  1. Install Xcode 27.1 on a Mac running macOS Tahoe 26.6 or later. Xcode 27 “will only install and run on Apple silicon Macs” (Xcode 27 notes, 162138432). An Intel build Mac is now a hardware purchase, not a setting.
  2. Remove -ld_classic from every target and pod. “The ld64 linker has been removed and the -ld_classic option is no longer supported” (165165518). Search OTHER_LDFLAGS in your .xcodeproj, your .xcconfig files and your Podfile post_install hooks.
  3. Move to the scene life cycle if you have not already (next section).
  4. Check you have a launch screen. Apps built with the 27.0 SDK must include one of UILaunchStoryboardName, UILaunchStoryboards, UILaunchScreen or UILaunchScreens in Info.plist, or they are rejected once the App Store accepts 27.0 SDK builds (iOS 27 notes, 168247372).
  5. Switch status bar control to view controllers. Set UIViewControllerBasedStatusBarAppearance to YES (see the React Native section for why).
  6. Fix the Swift 6.4 compile errors (SwiftUI @State, init accessors), then the deprecation warnings.
  7. Recompile every binary dependency that uses MetricKit’s hitch metrics (see “What broke”).
  8. Build, then run on a device and the iPhone Duo simulator. We have not done this step yet; see the end of the post.

The scene life cycle is no longer optional

The iOS 27 release notes, UIKit, Deprecations (141837548): “Apps built with the latest SDK must adopt the scene-based life cycle or they fail to launch.”

A pre-scene app does its setup in the app delegate, like this:

// Before: window created in the app delegate (no UIApplicationSceneManifest)
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = RootViewController()
    window?.makeKeyAndVisible()
    return true
  }
}

After the change, the app delegate only handles process-level work. The window belongs to a scene delegate:

// After: AppDelegate keeps process-level work only
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    true
  }
}

// New file: SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

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

Info.plist also needs the scene manifest. This is the exact block the React Native 0.88.1 template adds:

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

Code that has to move with it:

  • URL handling moves from application(_:open:options:) to scene(_:openURLContexts:). Cold-start URLs arrive in connectionOptions.urlContexts.
  • Universal links move from application(_:continue:restorationHandler:) to scene(_:continue:).
  • Foreground and background hooks move to sceneDidBecomeActive, sceneWillResignActive and the matching scene methods. Banking apps that blur the screen in the app switcher need to check this one.
  • Anything that reads UIApplication.shared.windows or keyWindow should get the window from the scene.

What React Native 0.88 changed on iOS

We diffed react-native@0.87.1 against 0.88.1, and @react-native-community/template@0.87.1 against 0.88.1. All four were pulled with npm pack on 8 Nov 2026. In 0.87.1, six files under React/ and Libraries/ mention UIScene or UIWindowScene. In 0.88.1, eleven do. The new ones are RCTReactNativeFactory, RCTAppDelegate and RCTLinkingManager.

Where React Native starts: AppDelegate in 0.87.1, SceneDelegate in 0.88.1

The 0.88.1 template’s AppDelegate.swift shrinks to return true. The new SceneDelegate.swift creates the factory and calls a new entry point.

What the native diff shows:

  • RCTReactNativeFactory.h adds startReactNativeWithModuleName:inWindow:connectionOptions: (plus a variant that takes initialProperties). The header comment says it should be called “from scene:willConnectToSession:options: in your app-owned SceneDelegate“. It also notes that “only the first item in URLContexts and userActivities is used”.
  • RCTLinkingManager adds +scene:openURLContexts: and +scene:continueUserActivity:. If you forward deep links today, call these from your scene delegate instead of the app delegate methods.
  • RCTUtils.mm adds RCTIsSceneDelegateApp(), which returns YES when Info.plist has a UIApplicationSceneManifest with at least one UISceneConfigurations entry.
  • RCTAppDelegate.h gets a new deprecation message: “For UIScene apps implement your own SceneDelegate with RCTReactNativeFactory”. The header also says scene-based apps “must keep UIApplicationSupportsMultipleScenes set to false“. It adds that defining RN_ALLOW_MULTIPLE_SCENES “downgrade[s] the unsupported-configuration crash to a warning”. Leave multiple scenes off.
  • The template’s Info.plist flips UIViewControllerBasedStatusBarAppearance from false to true.

That last line matters because of RCTStatusBarManager.mm in 0.88.1:

// UIKit makes the app-level setters no-ops only for apps built with the iOS 27 SDK.
static BOOL RCTAppLevelStatusBarIsNoOp()
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270000
  if (@available(iOS 27.0, *)) {
    return YES;
  }
#endif
  return NO;
}

If your app keeps UIViewControllerBasedStatusBarAppearance = NO, StatusBar.setBarStyle and setHidden do nothing once you build with the iOS 27 SDK. React Native logs: “Set UIViewControllerBasedStatusBarAppearance to YES to control the status bar from JavaScript”. The default root view controller in 0.88.1 (RCTRootViewController) reads its style, hidden state and animation from a new RCTStatusBarAppearance class. If you use a custom root view controller, return those three values from it.

For a React Native app on 0.87 or older, the order that follows from this is:

  1. Upgrade to 0.88.1.
  2. Add SceneDelegate.swift from the template, with your module name and initialProperties.
  3. Strip AppDelegate down to process-level work.
  4. Add the scene manifest and flip UIViewControllerBasedStatusBarAppearance to YES.
  5. Move deep-link forwarding to the two RCTLinkingManager.scene methods.

The minimum iOS version React Native’s CocoaPods helper sets did not move: min_ios_version_supported returns '15.1' in 0.79.7, 0.87.1, 0.88.1 and 0.89.0-rc.0.

Each item below is from Apple’s notes, with the fix Apple gives.

Linker flag -ld_classic

React Native added -Wl -ld_classic to OTHER_LDFLAGS only when building with Xcode 15.0. 0.80.0 still had that code in scripts/cocoapods/utils.rb, and 0.81.0 removed it. A project that was ever built on Xcode 15.0 with an older React Native release may still carry the flag in its own build settings. Xcode 27 no longer supports it.

// Before (project.pbxproj / xcconfig)
OTHER_LDFLAGS = $(inherited) -Wl -ld_classic
// After
OTHER_LDFLAGS = $(inherited)

SwiftUI @State is now a macro

The iOS 27 release notes (105893279) say three things about this. Assigning in init to an @State property that already has an initial value no longer compiles. Composing @State with other wrappers or macros is unsupported. A private memberwise initialiser is no longer synthesised.

// Before: compiles with the iOS 26 SDK
struct AmountField: View {
  @State private var text = ""
  init(prefill: String) { _text = State(initialValue: prefill) }
  var body: some View { TextField("Amount", text: $text) }
}

// After: no initial value on the declaration
struct AmountField: View {
  @State private var text: String
  init(prefill: String) { _text = State(initialValue: prefill) }
  var body: some View { TextField("Amount", text: $text) }
}

Init accessors declared after the getter

Xcode 27 lists this as a Swift compiler known issue (180969028). A computed property with an init accessor and a literal initial value fails to compile if the getter is declared first (an SE-0508 source break). Apple’s workaround is to declare the init accessor first.

Unqualified stat()

Xcode 27 and the iOS 27 notes (177911316): unqualified stat() calls inside your own FilePath or FileDescriptor extensions may conflict with new API. Call Darwin.stat() instead.

MetricKit hitch metrics

ScrollHitchTimeMetric and scrollHitchTime(_:) were removed (180455992). HitchTimeMetric.ratio now uses HitchTimeRatio (180024784). Apple says to recompile to avoid a missing-symbol crash or a launch crash. This applies to prebuilt analytics SDKs in your Podfile too. Ask the vendor for an Xcode 27 build.

Smaller ones

  • PreviewProvider is deprecated (144168701). Move previews to #Preview.
  • FileDocument and ReferenceFileDocument are deprecated in favour of Document (178776840, 177458781).
  • canOpenURL: is deprecated (179874781).
  • toolbarMinimizeBehavior becomes toolbarMinimizationBehavior (177954148). In UIKit, UINavigationItem.navigationBarMinimization replaces barMinimizeBehavior (177953926).
  • Interface Builder uses a new toolchain compile mode for UIKit documents by default (114401122). Set IBC_COCOATOUCH_COMPILER_MODE = simulator if a storyboard misbehaves.
  • UI-test target crashes now default to “failure” severity (168107814). CI runs that used to pass with a crash in them will turn red.

Xcode 27.1-specific notes

From the Xcode 27.1 release notes:

  • Projects that use iOS 27.1-only APIs show compile errors for Mac Catalyst (185924957). Wrap them in #if !targetEnvironment(macCatalyst).
  • Projects targeting iOS 27.1 show no Mac Catalyst run destination (187046347). Apple’s workaround is a Mac Catalyst minimum deployment of 27.0.
  • In the iPhone Duo simulator runtime, StandBy and most app extensions are unavailable, and the first launch is slow (187708500, 187708663, 187708767).

Xcode 27.2 (29 Oct 2026) adds one known issue worth knowing if you build Mac or TV targets in the same workspace. Its macOS, watchOS, tvOS and visionOS SDKs “incorrectly report 27.1 as a valid deployment target” (187160501).

How we checked, and what we have not run

  • Docs: we read the Xcode 27, 27.1 and 27.2 release notes and the iOS 27 release notes (the Markdown versions of Apple’s pages), Apple’s news post of 9 Sep 2026 and the upcoming-requirements page, all on 8 Nov 2026. The iOS 27.1 release notes page did not load for us, so nothing here comes from it.
  • Native sources: npm pack of react-native 0.79.7, 0.80.0 to 0.83.0, 0.87.1, 0.88.1 and 0.89.0-rc.0, plus @react-native-community/template 0.87.1 and 0.88.1. We diffed Libraries/AppDelegate, Libraries/LinkingIOS, React/CoreModules/RCTStatusBarManager.mm and the template’s ios/ folder in a Linux container.
  • Not run yet: building any of our apps with Xcode 27.1, and checking launch, deep links and the status bar on a device or the iPhone Duo simulator. We will add those results here once the banking app’s scene migration is built and tested.

Need this built? Let’s talk.

We build and run apps for Mac, TV, mobile and the web, and write about what we learn.