SwiftUI.State macro overreleasing object from Xcode 27 Beta 3?

Here is a simple class that implements a timer:

import AsyncAlgorithms

final class Timer {
  private var task: Task<Void, Never>?
  
  init() {
    let id = ObjectIdentifier(self)
    print(id, "init")
  }
  
  deinit {
    let id = ObjectIdentifier(self)
    print(id, "deinit")
    self.task?.cancel()
  }
  
  func start() {
    if let _ = self.task {
      return
    }
    let id = ObjectIdentifier(self)
    self.task = Task.immediate {
      print(id, "start")
      defer {
        print(id, "stop")
      }
      for await _ in AsyncTimerSequence.repeating(every: .seconds(1.0)) {
        let now = Date.now
        print(
          id,
          now.formatted(
            date: .omitted,
            time: .standard
          )
        )
      }
    }
  }
}

And here is a simple SwiftUI app to start a timer:

import SwiftUI

@main
struct StateDemoApp: App {
  @State private var timer = Timer()
  
  init() {
    self.timer.start()
  }
  
  var body: some Scene {
    WindowGroup {
      EmptyView()
    }
  }
}

Launching the app from Xcode 26.6 runs correctly:

ObjectIdentifier(0x0000000c0c96c020) init
ObjectIdentifier(0x0000000c0c96c020) start
ObjectIdentifier(0x0000000c0c96c020) 10:40:19 PM
ObjectIdentifier(0x0000000c0c96c020) 10:40:20 PM
ObjectIdentifier(0x0000000c0c96c020) 10:40:21 PM
...
...
...

Launching the app from Xcode 27 Beta 3 breaks:

ObjectIdentifier(0x0000000a22c245a0) init
ObjectIdentifier(0x0000000a22c245a0) start
ObjectIdentifier(0x0000000a22c245a0) deinit
ObjectIdentifier(0x0000000a22a2ca80) init
ObjectIdentifier(0x0000000a22c245a0) stop

This makes no sense to me. Why did my Task stop? Why was my Timer deallocated?

Here is a repro:

https://github.com/vanvoorden/2026-07-16

Please let me know if you have any ideas why this happened. Thanks!

Answered by DTS Engineer in 898245022

Hello @vanvoorden,

I believe this is related to the "Important" note found in TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder:

Paraphrasing:

Assigning to a @State property that already has an inline initial value is not supported and does not produce the expected behavior at runtime.

I'm reaching out to some folks to see if this needs to be clarified further in the technote, clearly you are not "assigning" a value in your example, but I still believe what you are doing is producing unexpected behavior with the State macro due to the same underlying reason.

I believe these prints are from your call to self.timer.start() in your App's init:

ObjectIdentifier(0x0000000a22c245a0) init ObjectIdentifier(0x0000000a22c245a0) start ObjectIdentifier(0x0000000a22c245a0) deinit

According to my theory, I don't believe the @State has actually been initialized at this point, so your call to start() actually initializes a Timer, starts it, and then as soon as that scope exits ARC destroys that Timer.

Then, a little bit later, the @State managed Timer is actually created, resulting in these logs:

ObjectIdentifier(0x0000000a22a2ca80) init ObjectIdentifier(0x0000000a22c245a0) stop

(well, the ordering and identifiers might be a little out of order in my post here, but you get the idea)

If you call start() in a .task on the view instead of in the App's init, I believe you will see what you expect.

-- Greg

This is still broken for me in Xcode 27 Beta 4. :(

Accepted Answer

Hello @vanvoorden,

I believe this is related to the "Important" note found in TN3211: Resolving SwiftUI source incompatibilities for State and ContentBuilder:

Paraphrasing:

Assigning to a @State property that already has an inline initial value is not supported and does not produce the expected behavior at runtime.

I'm reaching out to some folks to see if this needs to be clarified further in the technote, clearly you are not "assigning" a value in your example, but I still believe what you are doing is producing unexpected behavior with the State macro due to the same underlying reason.

I believe these prints are from your call to self.timer.start() in your App's init:

ObjectIdentifier(0x0000000a22c245a0) init ObjectIdentifier(0x0000000a22c245a0) start ObjectIdentifier(0x0000000a22c245a0) deinit

According to my theory, I don't believe the @State has actually been initialized at this point, so your call to start() actually initializes a Timer, starts it, and then as soon as that scope exits ARC destroys that Timer.

Then, a little bit later, the @State managed Timer is actually created, resulting in these logs:

ObjectIdentifier(0x0000000a22a2ca80) init ObjectIdentifier(0x0000000a22c245a0) stop

(well, the ordering and identifiers might be a little out of order in my post here, but you get the idea)

If you call start() in a .task on the view instead of in the App's init, I believe you will see what you expect.

-- Greg

If you call start() in a .task on the view instead of in the App's init, I believe you will see what you expect.

So this is not a bad idea… but unfortunately does not quite get me what I would have hoped for.

If I inspect the SwiftUI view lifecycle here:

@main
struct StateDemoApp: App {
  @State private var timer = Timer()
  
  init() {
    print("StateDemoApp init")
    self.timer.start()
  }
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onAppear {
          print("StateDemoApp onAppear")
        }
        .task {
          print("StateDemoApp task")
        }
    }
  }
}

and then put additional print logs in ContentView itself I get the init log from StateDemoApp called before the init log from ContentView and the onAppear and task logs from StateDemoApp called after the onAppear and task logs from ContentView. Ideally I would like to start this Timer before ContentView might be doing something else with global state that might break if the Timer did not start first.

I can also workaround by moving the Timer construction to init:

@main
struct StateDemoApp: App {
  @State private var timer: Timer
  
  init() {
    print("StateDemoApp init")
    self.timer = Timer()
    self.timer.start()
  }
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

If this works correctly from Xcode 26 and Xcode 27 then this might be best for me.

Ah sure, if you need it to start early, put it in the App init, (just be sure that the work done in there isn’t heavy, as it will delay UI presentation).

Also, I checked with a colleague and they agreed that my theory above is correct, please file a bug report for more clarity around this anti-pattern in the docs, and then post your Feedback number here so that I can follow-up with it.

Thanks!

— Greg

I checked with a colleague and they agreed that my theory above is correct

So if the expected behavior is:

According to my theory, I don't believe the @State has actually been initialized at this point, so your call to start() actually initializes a Timer, starts it, and then as soon as that scope exits ARC destroys that Timer.

Then, a little bit later, the @State managed Timer is actually created, resulting in these logs:

ObjectIdentifier(0x0000000a22a2ca80) init ObjectIdentifier(0x0000000a22c245a0) stop

And the "bad" example from TN3211 is:

struct ContentView: View {
    @State private var counter: Int = 0


    init() {
        self.counter = 42
    }
}

I kind of start to think here that if State has an inline initial value and the developer attempts to set a value in init we want to either crash at runtime or at least try to display one of those purple warnings in Xcode.

Something other than just sort of failing silently might be a very good idea here considering all the code that is probably out there either using this example from TN3211 or my example.

SwiftUI.State macro overreleasing object from Xcode 27 Beta 3?
 
 
Q