Skip to content

Instantly share code, notes, and snippets.

@drunknbass
Last active July 22, 2026 20:59
Show Gist options
  • Select an option

  • Save drunknbass/97c2974a8f6951cfb42a7b59813b0068 to your computer and use it in GitHub Desktop.

Select an option

Save drunknbass/97c2974a8f6951cfb42a7b59813b0068 to your computer and use it in GitHub Desktop.
Xcode 27 / iOS & iPadOS 27 migration guide — verified APIs, compiler changes, and SwiftUI best practices

Xcode 27 / iOS & iPadOS 27 Migration Guide

Research snapshot: July 22, 2026
Locally verified with: Xcode 27 beta build 27A5194q, Apple Swift 6.4 (swiftlang-6.4.0.20.104), iOS 27.0 SDK
Scope: the 12 unique X posts supplied for this research, the related Xcode 27 @State migration discussed immediately before them, a July 22 inventory pass across all 87 posts then listed in Blake Crosley's Apple Ecosystem series (published April 28 through June 15, 2026), and authenticated Grok searches for follow-up X discussion across the resulting topic clusters. The repeated X link 2076630236222124076 is counted once. X and the series were used as discovery material; migration claims below are promoted to Confirmed only when Apple material or a reproducible SDK/compiler result supports them.

This guide distinguishes three evidence levels:

  • Confirmed: Apple documentation, Apple WWDC material, the exported Apple-authored Xcode 27 coding skills, or a reproducible SDK/compiler result.
  • Beta discrepancy: Apple has documented the intended behavior, but the current beta SDK, diagnostics, or reference page does not fully agree yet.
  • Reverse-engineered / private: an observation about private framework implementation. It is useful for testing and understanding behavior, but is not an API contract and must not become a shipping dependency.
  • Secondary / not independently confirmed: useful analysis or a paraphrased lab finding for which no public Apple transcript or stable contract was found. Treat it as a hypothesis to measure, not a migration rule.

Executive migration checklist

  1. Build every target with Xcode 27 and treat new warnings as migration work, even if the deployment target remains older.
  2. For UIKit targets, verify scene-lifecycle adoption before anything else. An app built with the 27 SDK that remains on the app-only lifecycle does not launch.
  3. Verify every distributable iOS/iPadOS target has a launch-screen key; App Store Connect rejects 27-SDK uploads without one.
  4. Audit custom initializers for @State properties that also have declaration defaults. Remove the redundant declaration default when the initializer supplies the initial value.
  5. Audit class-valued @State for initializer side effects. Xcode 27 makes class construction lazy and once-per-view-lifetime.
  6. Ensure every bound TabView selection identifies a visible, available tab before changing tab visibility.
  7. Replace canOpenURL(_:) preflight logic with an attempted open and completion handling; reduce LSApplicationQueriesSchemes to no more than 25 entries for apps linked on or after iOS 27.
  8. Handle errors inside discarded throwing Task closures, or retain and observe the task result.
  9. Audit lazy stacks for unstable absolute-offset logic, dynamic leaf subview counts, layout-changing onAppear, and row state that must outlive scrolling.
  10. In UIKit/AppKit views, evaluate Observation tracking before retaining manual redraw plumbing; back-deployment requires the documented Info.plist opt-in.
  11. Add .swipeActionsContainer() around custom scrollable row layouts that use .swipeActions outside List.
  12. Test iPad custom chrome and sidebar content in inactive windows; use @Environment(\.appearsActive) only for custom elements the system cannot style automatically.
  13. If a UIKit tab architecture should prefer a sidebar on supported iPhone layouts, gate tabBarController.sidebar.preferredPlacement = .sidebar to iOS 27.
  14. Test continuous resizing and all orientation configurations on the current seed; Beta 4 still documents several iPad/iPhone Mirroring defects.
  15. Add a small XCUITest VoiceOver suite for critical reading order and spoken output using Xcode 27's XCUIVoiceOverService; keep per-screen accessibility audits as a separate layer.
  16. If the app uses ImageCreator, migrate before release: the iOS 27 SDK deprecates it with ImagePlaygroundViewController or imagePlaygroundSheet as the user-driven replacements.
  17. If the app uses On Demand Resources, plan a move from NSBundleResourceRequest to Background Assets.
  18. Evaluate the new SwiftUI reorder, document, toolbar-overflow, item-presentation, and AsyncImage(request:) APIs where they delete custom infrastructure; availability-gate them for older deployment targets.
  19. Apply the six Apple-authored SwiftUI performance rules covered below: stable environment defaults, unary rows, Equatable observable property types where practical, prepared ForEach data, narrow value inputs, and real subview boundaries.
  20. Remove any dependency on _UIScrollEdgeEffectViewInteraction, updatePocket(...), or other underscored scroll-pocket implementation details.

Source inventory and disposition

Supplied source Claim Disposition
Vistar: canOpenURL and scheme limit canOpenURL(_:) deprecated; query-scheme cap reduced to 25 Confirmed in Apple docs/release notes, with a beta SDK annotation discrepancy
Julien Sagot: scroll-pocket hook Private interaction continues receiving pocket geometry callbacks Reverse-engineered/private; not a supported migration API
Vincent: class default in @Entry Avoid an inline fresh class default in the environment Confirmed by the Apple-authored Xcode 27 SwiftUI Specialist skill
Vincent: top-level row if/else Avoid branch-dependent top-level row shapes Confirmed by the Apple-authored skill
Vincent: non-Equatable observable property Prefer Equatable property types in @Observable models Confirmed by the Apple-authored skill and Swift 6.4 macro expansion
Vincent: iPhone tab-to-sidebar placement UIKit can prefer a sidebar where only tab bar or sidebar can fit Confirmed beta API
Vincent: expensive work in ForEach Avoid scalable transforms in the ForEach data expression Confirmed by the Apple-authored skill
Vincent: unnecessary value-type view inputs Pass only the value fields a view uses Confirmed by the Apple-authored skill, with reference-type nuance
Vincent: computed properties used to split views Extract real View types to create invalidation boundaries Confirmed by the Apple-authored skill, with a small-fragment exception
Vincent: inactive iPad appearance iPad apps gain inactive-window appearance Confirmed by WWDC26; public reference-page text currently lags
Vincent: discarded throwing Task warning Swift warns when an unobserved task can discard thrown errors Confirmed by Swift 6.4 compiler behavior
Vincent: swipe actions outside List Swipe actions work in custom scrollable containers Confirmed beta API

Apple states that Xcode 27 includes a SwiftUI Specialist skill and a What's New in SwiftUI skill, exportable with xcrun agent skills export. The six best-practice findings in this guide were checked against a fresh export from Xcode 27, not inferred from the screenshots alone. See What's new in SwiftUI, WWDC26.

Grok/X follow-up search findings

The follow-up searched the topics rather than individual account timelines so that adjacent reports could surface. It produced a small number of useful leads; each was then checked against a primary source.

