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.
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)
}
}