This app is an example of MVVM architecture using UIKit and Combine.
I created an empty project called Movies using UIKit.
I prefer not to add support for Core Data or unit testing at the beginning and add it later when needed.
The deployment target is set to iOS 13.
I want to build the views programmatically, so I am removing the initial storyboard, and I’ll continue without them for the rest of the project.
The Main.storyboard file, the entries relative to it in the Info.plist file, and the project files have to be deleted.
In the SceneDelegate, connect the scene to the view controller. We can give a background color to the view controller to check that it has been connected properly.
I used the tool Postman to request the movies’ JSON.
With the help of this online tool, I quickly obtained a draft of the model in Swift.
After some adjustments to the online generated model, I added the JSON file to the project, a target for unit testing, and tested the decoding process.
The main view that contains all the movies organized by genres is a clear example of NSCollectionLayoutGroup, giving the collection view a compositional layout (available feature in UIKit since iOS 13).
Movies view model
I decided to use a mix of native technologies:
- Combine to publish the changes of the model into the view controller, that reacts to these changes updating the data source.
- The async/await version of URLSession to request the data from the server.
- I changed the way in which the collection view is rendered using UICollectionViewDiffableDataSource (introduced by Apple in 2019 for iOS 13 and later).
Model stubbing and testing
I added a few unit tests to verify some functionalities of the model. I needed to create several movies for the tests, so I decided to stub them because they need a lot of parameters, and this would make the tests enormous.
The stub methods added may be helpful with other structs or classes in the future. So I decided to do it generic, and to avoid verbosity, I am leveraging
Testing network layer
I separated the client from the view model. I created a generic client that I plan to use later to fetch the movies’ images.
I created a Networking protocol that overrides the async data request of URLSession so that we can mock URLSession. Check this super-interesting article about testing async-await functions!
Testing view model integration with the client
I built a test that checks that the movies’ publisher is publishing the values that will be consumed by the view when the request gets the movies’ data correctly from the server.
Loading Movies’ images
On the main screen, the movies’ images are loaded. Many movies are repeated in many genres, so this presents many challenges:
- We want to cache the images using NSCache.
- As the movies can exist in many categories (they have more than one genre), we may perform many requests to the same URL before any of them has finished and cached the image.
Using Combine, I created a dictionary of publishers, each identified by its image URL. With
share(), we allow a publisher to notify multiple subscribers (all the collection view cells with the same image) at once. If the publisher already exists, we don’t make a new one; we return the stored one.
Once we’ve got the image, it’s cached using NSCache and published as an individual value using
I created a table view to represent the movie’s information, so it’s scrollable. The images, title, and rating view are the header of the table view.
I used MVVM again with Combine publishers to provide the view with reactiveness.
I built the view programmatically.
To avoid re-downloading the poster image again, I moved all the images’ cache to the HTTP client, and I passed the same client that I used in the movies’ view model to the new view model.
The movie detail view controller subscribes to the publisher of the image if the publisher has already finished and cached the image. The publisher we pass to the detail view model is a Just publisher containing the image.
I decided to use UserDefaults with a Combine approach to store the user’s rating for a movie.
By creating a computed variable, we can generate a publisher with the key path and subscribe to changes for the UserDefault’s key.
I inject UserDefaults into the models, and both view models share the same instance.
In the movies’ view, I added a star to those movies with a rating.
Everything updates consistently and reactively when a rating is updated.
Any vision-impaired user can use the app thanks to VoiceOver and Dynamic types.
To enable VoiceOver, say: “Hey Siri, enable VoiceOver”.
I am also supporting dark and light modes.