X discovery What survived verification
Arturo Rivas linking a SwiftPills @State explainer The article correctly demonstrates throwaway pre-27 class allocations and the new lazy behavior, but adds no contract beyond Apple's release notes and TN3211.
Daniel Saidi on the reorder sample's missing apply Novel and confirmed: ReorderDifference.apply(to:) is absent from the locally installed beta SDK. Apple's WWDC sample defines the helper in app code and imports Apple's open-source swift-collections; see the correction in section 12.
Mysk on Beta 4 canOpenURL Corroborates the supplied post; Apple's Beta 4 release notes now explicitly carry the deprecation and replacement guidance.
Leandro Rocha on mandatory scenes Confirmed by TN3187 and Apple's SDK-linking clarification.
Jacob Bartlett on Instruments 27 launch-time analysis A useful flame-graph workflow, not a source migration rule; it is included in the verification tooling discussion.
Julien Sagot on ScrollPocket v8 Further private implementation evidence only. It does not change the public-API conclusion in section 8.

Targeted searches for the reported @State premature-deinitialization case, variadic-generic metadata crash, @ContentBuilder regressions, and SwiftData observer APIs did not produce additional reproducible X evidence. They are therefore not promoted beyond Apple-documented behavior or explicit beta caveats.


1. @State is now a macro: lazy class construction and source compatibility

Status: Confirmed. Apple documents the change in SwiftUI updates, explains it in TN3211, and demonstrates it in What's new in SwiftUI.

What changed

When code is built with Xcode 27, @State has a same-named attached macro layer. For a class-valued declaration such as:

@MainActor
@Observable
final class StickerStore {}

struct StickerStoreView: View {
    @State private var store = StickerStore()

    var body: some View {
        StickerList(store: store)
    }
}

the initializer expression can be captured and evaluated lazily. Apple describes the user-visible result as one class construction for the lifetime of the view's state identity, instead of a throwaway allocation whenever Swift recreates the lightweight View value.

This behavior back-deploys to the releases where Observation was introduced, beginning with iOS 17 and macOS 14, when the app is built with Xcode 27. It is a compiler/framework change, not a reason to raise the deployment target by itself.

The local macro expansion for @State private var model = Model() generated storage shaped like:

private var __model = SwiftUICore.State._makeStorage {
    Model()
}

along with compatibility peers for _model and $model. LazyState is an implementation detail selected behind _makeStorage; do not spell or depend on it in app code.

The custom-initializer trap

This old pattern is now invalid:

struct StickerPageView: View {
    @State private var page = StickerPage()
    let title: String

    init(title: String) {
        page = StickerPage(title: title)
        self.title = title
    }
}

It can diagnose that self.title is used before initialization because assigning the macro-generated state property uses self.

Do not merely reorder the assignments:

// Wrong migration: compiles, but the declaration initializer still wins.
init(title: String) {
    self.title = title
    page = StickerPage(title: title)
}

In a local Xcode 27 runtime probe, the analogous reordered Int state returned the declaration value 0, not the initializer assignment 42.

Remove the declaration default when the initializer owns initialization:

struct StickerPageView: View {
    @State private var page: StickerPage
    let title: String

    init(title: String) {
        page = StickerPage(title: title)
        self.title = title
    }
}

Migration rules

  • Search for @State declarations with both = and assignment in init.
  • If init supplies the value, remove the declaration default and initialize exactly once.
  • Keep view-owned @Observable reference models in private @State.
  • Do not operate on a state-owned object from the surrounding View.init. Start lifecycle work from .task, an explicit user action, or another rendered-view lifecycle boundary.
  • Do not put analytics, registration, detached tasks, file access, or other externally visible side effects in a state initializer. Laziness can change when and how often those effects occur.
  • Remember that “once” means once for the retained SwiftUI state identity. Removing the view or changing an explicit .id(...) can create a new lifetime and a new object.

2. URL-scheme probing: canOpenURL(_:) deprecation and the 25-entry cap

Status: Confirmed intent and limit; beta SDK discrepancy. Sources: supplied X post, Apple canOpenURL(_:) documentation, and iOS & iPadOS 27 release notes.

Apple's current documentation says:

  • canOpenURL(_:) is deprecated in the 27 releases.
  • Attempt the open and handle failure instead of preflighting.
  • Universal Links remove the need for custom-scheme validation in many cases.
  • Apps linked on or after iOS 27 may list at most 25 schemes in LSApplicationQueriesSchemes.

Beta discrepancy

The iOS 27 beta SDK in Xcode build 27A5194q still declares Objective-C canOpenURL: as API_AVAILABLE(ios(3.0)), and a Swift source probe produced no deprecation warning. Treat this as beta annotation lag, not evidence that the deprecation was withdrawn: both the Apple reference page and the release notes declare the migration direction.

Migrate preflight code

Before:

if UIApplication.shared.canOpenURL(destination) {
    UIApplication.shared.open(destination)
} else {
    showUnavailableMessage = true
}

After:

UIApplication.shared.open(destination, options: [:]) { succeeded in
    if !succeeded {
        showUnavailableMessage = true
    }
}

In SwiftUI, prefer the openURL environment action where it fits the UI architecture:

@Environment(\.openURL) private var openURL

Button("Open destination") {
    openURL(destination) { accepted in
        if !accepted {
            showUnavailableMessage = true
        }
    }
}

Audit guidance

  • Count the final built app's LSApplicationQueriesSchemes, not only one source plist; build settings and generated plists can merge values.
  • Remove probes used only to customize UI based on whether another app is installed.
  • Prefer Universal Links, system share sheets, document pickers, and platform routing.
  • If a custom scheme is still required, attempt it and provide a fallback destination or a clear failure state.
  • Test apps linked with the iOS 27 SDK. The 25-entry rule is link-SDK dependent, not simply the device OS version.

3. Swift 6.4 warns about discarded throwing tasks

Status: Confirmed. Sources: supplied X post and the compiler's linked diagnostic, NoUseUnstructuredThrowingTask.

This code now warns by default:

Task {
    try await lander.fly(to: moon)
}

The Xcode 27 compiler emitted:

unstructured throwing task created by 'init(name:priority:operation:)'
is not used, which may accidentally ignore errors thrown inside the task

Preferred fixes

Handle the error inside fire-and-forget work:

Task {
    do {
        try await lander.fly(to: moon)
    } catch is CancellationError {
        // Expected if the owning lifecycle cancels the work.
    } catch {
        logger.error("Flight failed: \(error)")
    }
}

Or retain and observe structured results:

let flight = Task {
    try await lander.fly(to: moon)
}

try await flight.value

An explicit discard such as _ = Task { ... } silences the diagnostic, but use it only when deliberately ignoring both the task handle and failure. In SwiftUI, .task is often a better lifecycle boundary because SwiftUI cancels it when the view disappears; thrown non-cancellation errors still need an intentional policy.


4. Swipe actions in custom scrollable containers

Status: Confirmed beta API. Sources: supplied X post, swipeActionsContainer() documentation, SwiftUI updates, and WWDC26.

The row keeps the existing .swipeActions modifier. The custom container gains .swipeActionsContainer():

struct StickerList: View {
    @State private var stickers: [Sticker]

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(stickers) { sticker in
                    StickerRow(sticker: sticker)
                        .swipeActions {
                            Button("Delete", role: .destructive) {
                                stickers.removeAll { $0.id == sticker.id }
                            }
                        }
                }
            }
        }
        .swipeActionsContainer()
    }
}

The container coordinates mutual exclusion and dismissal: one row open at a time, close on scrolling, and close when tapping elsewhere. Without it, .swipeActions outside List has no effect. Adding it to List is a no-op because List already supplies the coordination.

Availability:

