Skip to content

Instantly share code, notes, and snippets.

@andreashanft
Created April 26, 2020 14:02
Show Gist options
  • Save andreashanft/53590d074ec54860deb51e5e168d5cc2 to your computer and use it in GitHub Desktop.
Save andreashanft/53590d074ec54860deb51e5e168d5cc2 to your computer and use it in GitHub Desktop.
Method Reference Issue
class CaptureClass {
var action: ((Int) -> Void)?
init() {
// Passing a method reference to an escaping closure implicitly captures self
// and in this case causes a refernce cycle!
addAction(takesInt)
// To avoid memory leak:
addAction { [weak self] in self?.takesInt($0) }
}
func addAction(_ f: @escaping (Int) -> Void) {
// store a strong ref to f, which might include a strong ref to a captured self
action = f
}
func takesInt(_ i: Int) {}
deinit {
print("👍")
}
}
var test = CaptureClass()
test = CaptureClass()
@andreashanft
Copy link
Author

Hi Nils,
Thank you very much for your thoughts and great workarounds. Unfortunately I am not sure if they actually can help in the specific case I am facing using Combine.

I have attached a pseudo-code example and also added an idea for another workaround which unfortunately is not working:

import UIKit
import Combine

struct Presentation {
    let title: String
}

final class ViewModel {
    @Published private (set) var presentation = Presentation(title: "Some Title")
}

final class Proxy<T: AnyObject> {
    unowned let o: T

    init(original: T) {
        self.o = original
    }
}

final class NotReallyAViewController {
    let viewModel = ViewModel()
    var subscription: AnyCancellable?
    lazy var proxy = Proxy(original: self)

    func start() {
        // Causes retain cycle
        //subscription = viewModel.$presentation.sink(receiveValue: presentationDidChange)

        // All good but not so nice syntax
        subscription = viewModel.$presentation.sink(receiveValue: { [unowned self] in self.presentationDidChange(presentation: $0)})

        // Still a retain cycle
        //subscription = viewModel.$presentation.sink(receiveValue: proxy.o.presentationDidChange)
    }

    func presentationDidChange(presentation: Presentation) {
        print("presentationDidChange: \(presentation.title)")
    }

    deinit {
        print("☠️")
    }
}

var test: NotReallyAViewController? = .init()
test?.start()
test = nil

The proxy workaround was inspired by behaviour I observed in my code: I have a publisher on a view that is connected to a method on the view model, the subscription is stored in the view controller. This does not seem to cause a retain cycle. :thinking_face:

view.button.onTapPublisher.sink(receiveValue: viewModel.backAction)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment