8 min read

Issue #82: Why Your Views Re-Run, Deadlines in Review, SwiftData Behind a Wall

Hey folks 👋

The beta season lull has a nice side effect: once everyone has finished listing what's new, the community goes back to the quiet stuff that actually makes apps feel fast. This edition is full of it. Why does a view re-run when nothing it reads has changed? Where should persistence stop and your feature begin? How long should an async call be allowed to take before someone pulls the plug? Plus an App Store form that quietly redefines what counts as a social app.

Xcode 27 and iOS 27 are still in beta, so treat everything below as beta behavior until the fall. 🧘


SwiftUI: Your @Entry default may be reallocating on every access

Store a class in an @Entry default and every fallback access runs the initializer again. New reference, new identity, and SwiftUI decides the environment value changed.

extension EnvironmentValues {
    @Entry var logger = AppLogger() // ⚠️ new instance on every access
}

Xcode 27 now warns about it:

Storing a class type in '@Entry var logger' may invalidate dependents on every update because the default value is reallocated on every access. Initialize the default value elsewhere and reference it from the '@Entry' declaration.

The cost is real. Any view that reads \.logger without an injected value gets re-evaluated whenever an unrelated environment value changes further up the tree. Fix it by moving the instance into stable storage, or drop the fallback entirely and make the value optional so a missing injection is a loud configuration error instead of a silent one.

private let defaultLogger = AppLogger()

extension EnvironmentValues {
    @Entry var logger = defaultLogger
}

The warning only fires for class types, but the same trap applies to Date(), UUID(), and any struct that wraps a freshly allocated reference.

The hidden cost of unstable SwiftUI environment defaults Why Xcode 27 warns when an @Entry default allocates a new class instance, and how to provide a stable default instead.

The hidden cost of unstable SwiftUI environment defaults
Learn why Xcode 27 shows a warning when an @Entry declaration creates a new class instance for every fallback access, how unstable environment defaults can cause unnecessary SwiftUI view updates, and how to make them stable.

SwiftUI: Binding(get:set:) in a body is a re-render tax

Closure bindings created inline get rebuilt on every body evaluation. Fresh closures, fresh heap allocations, and nothing SwiftUI can meaningfully compare. Edit an unrelated text field in the same view and the rows can re-evaluate anyway.

Move the read and write logic into a labeled subscript on the model and pass the projection instead:

@Observable
final class TeamPermissionsModel {
    var grantedPermissionIDs: Set<Permission.ID> = []
    var teamName = ""

    subscript(isGranted permissionID: Permission.ID) -> Bool {
        get { grantedPermissionIDs.contains(permissionID) }
        set {
            if newValue {
                grantedPermissionIDs.insert(permissionID)
            } else {
                grantedPermissionIDs.remove(permissionID)
            }
        }
    }
}

// In the body:
PermissionRow(
    permission: permission,
    isGranted: $model[isGranted: permission.id]
)

Now the row only re-evaluates when grantedPermissionIDs actually changes. Hiding the Binding(get:set:) call inside a model helper does not fix anything, by the way. What matters is whether the binding is a new pair of closures or a stable projection.

Custom bindings in SwiftUI: closures vs subscripts How closure-based bindings affect view updates, and how labeled subscripts give SwiftUI a stable dependency to track.

Custom bindings in SwiftUI: closures vs subscripts
Learn how closure-based bindings can affect SwiftUI view updates, and how labeled subscripts give the framework a more stable way to track changes.

SwiftUI: Add Equatable, skip the invalidation

The @Observable macro generates two overloads of shouldNotifyObservers. The unconstrained one always returns true. The Equatable one compares the values. If your custom type does not conform, every assignment counts as a mutation, even when nothing in it changed.

struct DeliveryProgress: Equatable { // without Equatable, every assignment invalidates
    var completedStops: Int
    var totalStops: Int
}

@MainActor
@Observable
final class DeliveryModel {
    var progress: DeliveryProgress
}

This bites hardest when a model is fed by polling, an async sequence, or a framework callback that keeps handing you the same value. Arrays inherit the behavior, so [DeliveryStop] gets element-wise comparison once DeliveryStop is Equatable. In-place mutations like append() still notify, since they go through the modify accessor.

Equatable properties in @Observable classes Prevent unnecessary SwiftUI view updates by making custom types stored in @Observable properties conform to Equatable.

Equatable properties in @Observable classes
Prevent unnecessary SwiftUI view updates by making custom types stored in @Observable properties conform to Equatable.

Swift Evolution: withDeadline returned for revision again

SE-0526 has now been through two reviews and come back for revision both times. Worth watching anyway, because it is the shape timeouts will eventually take: an absolute instant instead of a duration, so nested calls stop leaking budget as the deadline gets passed down the stack.

let clock = ContinuousClock()
let deadline = clock.now.advanced(by: .seconds(10))

async let user = withDeadline(deadline, clock: clock) { try await fetchUser() }
async let prefs = withDeadline(deadline, clock: clock) { try await fetchPreferences() }

let (userData, prefsData) = try await (user, prefs)

Nested deadlines compose to the minimum without any clock conversion, and the operation closure is non-escaping and nonisolated(nonsending), so it can touch actor-isolated state. The part I did not expect: the proposal also gives CancellationError a reason.

