Retain cycle in SwiftUI view struct

The speaker mentions there is "less risk of creating a retain cycle in when capturing self in a closure within a value type", such as a SwiftUI view struct.

Can you elaborate on when this could actually occur?

Thanks!
Answered by Developer Tools Engineer in 615145022
You can still create retain cycles indirectly. For example, suppose you write something like:

Code Block swift
class MyObject {
var function = { () }
}
struct MyStruct {
let obj = MyObject()
func structMethod() { ... }
init() {
obj.function = { structMethod() }
}
}

The closure will capture self, which retains obj, which retains the closure, so this forms a retain cycle. Most examples like this are pretty contrived, which is why we decided to not require self. before bar() in that closure, but they can happen.
Accepted Answer
You can still create retain cycles indirectly. For example, suppose you write something like:

Code Block swift
class MyObject {
var function = { () }
}
struct MyStruct {
let obj = MyObject()
func structMethod() { ... }
init() {
obj.function = { structMethod() }
}
}

The closure will capture self, which retains obj, which retains the closure, so this forms a retain cycle. Most examples like this are pretty contrived, which is why we decided to not require self. before bar() in that closure, but they can happen.
One follow up...

Above, how would you break / avoid the cycle? You can't create a weak reference to MyStruct in the closure because it is not a reference type. Might just be don't get yourself in this situation :)

Thanks



I was wondering about this, too, and would also be interested in how this could, in theory, be solved. I say "in theory", because in practice it is probably a bad idea to have reference types as properties inside value types in the first place.

My guess is that there's really no way as a struct does not provide a deinit method, is that right?

That being said, does the example as given compile in Swift 5.3? I didn't have the chance to set up a test environment for me yet (sorry), but under 5.2 (where I use an explicit self. of course) this throws a

Escaping closure captures mutating 'self' parameter

for me anyway. I'm having a hard time constructing an example for such a retain cycle on the top of my head, what am I overlooking? Hm...

I'm seeing this in SwiftUI views that hold timers with closure handlers that reference self.

The closure clearly needs the self, but can't take the struct view as a weak.

Do I need to move the properties being accessed to the VM?

When I look at swiftUI samples that use timer, generally they are using combine. How does this avoid the issue?

struct MyView {
   private func startDismissTimer() {
      let oneEightyTimer: Float =  7
      // Schedule the timer to dismiss the immersive space after X seconds
      vm.timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(oneEightyTimer), repeats: false) { _ in
         Task {
            do {
               await viewScreenEntity.setOpacity(0.0, animated: true, duration: 1.0)
               await dismissImmersiveSpace()
               Constants.fadeTheMainWindow = false
            }
         }
      }
   }

Foundation’s Timer API has a deep Objective-C heritage and it doesn’t play well with modern constructs, including SwiftUI and Swift concurrency [1]. How you deal with that depends on the context:

  • If the timer is tied to the view, you can certainly move it down into the view model.

  • In some cases the timer is actually tied to the model, and it’s fine to move it down there too.

However, it might be better to just move away from timer completely:

  • If the timer is being used to drive animations, lean into SwiftUI’s dedicated animation infrastructure.

  • Or adopt Swift concurrency. It doesn’t have a timer per se. The standard approach is to use one of its Task.sleep(_:) routines.

try await Task.sleep(for: .seconds(1))

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Swift concurrency relies on being able to check isolation at compile time. Foundation timers are scheduled on the run loop, which isn’t something the compiler has a model for.

Retain cycle in SwiftUI view struct
 
 
Q