Contents

TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder

Update existing code for two foundational changes in SwiftUI built with Xcode 27.

Overview

SwiftUI in Xcode 27 introduces two changes to the language-level shape of the framework:

  • State is a Swift macro rather than a property wrapper. The macro enables lazy evaluation of a property’s initial value, and the compiler treats the property like any other stored property of the view.

  • Result builders are unified under ContentBuilder. In order to drastically improve typechecking performance, previously distinct builders such as ViewBuilder are unified under @ContentBuilder, and builder blocks no longer carry specific protocol constraints on their contents, drastically improving typechecking performance.

The vast majority of SwiftUI code in your app is unaffected and continues to compile and behave the same way without modification. A small number of patterns that depended on the previous implementations now produce build errors. The following sections describe each known source incompatibility, the recommended remediation, and the underlying cause.

To locate a specific build error, see the Diagnostic reference at the end of this document.

Understanding the changes

Before diving into specific errors, it helps to understand what changed and why. Both changes were made to target specific developer pain-points, but share a common theme that causes source incompatibility: the compiler now has different information about your code, meaning some code that compiled before is now unsupported.

How @State evolved to support laziness

Previously, when you wrote a state variable, that state value would be recreated every time its containing view was initialized. In SwiftUI, views are intended to be lightweight values that are created frequently, and so views and their contained state values are recreated frequently.

This behavior is fine when the state contains value types, but reference types must heap allocate every time they’re instantiated, meaning this recreation causes performance degradations, or unintended work to initialize these objects.

By making @State a macro, SwiftUI can ensure that objects held in state are only ever initialized one time, when the view is first created, avoiding this performance issue.

How result builders changed to be unified under @ContentBuilder

SwiftUI previously defined a separate result builder for each kind of content you compose. Every primitive those builders share, such as Group, ForEach, and Section, provided a separate initializer for each builder. Because these initializers differ only in the type of content the builder produces, and that type doesn’t appear at the call site, the compiler infers which initializer to use.

Inferring the initializer requires typechecking the entire closure body. When that body contains more overloaded primitives, the compiler repeats this work for each candidate while keeping its choices consistent across the closure. The cost grows quickly as builders nest, and deeply nested uses of Group, Section, or ForEach can exceed the compiler’s limits, producing the error:

the compiler is unable to type-check this expression in reasonable time

ContentBuilder replaces these builders with a single builder that imposes no constraints on its contents. It assembles arbitrary content, and that content conforms to the protocol its context requires only when its elements do. Each primitive provides one builder-based initializer, so the compiler no longer searches indistinguishable overloads or typechecks closure bodies to choose one. Your code keeps the same syntax and type safety, and typechecks substantially faster.

Migrating code that uses @State

@State now participates in initialization the same way any stored property does, meaning the compiler now diagnoses several patterns that were previously accepted, but never well-defined.

The content within this section covers these patterns that no longer compile.

Initialize stored properties before assigning to @State in init

What happens: A view whose init assigns to a @State property before all other stored properties have been initialized produces one of the following diagnostics:

error: 'self' used in property access '_counter' before 'super.init' call
error: variable 'self.counter' used before being initialized

The following code reproduces the error:

import SwiftUI

struct ContentView: View {
    var name: String
    @State private var counter: Int

    init(name: String) {
        self.counter = 42
        self.name = name
    }

    var body: some View { Text("\(name): \(counter)") }
}

How to fix it: Initialize all non-@State stored properties before assigning to any @State property.

Why this occurs: The @State macro synthesizes a backing storage property, _counter in the example above. Assigning to this property before the remainder of self is initialized is a use of self prior to full initialization, which the compiler now reports. This pattern arises when a view’s initial state must be derived from a value passed to its initializer.

Avoid composing additional property wrappers with @State

What happens: Applying another property wrapper to a property declared with @State produces:

error: invalid redeclaration of synthesized property '_counter'

How to fix it: Remove the additional property wrapper.

Why this occurs: The composed property wrapper and the @State macro each attempt to synthesize a backing storage property using the same underscore-prefixed name.

Define memberwise initializers explicitly when a view has private @State