API iOS macOS watchOS visionOS tvOS
swipeActionsContainer() 27 27 27 27 unavailable
Existing row .swipeActions 15 12 8 1 unavailable
New onPresentationChanged overload 27 27 27 27 unavailable

Gate the container modifier when supporting older OS versions, or isolate the new layout in an @available view. Test full-swipe behavior, VoiceOver actions, right-to-left layout, nested horizontal gestures, row identity, and grid layouts.


5. iPadOS 27 inactive-window appearance

Status: Confirmed by Apple WWDC material; public reference text currently lags. Sources: supplied X post, WWDC26 inactive-appearance demonstration, and appearsActive.

iPadOS 27 automatically gives an app a distinct inactive appearance when another window becomes active. Standard SwiftUI controls, labels, selection, and materials generally adapt without app code.

Use appearsActive for custom content that does not inherit the correct system treatment:

struct SidebarFooterView: View {
    @Environment(\.appearsActive) private var appearsActive

    var body: some View {
        MyAccountView()
            .opacity(appearsActive ? 1 : 0.5)
    }
}

Do not manually dim the whole app. Prefer semantic colors and materials, then adjust only custom chrome that remains visually overemphasized.

Documentation discrepancy

The current appearsActive reference page still describes non-macOS platforms as always returning true, while Apple's WWDC26 transcript explicitly demonstrates and recommends it for inactive iPad windows. This appears to be stale beta documentation. Test behavior on the latest iPadOS 27 seed and re-check the page before final release.

Audit custom sidebars, selection indicators, destructive controls, Metal/canvas content, toolbars, and custom glass. Also keep scene lifecycle separate from appearance: .inactive scene phase and appearsActive == false are related concepts but are not interchangeable ownership or work-cancellation signals.


6. UIKit tab bar can prefer a sidebar on supported iPhone layouts

Status: Confirmed beta API. Sources: supplied X post, preferredPlacement, and Sidebar.Placement.

UIKit adds an iOS 27 UITabBarController.Sidebar.preferredPlacement property for contexts where the tab bar and sidebar are mutually exclusive. The default .automatic resolves to a tab bar on iOS. Apps can prefer .sidebar when the context supports it:

let controller = UITabBarController()
controller.tabs = makeTabs()
controller.mode = .tabSidebar

if #available(iOS 27, *) {
    controller.sidebar.preferredPlacement = .sidebar
}

Important boundaries:

  • This is UIKit API, available in iOS 27 and visionOS 27 and unavailable on tvOS.
  • .sidebar means “display the sidebar when supported,” not “force a sidebar at every width.”
  • The property does not create a useful navigation hierarchy by itself. Configure tabs/groups and a tab-sidebar mode that can be represented as either placement.
  • On platforms that can show multiple placements simultaneously, such as iPadOS, Apple says this preference has no effect.
  • Preserve destination selection independently of whether the system currently renders tabs or a sidebar.
  • Test size changes without destructively resetting navigation or user customization.

Do not infer a specific future device from the API. The supported conclusion is that UIKit navigation is becoming more adaptive to available presentation context.


7. Apple-authored SwiftUI performance and correctness guidance

These are not iOS 27 runtime APIs. They are current best practices shipped in Xcode 27's Apple-authored SwiftUI Specialist skill. Apple introduced the skill publicly in What's new in SwiftUI. They can improve apps that deploy to older OS releases as well.

7.1 Use stable environment defaults for fresh reference or changing values

Source: supplied X post.
Status: Confirmed.

@Entry wraps its default expression in a computed getter. Therefore a fresh class default is recreated on every fallback read:

// Avoid: a fresh instance on every fallback read.
extension EnvironmentValues {
    @Entry var theme = Theme()
}

Any environment update can cause readers to reread their keys. A different reference then looks like a changed environment value, invalidating readers even though the relevant dependency did not change. Different readers also receive different fallback objects.

Choose one stable shape:

// A. Keep @Entry, back it with one stable instance.
extension EnvironmentValues {
    @Entry var theme = _defaultTheme
    private static let _defaultTheme = Theme()
}
// B. Use a manual key with a stored default.
private struct ThemeKey: EnvironmentKey {
    static let defaultValue = Theme()
}

extension EnvironmentValues {
    var theme: Theme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}
// C. Model the absence honestly.
extension EnvironmentValues {
    @Entry var theme: Theme?
}

Use the optional form when a missing injection is meaningful. Do not “fix” the problem by giving the class a permissive Equatable implementation; that can mask invalidation while still constructing new objects and running initializer side effects.

Nuance: not every inline value default is unstable. Literals, nil, fixed enum cases, and structs whose fields resolve to the same values or stable references are fine. The deciding question is whether evaluating the expression again can produce a different result. Date(), UUID(), random values, and fresh nested references are unstable.

7.2 Keep lazy collection rows unary

Source: supplied X post.
Status: Confirmed.

A top-level if/else or switch can make a row's structural identity branch-dependent. A lazy container may then evaluate every row body just to establish the full identity set.

// Avoid: branch-dependent top-level shape.
List(items) { item in
    if item.isHighlighted {
        Label(item.title, systemImage: "star.fill")
    } else {
        Text(item.title)
    }
}

Wrap the branch in a real single-root container:

List(items) { item in
    VStack {
        if item.isHighlighted {
            Label(item.title, systemImage: "star.fill")
        } else {
            Text(item.title)
        }
    }
}

Group is a passthrough, not a layout container, and does not make the row unary. If the intent is to omit elements entirely, filter upstream rather than returning a zero-view row. Avoid AnyView for the same reason: it erases structural identity.

7.3 Prefer Equatable stored-property types in @Observable models

Source: supplied X post.
Status: Confirmed by the Xcode skill and Swift 6.4 macro expansion.

The current Observation macro generates overloads conceptually equivalent to:

func shouldNotifyObservers<T>(_ old: T, _ new: T) -> Bool { true }
func shouldNotifyObservers<T: Equatable>(_ old: T, _ new: T) -> Bool { old != new }
func shouldNotifyObservers<T: AnyObject>(_ old: T, _ new: T) -> Bool { old !== new }

For a non-Equatable value type, every assignment notifies observers. An Equatable type can suppress redundant sets:

struct Coordinate: Equatable {
    var latitude: Double
    var longitude: Double
}

@MainActor
@Observable
final class LocationModel {
    var coordinate = Coordinate(latitude: 0, longitude: 0)
}

This matters most for frequently assigned polling, streaming, timer, or synchronization values. It is a preference, not a command to force dubious equality onto every type. Collections become Equatable only when their elements do. Reference-typed properties already have an identity comparison path; make a reference type Equatable only when it has a sound domain equality definition.

7.4 Prepare scalable collection transforms outside ForEach

Source: supplied X post.
Status: Confirmed.

This repeats filtering and sorting on every body evaluation:

ForEach(
    contacts
        .filter { $0.name.localizedCaseInsensitiveContains(query) }
        .sorted { $0.name < $1.name }
) { contact in
    ContactRow(contact: contact)
}

Expose prepared results from the model and recompute when actual inputs change:

@MainActor
@Observable
final class ContactsModel {
    var contacts: [Contact] = [] { didSet { search() } }
    var query = "" { didSet { search() } }
    private(set) var results: [Contact] = []

