This is my implementation of a the Veepee Assessment. The specification readme is here.
VeepeeAssessment.mov
- Download the project:
git clone https://github.com/yeniel/VeepeeAssessment
- Wait until all Swift Package Manager are fetched.
- CocoaPods
- SwiftLint: Linter I have used to static code analysis.
- Swift Package Manager
- Factory: Dependency injector, based on container-based dependency injection pattern.
- Stinsen: Router based on coordinator pattern.
- Quick: Unit tests.
- Nimble: Unit test assertions.
- SnapshotTesting: Snapshot tests for views.
- OHHTTPStubs: Stub network requests to test data layer.
First of all, I want to say that when I thought this app it was as a big project. Therefore this project, in the future, could have more features, and all the current ones could be more complex. In some parts the design patterns seem like an overkill or maybe add needless complexity, but I chose them to show my knowledge.
I tried to follow the bases of a Clean Architecture and the SOLID principles. The intention is to have a testable, robust and scalable code and avoid bad smells like:
- God entities.
- Repeated code.
- Non testable code.
- Coupled code.
- Lower cohesion.
I implemented the repository pattern for forecast request. The repository pattern is good to manage collection of items.
For the forecast list, I implemented two concreate data source, one is the api data source to get data from the OpenWeatherMapApi; and the other is a local data source to get data from CoreData
.
The cache logic have was be added in the repository class. The cached expired every 3 hours, I took this interval because is the smallest interval between two forecasts in the API.
We could add also a memory data source or just cache in memory in the repository class. But for this small project with only one request is not needed.
The concrete implementation of the api client is based in URLSession
.
I used data transfer object to parse the json of the api responses using the Codable
protocol.
A mapper to map the dtos to domain models.
The ApiClientError
is used to map the API errors. The CoreData
errors are only catched.
Core models of the business.
The model DomainError
is used to specify business errors like forecast not found.
If this project was scalated I would add the business logic in the use cases, to diferentiate it from the presentation logic. In the use cases I also map data errors to domain erros. Use cases helps me to deacoplate the layers. The comunication between the presentation layer and data layer is made throught the use cases (I don't have any repository in viewmodels).
I implemented the MVVM pattern. The views are in SwiftUI and I used Async/Await through all the app for asynch process.
The view models contain the presentation logic.
I used the Stinsen package to deacoplate the navigation logic from the views using the coordinator pattern. This approach helps me to test better in case I have to add more complexity to the navigation in the future.
I mapped domain models to ui models (ForecastUI
and ForecastDetailUI
). The intention is to give the views a more specific models and avoid adding logic to it (eg. measurement formats). Also give the view only the data that it needs.
I used Quick for all tests and Nimble for the assertions. The ObjectMother
provides me mocked models. I try to follow as much as I can FIRST principles.
To increase the speed I change a little the entry point of the app, in VeepeeAssessmentApp.swift
.
I cut the app to a simple view in case we are running the tests.
I configured the scheme to randomize the execution order.
I set the App Language to English and the Region to Spain.
The coverage is 64,8%
- Repository: Test cache logic and map dto to domain model.
- ApiDataSource: Test request url and errors.
- UrlSessionApiClient: Test json parse to Codable Dtos. I used OHHTTPStubs to stub request using mocked jsons.
- Use Cases: Test business logic, erros.
- ViewModels: Test presentation logic.
- Views: Snapshots to test all the design. The snapshot images are in Snapsgots
- DateUtils and NumberUtils
I chose Bitrise as CI. I created a workflow with a trigger on every push on main branch. You can see the badge of the status in the top of this README, and if you click on it you will see the Bitrise builds. There also a badge of code coverage.
I added the following steps:
- Build.
- Run tests.
- Send coverage to Codecov
In the console appears the message:
NavigationLink presenting a value must appear inside a NavigationContent-based NavigationView. Link will be disabled..
Tracking issue here