SwiftUI Performance and Interop in iOS 27

A LazyVStack does not know how tall it is. It estimates its own height from the average size of the views it has already placed and the number it expects remain, then corrects that estimate live as you scroll.1 Three WWDC26 sessions from the UI Frameworks team take that one fact (and its analogues in graphics and interop) and turn it into a working model of how SwiftUI behaves under load in iOS 27: how scrolling stays smooth, how GPU effects compose, and how SwiftUI slots into an AppKit or UIKit app you already shipped.

The three sessions read as one argument made three ways. Lazy stacks perform well when you stop fighting their estimation; shader effects compose when you treat each modifier as a stage in a pipeline; and interop works when you let @Observable and the representable protocols carry the seam. None of the three is a feature announcement. Each is a mechanism explained well enough that you can predict the framework instead of guessing at it.

TL;DR / Key Takeaways

  • A LazyVStack only evaluates the views filling the visible rect; off-screen heights and the content offset are estimated, so absolute content-offset reads are unstable and you should prefer relative-visibility APIs like .onScrollTargetVisibilityChange.12
  • Prefetching breaks the work of showing a view across multiple frames before it appears; set views up in their initializer (not onAppear) so prefetched work is not thrown away.1
  • Avoid a dynamic number of subviews in a ForEach leaf: filtering with a conditional in the body keeps views alive by index, so filter at the data level (a Predicate on a Query) instead.1
  • SwiftUI exposes three shader entry points (colorEffect, distortionEffect, layerEffect), increasing in power; only layerEffect can sample neighboring pixels, which is what blur and domain-warp need.3456
  • Shaders are stateless, so animation comes from feeding a TimelineView timestamp in as a parameter; nothing carries between frames.37
  • AppKit and UIKit get automatic redraw from @Observable (no more manual needsDisplay), and SwiftUI drops into an existing app through NSHostingView, NSGestureRecognizerRepresentable, NSHostingMenu, and NSHostingSceneRepresentation.8910

Lazy Stacks Run On Estimates

Watch on Apple Developer ↗
Rens, a UI Frameworks engineer, explains that a LazyVStack lays out top to bottom and stops once the visible rect is filled, estimating the rest.

The first thing to internalize about a lazy stack is that it trades correctness for efficiency on purpose. Unlike a VStack, a LazyVStack does not evaluate or render views that are not visible; it lays its views out top to bottom and stops once the visible rect is filled, adding views as they scroll in and removing them as they scroll out.1 The payoff is obvious. The cost is subtle: because the stack never loads every view, the heights of off-screen views are estimated from the average of what came before, the ideal width collapses to the width of the first subview, and the space above the visible region is itself approximate.1

That estimation is not a bug you route around once; it is the substrate every other lazy-stack decision sits on. Session 321 makes the consequence concrete with an orientation change. Rotate an iPhone and the topmost visible view stays anchored, but the stack has not yet measured the exact new layout of the views above it. Scroll back to the top and the stack must reconcile: it corrects the estimated space above the visible region and updates the scroll view’s content offset by the same amount so the content offset at the top lands on zero.1 The lazy stack and its enclosing scroll view coordinate position and offset precisely so that, as estimates update, the relative position of the visible subviews never jumps.1

The actionable corollary is a rule about which scroll APIs to trust. Because the absolute content offset is estimated, reading it (with .onScrollGeometryChange, say, to hide a button after 100 points) gives you a threshold that drifts as the estimates settle.2 The stable signal is relative visibility. The .onScrollTargetVisibilityChange modifier fires when the set of subviews visible in the scroll view changes, so a “scroll to showcase” button can key its visibility to which rows are on screen, with a threshold (the session uses 80%), instead of an unstable pixel count.21 The same logic indicts .scrollTransition: a transform that pushes a view out of its original frame can make the stack believe a visible view is off-screen and drop it early, so any scroll transition must keep views that would not normally be visible from being pushed into the visible rect.111

Why Your View Structs Aren’t The Subviews

