It does not matter what name you are going to use, VIPER, RIBs, Clean Swift. The idea behind all of the ideas is to follow Clean Architecture and making the life of the developers better :D.
VIPER is followed by:
- Dumb and passive.
- Communicate the events to the Presenter
- Never ask for data
- Deal with animation
- Deal with UI only
- Access only ViewModels with the needed data already formatted
- Business logic
- Has Services to perform requests
- Communicate back to Presenter via delegate
- Receives the View events
- Convert Models to ViewModels
- Interact with Interactor
- Interact with Router
- Is the center of the Module
- Navigate to other Modules
- Protocols
- Define the communication between the
VIPER
parts (Interactor
,Presenter
...) - It is the FIRST file to code.
- Create the Module
Struct
- Data formatted
-
String
-UIImage
-Url
- For instance if you have a
cell
with title which has aname
andlastName
combined, TheViewModel
should have atitle: String = name + lastName
.
All the files and structure is called a Module
which will be one per screen/ ViewController / View
. We never use the same component (Interactor
, Presenter
...) in a different Module
.
If there is something that needs to be shared between Modules
, it should be in a Protocol
extension or Class
, such as Services
.
The Modules
not necessary needs all the components. Maybe the View
does not require any request for data, so the Interactor is not needed, but every View
needs at least a Presenter
.
One important thing about the View
is that it needs to be passive and NEVER ask for something like presenter.tracks()
.
The communication should always be passive, using the Presenter
as an event handler. Some Clean Architectures have a "Presenter"
with the name of EventHandler
. We decided to keep Presenter
to simplify and be easier to understand and avoid over-engineering by having both.
override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad()
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
presenter.didSwipeToDelete(at: indexPath.row)
}
}
The Presenter
is the centralized part of the VIPER architecture. It will receive the UI events and redirect it to Interactor
or Router
and implement the Interactor
delegate converting Models
to ViewModel
presenting the data to the View
.
Most of the Presenter
public methods are View
events, with that said testing the Presenter
is almost like a UI test and so much faster and less prone to flakiness than XCUI
.
func viewDidLoad() {
interactor.fetchTracks()
}
func didFetch(tracks: [Track]) {
self.tracks = tracks
view?.update(with: tracks.map(toViewModel))
}
You can choose to have a initializer on ViewModel
that receives a model
or do it on the presenter
.
private func toViewModel(track: Track) -> TrackViewModel {
return TrackViewModel(
title: "\(track.title) - \(track.artist)",
color: color(for: track))
}
Interactor
will have most of the business logic and handle the retrieving data from Services
. The Interactor
is usually initialized with a Service
Protocol
to do request and handle data.
func fetchTracks() {
service.fetch { [weak self] tracks in
self?.delegate?.fetched(tracks: tracks)
}
}
The Router
is responsible for redirecting to new Modules
using their Factories
. Usually, It has a weak reference to the View Controller
to be able to push to a new Module
.
func openTrackScreen(track: Track) {
viewController?.push(trackModuleFactory.makeModule(track: track), animated: true)
}
The Contract
is a file which will have the Protocols
which each part will communicate with each other. All the communication is based on Protocols
, following the Protocol Oriented Programming
.
The exciting part of the Contract
is that you have all the Protocols
here, having a big picture of how it behaves.
It is the first file to code, defining what the components will implement and how the will be related.
Following the Protocol Oriented Proggramming which says that you first have to think about the Protocols
and later the classes
protocol TracksPresenting: class {
var view: TracksViewing? { get set }
func viewDidLoad()
func didTapAdd(title: String, artist: String)
func didSwipeToDelete(at row: Int)
}
protocol TracksViewing: class {
func update(viewModels: [TrackViewModel])
func showError(_ description: String)
}
protocol TracksInteracting: class {
var delegate: TracksInteractorDelegate? { get set }
func fetchTracks()
func addTrack(title: String, artist: String)
func delete(track: Track)
}
protocol TracksInteractorDelegate: class {
func didFetch(tracks: [Track])
func handleError(_ error: ServiceError)
}
protocol TracksRouting: class {
var viewController: UIViewController? { get set }
}
Creates the Module
injecting the dependencies and returns the View
.
It usually has only one method to assemble the entire Module
.
static func makeModule(service: TrackServicing = TrackService()) -> UIViewController {
let router = TracksRouter()
let interactor = TracksInteractor(service: service)
let presenter = TracksPresenter(interactor: interactor, router: router)
let viewController = TracksViewController(presenter: presenter)
router.viewController = viewController
presenter.view = viewController
interactor.delegate = presenter
return viewController
}
One of the best thing about VIPER is Testing. When we get the Presenter
as example, we can see that it looks like an UI
test. it is very clear what is should do.
func test_viewDidLoad_shouldFetchTracks() {
subject.viewDidLoad()
XCTAssertEqual(mockInteractor.fetchTracksCallCount, 1)
}
func test_didTapAdd_shouldAddTrack() {
let artist = "mockArtist"
let title = "mockTitle"
subject.didTapAdd(title: title, artist: artist)
XCTAssertEqual(mockInteractor.addTrackTitleArtistCallCount, 1)
XCTAssertEqual(mockInteractor.addTrackTitleArtistSpy?.title, title)
XCTAssertEqual(mockInteractor.addTrackTitleArtistSpy?.artist, artist)
}
func test_didSwipeToDelete_whenTracksAvailable_shouldDeleteTrack() {
let tracks = MockTrackServicing.makeMockTracks()
subject.didFetch(tracks: tracks)
subject.didSwipeToDelete(at: 1)
XCTAssertEqual(mockInteractor.deleteTrackCallCount, 1)
XCTAssertEqual(mockInteractor.deleteTrackSpy, tracks[1])
}