Demo app consisting of two screens - Login and Confirmation - showcasing the use of SwiftUI and MVVM architecture. The project shows how to achieve solid separation of concerns and enhanced testability.
Registration | Confirmation |
---|---|
The app utilizes MVVM architecture. I chose MVVM because of:
- separation of concerns
- improved testability
- to make the project structural and simple to manage
- When the app starts it tries to load saved user data from the store (
UserDefaults
). - If there is no user data, registration screen is shown.
- After collecting registration data from the user, the app saves it and the user is taken to the confirmation screen.
- If the user data is loaded from the store at the app startup, the app proceeds straight to the confirmation screen. The user has an option to unregister which clears user data and shows the registration screen again.
The app contains 2 pages - registration and confirmation screens. The following table lists key types used to build the app.
Model & Data Layer | ViewModel | View |
---|---|---|
User UserStorage UserService |
RegistrationViewModel ConfirmationViewModel |
RegistrationView ConfirmationView |
Additionally, there is a set of types responsible for input validation: Validator
, RegexValidator
, DateValidator
, RegexRule
.
The registration page centers around collecting information entered by the user. The UI layout is defined in RegistrationView
; it is based on a Form
view and a set of accessory views.
RegistrationView
is backed by its view model (RegistrationViewModel
) that acts as the intermediary between model/data layer (User
, UserService
) and the view. It validates the data entered by the user (via types implementing the Validator
protocol). Additionally, it retrieves the actions from the view (e.g. tapping on the Register button).
The confirmation page shows the information about the user. In this case, it's the information collected in the registration page. However, neither confirmation view (ConfirmationView
) nor its view model (ConfirmationViewModel
) know anything about the registration. They rely solely on the data layer as a single source of truth.
This is where I faced the biggest challenge. I noticed that in SwiftUI, navigation and flow control is confined to the view layer (e.g. NavigationLink
in views).
However, what I wanted was to achieve a higher level of view reusability and testability. I wanted the views not to know about each other.
In my solution, the app instance (DemoApp
) acts as a router, deciding which UI components to show.
-
The app ships with 17 tests that fall into 3 categories: unit, integration and UI. Classic unit testing is done for the validation logic (
EmailValidationTests
,UserDefaultsUserStorageTests
). Tests from the integration category can be found in e.g.RegistrationViewModelTests
. There is also a set of UI tests, verifying that the main user flow works as expected (DemoAppUITests
). -
I wanted to achieve high code coverage. The current value sits at 94%. Although the metric is obviously not perfect, it is a great indicator for deciding whether particular parts of code need more testing.
Tests | Code coverage |
---|---|
All the main classes in the app allow for constructor dependency injection. I wanted this in order to be able to be flexible when testing. Constructor injection might not be the most scalable way of injecting - however, it is a valid pattern and does the job, keeping things separated. Mock classes were added to be used in tests (e.g. InMemoryUserStorage
, UserServiceMock
)
I added a new build configuration (Testing
). The main reason of this was that I wanted to be able to exclude SwiftUI previews from code coverage computation.
To exclude code from code coverage computation I can simply wrap it with #if !TESTING ... #endif
.
There are certain things I'd like to improve. Since this was my first SwiftUI project I certainly did not choose the best solutions. Sometimes, I cut the corners not to spend too much time on perfecting things. The following list contains things that I'd like to improve:
-
Make the
UserService
calls asynchronous to fit real-world scenarios of registering. The work on it has been started in thepromises-in-storage
branch. I'm refactoring it to use Combine's Promise pattern. -
Better UI routing. Now it's the main app struct doing the routing. I'd like to change this and try the new API from Apple - iOS 16's
NavigationStack
-
Improve animations. SwiftUI animations is something I have not explored much in this project. I'd like to change that.
-
More functional reactive programming using Combine. I barely touched the surface in this project using Combine. It looks like something that could save lots of time and reduce the amount of boilerplate code in the project.