The subviews a lazy stack loads do not map one-to-one onto the view structs you wrote. A ForEach of StepView resolves to one StepView per step, but if each StepView body returns two top-level views (a diagram and instructions) with no enclosing layout, the stack loads each of those separately.1 The number that matters to the stack is the resolved subview count, not the struct count.

The trap is a dynamic subview count. If a StepView returns one subview or zero depending on an environment value, the stack can no longer trust indices, because the count of earlier views could change. So it keeps earlier StepView instances alive just in case, which means an unrelated environment change can trigger body evaluations for views scrolled off-screen and the stack will not release their state.1 The fix is to move the filter off the view and onto the data: if you use SwiftData, put the condition in a Predicate on the Query so the subview count is known without constructing any views.1 Unwrapping an optional in a body has the same alive-longer effect; the cleaner move is to show a ContentUnavailableView higher up rather than letting the lazy stack hold partially-resolved rows.1

Prefetching is the mechanism that makes the estimates feel fast. While scrolling, a scroll view has only until the frame deadline to update the offset, render views, and run your offset-change work; if showing a new view blows that budget, the frame drops and you see a hitch.1 To prevent it, the lazy stack checks whether there is spare time and, if so, does part of the work of a soon-to-appear view early (evaluating its body and layout, even spreading a nested LazyHStack across frames) so that by the time the view appears, most of the work is already done.1 This is why the session is emphatic about onAppear: if you set a view up in onAppear, you discard the prefetched work and force a redo when it appears, sometimes pulling in more views than needed and degrading scroll. Set the view up in its initializer so it arrives in a reasonable state, and reserve onAppear for genuinely appearance-bound work like fetching the next page in an infinite scroll.1 On a reversed scroll, a body may even run during prefetching while onAppear never fires at all.1

Advanced Graphics Are Just A Pipeline

Watch on Apple Developer ↗
Haotian, a UI Frameworks engineer, frames advanced effects as standard pipes connected together: the “advanced” lives in the construction, not the complexity.

Session 322 reframes “advanced graphics” as composition. Each SwiftUI modifier is a pipe that takes data in, transforms it, and passes it along; the advanced result lives in how you connect the pipes, not in any single complex API.3 The session builds an Apple Music-style live-lyrics view by chaining ordinary stages: blur the cover art so it recedes, run a shader over it, drive that shader with time, and sync a transcript scroll to the same time source.3

The shader stage is where the real choice is. SwiftUI calls Metal shader functions through three effect entry points that climb in capability. colorEffect transforms each pixel’s color given its position and original color, which is enough for something like a grayscale conversion.4 distortionEffect instead maps one position to another (you tell SwiftUI to sample this position’s color from that position), which handles geometric warps with no color involved.5 layerEffect is the most flexible: it hands the shader the entire view’s layer, so the output pixel can sample its neighbors or the whole region, which is exactly what blur and richer warps require.63

The session’s domain-warp background uses layerEffect. A uniform float2 offset shifts every pixel by the same amount, which only slides the image; organic motion needs per-pixel variation, so the shader samples a precomputed NoiseTexture (passed in as an image, arriving on the Metal side as a texture2d) whose red and green channels supply a different X and Y offset at each UV coordinate.3 Sampling the noise once twists the image; sampling it twice, the second time at a position the first sample shifted, produces flowing blobs. That second-order technique is domain warping, and the session points to its downloadable sample app with a live preview for the parameters.3

Two framework facts make the animation work. Shaders are stateless: they keep no memory of the previous frame, and the output depends only on the parameters you pass in.3 So motion cannot come from inside the shader; it has to be fed in, and a TimelineView is the pipe that supplies it, firing every frame with a timestamp on an animation schedule.73 Pass that timestamp into the shader, add it to the noise sample position, and the pattern flows. The transcript side reuses the same time source from the other direction: the playback timestamp picks the current line (bold and clear, the rest faded), and an onChange keeps that line centered as time advances.123 The floating timestamp on the active line is positioned not with offset (which would need both views’ sizes) but with an alignment-guide override that redefines an alignment’s point semantically, so the subview’s top edge attaches to its container’s bottom edge without a manual offset.133

