Make self weak in methods in Swift
Asked Answered
E

5

27

I have a Swift class that needs to store a table of its own methods. Unfortunately this is causing a reference cycle, because its table retains references to self via the methods it stores.

Example leaky code below:

typealias Callback = ()->()

class CycleInducingClass : NSObject {
    var myCallbacks = [Callback]()  

    override init() {
        super.init()
        myCallbacks.append(myInternalFunction)
    }

    func myInternalFunction() {
        NSLog("lolol: %d", self.myCallbacks.count)
    }
}

The only solution I've found so far is to instead do this:

myCallbacks.append({[unowned self] in self.myInternalFunction()})

That's pretty ugly, and prone to error. Any better ideas? Is there some trick for making the function references themselves be weak? i.e. to make the myCallbacks array of type myCallbacks : [WeakCallback]() or something? As far as I can tell I can't even build a convenience function weaken as syntactic sugar over the ugly closure wrapper above.

Epilate answered 1/9, 2014 at 22:59 Comment(2)
how about adding a parameter to myInternalFunction? that could be declared weak.....not pretty thoughAlasteir
I've also tried just making all my internal functions closures, i.e. let myInternalFunction = {[unowned self] in ...} that works, but is also rather ugly.Epilate
J
13

You can certainly build a function for this. I don't know if it makes it dramatically better, but it is less error-prone.

func methodPointer<T: AnyObject>(obj: T, method: (T) -> () -> Void) -> (() -> Void) {
  return { [unowned obj] in method(obj)() }
}
...
myCallbacks.append(methodPointer(self, CycleInducingClass.myInternalFunction))

Alternately, you could manage your callbacks as method pointers:

typealias Callback = (CycleInducingClass) -> () -> Void
...
myCallbacks.append(CycleInducingClass.myInternalFunction)

In that case, you'd need to pass self when you called them (which may be fine if you don't actually do this a lot):

self.myCallbacks[0](self)()

All of this is based on the fact that a method on type T with signature (input) -> (output) is equivalent to a function with the signature (T) -> (input) -> (output).

In case you're curious (I was), overriding works correctly in this case. So if you subclass CycleInducingClass and override myInternalFunction, the correct version will be called. (That actually surprises me a little, and I don't yet know exactly why it works, but it does.)

EDIT: Here's the answer to that: https://devforums.apple.com/message/1036509#1036509

Journalize answered 8/9, 2014 at 13:30 Comment(2)
Ah interesting, I didn't realize that you statically access methods and they would give you a function mapping instances of your class to the class method. That's the ingredient I was missing. Thanks!Epilate
What if the function takes parameters? How would that work?Amaral
K
3

Robs answer worked for me. I did refactor it to be a little more OO though so I thought I would share it here in case it helps someone else:

public protocol WeakCallback{ 
    func invoke()
}

public class WeakCallbackInstance<T: AnyObject> : WeakCallback{
    private let callback: ()->Void
    private weak var target: T?

    public init(target: T, action: (T)->()->Void){

        self.target = target
        callback = { [weak target] in
            action(target!)()
        }
    }

    public func invoke(){
        callback()
    }
}

class ExampleUsage{

    func usage(){
        var callbacks = [WeakCallback]()

        let one = WeakCallbackInstance(target: DummyCallbackOne(), action:DummyCallbackOne.callbackOne)
        let two = WeakCallbackInstance(target: DummyCallbackTwo(), action:DummyCallbackTwo.callbackTwo)

        callbacks.append(one)
        callbacks.append(two)
        callbacks.first?.invoke()
    }
}

class DummyCallbackOne{
    func callbackOne(){
    }
}

class DummyCallbackTwo{
    func callbackTwo(){
    }
}
Kazantzakis answered 22/1, 2015 at 14:40 Comment(0)
B
0

In Swift 4 (I am not sure when the syntax became available), simply do { [weak self] (params) in } to make self weak. It basically is to [unowned self], what Self? is to Self!. The compiler even requires self?.foo instead of simply self.foo.

Bonn answered 28/1, 2018 at 10:18 Comment(0)
R
0

With Swift 5.2, callAsFunction allows for nice syntax for this, until argument labels come into play.

public struct WeakMethod<Reference: AnyObject, Input, Output> {
  public init(
    reference: Reference?,
    method: @escaping Method
  ) {
    self.reference = reference
    self.method = method
  }

  public weak var reference: Reference?
  public var method: Method
}

public extension WeakMethod {
  struct ReferenceDeallocatedError: Error { }

  typealias Method = (Reference) -> (Input) -> Output

  /// - Throws: ReferenceDeallocatedError
  func callAsFunction(_ input: Input) throws -> Output {
    guard let reference = reference
    else { throw ReferenceDeallocatedError() }

    return method(reference)(input)
  }
}

public extension WeakMethod where Input == () {
  init(
    reference: Reference?,
    method: @escaping (Reference) -> () -> Output
  ) {
    self.reference = reference
    self.method = { reference in
      { _ in method(reference)() }
    }
  }

  /// - Throws: ReferenceDeallocatedError
  func callAsFunction() throws -> Output {
    try self( () )
  }
}
final class WeakMethodTestCase: XCTestCase {
  func test_method() throws {
    var reference: Reference? = Reference()

    let assign1234 = WeakMethod(reference: reference, method: Reference.assign1234)

    try assign1234()
    XCTAssertEqual(reference?.property, 1234)

    reference = nil
    XCTAssertThrowsError( try assign1234() ) {
      XCTAssert($0 is WeakMethod<Reference, (), Void>.ReferenceDeallocatedError)
    }
  }

  func test_closure_noParameters() throws {
    var reference: Reference? = Reference()

    let assign1234 = WeakMethod(reference: reference) {
      reference in { reference.property = 1234 }
    }

    try assign1234()
    XCTAssertEqual(reference?.property, 1234)

    reference = nil
    XCTAssertThrowsError( try assign1234() ) {
      XCTAssert($0 is WeakMethod<Reference, (), Void>.ReferenceDeallocatedError)
    }
  }

  func test_closure_1Parameter() throws {
    var reference: Reference? = Reference()

    let assign = WeakMethod(reference: reference) {
      reference in { reference.property = $0 }
    }

    try assign(1234)
    XCTAssertEqual(reference?.property, 1234)

    reference = nil
    XCTAssertThrowsError( try assign(1234) ) {
      XCTAssert($0 is WeakMethod<Reference, Int, Void>.ReferenceDeallocatedError)
    }
  }
}

private final class Reference {
  var property = 1

  func assign1234() {
    property = 1234
  }
}

Unfortunately, as you keep adding parameters, you'll need to keep adding initializer+method pairs, like so:

init<Input0, Input1>(
  reference: Reference?,
  method: @escaping (Reference) -> (Input0, Input1) -> Output
)
where Input == (Input0, Input1) {
  self.reference = reference
  self.method = { reference in
    { method(reference)($0.0, $0.1) }
  }
}

/// - Throws: ReferenceDeallocatedError
func callAsFunction<Input0, Input1>(_ input0: Input0, _ input1: Input1) throws -> Output
where Input == (Input0, Input1) {
  try self( (input0, input1) )
}
Reliable answered 24/3, 2020 at 21:40 Comment(0)
T
-1

wrapped no param function with block

myCallbacks.append({ [unowned self] in self.myInternalFunction() })

wrapped param function with block

myCallbacks.append({ [unowned self] page in self.reloadData(page: page) })
Thaw answered 4/1, 2018 at 3:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.