5

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?

Antoine
  • 23,526
  • 11
  • 88
  • 94
  • I would argue that this is bit of an anti pattern: One of the advantages of RAC is to move from callbacks to more concise code, and here you are doing the exact opposite. – Jakub Vano Jul 01 '15 at 07:34
  • Totally agree, however, I find myself using callbacks at multiple locations. Passing a method sometimes makes sense, although the example I'm giving here is quite bad. For instance, a mapping method is something I share across signals. – Antoine Jul 01 '15 at 07:39
  • Okay, reuse of this kind makes sense to me as well. Have you seen solutions proposed here? http://stackoverflow.com/questions/25613783/make-self-weak-in-methods-in-swift – Jakub Vano Jul 01 '15 at 07:58
  • The answer linked to by @JakubVano looks Probably how I would approach this. – Ash Furrow Dec 07 '15 at 22:27

1 Answers1

12

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.

Community
  • 1
  • 1
Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
  • Thanks Ash, I couldn't wish a better clarified answer! Seems to be the best solution to what I want to achieve, although I'm not sure if this will make things more readable and better then just writing down the closure (taking my example). I'll use your example in future implementations and will check out what suits the best. Thanks! – Antoine Dec 10 '15 at 09:08