SwiftUI Drops Into An AppKit Or UIKit App

Watch on Apple Developer ↗
David Nadoba, a UI Frameworks engineer, notes that SwiftUI was designed from the start to work alongside AppKit and UIKit, the way Swift was designed to work with Objective-C.

The interop session opens with a point that reframes the whole adoption question: most apps already use SwiftUI implicitly. In the new design, AppKit controls like NSSlider, NSSwitch, and NSSegmentedControl are rendered with SwiftUI under the hood, and Liquid Glass shares large parts of its implementation across frameworks through SwiftUI too.8 So “adopting SwiftUI” is less a rewrite than a decision about where to make the seam explicit.

The first step needs no SwiftUI at all. AppKit and UIKit now observe @Observable types automatically: mark a model class @Observable, read its properties inside a draw method like drawKnob, and AppKit tracks each access and redraws when any accessed property changes, retiring the manual needsDisplay = true you used to write whenever one slider’s value affected another’s appearance.814 Observation extends past draw(_:) to updateConstraints(), layout(), updateLayer(), and the NSViewController equivalents, and UIKit reaches further still into UIButton, UICollectionViewCell, and more.8 It is on by default in the 2026 releases and back-deployable to macOS 15 (NSObservationTrackingEnabled) and iOS 18 (UIObservationTrackingEnabled) via Info.plist.8

Once the model is @Observable, the actual SwiftUI seam is small. The session rebuilds a slider-based color picker as a circular SwiftUI control drawn with Canvas (an immediate-mode API analogous to drawRect, with withCGContext to reuse existing Core Graphics code), reusing the very same @Observable ColorModel.158 To embed it where AppKit expects a view, wrap it in NSHostingView, a subclass of NSView; because the model already drives updates, that wrapping is all that is required.168 Existing gesture code carries over without rewriting: a ForceClickGestureRecognizer reaches a SwiftUI view through NSGestureRecognizerRepresentable (implement makeNSGestureRecognizer and handleNSGestureRecognizerAction), then attaches with the ordinary .gesture modifier and coexists with SwiftUI’s own drag gesture.178 The same representable family includes NSViewRepresentable for embedding NSViews the other direction.8

The seam scales up to menus and scenes. A SwiftUI View holding a Button and a Picker becomes a real menu through NSHostingMenu (an NSMenu subclass), set as the submenu of an NSMenuItem added to the main menu, with keyboardShortcut giving the action a non-gesture path for input devices that cannot force-click.188 Whole SwiftUI scenes attach too: a MenuBarExtra reaches an existing app through NSHostingSceneRepresentation, added via addSceneRepresentation in applicationWillFinishLaunching, with a Settings scene’s Toggle controlling whether the extra is inserted and the openSettings() environment action opening settings from an @IBAction.198 The session’s closing point is the load-bearing one: every API it covers ships in the 2026 releases or earlier, and there is no expectation that an app be entirely SwiftUI to benefit.8

What To Adopt First

A release explained as mechanism rewards ordering by leverage, not novelty.

  1. Switch model classes to @Observable in your AppKit/UIKit code. It deletes manual needsDisplay calls, gives every observing draw and layout method automatic redraw, and is the prerequisite that makes a later NSHostingView drop-in trivial.814 It is the lowest-risk, highest-immediate-payoff move in all three sessions.
  2. Audit lazy stacks for dynamic subview counts. Any ForEach leaf that conditionally returns zero or one subview, or unwraps an optional in its body, is keeping views alive by index; move the filter to a Predicate on a Query (or higher in the hierarchy) and both memory and scroll-to-item performance improve.1
  3. Move view setup out of onAppear and into initializers. Prefetching only helps if the prefetched work survives; setup that mutates size or content in onAppear throws that work away.1 This is a quiet, broad scrolling-smoothness win.
  4. Replace absolute scroll-offset reads with relative-visibility APIs. Anything keyed to content offset will drift; .onScrollTargetVisibilityChange keys to which rows are actually visible.21
  5. Reach for shaders only where a small effect earns its place. Start at colorEffect or distortionEffect; escalate to layerEffect only when an effect must sample neighbors, and drive any motion with a TimelineView timestamp rather than expecting state inside the shader.4567

