This is a small example project to demonstrate SwiftUI/Combine in an archetecture inspired by Clean Architecture by Robert Martin.
The Presentation layer consists of Views and ViewModels.
The Views are concerned with displaying information on screen and the immediate handling of user input. The ViewModels are concerned with making simple presentation decisions and interacting with Domain objects such as Usecases and Entities.
This is the core of your Application. The Presentation and Data layers of dependencies on your Domain, but not the other way around. The domain is independent of the Presentation and Data layers, meaning it could be compiled and tested without them.
This layer consists of Usecases, Entities, and Interactors.
Usescases are protocols (in non-swift land: interfaces) which the DataModels depend on. They should be very simple and should not be defined together. The are implemented by other objects (Interactors, Repositories, or DataStores, depending on complexity.)
Entities are simple data objects that your application needs to function. Functionality should be placed in entities sparingly, and should be instead placed in Usecases.
Interactors are one of the objects that can implement Usecases. Interactors are used in more complicated cases where a Repository or Datastore alone won't satisfy the requirements of a Usecase.
Repositories are protocols, much like Usecases, that define what interactors depend on. It is the data layer's responsibility to fulfil these requirements by providing objects that implement these protocols.
The data layer is concerned with retrieving and saving of data.
Datastores are simple objects that can retrieve/load/save Entities. They implement repository protocols for interactors to use.
Less is more. Depending on the functionality one is trying to provide, one or more of these layers may be unnecessary. For example, if the functionality is purely presentation, then the domain and data layers shouldn't be engaged. However, care must also be taken not to make domain or data decisions in the presentation layer.
There are also platform specific constructs which don't fit neatly into these layers. Such as the AppDelegate. Limiting use of these constructs to what is absolutely necessary is recommended.
The app has a small number of features:
- A list of Things
- Create a Thing
- A thing can be "visible" or "invisible" which is indicated in the list.
- We can fail at creating a Thing
- There is a detail view in which a Thing's visibility can be changed
The View is accomplished in SwiftUI. There is a ListView, a ListItem, and a DetailView for Thing. Functionality
has been abstracted out from the view code into the ViewModel, which is defined as an extention to the ListView
in order to scope it (so the type is ThingListView.ViewModel
).
The ViewModel is an ObservableObject
, which means it has @Published
properties to which SwiftUI
can directly bind. It also has functionality which can easily be called from SwiftUI actions. This ViewModel
is a little overloaded, so it can not only list Things, but update them. This functionality is implemented
in the DetailView, so it will need to know about the ViewModel. This is done by the @EnvironmentObject
in
the ThingListView. It is created and passed in by the owner of the ListView, and is then available as an
@EnvironmentObject
to any decendent. Note how it's used in the ListView and the DetailView, but not in
the intermediate ListItem.
The ViewModel accomplishes its goals by using UseCases and Entities. There is one simple Entity for this app called Things, and there are several use cases for listing, creating, modifying the Things, and also one to simulate an error.
This app contains a single data store to manage the Things. This is obviously a trivially simple implementation, but normally this is where service code or disk storage code would go. In more complex situations, it may be advantageous to use a Repository to manage multiple data stores.
The declarative syntax and view binding features of SwiftUI making it much easier to extract and reuse SwiftUI views. With this in mind, any grouping of SwiftUI primitive views is an excellent candidate for extraction, naming, and reuse. Liberal extraction will create small, easy to read, reusable views.
@State
binding should be used for internal view implementations, and @ObservableObject
and @EnvironmentObject
should
be used for app state objects (such as ViewModels).
The ViewModel and the Datastore are both fully unit tested. UI components, even SwiftUI components, are hard to unit test. Pushing as much flow out from the UI Component into the ViewModel is the easiest way to maximize the test coverage of presentation code.
SwiftUI code can be unit tested in conjunction with the ViewInspector library.