    private func search() {
        results = contacts
            .filter { $0.name.localizedCaseInsensitiveContains(query) }
            .sorted { $0.name < $1.name }
    }
}

Cheap bounded transformations such as prefix(n) on a small collection are fine. The rule targets work that scales with collection size, allocates new elements, or performs I/O/formatting.

7.5 Narrow value-type inputs to what the view actually reads

Source: supplied X post.
Status: Confirmed, with reference-type nuance.

For a value-type input, the stored parameter list is the view's invalidation and comparison surface:

// Avoid when only the name is used.
struct NameTag: View {
    let account: Account
    var body: some View { Text(account.name) }
}
struct NameTag: View {
    let name: String
    var body: some View { Text(name) }
}

This reduces unrelated invalidations and avoids deep comparisons of large decoded value graphs. Forwarding a field to a child counts as using it.

Do not apply the rule mechanically to @Observable class references. SwiftUI compares a class by identity, while Observation tracks individual property reads during body. Passing one stable observable model can be cheaper and more precise than copying a large value payload through the view tree.

7.6 Extract real View types instead of computed sections

Source: supplied X post.
Status: Confirmed.

Computed some View properties improve readability but remain inside the parent's invalidation boundary:

struct ProfileView: View {
    var body: some View {
        VStack {
            header
            details
        }
    }

    private var header: some View { /* ... */ }
    private var details: some View { /* ... */ }
}

Use separate view types for meaningful sections, with narrow inputs:

struct ProfileView: View {
    var body: some View {
        VStack {
            ProfileHeader(name: name)
            ProfileDetails(isExpanded: isExpanded)
        }
    }
}

A separate View gives SwiftUI a distinct unit it can skip when inputs have not changed. Tiny static fragments reused a few times, styling expressions, and conveniences with no independent invalidation story can remain computed properties. The rule is aimed at section-level decomposition and performance boundaries, not eliminating every helper.


8. Scroll-pocket findings: useful reverse engineering, not an API

Status: Reverse-engineered/private beta behavior. Source: supplied X post.

The post reports that private _UIScrollEdgeEffectViewInteraction remains installed even when no pocket is visible and receives a private callback shaped like:

updatePocket(_:contentRect:velocity:isTracking:shouldAnimateVisibility:)

That observation may explain how the system coordinates content geometry, velocity, tracking, and visibility. It does not establish a contract. The leading underscore, absence from public SDK headers, and method-hooking approach all make it unsuitable for App Store code. The alleged internal “ScrollPocket v8” rewrite is likewise a beta implementation finding, not an announced platform API.

Supported public surface

UIKit publicly provides iOS 26-era scroll-edge APIs that remain available in the iOS 27 SDK:

  • UIScrollEdgeEffect through topEdgeEffect, bottomEdgeEffect, leftEdgeEffect, and rightEdgeEffect on UIScrollView.
  • Public effect styles such as automatic, soft, and hard.
  • UIScrollEdgeElementContainerInteraction for a container of controls or glass elements that overlays a scroll edge and should affect the effect's shape.

Example:

scrollView.topEdgeEffect.style = .soft

let interaction = UIScrollEdgeElementContainerInteraction()
interaction.scrollView = scrollView
interaction.edge = .bottom
buttonContainer.addInteraction(interaction)

Migration policy

  • Do not swizzle, hook, instantiate, cast to, or inspect underscored pocket classes in production.
  • Do not use private callback signatures as selectors or reflection keys.
  • If an iOS 26 visual recreation changed under iOS 27, rebuild it from public edge-effect APIs and observable scroll state, or accept the system's current rendering.
  • Keep screenshot-diff investigations and hierarchy inspection in test/debug tooling only.
  • Re-test each beta. Internal behavior can change without deprecation or release-note coverage.

9. Lazy stacks and scrolling: design for estimates and prefetching

Status: Confirmed by Apple. Secondary synthesis: SwiftUI Performance and Interop in iOS 27. Primary source: Dive into lazy stacks and scrolling with SwiftUI — WWDC26.

This is not an iOS 27-only API change. It is Apple's current explanation of behavior that can expose existing problems when an app is rebuilt, resized, or measured with newer tools.

9.1 Absolute lazy-stack geometry is estimated

A LazyVStack lays out only enough content to fill the visible rect. It estimates off-screen heights from the average of views already placed and the expected remaining count. Its enclosing scroll view therefore has an estimated content size and content offset that are corrected as more rows are measured. An orientation or size change can make those corrections especially visible.

Do not drive important UI from an exact absolute offset when the content is lazy:

// Fragile: the 100-point threshold can drift as estimates settle.
.onScrollGeometryChange(for: Bool.self) { geometry in
    geometry.contentOffset.y <= 100
} action: { _, isNearTop in
    showsJumpButton = isNearTop
}

Prefer a relative signal tied to actual visible targets:

ScrollView {
    LazyVStack {
        ForEach(steps) { step in
            StepRow(step: step)
                .id(step.id)
        }
    }
    .scrollTargetLayout()
}
.onScrollTargetVisibilityChange(
    idType: Step.ID.self,
    threshold: 0.8
) { visibleIDs in
    showsJumpButton = steps.first.map { visibleIDs.contains($0.id) } ?? false
}

Also audit scrollTransition: a transform that paints a row far outside its original layout frame can conflict with lazy culling. The lazy container decides visibility from the reported layout position, not every pixel the transformed view eventually paints.

9.2 One data element should resolve to one lazy child

The lazy container works with resolved subviews, not merely the number of View structs in source. A ForEach leaf that conditionally resolves to zero or one child forces the stack to retain earlier view values to preserve index meaning:

// Avoid filtering inside a repeated leaf.
ForEach(steps) { step in
    if step.detailLevel <= selectedDetailLevel {
        StepRow(step: step)
    }
}

Filter the data before construction, or express the filter in a SwiftData query predicate:

let visibleSteps = steps.filter {
    $0.detailLevel <= selectedDetailLevel
}

ForEach(visibleSteps) { step in
    StepRow(step: step)
}

This is related to, but more specific than, the unary-row rule in section 7.2. It improves memory release, off-screen invalidation behavior, and programmatic scroll-to-ID performance. Optional unwrapping that returns no row has the same structural cost; handle missing prerequisites above the lazy collection when possible.

9.3 Preserve prefetched work

Lazy stacks may evaluate a row body and perform layout during spare frame time before the row appears. If the user reverses direction, body can run without onAppear ever running. A row that changes most of its content or size from onAppear discards that work and forces new layout at the frame deadline.

Use these boundaries:

  • Put immutable, synchronous presentation setup in the row initializer or prepared row model.
  • Keep row construction cheap and side-effect free. “Set up in the initializer” does not authorize network access, analytics, or touching @State from a transient View.init.
  • Let an owned model/cache start idempotent, cancellable loading when that model is created, or use .task(id:) for lifecycle-bound asynchronous work.
  • Keep onAppear for genuinely appearance-bound signals such as requesting the next page at the end of an infinite list.
  • Do not use onGeometryChange to write a size into state and then change the same row's layout if a custom Layout can express the dependency in one pass.

Lazy rows and their @State can be released after scrolling away. State that must survive that must live in an outer model or data source:

// Transient UI state is fine here.
@State private var isPressed = false

// Durable state belongs in the model, keyed by step ID.
@Bindable var progress: ReadingProgress

Lazy-stack audit