public struct CancellationError: Error {
  @nonexhaustive
  public enum Reason {
    case taskCancelled
    case deadlineExpired
    case custom(String)
  }
}

That is arguably a bigger deal than the deadline itself. Since Swift 5.5, CancellationError has been an empty type, so "something got cancelled, good luck" was the whole error. Being able to tell a deadline expiry from a real Task.cancel() changes how you write recovery code.

SE-0526: withDeadline Run an async operation against a composable absolute deadline, with cancellation reasons attached to CancellationError.

swift-evolution/proposals/0526-deadline.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

Architecture: Keep SwiftData out of your views

@Query plus modelContext in the view is great until you add validation, sync, previews, or tests. Natascha Fadeeva's version puts a repository between the two and, crucially, splits the persisted type from the value type the feature actually passes around.

@Model
final class PersistedItem {
    var id: UUID
    var timestamp: Date
}

struct Item: Identifiable, Equatable, Sendable {
    let id: UUID
    let timestamp: Date
}

@MainActor
protocol ItemRepository {
    func fetchItems() throws -> [Item]
    func addItem(timestamp: Date) throws
}

You pay for it with mapping code. You get back a Sendable value type that previews and tests can produce freely, plus a single place where fetch descriptors and saves live.

Keeping SwiftData behind a boundary Draw a line between the UI and the persistence layer with a small repository and a plain value type. tanaschita.com Natascha Fadeeva

If you want the other direction, Point-Free's episode 372 goes through what SwiftData gained this year using Apple's Trips sample: sectioning, Codable support, and observing changes outside the view.

Keeping SwiftData behind a boundary
Learn how to keep SwiftData out of SwiftUI views. We can create a boundary between the UI and the persistence layer when working with SwiftData by designing a simple repository.

App Store: the age rating form now asks if you are social media

Since July 9, the App Store Connect age rating questionnaire includes questions about social media capabilities, defined as the ability to redistribute, amplify, or interact with user-generated content through a social feed or similar discovery method. Answer yes and your app lands in the Social Media Time Allowance category, picks up a Social Media content descriptor on its product page, and gets a minimum 13+ rating. Category does not matter, capability does. If the feature is disabled for anyone under 13, you stay out of the category for those users.

You can answer now. From September 2026 the answers are required for any new app or update, and for notarization for alternative distribution.

Age rating questionnaire now includes social media questions The questionnaire now feeds the new Time Allowances parental controls in iOS 27, iPadOS 27, and macOS 27. Apple Developer News

Age rating questionnaire now includes social media questions - Latest News - Apple Developer
As announced in June, new Time Allowances in iOS 27, iPadOS 27, and macOS 27, or later, give parents more flexible ways to manage the time their kids spend in apps across categories, including Entertainment, Games, and Social Media.To support this, the age rating questionnaire in App Store Connect now includes questions about your app’s social media capabilities. A social media capability is defined as the ability to redistribute, amplify, or interact with user-generated content through a social feed or similar discovery method. The Time Allowance category for Social Media is based on whether your app or game offers social media capabilities, regardless of the app category selected in App Store Connect. Apps with these capabilities will display a new Social Media content descriptor on their App Store product page. If you indicate that your app or game includes social media capabilities but they are disabled for anyone under 13, it won’t be included in the Time Allowance category for Social Media for users under 13. For more details, refer to Introducing Time Allowances.You can review and answer these questions starting today. As previously shared, beginning in September 2026, responses will be required when submitting new apps or updates to the App Store, or when submitting apps for notarization for alternative distribution.Set an app age ratingAge rating values and definitions

Quick Hits

  • The first iOS 27 public beta shipped on July 13, identical to developer beta 3 (build 24A5380h), which landed July 6. App Store Connect accepts TestFlight builds made with the Xcode 27 betas, but Xcode 27 itself is still beta, with the stable release expected in the fall.
  • Swift.org's June digest went up on July 2 and is worth ten minutes: parts of the OS kernel are being written in Swift, the QUIC transport layer was rewritten in Swift and open sourced, and Swift 6.4 is previewing up to 4x faster URL parsing plus async code in defer blocks.
  • Also in that digest: SE-0474 (yielding accessors) was accepted, and SE-0529 (FilePath in the standard library) and SE-0527 (RigidArray and UniqueArray) were accepted with modifications.
  • Swift Package Index joined Apple and stays open source, with a package registry as the stated goal.
  • Point-Free's episode 371 makes the case that UIKit is far from dead, wiring state-driven navigation and transactions into a UIKit feature with UIKitNavigation.
  • Fatbobman's debugging notes on two SwiftUI animation bugs is the kind of post you bookmark before you need it.
  • If Sendable still feels like a compiler mood rather than a rule, Natascha Fadeeva's Understanding Sendable in Swift is a clean explanation.
  • AsyncImage finally takes a URLRequest in iOS 27, so authenticated images, cache policies, and timeouts no longer require your own loader. There is an asyncImageURLSession(_:) modifier for a shared session too.

That's it for this one. Half of these posts are really the same lesson: SwiftUI can only skip work it can prove is unnecessary, so give it something stable to compare. See you in two weeks!