I can’t thank you enough for your help, this is so so helpful to get some input from an experienced Combine developer.
I was told by several developers in my local area to stay away from Combine, as if it is a bad framework or something, and now after multiple days of heavy research I’m a little unclear why. As you said, it seems like a really amazing framework. For example, my game map is composed of Tile
objects (custom class), and each Tile object has around 10 properties that it publishes. I can easily have 10,000 Tile
objects, so that is a lot of publishers, and performance seems amazing. So I was really confused about what I was being told in my local area.
The issue with CurrentValueSubject
is that most of my CurrentValueSubjects
in my game are properties that are collections, for example:
public class Player: ObservableObject {
public private (set) var units = CurrentValueSubject<[Unit], Never>([])
...
}
I have several dozen classes that are subscribers to the units property, things that need to know when either a unit has been added or a unit has been removed (i.e. killed). So I have code like the following in lots of places in my game:
public class UnitsMapLayer {
public init(player: Player) {
// Bunch of initialization stuff here
player.units
.sink(receiveValue: { units in
DispatchQueue.main.async {
// Do SpriteKit rendering stuff that updates
// the display of the units that are on the map
}
})
.store(in: &cancellable)
}
}
So when the subscriber above receives the updated units collection, the subscriber gets the whole collection, but it has no idea which item in the collection was the item that caused the publisher to fire its change. Was a new unit added? Was a unit removed? Unless I keep a private/local copy of the units collection within the UnitsMapLayer
, I have not figured out any way with Combine for the subscriber to know. So that’s what I’ve been doing: I keep a local copy of the units property, then loop through both the local units property and the new units collection in the sink, comparing them, to find whether a unit was added or removed, then either add a new SKNode
to my SpriteKit node graph if it was a new unit, or go find the already existing SKNode
and remove it if a unit was removed. Technically that technique works, but there is no way that is the “normal” way of doing things with Combine, it just doesn’t seem right, and I’m sure I’m just not thinking about it in a “functional programming way” or something.
Based on continuing research online, I found this: shareup.app → Designing and writing a custom, generic Combine Publisher in Swift. The key sentence in that article is “The core business logic of the application modifies the application state in response to those actions and delivers a new immutable snapshot of the application state to the UI, which re-renders itself.” So I refactored so that each time the units collection changes, the rendering code inside UnitsMapLayer
completely removes all unit SKNodes, then re-adds every unit, based on the new immutable collection of units received in the sink. Again, this also technically works, it’s just that now if I have 500 units (a definite possibility) and a single new unit is added, I am removing 500 nodes from the SpriteKit node graph and adding back 501, all just because I have a single new unit to add. It just seems really wasteful and non-scalable in terms of CPU usage. But that technique is precisely what the person above is describing. It is essentially the concept of “nuke and pave” (completely remove everything and add back everything fresh). If this is the Combine and SpriteKit way to do things, I suppose that is fine, it just doesn’t seem quite right. What if I had 100,000 units? This technique just doesn’t seem to scale, which is why it feels wrong to me.
So I’m still stumped, and still very confused as to why I can’t seem to find anyone else asking this question anywhere online, it seems like such a basic fundamental thing. I have hundreds of situations all over my codebase like this, so it’s very problematic for me right now. I’ve been searching for weeks now and have not found a solution. Maybe this is one of the reasons why my local developers told me not to use Combine, I don’t know.
I also found this: How to create and emit custom event with Combine and Swift? - Stack Overflow which the developer describes how to emit custom events with Combine, so I am going to try to learn how to do that next, as that may work.
But I’m still confused as to why I can’t seem to find an answer to this question. I think what doesn’t make sense to me is: how would you not run into this problem if using a CurrentValueSubject
? It seems like lots of publishers would be collections, not just simple single/scalar values, and it would be highly beneficial to the subscriber to know the pertinent item that caused the collection to change. That’s why I still think I’m just not understanding something fundamental about Combine and functional reactive programming.
I feel like I’m wanting something like what the Swift reduce
function does: it passes in an accumulator plus the new value to its closure. Is there some way to tell the Combine sink to pass in both the entire new collection, plus the item that caused the collection to change?
One final comment in case anyone out there is reading this and does not use/know SpriteKit: SpriteKit is very much a state management framework (at least that’s how I think of it). I don’t tell SpriteKit how to actually render sprites on the screen, I add SKNodes
to a huge node graph, and set certain properties on each SKNode
such as position, then hand that node graph off to the SpriteKit engine, which then figures out how to actually render the pixels on the screen. So it abstracts away the step of actually doing the rendering. So all of my “rendering” code is just adding SKNodes
to the SpriteKit node graph. Which is awesome by the way. So that’s why I thought Combine would be such a good companion to SpriteKit.