What happens: In Swift, a type whose stored properties are all private and that does not declare an explicit initializer receives a synthesized memberwise initializer with private access, callable from initializers defined in extensions of the type. Views that declare a @State property no longer receive this synthesized initializer, and call sites in extensions fail to resolve:

struct Foo: View {
    @State private var bar = 0
    private let baz: Int
}

extension Foo {
    init(_ bar: Int, baz: Int) {
        self.init(bar: bar, baz: baz) // error: no exact matches in call to initializer
    }
}

How to fix it: Define the memberwise initializer explicitly rather than relying on synthesis.

Why this occurs: The expansion of the @State macro causes the compiler to skip synthesis of the memberwise initializer, per Init Accessors.

Migrating code that uses result builders

Many of SwiftUI’s result builders are unified under @ContentBuilder, improving typechecking performance. To accomplish this, the constraint that previously required builder block contents to conform to View has been removed. This change causes a few source incompatibilities, most of which relate to no longer being able to use that view constraint to choose between otherwise ambiguous overloads.

Use the closure-based forms of background and overlay for ShapeStyle expressions

What happens: A call to the deprecated, non-builder forms of background(alignment:content:) or overlay(alignment:content:) that passes a ShapeStyle expression composed with modifiers such as opacity(_:) or blendMode(_:) produces:

error: ambiguous use of 'opacity'
error: ambiguous use of 'blendMode'

The following code reproduces the error:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello")
            .overlay(Color.blue.opacity(0.70).blendMode(.overlay))
    }
}

How to fix it: Use the closure-based form of background(_:) or overlay(_:):

import SwiftUI

struct ContentView: View {
    var body: some View {
        Rectangle()
            .overlay { Color.blue.opacity(0.3).blendMode(.overlay) }
    }
}

Why this occurs: Both background and overlay provide overloads that accept either a View or a ShapeStyle, and modifiers such as opacity(_:) on a ShapeStyle return either a ShapeStyle or a View. The View constraint that ViewBuilder previously imposed allowed the compiler to select a single resolution for these expressions. Without that constraint, the competing overloads are equally viable and the expression becomes ambiguous. The closure-based form selects the builder overload directly and resolves the ambiguity.

Disambiguate types and members that collide across modules

What happens: If a project imports a module that declares a type whose name matches a SwiftUI type, or a type with members that match those of a SwiftUI type, the compiler may report an ambiguity at the use site. This applies to any duplicated type, such as a framework’s own Text declaration whose overloads match those of SwiftUI’s Text, and to any duplicated static member of such a type, such as red, green, blue, or clear.

For example, when a project imports a module that declares its own Color type with a clear member, the compiler produces:

error: ambiguous use of 'clear'

The following code reproduces the error:

// In MyPackage:
public struct Color {
    public static let clear = Color()
}

// In the app:
import SwiftUI
import MyPackage

struct ContentView: View {
    var body: some View {
        Color.clear
    }
}

How to fix it: Fully qualify the type with its module name, or rename the conflicting type or member in the other module.

import SwiftUI
import MyPackage

struct ContentView: View {
    var body: some View {
        SwiftUI.Color.clear
    }
}

Why this occurs: The View constraint on @ViewBuilder previously allowed the compiler to exclude non-View-conforming candidates during overload resolution. With the constraint removed, both candidates are equally valid and the compiler reports the ambiguity.

Update generic constraints that reference TupleView

What happens: Code that names TupleView as a nested generic type argument may produce one of the following diagnostics:

error: cannot convert value of type 'VStack<TupleContent<Text, Text>>' to expected argument type 'VStack<TupleView<(Text, Text)>>'

error: cannot convert value of type 'Label<TupleContent<Text, Text?>, Image?>' to expected argument type 'Label<TupleView<(Text, Optional<Text>)>, Optional<Image>>'

The following code reproduces the error:

import SwiftUI

struct CardView<Content: View>: View {
    var content: Content
    var body: some View { content }
    init(@ContentBuilder content: () -> Content) {
        self.content = content()
    }
}

extension CardView where Content == VStack<TupleView<(Text, Text)>> {
    init(title: String, subtitle: String) {
        self = CardView {
            VStack {
                Text(title)
                Text(subtitle)
            }
        }
    }
}

