Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid a retain cycle when using an array of delegates in Swift [duplicate]

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...
}
like image 871
Leontien Avatar asked Feb 25 '17 14:02

Leontien


People also ask

How do I stop retaining cycle in Swift?

We can solve this in two ways. First, we can use [unowned self]: Now the closure doesn't have a strong reference anymore. Just be careful when using [unowned self] since that, if the object has already been deallocated when the closure is called, a crash will occur.

Why We Use weak for delegate in Swift?

However, using a strong delegate property in a , b will never get deallocated since a is holding on to b strongly. Using a weak reference, as soon as b loses the strong reference from c , b will dealloc when c deallocs. Usually this is the intended behaviour, which is why you would want to use a weak property.


2 Answers

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 ...
}
like image 42
Leontien Avatar answered Dec 03 '22 05:12

Leontien


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()
like image 86
matt Avatar answered Dec 03 '22 07:12

matt