Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactiveCocoa retain cycle when passing closure as parameter

I'm using ReactiveCocoa in Swift as followed:

registerButton?.rac_signalForControlEvents(UIControlEvents.TouchUpInside).subscribeNextAs(registerButtonTapped)

private func registerButtonTapped(button: UIButton){
    // Method here
}

Which creates a retain cycle.

I know the solution is as followed:

registerButton?.rac_signalForControlEvents(UIControlEvents.TouchUpInside).subscribeNextAs({ [weak self] (button:UIButton) in
    self?.registerButtonTapped(button)
})

But this enforces me to use the subscribeNextAs block and not the nicer oneliner passing the method.

Any idea how to use the oneliner without a retain cycle?

like image 729
Antoine Avatar asked Jun 30 '15 14:06

Antoine


1 Answers

OK, so the answer linked to by Jakub Vano is great, but it's not quite general-purpose. It constrains you to use a function that has no parameters and returns Void. Using Swift generics, we can be a little smarter about this to use functions that accept any parameters and use any return type.

So the first thing is that, for weak relationships, you must use an object. Structs can't be referred to weakly. So we'll constrain the instance type to be AnyObject, a protocol that all classes conform to. Our function declaration looks like the following:

func applyWeakly<Type: AnyObject, Parameters, ReturnValue>(instance: Type, function: (Type -> Parameters -> ReturnValue)) -> (Parameters -> ReturnValue?)

So the function takes an instance, and a function that takes an instance and returns a Parameters -> ReturnValue function. Remember, closures and functions in Swift are interchangeable. Neat!

Note that we're having to return an optional ReturnValue because the instance might become nil. I'll address later how to get around this.

OK so now you need to know a really neat trick: Swift instance methods are actually just curried class methods, which is perfect for our needs 🎉

So now we can call applyWeakly with the class function that returns an instance function when you call it with an instance. The applyWeakly implementation is fairly straightforward.

func applyWeakly<Type: AnyObject, Parameters, ReturnValue>(instance: Type, function: (Type -> Parameters -> ReturnValue)) -> (Parameters -> ReturnValue?) {
    return { [weak instance] parameters -> ReturnValue? in
        guard let instance = instance else { return nil }
        return function(instance)(parameters)
    }
}

Super. So how would you use this? Let's take a very simple example. We have a class with a parameter that holds a closure, and that closure will refer to its own instance method (this is just demonstrating the reference cycle problem – your question involves two objects but I'm simplifying down to one).

class MyClass {
    var closure: (String -> String?)!

    func doThing(string: String) -> String {
        return "hi, \(string)"
    }

    init() {
        closure = doThing // WARNING! This will cause a reference cycle
        closure = applyWeakly(self, function: MyClass.doThing)
    }
}

We have to use an implicitly-unwrapped optional for the closure type so we can refer to an instance method in our init function. That's ok, just a limitation of this example.

So this is great, and will work, but it's kind of icky that our closure type is String -> String? but our doThing type is String -> String. In order to return a non-optional string, we need to either force-unwrap an optional (😱) or use unowned.

Unowned references are like weak ones, except they're non-zeroing. That means if the objects is deallocated and you use a reference to it, your app will explode. In your case, though, it's applicable. Here's more info on unowned vs weak.

The applyUnowned function would look like this:

func applyUnowned<Type: AnyObject, Parameters, ReturnValue>(instance: Type, function: (Type -> Parameters -> ReturnValue)) -> (Parameters -> ReturnValue) {
    return { [unowned instance] parameters -> ReturnValue in
        return function(instance)(parameters)
    }
}

I hope that clarifies things – happy to answer any follow-up questions. This problem has been in the back of my mind for a while, and I'm glad to have finally recorded my thoughts down.

like image 166
Ash Furrow Avatar answered Nov 20 '22 19:11

Ash Furrow