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()
@nilsgrabenhorst
Copy link

Hi Andreas,
in this specific case there is a workaround by currying the method to be captured.

class CaptureClass {
    var action: ((CaptureClass) -> (Int) -> Void)?

    init() {
        // If you curry the method it won't capture self at all.
        // It is now a function that takes `self` as a parameter,
        // returning a function with the same signature as the original method.
        addAction(CaptureClass.takesInt)
    }

    // Unfortunately this only works if the method is a member of `CaptureClass`.
    func addAction(_ f: @escaping (CaptureClass) -> (Int) -> Void) {
        action = f
    }

    func takesInt(_ i: Int) {}

    func performAction(_ i: Int) {
        action?(self)(i)
    }

    deinit {
        print("👍 K THX BYE")
    }
}

var test = CaptureClass()
test = CaptureClass()

If the function is not a member, you'll have to end up with two
arguments when setting the action. Here I do that in the initializer. It's not that great at the call site though.

class Action<T: AnyObject> {
    var action: ((Int) -> Void)?

    init(on actor: T, action: @escaping (T) -> (Int) -> Void) {
        // either store action and actor in separate properties (actor in a weak var),
        // or wrap it in a closure capturing actor weakly like this:
        self.action = { [weak actor] (i: Int) in
            guard let actor = actor else { return }
            action(actor)(i)
        }
    }

    func perform(_ i: Int) {
        action?(i)
    }
}

class TestActor {
    func bark(times: Int) {
        print((0..<times).map { _ in "woof!" })
    }
    deinit {
        print("Oh noes  ☠️")
    }
}

var testActor: TestActor? = TestActor()
let action  = Action(on: testActor!, action: TestActor.bark(times:))

action.perform(3)
testActor = nil
action.perform(3)

    // prints:
    // ["woof!", "woof!", "woof!"]
    // Oh noes  ☠️

I don't have any better idea, sorry...

@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