Search for LazyVStack, LazyHStack, onScrollGeometryChange, onAppear, onGeometryChange, scrollTransition, scrollTo, and conditional or optional content directly inside repeated leaves. Re-test with variable-height rows, rotation/resizing, fast reverse scrolling, programmatic jumps near the end, and enough rows to force real lazy behavior.


10. Observation-driven UIKit/AppKit interop

Status: Confirmed by Apple and present in the SDK. Secondary synthesis: SwiftUI Performance and Interop in iOS 27. Primary source: Use SwiftUI with AppKit and UIKit — WWDC26 and Apple's UIKit observation-tracking documentation.

UIKit and AppKit can automatically track properties read from @Observable models in supported drawing, layout, constraint, and controller update methods. When an accessed property changes, the framework schedules the relevant method again. This can replace manual needsDisplay/setNeedsLayout fan-out when one model property affects multiple views.

import Observation
import UIKit

@Observable
@MainActor
final class ColorModel {
    var hue = 0.6
    var saturation = 1.0
    var brightness = 1.0
}

final class ColorSwatchView: UIView {
    var model: ColorModel

    init(model: ColorModel) {
        self.model = model
        super.init(frame: .zero)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }

    override func draw(_ rect: CGRect) {
        UIColor(
            hue: model.hue,
            saturation: model.saturation,
            brightness: model.brightness,
            alpha: 1
        ).setFill()
        UIBezierPath(rect: rect).fill()
    }
}

Apple says UIKit's coverage extends beyond UIView/UIViewController to types such as UIButton and UICollectionViewCell. The behavior is enabled by default in the 2026 OS releases and later. To back-deploy the integration, add the Boolean UIObservationTrackingEnabled key for iOS 18+; AppKit uses NSObservationTrackingEnabled for macOS 15+.

Migration guidance:

  • Add @Observable to the shared model and read only the properties the drawing or layout method truly depends on.
  • Enable the back-deployment key only after testing the oldest supported runtime.
  • Remove manual invalidation calls incrementally. Validate that every affected override is observation-tracked before deleting a fallback.
  • Keep the model @MainActor when it directly drives UI state.
  • Use one shared model at the seam. Do not duplicate UIKit state into a second SwiftUI-only model.

Incremental SwiftUI adoption does not require a rewrite:

  • Embed SwiftUI in UIKit with UIHostingController or UIHostingConfiguration.
  • Embed UIKit in SwiftUI with UIViewRepresentable/UIViewControllerRepresentable.
  • Reuse an existing recognizer through UIGestureRecognizerRepresentable (available from iOS 18) and attach it with .gesture(...).
  • On macOS, the corresponding APIs include NSHostingView, NSViewRepresentable, NSGestureRecognizerRepresentable, NSHostingMenu, and NSHostingSceneRepresentation.

The installed iOS 27 SDK confirms UIGestureRecognizerRepresentable, UIHostingController, and UIHostingConfiguration; the macOS-specific menu and scene wrappers must not be presented as iOS APIs.


11. UIKit scene lifecycle and resizable-layout mandate

Status: Confirmed breaking requirement. Secondary source: UIKit's Scene Mandate. Primary sources: Modernize your UIKit app — WWDC26 and Transitioning to the UIKit scene-based life cycle.

Apple states that a UIKit app built with the latest 27 SDKs must use UIScene lifecycle or it will not launch. Older binaries are not retroactively broken; the requirement is triggered by rebuilding with the 27 SDK. Multiple-window support is still optional.

Detect an affected target

Check all three surfaces:

  1. The built Info.plist contains UIApplicationSceneManifest with a usable UISceneConfigurations entry, or the app uses a dynamic scene configuration.
  2. The app delegate implements application(_:configurationForConnecting:options:) when configuration is dynamic.
  3. A scene delegate conforms to UIWindowSceneDelegate, and UI lifecycle/window ownership has actually moved there.

Treat a manifest with no configuration, a configuration method with no delegate, or a scene delegate that leaves all UI lifecycle logic in AppDelegate as a partial migration requiring review.

For a programmatic single-window app, the essential shape is:

final 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 }

        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = RootViewController()
        window.makeKeyAndVisible()
        self.window = window
    }
}

UIApplicationDelegate remains the home for process-wide work such as shared service setup and push registration. Move UI lifecycle events to their scene equivalents (sceneDidBecomeActive, sceneWillResignActive, sceneDidEnterBackground, and sceneWillEnterForeground) as one coherent set. Use UIWindow(windowScene:), not UIWindow(frame: UIScreen.main.bounds).

Scene adoption is only the first adaptivity pass

iOS 27 makes iPhone Mirroring windows freely resizable, and iPhone-only apps on iPad can be fully resizable while still reporting the phone idiom. Apple therefore says:

  • Remove UIScreen.main from window-relative layout. Use the closest view/controller's bounds or trait collection; use window.windowScene?.screen only when the physical screen itself is truly required.
  • Replace UIScreen.main.scale in view/controller code with traitCollection.displayScale. If the result is cached outside an automatically re-run layout/draw method, observe UITraitDisplayScale and refresh the cached value.
  • Do not use userInterfaceIdiom for layout. Prefer size classes or the surrounding view's dimensions.
  • Do not use interface orientation for layout. In resizable environments it is a preference, and iPhone Mirroring reports portrait regardless of aspect ratio. Use size classes or bounds.
  • Test continuously resized widths, not only named devices and portrait/landscape snapshots.

Launch screen is now an App Store submission gate

Status: Confirmed App Store requirement. Apple documents this in TN3208 and the iOS/iPadOS 27 release notes.

An iOS or iPadOS app uploaded with the 27 SDK must have at least one of these keys in its built Info.plist:

UILaunchStoryboardName
UILaunchStoryboards
UILaunchScreen
UILaunchScreens

Without one, App Store Connect rejects the upload. Newer Xcode templates normally generate UILaunchScreen when automatic Info.plist generation and the Launch Screen (Generation) setting are enabled, but existing targets, extensions packaged as apps, and hand-maintained Info.plists need an explicit audit. Verify the archived product's Info.plist, then delete the installed app before visually retesting the launch screen because the system caches launch snapshots.

Beta 4 resizing and orientation caveats

Status: Beta known issues, not intended final contracts. The current iOS & iPadOS 27 Beta 4 release notes list several build-linked defects:

  • An iPad app that does not list all four interface orientations can be treated as non-continuously resizable. Apple's current workaround is to include all four in UISupportedInterfaceOrientations.
  • iPhone Mirroring can initially ignore the target's supported orientations, and UIRequiresFullScreen scenes can receive the wrong continuous-resize behavior.
  • An iPhone-only app on iPad that declares only portrait or only landscape can lay out incorrectly when the iPad is in an unsupported orientation.

Do not architect around these defects. Keep an explicit beta test matrix, recheck each item on every seed, and remove workarounds once Apple marks the issue resolved. UIRequiresFullScreen is itself deprecated; TN3192 is the durable migration path.

Two additional 27 behaviors deserve targeted UIKit testing:

  • Siri and Apple Intelligence may start a UIDragInteraction session without a user drag gesture. Keep dragInteraction(_:sessionWillBegin:) free of modal presentation and drag-start animation; perform visible feedback after the session moves.
  • Audit custom presentation controllers and code that assumes traits jump directly from a presenting view controller. In 27-linked apps, presentation intermediates can participate in trait propagation; read traits from the view or controller whose layout is being computed.