How to fix it: Prefer opaque types such as some View rather than spelling the concrete builder return type. When a concrete spelling is required, replace TupleView with TupleContent in the constraint:

extension CardView where Content == VStack<TupleContent<Text, Text>> {
    init(title: String, subtitle: String) {
        self = CardView {
            VStack {
                Text(title)
                Text(subtitle)
            }
        }
    }
}

For projects with a minimum deployment target earlier than iOS 27, iPadOS 27, macOS 27, or visionOS 27, where TupleContent is unavailable, retain the TupleView constraint and construct the TupleView explicitly inside the builder:

extension CardView where Content == VStack<TupleView<(Text, Text)>> {
    init(title: String, subtitle: String) {
        self = CardView {
            VStack {
                TupleView((
                    Text(title),
                    Text(subtitle)
                ))
            }
        }
    }
}

Why this occurs: The unified @ContentBuilder produces TupleContent rather than TupleView as the concrete return type for multi-expression builder blocks. When TupleView appears as a nested generic argument, the contextual type does not propagate deeply enough to redirect the inner builder’s return type, and the resulting type does not match the declared constraint.

Provide an explicit empty content value in nested builders when MapKit is in scope

What happens: When SwiftUI and MapKit are both in scope in a source file, an empty result builder block — including a conditional compilation block whose only branch is excluded — within a nested builder produces:

error: return type of property 'body' requires that 'EmptyMapContent' conform to 'View'

The following code reproduces the error:

import SwiftUI
import MapKit

struct ContentView: View {
    var body: some View {
        Group { }
    }
}

The same error occurs when a conditional compilation block resolves to an empty body — for example, when the active configuration causes the only branch to be excluded:

import SwiftUI
import MapKit

struct ContentView: View {
    var body: some View {
        Group {
            #if MY_CONDITION
            MyView()
            #endif
        }
    }
}

How to fix it: Supply an explicit EmptyContent (or EmptyView) value:

import SwiftUI
import MapKit

struct ContentView: View {
    var body: some View {
        Group {
            EmptyContent()
        }
    }
}

For the conditional compilation case, add an #else branch that produces an explicit EmptyContent() (or EmptyView()):

import SwiftUI
import MapKit

struct ContentView: View {
    var body: some View {
        Group {
            #if MY_CONDITION
            MyView()
            #else
            EmptyContent()
            #endif
        }
    }
}

Why this occurs: Without the View constraint on the builder, an empty body is ambiguous when MapKit is also in scope: MapKit declares its own result builder that produces EmptyMapContent, and the compiler has no basis to choose between the two. Supplying an explicit EmptyContent() (or EmptyView()) anchors the resolution to a concrete View-conforming expression.

Extract deeply branching content from Chart closures when back-deploying

What happens: For projects whose minimum deployment target is earlier than iOS 27, iPadOS 27, macOS 27, or visionOS 27, a Chart that contains deeply branching if / else if or switch statements may produce:

error: the compiler is unable to type-check this expression in reasonable time

This diagnostic does not occur in projects whose minimum deployment target is iOS 27, iPadOS 27, macOS 27, or visionOS 27. The threshold for the diagnostic is approximately ten or more branches.

The following code reproduces the error:

import SwiftUI
import Charts

struct DataPoint {
    var index: Int
    var rate: Double
    var signal: Double
    var noise: Double
    var errors: Double
    var throughput: Double
    var txRate: Double
    var rxRate: Double
    var txFrames: Double
    var rxFrames: Double
    var channel: Double
    var bandwidth: Double
    var defaultValue: Double
}

struct MetricChartView: View {
    var selectedMetric: String
    var dataPoints: [DataPoint]

