Search code examples
iosswiftswift3closuresobservers

Swift - How to properly remove blocks from an array when a caller is deallocated?


I have an array of 'updateBlocks' (closures) that I use in a singleton class to notify any observers (UIViewControllers, etc) when data updates.

I am wondering what the best way to remove the observer would be so that it is not executed when the observer is deallocated (or no longer wants updates).

Here is my current setup:

MySingleton Class

var updateBlock: (() -> ())? {
    didSet {
        self.updateBlocks.append(updateBlock!)
        self.updateBlock!() // Call immediately to give initial data
    }
}
var updateBlocks = [() -> ()]()

func executeUpdateBlocks() {
    for block in updateBlocks {
        block()
    }
}

MyObserver Class

MySingleton.shared.updateBlock = {
    ...handle updated data...
}

MySingleton.shared.updateBlock = nil // How to properly remove???

Solution

  • Your singleton design has some problems.

    Having updateBlock be a variable who's didSet method appends a block to your updateBlocks array is bad design.

    I would suggest getting rid of the updateBlock var, and instead defining an addUpdateBlock method and a removeAllUpdateBlocks method:

    func addUpdateBlock(_ block () -> ()) {
       updateBlocks.append(block)
    }
    
    func removeAllUpdateBlocks() {
       updateBlocks.removeAll()
    }
    func executeUpdateBlocks() {
        for block in updateBlocks {
            block()
        }
    

    }

    If you want to remove single blocks then you'll need some way to keep track of them. As rmaddy says, you would need some sort of ID for each block. You could refactor your container for your blocks to be a dictionary and use sequential integer keys. When you add a new block, your addBlock function could return the key:

    var updateBlocks = [Int: () -> ()]()
    var nextBlockID: Int = 0
    
    func addUpdateBlock(_ block () -> ()) -> Int {
       updateBlocks[nextBlockID] = block
       let result = nextBlockID
       nextBlockID += 1
    
       //Return the block ID of the newly added block
       return result
    }
    
    func removeAllUpdateBlocks() {
       updateBlocks.removeAll()
    }
    
    func removeBlock(id: Int) -> Bool {
       if updateBlocks[id] == nil {
         return false 
       } else {
         updateBlocks[id] = nil
         return true
       }
    
    func executeUpdateBlocks() {
        for (_, block) in updateBlocks {
            block()
        }
    

    If you save your blocks in a dictionary then they won't be executed in any defined order.