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()