The throughline across the three: predict the framework before you push on it. Lazy stacks estimate, shaders forget, and interop is a seam, not a rewrite. Build with those three facts in mind and the rest follows.

FAQ

Why does my SwiftUI lazy-stack scroll position jump or drift?

Because a LazyVStack does not load off-screen views, it estimates their heights and the space above the visible region, so the absolute content offset is an estimate that the framework corrects as it learns the real layout (after an orientation change, for example, it reconciles the estimate when you scroll back to the top).1 If you key UI to the absolute offset, the threshold drifts as estimates settle. Use .onScrollTargetVisibilityChange, which fires based on which subviews are actually visible, instead.2

How do I keep scrolling smooth in a SwiftUI lazy stack in iOS 27?

Let prefetching do its job: set views up in their initializer so the work the lazy stack performs before a view appears is not discarded, and avoid mutating a view’s size or contents in onAppear.1 Also avoid a dynamic number of subviews in ForEach leaves and avoid layout changes (like an onGeometryChange-driven height) after a view appears, since both force the stack to redo work or recompute positions mid-scroll.1

When should I use colorEffect, distortionEffect, or layerEffect?

Use colorEffect to transform each pixel’s color from its position and original color (a grayscale filter, for instance).4 Use distortionEffect for geometric effects, where you map an output position to a source position to sample from.5 Use layerEffect when the output pixel depends on more than one input pixel, because it gives the shader the whole view layer to sample neighbors or the entire region, which is what blur and domain warping need.6

How do I animate a Metal shader in SwiftUI?

Shaders are stateless: they keep no memory of the previous frame and depend only on their parameters, so you cannot animate from inside the shader.3 Feed in a value that changes over time. A TimelineView on an animation schedule fires every frame with a timestamp; pass that timestamp into the shader as a parameter (the session adds it to the noise sample position) and the effect animates.73

Can I add SwiftUI to an existing AppKit or UIKit app without rewriting it?

Yes, and the session is explicit that no app needs to be entirely SwiftUI to benefit.8 Mark your model @Observable so AppKit and UIKit redraw automatically, then embed SwiftUI views with NSHostingView, bring existing gesture recognizers across with NSGestureRecognizerRepresentable, build menus with NSHostingMenu, and attach SwiftUI scenes from your app delegate with NSHostingSceneRepresentation; all of these ship in the 2026 releases or earlier.816171819

The full Apple Ecosystem cluster: the SwiftUI substrate (result builders, opaque types, the value-typed view tree) that explains why a lazy stack resolves view structs into a different set of subviews; the iOS 27 SwiftUI surface (reordering, documents, toolbars, errors) that this performance-and-interop story sits beside; the @Observable internals that now drive automatic redraw in AppKit and UIKit too; and the Liquid Glass patterns whose cross-framework implementation the interop session attributes to shared SwiftUI. The hub is the Apple Ecosystem Series. For broader iOS-with-AI-agents context, see the iOS Agent Development guide.

