General
The Sneaky Coordinator Leak in UIViewRepresentable
0 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
PreviousThe Sky Becomes a Data Center: The Race to Put AI in OrbitNextThe Software Engineer Interview Roadmap 2026 (with Resources)
Related posts
Best Early Black Friday Espresso Machine Deals (2026)
0Wired
I Read the Clankers' Code
0Hacker News
A Right-Wing Think Tank Has a Plan to Prevent Democrats From Being Seated in Congress
0Wired
White Noise Machines Worthy of Your Nightstand in 2026: Snooz, Hatch, and More
0Wired
F1 in Mexico City: Yet another Ferrari fiasco, but that’s not all
0Ars Technica
China publicly discloses plan for a lunar landing within three years
0Ars Technica