Architecture & Agility: 03 - AppCore & UI
AppCore: Connecting the dots
In the last two articles we have seen how UseCases
encapsulate our business logic and how Features
group and coordinate them.
Now let’s connect the Features
which results into the AppCore
. This AppCore
then will receive messages from the UI and other sources.
typealias Input = (Message) -> ()
typealias Output = (Message) -> ()
func createAppCore(
store: StateStore,
receivers: [Input],
rootHandler: @escaping Output) -> Input
{
store.updated {
#if targetEnvironment(simulator)
print("\(store.state())")
#endif
}
let features: [Input] = [
createTodoListFeature (store: store, output: rootHandler),
createTaggingFeature (store: store, output: rootHandler),
createJournalingFeature(store: store, output: rootHandler)
]
return { msg in
(receivers + features).forEach { $0(msg) }
}
}
Exactly like our features, AppCore
is just a function that we will use as an Input
.
{ msg in
(receivers + features).forEach { $0(msg) }
}
All it does is iterating over all Features
and passing each one the message it just received. And as we have seen before, a Feature
will pattern match to check if it is interested in this message and use its UseCases
accordingly.
Connecting the UI
Core|UI is fully decoupled from the UI, it isn’t part of the core and must be hooked up from the outside. In this example I’ll use SwiftUI, but as it is fully decoupled using any other UI framework on any platform should work — the core is easily portable, thanks to its decoupled nature.
import SwiftUI
@main
final
class TodosApp: App {
private let store: StateStore = createStore()
private lazy var viewState: ViewState = ViewState(store: store)
private lazy var rootHandler: (Message) -> ()
= createAppCore(store: store, receivers: [viewState.handle(msg:)], rootHandler: { self.rootHandler($0) })
var body: some Scene { WindowGroup { ContentView( viewState: viewState, rootHandler: rootHandler ) } }
}
First, we create a StateStore
, which we will pass into a ViewState
object and the core.
The ViewState
translates our app state for SwiftUI.
import SwiftUI
final
class ViewState: ObservableObject {
@Published var items : [ TodoItem ] = []
@Published var entries: [ Entry ] = []
@Published var tags : [ Tag ] = []
@Published var deleted: TodoItem?
init(store: StateStore, dismissToastAfter: Double = 2) {
self.dismissToastAfter = dismissToastAfter
store.updated {
self.process(appState: store.state())
}
process(appState: store.state())
}
private let dismissToastAfter: Double
func handle(msg: Message) {
if case .todo(.response(.wasDeleted(item: let item))) = msg {
deleted = item
DispatchQueue.main.asyncAfter(deadline: .now() + dismissToastAfter) {
self.deleted = nil
}
}
}
private func process(appState: AppState) {
items = appState.items .sorted(on: \.creationDate, by: <)
entries = appState.entries.sorted(on: \.creationDate, by: <)
tags = appState.tags .sorted(on: \.name, by: <)
}
}
if the store gets updated, ViewState
will process the new state
init(store: StateStore, dismissToastAfter: Double = 2) {
//…
store.updated {
self.process(appState: store.state())
}
//…
}
private func process(appState: AppState) {
items = appState.items .sorted(on: \.creationDate, by: <)
entries = appState.entries.sorted(on: \.creationDate, by: <)
tags = appState.tags .sorted(on: \.name, by: <)
}
As items
, entries
and tags
are tagged @Published
, SwiftUI can listen to those change and react accordingly.
So every time our state changes, the UI will pick up the changes automatically.
And how does the App handle UI events?
Quite simple: it just sends messages, which in SwiftUI looks like
Button { rootHandler(.todo(.add(TodoItem(text:text)))) } label: { Text("add") }
or
.onDelete {
if let idx = $0.first { rootHandler(.todo(.delete(viewState.items[idx]))) }
}
The rootHandler
here is the Input
function that we also refer to as AppCore
.
class TodosApp: App {
//...
private lazy var rootHandler: (Message) -> ()
= createAppCore(store: store, receivers: [viewState.handle(msg:)], rootHandler: { self.rootHandler($0) })
var body: some Scene { WindowGroup { ContentView( viewState: viewState, rootHandler: rootHandler ) } }
}
So all UI events are handled the same way:
- an event occurs
- it is send as a message to the
AppCore
- the
AppCore
processes it into a new state - the state changes trigger the UI to refresh itself
I chose this way, as it resembles the same unidirectional flow that I have in each UseCase
and Feature
, but another way, that might feel more natural for SwiftUI could also be to only have bindings in the view code and have the ViewState generate the message once the UI writes to a binding.
The Code
At this point I want to share my code with you, though I have not yet highlighted all aspects of Core|UI.
Real World Code Snippets
I think, the following code line demonstrate quite strongly the strength of the combination of a Command Interpreter and a powerful DSL.
At first it might look strange, but just read it. Literally read it:
rootHandler( .chat(.send(ChatMessage(from: user, to: receiver, body: message).alter(.append(image)))) )
the same code formatted differently:
rootHandler(
.chat(
.send(
ChatMessage(
from: user,
to: receiver,
body: message
).alter(
.append(image)
)
)
)
)
It calls rootHandler
with the command that reads:
«Chat Feature, send the message from <user> to <receiver> with <image> append to it»
Isn’t that impressive? We encoded non-trivial behaviour in just one line of code, resembling near-to-natural English.
Not convinced? let’S try another example
rootHandler(
.contact(.replace((contact: contactToEdit!, with: contactToEdit.alter(.nickname(newNickName)))))
)
And the long form if you find that easier to read:
rootHandler(
contact(
.replace(
(contact: contactToEdit,
with: contactToEdit
.alter(
.nickname(newNickName)
)
)
)
)
)
This calls rootHandler
with the command:
«Contact Feature, replace the contact with itself having an altered nickname»
Again: A complex behaviour encoded in just one statement. We are actually telling the AppCore
, what we what it to do — not how to do it. Which is a big difference to the procedural style which is mostly used in OOP. This code is more reminiscent of Functional Programming.
Next…
Though I don’t consider it an integral part of the architecture I want to show you how we can take inspiration from Functional Programming and use immutable state.
If you look at the code you might expect the StateStore
(which is also a gateway in our UseCases) to be a class, but it is actually a tuple. I also want to explain you, why I have chosen to use that over a class.
As I mentioned earlier there will be an article on how to do BDD with Core|UI
I also want to dig a little into how this architecture helps during agile processes, though I will cover this briefly, as this will be the focus of a book I am currently writing.
Another topic I want to cover is: How to scale (and shrink) this architecture for project of different complexities. Turns out this architecture is fractal.
Stay tuned.