8

In one of my classes I use an array of delegates (the class is a singleton). This is causing an retain cycle. I know I can avoid the retain cycle when I use only one delegate by making the delegate weak. But this is not working for my array of delegates.

How can I avoid this retain cycle.

Example:

protocol SomeDelegate: class {
    func someFunction()
}

My Class

class SomeClass {
    // This class is a singleton!
    static let sharedInstance = SomeClass()

    var delegates = [SomeDelegate]()   // this is causing a retain cycle
    weak var delegate: SomeDelegate?   // this is ok.

    ... other code...
}
dfrib
  • 70,367
  • 12
  • 127
  • 192
Leontien
  • 612
  • 5
  • 22
  • The answer of that questions refers to classes that conform to AnyObject. I have an array of protocol objects that do not conform to AnyObject. – Leontien Feb 25 '17 at 15:26
  • Protocols cannot conform to themselves so the linked answer cannot be used right off the bat (we might think of replacing the `AnyObject` constraint with `SomeDelegate`) when using a protocol itself as the wrapped object you'd like to hold a weak link to. You could, however, look at [the following Q&A](http://stackoverflow.com/questions/32807948/using-as-a-concrete-type-conforming-to-protocol-anyobject-is-not-supported), implementing a custom container to allow holding `class` constrained protocol instances by weak reference. – dfrib Feb 25 '17 at 15:37
  • The link of dfri helped. I used the answer of Kyle Redfearn, because I don't need a generic solution. I will put my solution in a answer of this question, so it will be easier to find for others. – Leontien Feb 25 '17 at 18:16
  • Since you've now added your answer, I've now re-closed this Q&A, linking it to the the dupe target we found most appropriate (not identical question, but the solutions are the same/very similar). Side note: in case you didn't know, you may accept your own answers. – dfrib Feb 25 '17 at 19:47

2 Answers2

13

The problem is that weakDelegates is a strong reference and its reference to its elements of type WeakDelegateContainer is a strong reference.

Your situation is why the class NSHashTable exists. Initialize using weakObjects(). This will give you a set of ARC-weak references, each of which will be nilified and removed when the referenced object goes out of existence (with no need for any extra bookkeeping on your part, and no need for your WeakDelegateContainer type).

Your set will have to be typed as holding AnyObject, but you can easily mediate to ensure that you are supplying and retrieving SomeDelegate-conformant objects:

let list = NSHashTable<AnyObject>.weakObjects()
func addToList(_ obj:SomeDelegate) {
    list.add(obj)
}
func retrieveFromList(_ obj:SomeDelegate) -> SomeDelegate? {
    if let result = list.member(obj) as? SomeDelegate {
        return result
    }
    return nil
}
func retrieveAllFromList() -> [SomeDelegate] {
    return list.allObjects as! [SomeDelegate]
}

The function retrieveAllFromList() lists only objects that still exist. Any object that has gone out existence has been changed to nil in the NSHashTable and is not included in allObjects. That is what I mean by "no extra bookkeeping"; the NSHashTable has already done the bookkeeping.

Here is code that tests it:

func test() {
    let c = SomeClass() // adopter of SomeDelegate
    self.addToList(c)
    if let cc = self.retrieveFromList(c) {
        cc.someFunction() 
    }
    print(self.retrieveAllFromList()) // one SomeClass object
    delay(1) {
        print(self.retrieveAllFromList()) // empty
    }
}

