Example iOS app designed using MVVM-C and Clean Architecture. Uses Swift Concurrency, UIKit & SwiftUI.
The app retrieves images for any search query or tag via the Flickr API. It has three modules: ImageSearch, ImageDetails, HotTags.
![]() |
![]() |
![]() |
- Clean Architecture
- Explicit Architecture
- MVVM
- Flow coordinator implemented with closure-based actions
- Dependency Injection, DIContainer
- Protocol-Oriented Programming
- Data Binding using the lightweight Observable<T>
- Closure-based delegation using the lightweight Event<T>
- Pure functional transformations
- Delegating entity behavior
- Alternative DTO approach
- Reusable and universal NetworkService based on URLSession
- Reusable and universal SQLite wrapper around SQLite3
- Image caching service
- Configurable use of UIKit or SwiftUI for the same screen
- Advanced error handling
- Unit and integration tests for a number of components from all layers
Presentation (MVVM): coordinators, UI elements, SwiftUI views, UIKit storyboards, ViewControllers, ViewModels
Domain: entities, use cases, services, interfaces
Data: entity repositories, APIs, API/DB interactors (or network services and persistence storages), adapters
This architecture is highly modular, has a clear separation of concerns, and is easy to modify, scale, test and debug. It can be easily modularized with SPM.
It also allows to swap the implementation of any component without making changes to the rest of the app code. For example, for the ImageDBInteractor
protocol, in addition to the existing SQLiteImageDBInteractor
class that uses SQLite, we can add a SwiftDataImageDBInteractor
class that will use SwiftData. There will be no changes to the ImageDBInteractor
protocol, nor to repositories or other layers (Domain, Presentation). Or if some 3rd-party library changes its API, all you need to do is modify its adapter, nothing on the left of the diagram will change.
Thanks to these system properties - both comprehensive test coverage and compartmentalization to isolate changes - further changes can be made with confidence and without risk of regression.
The module assembly setup occurs in DIContainer
. Only those dependencies that are necessary for a specific module/component are initialized and injected into it, which in particular improves app performance and memory usage. View Models, as well as the entire Domain layer, are independent of any dependencies (with the exception of Apple's core Foundation framework).
ImageSearch module:
* searchImagesUseCase.execute(imageQuery)
* imageCachingService.cacheIfNecessary(data)
* imageCachingService.getCachedImages(searchId: searchId)
ImageDetails module:
* getBigImageUseCase.execute(for: image)
HotTags module:
* getHotTagsUseCase.execute()
ImageCachingService implements the logic for caching downloaded images and freeing memory. This helps keep the app's memory usage under control, since there can be a lot of downloaded images, and without caching, the app could quickly accumulate hundreds of MB of memory used. Downloaded images are cached and read from the cache automatically.
- SwiftEvents - the easiest way to implement data binding and notifications. Includes Event<T> and Observable<T>. Has a thread-safe version.
- URLSessionAdapter - a Codable wrapper around URLSession for networking
- SQLiteAdapter - a simple wrapper around SQLite3
For a Swift 6 version with SWIFT_STRICT_CONCURRENCY = complete, see the swift6 branch.
iOS 15.0+, Xcode 13.0+, Swift 5.5+