Phlippie Bosman
Phlippie Bosman

Reputation: 5710

Capture a method weakly

Note: This question is basically the same as this one, but for Swift 4 or 5.

Say I have a class that captures a closure:

class CallbackHolder {
  typealias Callback = (String) -> Void

  var callback: Callback

  init(_ callback: @escaping Callback) {
    self.callback = callback
  }

  func useCallback() {
    self.callback("Hi!")
  }
}

This class simply holds a callback in a variable, and has a function that uses that callback.

Now, say I have a client class that owns such a callback holder. This class wants the callback holder to call one of its methods as the callback:

class Client {
  func callback(string: String) {
    print(string)
  }

  lazy var callbackOwner = CallbackOwner(callback: callback)

  deinit {
    print("deinit")
  }
}

This class has a callback, a callback owner that calls that callback, and a deinit that prints something so that we know whether we have a retain cycle (no deinit = retain cycle).

We can test our setup with the following test function:

func test() {
   Client().callbackOwner.useCallback()
}

We want the test function to print both Hi! and deinit, so that we know that the callback works, and that the client does not suffer from a retain cycle.

The above Client implementation does in fact have a retain cycle -- passing the callback method to the callback owner causes the owner to retain the client strongly, causing a cycle.

Of course, we can fix the cycle by replacing

  lazy var callbackOwner = CallbackOwner(callback: callback)

with

  lazy var callbackOwner = CallbackOwner(callback: { [weak self] in 
    self?.callback($0) 
  })

This works, but:

So I am looking for a better way. I would like to capture the callback weakly at the CallbackOwner, not at the client. That way, new clients don't have to be aware of the danger of the retain cycle, and they can use my CallbackOwner in the most intuitive way possible.

So I tried to change CallbackOwner's callback property to

  weak var callback: Callback?

but of course, Swift only allows us to capture class types weakly, not closures or methods.

At the time when this answer was written, there did not seem to be a way to do achieve what I'm looking for, but that was over 4 years ago. Has there been any developments in recent Swift versions that would allow me to pass a method to a closure-capturing object without causing a retain cycle?

Upvotes: 1

Views: 256

Answers (1)

JeremyP
JeremyP

Reputation: 86651

Well one obvious way to do this would be to not hold a reference to CallbackHolder in Client i.e.

class Client {
    func callback(string: String) {
        print(string)
    }

    var callbackOwner: CallbackHolder { return CallbackHolder(callback) }

    deinit {
        print("deinit")
    }
}

In the above case, I'd probably make CallbackHolder a struct rather than a class.

Personally, I don't see any value in wrapping the callback in a class at all. Just pass the callback around, or even Client. Maybe make a protocol for it to adhere to

protocol Callbackable
{
    func callback(string: String)
}

extension Callbackable
{
    func useCallback() {
        self.callback(string: "Hi!")
    }
}


class Client: Callbackable {
    func callback(string: String) {
        print(string)
    }

    deinit {
        print("deinit")
    }
}

func test(thing: Callbackable) {
    thing.useCallback()
}

test(thing: Client())

Upvotes: 1

Related Questions