Alternatively, you can use NSPointerArray. Its elements are pointer-to-void, which can be a little verbose to use in Swift, but you only have to write your accessor functions once (credit to https://stackoverflow.com/a/33310021/341994):

let parr = NSPointerArray.weakObjects()
func addToArray(_ obj:SomeDelegate) {
    let ptr = Unmanaged<AnyObject>.passUnretained(obj).toOpaque()
    self.parr.addPointer(ptr)
}
func fetchFromArray(at ix:Int) -> SomeDelegate? {
    if let ptr = self.parr.pointer(at:ix) {
        let obj = Unmanaged<AnyObject>.fromOpaque(ptr).takeUnretainedValue()
        if let del = obj as? SomeDelegate {
            return del
        }
    }
    return nil
}

Here is code to test it:

    let c = SomeClass()
    self.addToArray(c)
    for ix in 0..<self.parr.count {
        if let del = self.fetchFromArray(at:ix) {
            del.someFunction() // called
        }
    }
    delay(1) {
        print(self.parr.count) // 1
        for ix in 0..<self.parr.count {
            if let del = self.fetchFromArray(at:ix) {
                del.someFunction() // not called
            }
        }
    }

Interestingly, after our SomeClass goes out of existence, our array's count remains at 1 — but cycling through it to call someFunction, there is no call to someFunction. That is because the SomeClass pointer in the array has been replaced by nil. Unlike NSHashTable, the array is not automatically purged of its nil elements. They do no harm, because our accessor code has guarded against error, but if you would like to compact the array, here's a trick for doing it (https://stackoverflow.com/a/40274426/341994):

    self.parr.addPointer(nil)
    self.parr.compact()
Community
  • 1
  • 1
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    I tried this. I can declare 'var test = NSHashTable.weakObjects()', but when I declare 'var test = NSHashTable.weakObjects()' I get the error 'Using SomeDelegate as a concrete type conforming to protocol AnyObject is not supported'. I think the problem is that most solutions do not work for protocols. The given answer with the WeakDelegateContainer works for protocols. In this case the references will also be nilified, but I will clean my array for nilified references. – Leontien Feb 25 '17 at 19:09
  • Good response. Yes, I fell into the same trap pointed out in the comments, didn't I? – matt Feb 25 '17 at 19:20
  • NSPointerArray is new for me. As I understand it, I need a UnsafeMutableRawPointer to add to the weak objects array. I have trouble understanding how I get an UnsafeMutableRawPointer to my delegate object. – Leontien Feb 26 '17 at 15:52
  • Sorry twice. Modified my answer to recommend NSHashTable, and added code showing how to use it to store and retrieve SomeDelegate objects. – matt Feb 26 '17 at 17:03
  • Nice! Thanks for the explanation and the example. I changed my code and this a working fine and a cleaner solution! – Leontien Feb 26 '17 at 19:38
  • Added an explanation of how to use NSPointerArray, just in case it proves useful in the future. But I think NSHashTable is better for your purposes; you don't need ordering or duplicates. – matt Feb 26 '17 at 20:19
  • Indeed I stick to NSHashTable. I find that solution far more readable and understandable. – Leontien Feb 26 '17 at 20:26
2

I found the solution in Using as a concrete type conforming to protocol AnyObject is not supported. All credits to Kyle Redfearn.

My solution

protocol SomeDelegate: class {
    func someFunction()
}

class WeakDelegateContainer : AnyObject {
    weak var weakDelegate: SomeDelegate?
}

class SomeClass {
    // This class is a singleton!
    static let sharedInstance = SomeClass()

    fileprivate var weakDelegates = [WeakDelegateContainer]()

    func addDelegate(_ newDelegate: SomeDelegate) {
        let container = WeakDelegateContainer()
        container.weakDelegate = newDelegate
        weakDelegates.append(container)
    }

    func removeDelegate(_ delegateToRemove: SomeDelegate) {
        // In my case: SomeDelegate will always be of the type UIViewController
        if let vcDelegateToRemove = delegateToRemove as? UIViewController {
            for i in (0...weakDelegates.count - 1).reversed() {
                if weakDelegates[i].weakDelegate == nil {
                    // object that is referenced no longer exists
                    weakDelegates.remove(at: i)
                    continue
                }

                if let vcDelegate = weakDelegates[i].weakDelegate as? UIViewController {
                    if vcDelegate === vcDelegateToRemove {
                        weakDelegates.remove(at: i)
                    }
                }
            }
        }
    }

    ... other code ...
}
Community
  • 1
  • 1
Leontien
  • 612
  • 5
  • 22