Search UIKit targets for UIApplicationSceneManifest, UIWindowSceneDelegate, var window, UIScreen.main, userInterfaceIdiom, interfaceOrientation, UIDevice.current.orientation, supportedInterfaceOrientations, and UIWindow(frame:).


12. Additional confirmed SwiftUI 27 APIs worth migration review

Status: Confirmed in current Apple material; the locally inspected earlier beta SDK confirms the original subset, while explicitly noted Beta 4 additions are newer. Secondary overview: What's New in SwiftUI for iOS 27. Primary overview: What's new in SwiftUI — WWDC26.

These are adoption opportunities, not mandatory rewrites. Availability-gate them when the deployment target is below 27.

Area New surface Migration value
Reordering .reorderable() plus .reorderContainer(for:move:) and ReorderDifference Replaces hand-built drag gestures and index arithmetic in List, lazy stacks/grids, stacks, and custom layouts.
Documents Document, ReadableDocument, WritableDocument, DocumentReader, DocumentWriter, URLDocumentConfiguration Separates reading and writing, uses observable reference documents, moves I/O off the main actor, and supports coordinated URL access. Beta 4 deprecates FileDocument and ReferenceFileDocument; prefer the new model for new code and plan deliberate migrations for existing document apps.
Toolbar overflow ToolbarOverflowMenu, .topBarPinnedTrailing, .visibilityPriority(_:), .toolbarMinimizationBehavior Expresses what is essential, what may overflow, and how the bar minimizes as widths change. The final spelling changed during the beta cycle.
Item presentation alert(_:item:actions:) and confirmationDialog(_:item:actions:) One optional value controls presentation and supplies the action payload; no parallel Boolean or wrapper type.
Errors alert(error:actions:message:) Binds directly to Error?; LocalizedError supplies user-facing descriptions.
Images AsyncImage(request:) and .asyncImageURLSession(_:) Adds headers, timeout/cache-policy control, and a custom session. On 27 runtimes, ordinary AsyncImage(url:) also gains standard HTTP caching according to response headers.

Reordering example

ForEach(stickers) { sticker in
    StickerView(sticker: sticker)
}
.reorderable()
.reorderContainer(for: Sticker.self) { difference in
    // Apply difference.sources and difference.destination to the model.
}

The callback reports IDs and a destination (before(id) or end); the model remains the source of truth. For multiple sections, use the collection-ID overload and route by difference.destination.collectionID. The installed SDK marks reorder containers available on iOS, macOS, watchOS, and visionOS 27, and unavailable on tvOS.

Important: apply(to:) is sample code, not a SwiftUI API in the inspected SDK

The WWDC demo may look as though this is sufficient:

.reorderContainer(for: Sticker.self) { difference in
    difference.apply(to: &stickers)
}

Grok surfaced an X report that Xcode could not find apply. Inspection of Xcode 27 beta build 27A5194q confirms that ReorderDifference exposes only sources and destination; it has no apply(to:) member. Apple also says in the WWDC session that its helper uses the open-source Swift Collections package, and the session's code listing imports OrderedCollections and defines the extension in sample code.

Choose one explicit migration path:

  1. Apply sources and destination in your own collection model.
  2. Copy the small algorithm into app-owned code with tests for multi-item moves and cross-section moves.
  3. If the project already benefits from it, add Apple's official apple/swift-collections package at an intentionally pinned or bounded version and keep the helper visibly app-defined.

Do not write production code assuming a public difference.apply(to:) exists until the SDK interface actually declares it. Apple's current reorderContainer reference text appears ahead of or inconsistent with this beta SDK.

Item-bound alert example

@State private var itemToDelete: Item?

ContentView()
    .alert("Delete item?", item: $itemToDelete) { item in
        Button("Delete", role: .destructive) {
            delete(item)
        }
        Button("Cancel", role: .cancel) {}
    } message: { item in
        Text(item.name)
    }

For older OS support, place the new modifier in an if #available(iOS 27, *) branch and keep the existing isPresented/presenting fallback.

Document-model caution

In the new document model, snapshot(contentType:) and apply(snapshot:previous:) run on the main actor and must remain light. Serialization/deserialization belongs in the reader/writer. Autosave relies on registered undo actions; moving to the new protocols without an undo strategy can create a document that edits correctly but does not autosave as expected.

Beta 4 adds the umbrella Document protocol for common read/write documents and deprecates FileDocument and ReferenceFileDocument. This is newer than the locally inspected beta build: that build contains ReadableDocument and WritableDocument, but not Document or deprecation annotations on the old protocols. Treat the current release notes as the intended direction and verify the exact declarations in the Xcode seed used by CI before beginning a broad conversion.

Build-linked SwiftUI behavior to audit

These are not optional visual refinements; compiling against the 27 SDK changes assumptions in existing code:

  • TabView selection must remain visible. A binding that points to a hidden or otherwise unavailable tab may crash. Before hiding/removing a selected tab, move selection to a visible fallback in the same state transition.
  • Selectable Text has system interaction gestures. A Text with .textSelection(.enabled) now uses the system selection UI. If an existing custom gesture must win, attach it with .highPriorityGesture(...) and retest selection, accessibility, and scroll gesture arbitration.
  • Menu images are reduced by default. On iPadOS 27, most symbol images in menus are hidden unless semantics justify forcing .labelStyle(.titleAndIcon); review the current Human Interface Guidelines rather than restoring every icon mechanically.
  • Text-field border styles changed. .squareBorder and .roundedBorder are soft-deprecated. Prefer .bordered and use textInputBorderShape(_:) when a nondefault shape matters.
  • Toolbar naming moved during beta. Current release notes use toolbarMinimizationBehavior, replacing the earlier toolbarMinimizeBehavior. Expect source churn across seeds and verify against the SDK used by CI.

Swift Charts back-deployment regression

Status: Beta 3 known issue; reported fixed in Beta 4 on aligned platforms, so keep the regression test. A target whose minimum deployment version is below 27 could warn that conditional _ConditionalContent conformance to ChartContent is only available in 27 and then crash when loading that content. Apple's workaround is to extract the conditional marks into a function or property explicitly annotated with @ChartContentBuilder:

Chart(points) { point in
    marks(for: point)
}

@ChartContentBuilder
private func marks(for point: Point) -> some ChartContent {
    if showsRate {
        LineMark(x: .value("Time", point.date),
                 y: .value("Rate", point.rate))
    } else {
        LineMark(x: .value("Time", point.date),
                 y: .value("Signal", point.signal))
    }
}

Even after the seed containing the fix, run this path on the oldest supported OS. It crosses the compiler, Swift Charts result builder, and back-deployed runtime.


13. VoiceOver-driven accessibility E2E tests

Status: Confirmed new Xcode 27 API; beta runtime behavior still needs seed-by-seed validation. Primary sources: XCUIVoiceOverService, XCUIDevice.voiceOverService, and Apple's Accessibility Technologies Group Lab.

Xcode 27 adds programmatic VoiceOver control to XCUIAutomation. This closes an important gap: an end-to-end test can now validate the order in which VoiceOver visits elements and inspect what it actually speaks, rather than only asserting raw accessibility properties.

The entry point is:

let voiceOver = XCUIDevice.shared.voiceOverService

