Maybe you could shine some light on my dark and ignorant path? My destination is a SwiftUI app where I can inject components into views and have systems run some logic on those components. Every time I think I've arrived; weaknesses in the foundation makes me landslide back to the start. Let me show you what I've tried!
protocol DataComponent: Component {
associatedtype DataType
var data: DataType { get set }
init()
init(data: DataType)
}
struct NameComponent: DataComponent { var data = "default" }
Here's the basic data component. I'm using EntitasKit for my entity-component-system.
enum Entities {
case source
...
}
protocol EntityServicing {
var context: Context { get }
func entity(_ entity: Entities) -> Entity
}
final class EntityService: EntityServicing {
let context = Context()
func entity(_ entity: Entities) -> Entity {
switch entity {
case .source:
guard let first = context.entities.first else {
return context.createEntity()
}
return first
...
}
}
}
Here's the application scoped EntityService which holds the context for entities and their components. It is injected using Resolver.
Now, here's what a view looks like in SwiftUI.
struct ContentView: View {
@EntityState(.source, "Dale Cooper") var name: NameComponent
var body: some View {
Button(action: { name = NameComponent(data: "Dougie Jones") }) {
Text(name.data).padding()
}
}
}
EntityState is just what it sounds; a property wrapper that captures the state of an entity's component and makes it visible to SwiftUI.
@propertyWrapper
struct EntityState<C>: DynamicProperty where C: DataComponent {
@State private var state = C()
@Injected private var entityService: EntityServicing
private let entityID: Entities
private var entity: Entity { entityService.entity(entityID) }
init(_ id: Entities, _ data: C.DataType? = nil) {
self.entityID = id
if let data = data {
entity.set(C(data: data))
} else {
guard entity.has(C.cid) else {
entity.set(state)
return
}
}
_state = State(wrappedValue: entity.get()!)
}
public var wrappedValue: C {
get { entity.get()! }
nonmutating set { state = newValue }
}
mutating func update() {
entity.set(state)
}
}
SwiftUI notices the state change in the dynamic property. Clicking the button re-renders the view and updates the component on the entity.
This foundation starts to crumble when I add a system that changes the component.
class NameSystem: ReactiveSystem {
@Injected var entityService: EntityServicing
lazy var collector: Collector = Collector(
group: entityService.context.group(
Matcher(
all: [NameComponent.cid]
)
),
type: .added
)
let name = "Name System Added Here!"
func execute(entities: Set<Entity>) {
for e in entities {
if let data = e.get(NameComponent.self)?.data {
e.set(NameComponent(data: "Special Agent " + data))
}
}
}
}
The problem is that the SwiftUI state is never notified, but I want SwiftUI to know about the changes happening to components when they are updated by systems.
I have tried many different ways:
- Component/Entity/Context as ObservableObject using @Published. My understanding is that this is a good way to connect your data model to SwiftUI. I’ve @Published components, the data in components and entities, but the problem that keeps stopping me is getting the EntityState to re-render the view. Since the Context is injected I cannot use other property wrappers like @ObservedObject.
- Bindings. I’ve tried observing a component object in the EntityState and passing a component binding to the entity, but I can’t store bindings to different DataComponents in the same dictionary. I’m not even sure if changes to the bindings would get back to the EntityState to re-render the view. This same limitation goes for bindings to @States. Anyhow, I’ve got myself stuck down this road.
- Publisher and Subscriber. Turning EntityState into a subscriber so it can receive values requires it to be mutable. To make EntityState mutable it must become a class, but this breaks the SwiftUI re-rendering. The publisher could be a Subject of some kind or a custom publisher for components. I just can’t solve how to receive values and assigning them to the state so SwiftUI re-renders. The whole Combine area is a bit fuzzy for me. There’s probably a smart way to do something amazing that I don’t know about.
How can I assign components to the state from a system so that SwiftUI re-renders?
I hope you find this an interesting problem to solve, and that it made you as curious as I am to the solution. It gets me really excited at least.
Have a nice day everyone!