General
The Sneaky Coordinator Leak in UIViewRepresentable
Suprie Dev.to (EN Zone)
2 views
Let's talk a little about a memory leak I recently found while working with UIViewRepresentable.
The issue happened in a custom UIKit view wrapped inside SwiftUI. At first, nothing looked suspicious. The view worked fine. But after navigating in and out of the screen a few times, the Memory Graph Debugger showed that the Coordinator instances kept increasing.
Here is a simplified version of the setup:
import SwiftUI
import UIKit
final class CallbackButton: UIButton {
var onTap: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func handleTap() {
onTap?()
}
deinit {
print("CallbackButton deinit")
}
}
struct LeakyCallbackButtonView: UIViewRepresentable {
@Binding var title: String
func makeCoordinator() -> Coordinator {
Coordinator(title: $title)
}
func makeUIView(context: Context) -> CallbackButton {
let button = CallbackButton(type: .system)
button.setTitle(title, for: .normal)
// Looks harmless, but this strongly captures the coordinator.
button.onTap = context.coordinator.handleTap
return button
}
func updateUIView(_ uiView: CallbackButton, context: Context) {
uiView.setTitle(title, for: .normal)
}
final class Coordinator {
@Binding var title: String
init(title: Binding<String>) {
self._title = title
print("Coordinator init")
}
func handleTap() {
title = "Tapped at \(Date())"
}
deinit {
print("Coordinator deinit")
}
}
}
The suspicious line is this one:
button.onTap = context.coordinator.handleTap
It looks like we are only passing a function, but an instance method reference still needs to retain its instance. In this case, the closure stored button.onTap strongly retains the Coordinator.
In a normal UIKit view controller, many people would immediately fix this with [weak self]. But this is UIViewRepresentable.
UIViewRepresentable is a struct, so self is a value type. You cannot use [weak self] the same way you would inside a UIViewController, because weak only applies to class-bound references.
The fix is to weakly capture the Coordinator instead:
struct FixedCallbackButtonView: UIViewRepresentable {
@Binding var title: String
func makeCoordinator() -> Coordinator {
Coordinator(title: $title)
}
func makeUIView(context: Context) -> CallbackButton {
let button = CallbackButton(type: .system)
button.setTitle(title, for: .normal)
button.onTap = { [weak coordinator = context.coordinator] in
coordinator?.handleTap()
}
return button
}
func updateUIView(_ uiView: CallbackButton, context: Context) {
uiView.setTitle(title, for: .normal)
}
static func dismantleUIView(_ uiView: CallbackButton, coordinator: Coordinator) {
uiView.onTap = nil
}
final class Coordinator {
@Binding var title: String
init(title: Binding<String>) {
self._title = title
print("Coordinator init")
}
func handleTap() {
title = "Tapped at \(Date())"
}
deinit {
print("Coordinator deinit")
}
}
}
There are two fixes here:
button.onTap = { [weak coordinator = context.coordinator] in
coordinator?.handleTap()
}
This prevents the closure from strongly retaining the Coordinator.
And:
static func dismantleUIView(_ uiView: CallbackButton, coordinator: Coordinator) {
uiView.onTap = nil
}
This clears the callback when SwiftUI dismantles the UIKit view.
My takeaway:
// UIKit / UIViewController
{ [weak self] in
self?.doSomething()
}
// UIViewRepresentable
{ [weak coordinator = context.coordinator] in
coordinator?.doSomething()
}
UIViewRepresentable is a value-type wrapper, but the Coordinator is the reference object that usually carries UIKit-style lifetime and callback behavior. So when a UIKit view stores a closure, be careful not to accidentally retain the Coordinator through that closure.
Just a note for myself.
Read original: https://dev.to/suprie_32/the-sneaky-coordinator-leak-in-uiviewrepresentable-h0p
← Previous
Gemini Often Won't Search the Web — and Won't Tell You It Didn't
Next →
I Want More Coding Agents to Work Like This
Related
Implementing the Karatsuba multiplication algorithm on an 8-bit computer
General
9
Reddit r/programming
Building Galactic Idle: Our First Public Balancing Pass
General
7
Dev.to (EN Zone)
Can you offer payroll under your own brand without building it yourself?
General
6
Reddit r/webdev
I found a visual bug on a major government website, is it worth writing a blogpost over the technical process/bugfix involved to fix it?
General
7
Reddit r/webdev
Comments0
No comments yet — be the first