The locally inspected Xcode 27 beta headers and a compiler type-check confirm this Swift surface:

@MainActor
final class XCUIVoiceOverService {
    var isEnabled: Bool { get }

    func enable() throws
    func disable() throws
    func currentSpeech() throws -> Output
    func moveForward() throws -> Output
    func moveBackward() throws -> Output

    // iOS and macOS only
    func moveIn() throws -> Output
    func moveOut() throws -> Output

    final class Output {
        var utterance: String { get }
    }
}

The service is available on 27 runtimes. Forward/backward navigation and speech retrieval are declared across iOS, macOS, tvOS, watchOS, and visionOS; entering and leaving a container are declared only on iOS and macOS. Native visionOS applications remain a practical exception because XCUIAutomation does not currently support UI testing for apps built with the visionOS SDK.

Keep these tests in XCTest/XCUIAutomation

This is not a Swift Testing replacement for UI tests. XCUIAutomation is still driven from an XCTest UI-testing target:

import XCTest

@MainActor
final class CheckoutVoiceOverTests: XCTestCase {
    func testCheckoutSummaryReadingOrder() throws {
        let app = XCUIApplication()
        app.launchArguments = ["-uiTesting", "-checkoutFixture"]
        app.launch()

        XCTAssertTrue(
            app.otherElements["checkout.summary"].waitForExistence(timeout: 5)
        )

        let voiceOver = XCUIDevice.shared.voiceOverService
        try voiceOver.enable()
        defer { try? voiceOver.disable() }

        let title = try seek(
            "Order summary",
            using: voiceOver,
            maximumMoves: 20
        )
        XCTAssertTrue(title.utterance.contains("Order summary"))

        let total = try voiceOver.moveForward()
        XCTAssertTrue(total.utterance.contains("Total"))

        let button = try voiceOver.moveForward()
        XCTAssertEqual(button.utterance, "Place Order, button")
    }

    private func seek(
        _ phrase: String,
        using voiceOver: XCUIVoiceOverService,
        maximumMoves: Int
    ) throws -> XCUIVoiceOverService.Output {
        var output = try voiceOver.currentSpeech()

        for move in 0...maximumMoves {
            if output.utterance.localizedCaseInsensitiveContains(phrase) {
                return output
            }
            if move < maximumMoves {
                output = try voiceOver.moveForward()
            }
        }

        XCTFail("VoiceOver never reached: \(phrase)")
        return output
    }
}

The bounded search makes startup focus less fragile while still failing if a critical element is unreachable. Once the test reaches a stable landmark, assert the next few utterances directly to validate reading order.

What this catches that ordinary XCUITest misses

  • A visible control that is absent from VoiceOver navigation.
  • An unintended element inserted into the reading order.
  • A combined element whose spoken label/value/trait is wrong.
  • Heading, button, selected, adjustable, or other semantics that change the final utterance.
  • A custom container that VoiceOver cannot enter or leave correctly.
  • Localization or dynamic state that produces misleading speech even though the element remains queryable by identifier.

The important distinction is:

  • accessibilityIdentifier is a stable, nonlocalized test hook. It is not the user-facing spoken text.
  • accessibilityLabel, value, traits, and the surrounding accessibility structure contribute to the VoiceOver experience.
  • Output.utterance validates the synthesized result that a user hears.

Do not put test-oriented text into accessibilityLabel. Give reusable views a required stable identifier for automation and a separately localized, user-meaningful label.

Combine VoiceOver traversal with accessibility audits

XCUIApplication.performAccessibilityAudit(for:_:) is older—it has been available since iOS 17—but remains complementary. Run it on every important screen to catch broad issues such as descriptions, hit regions, contrast, element detection, Dynamic Type, clipped text, and traits:

@MainActor
func testCheckoutAccessibilityAudit() throws {
    let app = XCUIApplication()
    app.launchArguments = ["-uiTesting", "-checkoutFixture"]
    app.launch()

    XCTAssertTrue(
        app.otherElements["checkout.summary"].waitForExistence(timeout: 5)
    )

    try app.performAccessibilityAudit(for: .all)
}

Use the audit issue-handler only for a narrowly identified, documented exception. Returning true tells XCTest that the issue was handled and prevents it from being recorded; a blanket true silently defeats the audit.

A useful layered E2E strategy is:

  1. Ordinary XCUI journeys prove that critical workflows function.
  2. performAccessibilityAudit provides broad per-screen static checks.
  3. XCUIVoiceOverService covers a small number of high-value reading-order and spoken-output journeys.
  4. Test-plan configurations cover representative content sizes, locales, appearance, orientation, and accessibility settings.
  5. Manual testing with VoiceOver and other assistive technologies covers interaction quality that automation cannot model.

Stability and beta caveats

  • Pin the test plan's language and locale. System words such as “button” are localized, and punctuation or phrasing can change between OS releases.
  • Prefer exact utterance equality for a few critical semantics in a fixed configuration. Elsewhere, assert meaningful phrases and relative order to avoid turning system wording into a large snapshot surface.
  • Keep the VoiceOver suite small and normally serial. Enabling/disabling a device-wide service is heavier and more stateful than querying an element.
  • Always disable VoiceOver in cleanup. The throwing failure modes include failure to start or stop, calling navigation while VoiceOver is not running, and receiving no speech before the timeout.
  • The API returns speech, not the focused XCUIElement. It does not expose rotor selection, arbitrary custom-action invocation, or a VoiceOver “activate current item” operation. Use ordinary XCUI element actions for the functional part of the journey and retain manual assistive-technology testing.
  • Xcode 27's Device Hub also exposes accessibility settings, but an early-beta release note says its VoiceOver toggle may fail to enable VoiceOver on the device. That UI defect should not be confused with the separate XCTest service; validate both on the exact Xcode/OS seed used by CI.

Apple's accessibility lab recommends automated UI testing and explicitly calls out the new element-to-element VoiceOver API, while emphasizing that automation gets most—not all—of the way to a genuinely accessible experience.


14. ImageCreator discontinuation

Status: Confirmed deprecation and migration deadline. Secondary source: ImageCreator Is Deprecated. Primary sources: Apple's deprecation notice, ImageCreator documentation, and Create high quality images using Image Playground — WWDC26.

The Xcode 27 beta SDK declaration is:

@available(iOS 18.4, macOS 15.4, visionOS 2.4, *)
@available(anyAppleOS, deprecated: 27.0,
           message: "Use ImagePlaygroundViewController or imagePlaygroundSheet.")
final public class ImageCreator: Sendable

Apple says beta builds warn, TestFlight use can fail at runtime, and public 27 releases stop accepting the old implementation. There is no headless, programmatic Image Playground replacement.

Migration choices:

  • User-driven generation: use SwiftUI's imagePlaygroundSheet or ImagePlaygroundViewController in UIKit/AppKit.
  • Copy the result from the temporary completion URL into app-owned storage before the session ends.
  • Gate the feature with supportsImageGeneration and provide a fallback for unsupported capability, language/region, or user settings.
  • A feature that truly requires background/headless generation needs a different service or product design. That is an architectural change, not a one-line API rename.

Search for ImageCreator, images(for:style:limit:), and images(for:style:options:limit:) across app, extension, test, and sample targets.


15. Three-month Apple Ecosystem series triage

