🏎 A SwiftUI + Combine approach to stateful content management.
SwiftUI is pushing towards a state driven approach when writing apps. This has the benefits of creating apps that are easy to test, debug and code, since the content that will be shown is the content that is contained in the centralized state.
CryptoLevels is achieving this with two main components.
- The Store: which contains the data structures that will be represented in the app. The Store is also an @EnvironmentObject in the Views, so every time that there are changes, the UI redraws itself.
- The Orchestrator: which knows about the
Store
and theNetwork
. Its goal is to fetch data from the internet, using Combine to map the received data and updating theStore
to let the UI redraw.
The Combine powered Network Manager is only 7 lines of code and is packed with JSON to Model mapping.
func fire<Output: JSONDecodable>(at url: URL, output: Output.Type) -> AnyPublisher<Output, URLError> {
URLSession.shared.dataTaskPublisher(for: url)
.map({ $0.data })
.map({ output.init(json: JSONComposer.compose($0)) })
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
This example includes a StatefulContent
view which is managed by a ContentState
.
// The StatefulContent is a view managed by a ContentState
// which is obtained combining the right data from the Store.
//
// This approach has the benefit of adapting the UI with a
// reactive state management provided by an Orchestrator
// that updates the Store using Combine.
//
// This component is capable of handling
//
// - Correctly loaded content
// - Loading state
// - Empty data
// - Error message
//
StatefulContent(state: store.mapLevelState(), whenLoaded: {
LevelProgressList(self.store.level)
}, whenLoading: {
PulseLoader(self.store.loading)
}, onEmptyData: {
Text("No data to fetch. Try again later.")
}, onError: { error in
// The fetchCurrentLevel queries the Orchestrator that
// will update the Store, so it will render this view
// again when the data gets fetched. Or not.
//
// Retry mechanism is as easy as that
//
TryAgain(error: error, action: self.fetchCurrentLevel)
})
The ContentState
is built from the Store
, an @EnvironmentObject
which is responsible of representing
the data that will be shown in the app.
Since all actions are dispatched from an Orchestrator
, which is contained in the Store
,
dispatching an action that will later modify the entire app state based on its success or not, is really easy.
...
TryAgain(error: error, action: self.fetchCurrentLevel)
// The action in the Orchestrator will update the store
// and the UI will be redrawn invoking a single line of code
func fetchCurrentLevel() {
store.orchestrator.fetchCurrentLevel()
}
The whole example has been built using a single project. Just clone and build. No CocoaPods, no external dependencies.