References


  1. Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI.” developer.apple.com/videos/play/wwdc2026/321. Covers lazy-stack layout and height estimation, the estimated content offset, view-struct-to-subview resolution, the dynamic-subview-count trap, prefetching across frame deadlines, and onAppear versus initializer setup. 

  2. Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI”. The session presents onScrollTargetVisibilityChange (a modifier whose closure runs when the set of visible scroll targets changes) as the stable, relative-visibility alternative to absolute content-offset reads. 

  3. Apple, WWDC26 session 322, “Compose advanced graphics effects with SwiftUI.” developer.apple.com/videos/play/wwdc2026/322. Frames effects as a composable pipeline; covers blur, the three shader entry points, the NoiseTexture domain-warp technique, stateless shaders driven by time, and alignment-guide attachment. 

  4. Apple Developer Documentation: colorEffect(_:isEnabled:). Returns a new view that applies a shader transforming each pixel’s color, given its position and original color. 

  5. Apple Developer Documentation: distortionEffect(_:maxSampleOffset:isEnabled:). Applies a shader that maps the position of each pixel to a source position to sample from, for geometric effects. 

  6. Apple Developer Documentation: layerEffect(_:maxSampleOffset:isEnabled:). Applies a shader as a layer effect with access to the entire view layer, allowing each output pixel to sample multiple input pixels. 

  7. Apple Developer Documentation: TimelineView. A view that updates its content according to a schedule; on an animation schedule it supplies the per-frame timestamp session 322 feeds into its shader. 

  8. Apple, WWDC26 session 272, “Use SwiftUI with AppKit and UIKit.” developer.apple.com/videos/play/wwdc2026/272. Covers automatic @Observable redraw in AppKit/UIKit, observation back-deployment via Info.plist, Canvas, NSHostingView, NSGestureRecognizerRepresentable, NSHostingMenu, and NSHostingSceneRepresentation

  9. Apple Developer Documentation: NSGestureRecognizerRepresentable. A protocol that wraps an NSGestureRecognizer for use as a SwiftUI gesture, implemented with makeNSGestureRecognizer and handleNSGestureRecognizerAction

  10. Apple Developer Documentation: MenuBarExtra. A scene that renders a menu bar item; session 272 attaches it to an existing AppKit app through NSHostingSceneRepresentation

  11. Apple Developer Documentation: scrollTransition(_:axis:transition:). Applies a transition as a view scrolls within a scroll view; session 321 warns that a transform pushing a view into the visible rect can desync a lazy stack. 

  12. Apple Developer Documentation: onChange(of:initial:_:). Runs an action when a value changes; session 322 uses it to recenter the current transcript line. 

  13. Apple Developer Documentation: alignmentGuide(_:computeValue:). Sets a view’s alignment guide so the layout system positions it semantically; session 322 overrides a bottom guide to attach a subview’s top edge to its container’s bottom edge. 

  14. Apple Developer Documentation: Observable. The macro that makes a class’s mutable properties participate in the Observation system, which AppKit and UIKit track for automatic redraw in the 2026 releases. 

  15. Apple Developer Documentation: Canvas. An immediate-mode drawing view whose closure receives a GraphicsContext; session 272 uses it to redraw the circular color picker and notes withCGContext for reusing Core Graphics code. 

  16. Apple Developer Documentation: NSHostingView. An NSView subclass that hosts a SwiftUI view hierarchy inside an AppKit view tree. 

  17. Apple Developer Documentation: NSViewRepresentable. A wrapper that lets an NSView participate in a SwiftUI view hierarchy; session 272 names it alongside NSGestureRecognizerRepresentable as part of the representable family. 

  18. Apple Developer Documentation: NSHostingMenu. An NSMenu subclass that renders a SwiftUI view as menu content, added to the main menu as the submenu of an NSMenuItem

  19. Apple Developer Documentation: keyboardShortcut(_:modifiers:). Assigns a keyboard shortcut to a control’s action; session 272 adds one to the menu button so input devices that cannot force-click still reach the feature. 

Related Posts

Accessibility in iOS 27: Reading Apps & Custom Controls

iOS 27 accessibility for reading apps and custom controls: text navigation linking, causesPageTurn, UITextInput, the adj…

12 min read

What's New in SwiftUI for iOS 27

iOS 27 reworks SwiftUI lists, documents, toolbars, and errors: drag-to-reorder, a readable/writable document model, tool…

23 min read

From 76 to 100: Achieving a Perfect Lighthouse Score

A FastAPI site went from Lighthouse 76 with 0.493 CLS to perfect 100/100/100/100. The fix: critical CSS extraction, a CS…

10 min read