Search code examples
swiftmemory-managementmemory-leakssprite-kitcomputed-properties

Swift Reference Cycle When Capturing A Computed String Property Declared From Inside An Instance Method


I'm implementing an in-game store using Swift and SpriteKit. There is a class called Store which has a method setupItems() inside of which we declare and instantiate instances of a class StoreItem and also add each store item instance as a child of Store. Each StoreItem has an optional closure property called updateInventory which is set inside of setupItems() as well. This is an optional closure because some items don't have a limited inventory.

Store also has an unowned instance property storeDelegate which is responsible for determining how funds are deducted and how storeItems are applied once purchased. storeDelegate is unowned as it has an equal or greater lifetime than Store.

Now, here is where things get interesting - the closure updateInventory references a computed string variable called itemText which makes a calculation based on a property of storeDelegate. If itemText is declared and instantiated as a variable inside of setupItems() we have a reference cycle and store is not deallocated. If instead, itemText is declared and instantiated as an instance property of Store (right under the property unowned storeDelegate that it references) then there is no reference cycle and everything deallocates when it should.

This seems to imply that referencing storeDelegate from a computed variable inside an instance method of a class doesn't respect the unowned qualifier. Code examples follow:

Current Scenario

protocol StoreDelegate: AnyObject {
    func subtractFunds(byValue value: Int)
    func addInventoryItem(item: InventoryItem) throws
    var player: Player! { get }
}

class Store: SKSpriteNode, StoreItemDelegate {
    unowned var storeDelegate: StoreDelegate
    
    init(storeDelegate: StoreDelegate) {
        self.storeDelegate = storeDelegate
        setupItems()
        ...
    }
    func setupItems() {
        var itemText: String { 
            return "item info goes here \(storeDelegate.player.health)"
        }
        
        let storeItem = StoreItem(name: "coolItem")
        storeItem.updateInventory = {
            [unowned storeItem, unowned self] in
            // some logic to check if the update is valid
            guard self.storeDelegate.player.canUpdate() else {
                return
            }
            storeItem.label.text = itemText
        }

    }

The above leads to a reference cycle, interestingly if we move

var itemText: String { 
    return "item info goes here \(storeDelegate.player.health)"
}

outside of updateItems and make it an instance variable of Store right below unowned var storeDelegate: StoreDelegate, then there is no reference cycle.

I have no idea why this would be the case and don't see mention of it in the docs. Any suggestions would be appreciated and let me know if you'd like any additional details.


Solution

  • storeItem.updateInventory now keeps strong reference to itemText.

    I think the issue is that itemText holds a strong reference to self implicitly in order to access storeDelegate, instead of keeping a reference to storeDelegate. Anothe option is that even though self is keeping the delegate as unowned, once you pass ot to itemText for keeping, it is managed (ie, strong reference again).

    Either way, you can guarantee not keeping strong reference changing itemText to a function and pass the delegate directly:

    func setupItems() {
      func itemText(with delegate: StoreDelegate) -> String
        return "item info goes here \(storeDelegate.player.health)"
      }
      let storeItem...
      storeItem.updateInventory = {
        // ...
        storeItem.label.text = itemText(with self.storeDelegate)
       }
     }