    var body: some View {
        Chart(dataPoints, id: \.index) { dataPoint in
            if selectedMetric == "Rate" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rate))
                    .foregroundStyle(.blue)
            } else if selectedMetric == "Signal" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.signal))
                    .foregroundStyle(.green)
            } else if selectedMetric == "Noise" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.noise))
                    .foregroundStyle(.red)
            } else if selectedMetric == "Errors" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.errors))
                    .foregroundStyle(.orange)
            } else if selectedMetric == "Throughput" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.throughput))
                    .foregroundStyle(.purple)
            } else if selectedMetric == "TX Rate" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.txRate))
                    .foregroundStyle(.cyan)
            } else if selectedMetric == "RX Rate" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rxRate))
                    .foregroundStyle(.mint)
            } else if selectedMetric == "TX Frames" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.txFrames))
                    .foregroundStyle(.indigo)
            } else if selectedMetric == "RX Frames" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rxFrames))
                    .foregroundStyle(.brown)
            } else if selectedMetric == "Channel" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.channel))
                    .foregroundStyle(.teal)
            } else if selectedMetric == "Bandwidth" {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.bandwidth))
                    .foregroundStyle(.pink)
            } else {
                LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.defaultValue))
                    .foregroundStyle(.gray)
            }
        }
    }
}

How to fix it: Extract the branching logic into a separate function annotated with ChartContentBuilder:

import SwiftUI
import Charts

struct MetricChartView: View {
    var selectedMetric: String
    var dataPoints: [DataPoint]

    var body: some View {
        Chart(dataPoints, id: \.index) { dataPoint in
            marks(for: dataPoint)
        }
    }

    @ChartContentBuilder
    private func marks(for dataPoint: DataPoint) -> some ChartContent {
        if selectedMetric == "Rate" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rate))
                .foregroundStyle(.blue)
        } else if selectedMetric == "Signal" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.signal))
                .foregroundStyle(.green)
        } else if selectedMetric == "Noise" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.noise))
                .foregroundStyle(.red)
        } else if selectedMetric == "Errors" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.errors))
                .foregroundStyle(.orange)
        } else if selectedMetric == "Throughput" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.throughput))
                .foregroundStyle(.purple)
        } else if selectedMetric == "TX Rate" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.txRate))
                .foregroundStyle(.cyan)
        } else if selectedMetric == "RX Rate" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rxRate))
                .foregroundStyle(.mint)
        } else if selectedMetric == "TX Frames" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.txFrames))
                .foregroundStyle(.indigo)
        } else if selectedMetric == "RX Frames" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.rxFrames))
                .foregroundStyle(.brown)
        } else if selectedMetric == "Channel" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.channel))
                .foregroundStyle(.teal)
        } else if selectedMetric == "Bandwidth" {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.bandwidth))
                .foregroundStyle(.pink)
        } else {
            LineMark(x: .value("X", dataPoint.index), y: .value("Y", dataPoint.defaultValue))
                .foregroundStyle(.gray)
        }
    }
}

Why this occurs: Supporting back-deployment of @ContentBuilder in Swift Charts requires a compatibility overload of buildEither that returns a Charts-specific BuilderConditional type. The presence of this additional overload increases the size of the candidate set the compiler must consider when resolving branching expressions inside chart builders. As the number of branches grows, the candidate set grows superlinearly, and beyond a certain point the expression exceeds the compiler’s complexity budget. Projects whose minimum deployment target is iOS 27, iPadOS 27, macOS 27, or visionOS 27 are unaffected because the compatibility overload is not compiled in. Extracting the branches into a dedicated @ChartContentBuilder function isolates the type-checking work and keeps each expression within budget. In aggregate, this trade-off improves type-checking performance for chart content outside this specific case, and for all SwiftUI content in projects that import Swift Charts.

Diagnostic reference

To locate the section that addresses a specific build error, match the diagnostic text to the entry below.

Type

Error

Learn More

@State

'self' used in property access '_x' before 'super.init' call

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@State

variable 'self.x' used before being initialized

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@State

invalid redeclaration of synthesized property '_x'

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@State

Memberwise initializer not callable from extension

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@ContentBuilder

ambiguous use of 'opacity' or ambiguous use of 'blendMode' in background or overlay

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@ContentBuilder

ambiguous use of '<member>' for a SwiftUI member when another module declares a colliding type

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@ContentBuilder

cannot convert value of type ... TupleContent ... to expected argument type ... TupleView ...

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@ContentBuilder

return type of property 'body' requires that 'EmptyMapContent' conform to 'View'

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

@ContentBuilder

the compiler is unable to type-check this expression in reasonable time inside Chart

Tn3211 Resolving Swiftui Source Incompatibilities For State And Contentbuilder

Revision History

  • 2026-06-08 First published.

See Also

Latest