The series inventory contained 87 posts dated April 28 through June 15, 2026 at the time of review. It is valuable discovery material but is not itself a primary source. The migration guide incorporated the items with broad, immediate impact and linked them above.

Series area skimmed Disposition for a general Xcode 27 migration
SwiftUI performance/interop, What's New in SwiftUI, Xcode agent skills Incorporated and checked against WWDC transcripts, exported Apple skills, and SDK interfaces.
Accessibility and E2E testing Added a dedicated XCUIVoiceOverService section, kept automated accessibility audits as a separate layer, and compiler-verified the new VoiceOver Swift calls against the installed Xcode 27 beta.
UIKit scene lifecycle/adaptivity Incorporated as a launch-blocking requirement and layout audit.
ImageCreator discontinuation Incorporated as a feature-breaking migration.
Instruments 27 Use for validation: start in Time Profiler, distinguish high-CPU work from blocked/idle waits, use Top Functions and flame graphs, Swift executors, syscall inspection, and Run Comparisons. Primary source: Find hangs using Instruments — WWDC26. Grok also surfaced Jacob Bartlett's launch-time flame-graph walkthrough as a practical secondary workflow.
MetricKit 27 / StateReporting Optional production-telemetry project: new MetricManager async streams and state-specific performance attribution. Do not mix this into a source migration unless the app already consumes MetricKit. Primary source: Meet the new MetricKit — WWDC26.
SwiftData observation/history/performance Framework-specific adoption: ResultsObserver, HistoryObserver, @Attribute(.codable), fetchCount, and fetchIdentifiers are present in the 27 SDK. Review when the app uses SwiftData; do not add it to apps using another persistence layer. Primary source: What's new in SwiftData — WWDC26.
On Demand Resources Migration work for targets using NSBundleResourceRequest: iOS 27 deprecates On Demand Resources in favor of Background Assets. Inventory tags, hosting, download policy, eviction assumptions, and offline behavior before switching systems. Primary source: iOS & iPadOS 27 release notes.
Swift 6.3/6.4 language and testing posts Compile all modules and tests under the Xcode 27 toolchain; adopt language features deliberately rather than changing language mode as part of an unrelated UI migration.
Camera, HealthKit, App Intents, Foundation Models, Core AI, RealityKit, Vision, accessibility, Safari/WebKit, Metal, and other domain posts Treat as opt-in framework workstreams. They do not belong in every app's baseline migration checklist; open a separate verified plan when the codebase imports the relevant framework.

Secondary findings deliberately not promoted to confirmed rules

The highlighted performance article includes paraphrases from locally recorded WWDC group labs for which Apple publishes no official transcript. They are useful leads—coarsening values returned from onGeometryChange, keeping high-frequency scroll position out of the environment, avoiding deep UIKit/SwiftUI layout “sandwiches,” and using a separate UIWindow for overlays that must sit above sheets—but they are labeled Secondary / not independently confirmed here. Measure them in the affected app and validate public API contracts before turning them into automated migration rules.


16. Suggested codebase search plan

Run targeted searches and classify each hit rather than applying broad mechanical rewrites:

@State private var .*=
init(
canOpenURL
LSApplicationQueriesSchemes
Task {
.swipeActions
@Entry var
ForEach(
@Observable
private var .*: some View
LazyVStack
LazyHStack
onScrollGeometryChange
onGeometryChange
UIScreen.main
userInterfaceIdiom
interfaceOrientation
UIApplicationSceneManifest
UIWindowSceneDelegate
XCUIVoiceOverService
voiceOverService
performAccessibilityAudit
accessibilityIdentifier
accessibilityLabel
UILaunchStoryboardName
UILaunchScreen
UISupportedInterfaceOrientations
UIRequiresFullScreen
TabView(selection:
.textSelection(.enabled)
FileDocument
ReferenceFileDocument
ReorderDifference
NSBundleResourceRequest
dragInteraction
UIPresentationController
ImageCreator
MXMetricManager
_UIScrollEdge
updatePocket

Prioritize:

  1. UIKit targets that have not completed scene-lifecycle adoption or lack a launch-screen declaration.
  2. Build failures, new concurrency warnings, and a TabView selection that can name a hidden tab.
  3. Critical screens with no automated accessibility audit, and critical flows with no VoiceOver reading-order coverage.
  4. @State initializer conflicts and reference-valued initializer side effects.
  5. ImageCreator, On Demand Resources, and URL-scheme list overages/install-detection UI.
  6. Private UIKit dependencies and custom presentation/drag-interaction assumptions.
  7. Large/lazy collections with inline work, multi-shaped rows, unstable offset thresholds, or post-appearance layout feedback.
  8. Frequently updating observable properties and broad value inputs.
  9. UIScreen.main, idiom, orientation, and UIRequiresFullScreen assumptions in resizable UIKit layouts.
  10. Custom iPad chrome that looks wrong while inactive.
  11. Optional adoption of the new reorder, document, toolbar, image, swipe, and adaptive-sidebar APIs—without assuming sample-only helpers are framework APIs.

17. Verification matrix for migration branches

Test at least these combinations before merging a migration:

Build SDK Runtime Why
Xcode 27 / iOS 27 Latest iPhone seed New APIs, warning behavior, resizable/adaptive layouts
Xcode 27 / iPadOS 27 Multiple windows, active and inactive appearsActive, navigation adaptation, custom chrome
Xcode 27 / iPadOS 27 Continuous resizing, each declared orientation, iPhone-only compatibility Separate intended adaptivity from seed-specific orientation defects
Xcode 27 / iOS 27 VoiceOver enabled through XCUIAutomation, fixed locale Reading order, spoken semantics, container navigation, cleanup reliability
Xcode 27 / oldest supported OS Physical device or simulator Back-deployment and availability fallbacks
Xcode 27 archive Inspect archived Info.plist and validate an upload Scene manifest, launch-screen gate, capability and packaging failures
Xcode 26 baseline, if still available Existing supported runtime Compare state construction and regressions during transition

For performance rules, measure representative large data sets. A structurally cleaner row or narrower input is desirable, but the codebase should still use Instruments, signposts, _printChanges() in temporary diagnostics, and realistic scrolling/update workloads to prioritize work.

18. Primary source index

Bottom line

The first gates are now launch and distribution viability: scene lifecycle must be in place, and a 27-SDK upload must declare a launch screen. The next urgent migrations are build-linked semantics and diagnostics—@State initialization, visible TabView selection, selectable-text gestures, discarded throwing tasks, ImageCreator, On Demand Resources, and URL-scheme probing. Xcode 27's VoiceOver service is a meaningful new E2E layer: keep broad accessibility audits, then add a small set of real VoiceOver reading-order and utterance tests for critical journeys. The highest-leverage SwiftUI performance work is to respect lazy estimation and prefetching while making identity and dependency boundaries explicit: stable defaults, unary rows, prepared collections, narrow inputs, and real subviews. Observation makes an incremental UIKit/SwiftUI seam practical; it does not require an all-SwiftUI rewrite. New reorder, document, toolbar, image, swipe, and adaptive-sidebar APIs are adoption opportunities behind availability checks. Sample-only helpers, private scroll-pocket findings, and unpublished lab paraphrases are not framework contracts.

Because this guide was produced during the beta cycle, re-run the SDK/compiler probes and re-check the linked Apple release notes before shipping with the final Xcode 27 toolchain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment