Skip to content

Instantly share code, notes, and snippets.

@Kyle-Ye
Created March 8, 2026 18:12
Show Gist options
  • Select an option

  • Save Kyle-Ye/636a3aeb345304532db4f9028d53592e to your computer and use it in GitHub Desktop.

Select an option

Save Kyle-Ye/636a3aeb345304532db4f9028d53592e to your computer and use it in GitHub Desktop.
Disabling CALayerSystem Behavior in SwiftUI on iOS 26

Disabling CALayerSystem Behavior in SwiftUI on iOS 26

Starting with iOS 26, SwiftUI's UIHostingView may use CALayer-based rendering (caLayerSystem) instead of the traditional UIKit-based rendering (uikitSystem) when certain conditions are met. This can cause unexpected changes in the view hierarchy — for example, _UIGraphicsView and CGDrawingView subviews may be replaced by plain CALayer and CGDrawingLayer sublayers.

Here are two approaches to temporarily disable this behavior in your local debug environment.

Option 1: Environment Variable

Set the environment variable SWIFTUI_DISABLE_MIXED_VIEW_HIERARCHY=1 in your scheme's Run configuration (Edit Scheme → Run → Arguments → Environment Variables).

This forces platform.mayInsertCALayers to return false for uikitSystem, preventing the switch to CALayer-based rendering.

Note: This is a debug/development-only workaround. Do not ship with this.

Option 2: MayNotInsertCALayersEffect Modifier

SwiftUI includes an SPI modifier mayNotInsertCALayersEffect() that injects the .mayNotInsertCALayers property into the display list, preventing CALayer insertion for the modified subtree.

Step 1: Patch the SDK SwiftInterface

The modifier is hidden behind @_spi(Private). To use it, add the following declarations to your SDK's swiftinterface file:

File: <Xcode.app>/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/SwiftUICore.framework/Modules/SwiftUICore.swiftmodule/arm64-apple-ios-simulator.swiftinterface

These declarations exist under @_spi(Private) in Apple's internal SwiftUI interface but are stripped from the public SDK. Add them to the swiftinterface file:

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
public struct MayNotInsertCALayersEffect : SwiftUICore.RendererEffect {
  @_Concurrency.MainActor @preconcurrency public init()
  @available(iOS 18.0, tvOS 18.0, watchOS 11.0, macOS 15.0, visionOS 2.0, *)
  public typealias AnimatableData = SwiftUICore.EmptyAnimatableData
  @available(iOS 18.0, tvOS 18.0, watchOS 11.0, macOS 15.0, visionOS 2.0, *)
  public typealias Body = Swift.Never
}

@available(*, unavailable)
extension SwiftUICore.MayNotInsertCALayersEffect : Swift.Sendable {
}

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
extension SwiftUICore.View {
  @_alwaysEmitIntoClient nonisolated public func mayNotInsertCALayersEffect() -> some SwiftUICore.View {
      return modifier(SwiftUICore.MayNotInsertCALayersEffect())
  }
}

Step 2: Apply the Modifier

struct ContentView: View {
    var body: some View {
        VStack {
            Color.red
            Text("123")
        }
        .mayNotInsertCALayersEffect()
    }
}

Result

Without mayNotInsertCALayersEffect() (iOS 26.2 Simulator — CALayer rendering):

<_UIHostingView: 0x101d12ed0; frame = (0 0; 390 844); ...>
   | <CALayer: 0x600000c13990> (layer)
   | <CGDrawingLayer: 0x600002c34080> (layer)

With mayNotInsertCALayersEffect() (UIKit rendering restored):

<_UIHostingView: 0x101c0d680; frame = (0 0; 390 844); ...>
   | <SwiftUI._UIGraphicsView: 0x101d25ea0; frame = (0 47; 390 736.333); ...>
   | <SwiftUI.CGDrawingView: 0x101d26560; frame = (181.333 789.667; 27.6667 20.3333); ...>

How It Works

In iOS 26, UIHostingView evaluates whether to use CALayer-based rendering via two conditions:

  1. platform.mayInsertCALayers — controlled by the SWIFTUI_DISABLE_MIXED_VIEW_HIERARCHY env var or the Semantics.forced version
  2. !state.pointee.properties.contains(.mayNotInsertCALayers) — controlled by MayNotInsertCALayersEffect

If either condition is false, the hosting view stays on the traditional UIKit rendering path.

Under the hood, MayNotInsertCALayersEffect.effectValue(size:) returns Effect.properties(.mayNotInsertCALayers):

_MayNotInsertCALayersEffect.effectValue(size:):
    mov        w9, #0x200
    str        x9, [x8]
    str        wzr, [x8, #0x8]
    mov        w9, #0x2
    strb       w9, [x8, #0xc]
    ret
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment