Getting began with VIPER
Initially, it is best to learn my earlier (extra theoretical) article concerning the VIPER structure itself. It is a fairly respectable writing explaining all of the VIPER parts and reminiscence administration. I’ve additionally polished it a bit bit, final week. ⭐️
The issue with that article nonetheless was that I have not present you the actual deal, aka. the Swift code for implementing VIPER. Now after a full yr of tasks utilizing this structure I can lastly share all my finest practices with you.
So, let’s begin by making a model new Xcode venture, use the only view app template, identify the venture (VIPER finest practices), use Swift and now you are able to take the following step of constructing an superior “enterprise grade” iOS app.
Producing VIPER modules
Lesson 1: by no means create a module by hand, at all times use a code generator, as a result of it is a repetative job, it is fuckin’ boring plus it is best to deal with extra vital issues than making boilerplate code. You need to use my light-weight module generator known as:
This part is outdated, it is best to use the swift template repository.
Simply obtain or clone the repository from GitHub. You may set up the binary software by operating swift run set up –with-templates. This can set up the vipera app beneath /usr/native/bin/ and the fundamental templates beneath the ~/.vipera listing. You need to use your individual templates too, however for now I will work with the default one. 🔨
I normally begin with a module known as Essential that is the foundation view of the applying. You may generate it by calling vipera Essential within the venture listing, so the generator can use the correct venture identify for the header feedback contained in the template information.
Clear up the venture construction a bit bit, by making use of my conventions for Xcode, because of this assets goes to an Belongings folder, and all of the Swift information into the Sources listing. These days I additionally change the AppDelegate.swift file, and I make a separate extension for the UIApplicationDelegate protocol.
Create a Modules group (with a bodily folder too) beneath the Sources listing and transfer the newly generated Essential module beneath that group. Now repair the venture points, by choosing the Information.plist file from the Belongings folder for the present goal. Additionally do take away the Essential Interface, and after that you may safely delete the Essential.storyboard and the ViewController.swift information, as a result of we’re not going to want them in any respect.
Contained in the AppDelegate.swift file, it’s important to set the Essential module’s view controller as the foundation view controller, so it ought to look considerably like this:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder {
var window: UIWindow?
}
extension AppDelegate: UIApplicationDelegate {
func software(_ software: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(body: UIScreen.fundamental.bounds)
self.window?.rootViewController = MainModule().buildDefault()
self.window?.makeKeyAndVisible()
return true
}
}
Congratulations, you’ve got created your very first VIPER module! 🎉
UITabBarController & VIPER
I’ve a brilliant easy answer for utilizing a tab bar controller in a VIPER module. First let’s generate a couple of new modules, these are going to be the tabs. I will use the JSONPlaceholder service, so we could say a separate tab for every of those assets: posts, albums, images, todos (with the identical module identify). Generate all of them, and transfer them into the modules folder.
Now, let’s generate yet another module known as Dwelling. This can implement our tab bar controller view. In order for you you need to use the Essential module for this function, however I prefer to maintain that for animation functions, to have a neat transition between the loading display screen and my Dwelling module (all of it is determined by your wants).
So the primary logic that we will implement is that this: the primary view will notify the presenter concerning the viewDidAppear occasion, and the presenter will ask the router to show the Dwelling module. The Dwelling module’s view will likely be a subclass of a UITabBarController, it’s going to additionally notify it is presenter about viewDidLoad, and the presenter will ask for the correct tabs, by utilizing its router.
Right here is the code, with out the interfaces:
class MainDefaultView: UIViewController {
var presenter: MainPresenter?
override func viewDidAppear(_ animated: Bool) {
tremendous.viewDidAppear(animated)
self.presenter?.viewDidAppear()
}
}
extension MainDefaultPresenter: MainPresenter {
func viewDidAppear() {
self.router?.showHome()
}
}
extension MainDefaultRouter: MainRouter {
func showHome() {
let viewController = HomeModule().buildDefault()
self.viewController?.current(viewController, animated: true, completion: nil)
}
}
extension HomeDefaultView: HomeView {
func show(_ viewControllers: [UIViewController]) {
self.viewControllers = viewControllers
}
}
extension HomeDefaultPresenter: HomePresenter {
func setupViewControllers() {
guard let controllers = self.router?.getViewControllers() else {
return
}
self.view?.show(controllers)
}
}
extension HomeDefaultRouter: HomeRouter {
func getViewControllers() -> [UIViewController] {
return [
PostsModule().buildDefault(),
AlbumsModule().buildDefault(),
PhotosModule().buildDefault(),
TodosModule().buildDefault(),
].map { UINavigationController(rootViewController: $0) }
}
}
class HomeModule {
func buildDefault() -> UIViewController {
presenter.setupViewControllers()
return view
}
}
There’s one further line contained in the Dwelling module builder perform that triggers the presenter to setup correct view controllers. That is simply because the UITabBarController viewDidLoad methodology will get known as earlier than the init course of finishes. This behaviour is sort of undocumented however I assume it is an UIKit hack as a way to preserve the view references (or only a easy bug… is anybody from Apple right here?). 😊
Anyway, now you could have a correct tab bar contained in the venture built-in as a VIPER module. It is time to get some knowledge from the server and right here comes one other vital lesson: not all the pieces is a VIPER module.
Companies and entities As you would possibly observed there isn’t a such factor as an Entity inside my modules. I normally wrap APIs, CoreData and plenty of extra knowledge suppliers as a service. This fashion, all of the associated entities might be abstracted away, so the service might be simply changed (with a mock for instance) and all my interactors can use the service by way of the protocol definition with out figuring out the underlying implementation.
One other factor is that I at all times use my promise library if I’ve to cope with async code. The rationale behind it’s fairly easy: it is far more elegant than utilizing callbacks and non-compulsory end result components. You need to be taught guarantees too. So right here is a few a part of my service implementation across the JSONPlaceholder API:
protocol Api {
func posts() -> Promise<[Post]>
func feedback(for put up: Publish) -> Promise<[Comment]>
func albums() -> Promise<[Album]>
func images(for album: Album) -> Promise<[Photo]>
func todos() -> Promise<[Todo]>
}
struct Publish: Codable {
let id: Int
let title: String
let physique: String
}
class JSONPlaceholderService {
var baseUrl = URL(string: "https://jsonplaceholder.typicode.com/")!
enum Error: LocalizedError {
case invalidStatusCode
case emptyData
}
non-public func request<T>(path: String) -> Promise<T> the place T: Decodable {
let promise = Promise<T>()
let url = baseUrl.appendingPathComponent(path)
print(url)
URLSession.shared.dataTask(with: url) { knowledge, response, error in
if let error = error {
promise.reject(error)
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
promise.reject(Error.invalidStatusCode)
return
}
guard let knowledge = knowledge else {
promise.reject(Error.emptyData)
return
}
do {
let mannequin = attempt JSONDecoder().decode(T.self, from: knowledge)
promise.fulfill(mannequin)
}
catch {
promise.reject(error)
}
}.resume()
return promise
}
}
extension JSONPlaceholderService: Api {
func posts() -> Promise<[Post]> {
return self.request(path: "posts")
}
}
Often I’ve a mock service implementation subsequent to this one, so I can simply check out all the pieces I would like. How do I swap between these companies? Effectively, there’s a shared (singleton – do not hate me it is fully advantageous 🤪) App class that I exploit largely for styling functions, however I additionally put the dependency injection (DI) associated code there too. This fashion I can move round correct service objects for the VIPER modules.
class App {
static let shared = App()
non-public init() {
}
var apiService: Api {
return JSONPlaceholderService()
}
}
class PostsModule {
func buildDefault() -> UIViewController {
let view = PostsDefaultView()
let interactor = PostsDefaultInteractor(apiService: App.shared.apiService)
return view
}
}
class PostsDefaultInteractor {
weak var presenter: PostsPresenter?
var apiService: Api
init(apiService: Api) {
self.apiService = apiService
}
}
extension PostsDefaultInteractor: PostsInteractor {
func posts() -> Promise<[Post]> {
return self.apiService.posts()
}
}
You are able to do this in a 100 different methods, however I presently favor this method. This fashion interactors can immediately name the service with some further particulars, like filters, order, kind, and so on. Principally the service is only a excessive idea wrapper across the endpoint, and the interactor is creating the fine-tuned (higher) API for the presenter.
Making guarantees
Implementing the enterprise logic is the duty of the presenter. I at all times use guarantees so a primary presenter implementation that solely masses some content material asynchronously and shows the outcomes or the error (plus a loading indicator) is only a few traces lengthy. I am at all times making an attempt to implement the three primary UI stack components (loading, knowledge, error) by utilizing the identical protocol naming conventions on the view. 😉
On the view aspect I am utilizing my good outdated assortment view logic, which considerably reduces the quantity of code I’ve to write down. You may go along with the standard approach, implementing a couple of knowledge supply & delegate methodology for a desk or assortment view will not be a lot code in spite of everything. Right here is my view instance:
extension PostsDefaultPresenter: PostsPresenter {
func viewDidLoad() {
self.view?.displayLoading()
self.interactor?.posts()
.onSuccess(queue: .fundamental) { posts in
self.view?.show(posts)
}
.onFailure(queue: .fundamental) { error in
self.view?.show(error)
}
}
}
class PostsDefaultView: CollectionViewController {
var presenter: PostsPresenter?
init() {
tremendous.init(nibName: nil, bundle: nil)
self.title = "Posts"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been applied")
}
override func viewDidLoad() {
tremendous.viewDidLoad()
self.presenter?.viewDidLoad()
}
}
extension PostsDefaultView: PostsView {
func displayLoading() {
print("loading...")
}
func show(_ posts: [Post]) {
let grid = Grid(columns: 1, margin: UIEdgeInsets(all: 8))
self.supply = CollectionViewSource(grid: grid, sections: [
CollectionViewSection(items: posts.map { PostViewModel($0) })
])
self.collectionView.reloadData()
}
func show(_ error: Error) {
print(error.localizedDescription)
}
}
The cell and the ViewModel is outdoors the VIPER module, I are inclined to dedicate an App folder for the customized software particular views, extensions, view fashions, and so on.
class PostCell: CollectionViewCell {
@IBOutlet weak var textLabel: UILabel!
}
class PostViewModel: CollectionViewViewModel<PostCell, Publish> {
override func config(cell: PostCell, knowledge: Publish, indexPath: IndexPath, grid: Grid) {
cell.textLabel.textual content = knowledge.title
}
override func dimension(knowledge: Publish, indexPath: IndexPath, grid: Grid, view: UIView) -> CGSize {
let width = grid.width(for: view, gadgets: grid.columns)
return CGSize(width: width, top: 64)
}
}
Nothing particular, if you would like to know extra about this assortment view structure, it is best to learn my different tutorial about mastering assortment views.
Module communication
One other vital lesson is to discover ways to talk between two VIPER modules. Usually I am going with easy variables – and delegates if I’ve to ship again some kind of data to the unique module – that I move round contained in the construct strategies. I will present you a extremely easy instance for this too.
class PostsDefaultRouter {
weak var presenter: PostsPresenter?
weak var viewController: UIViewController?
}
extension PostsDefaultRouter: PostsRouter {
func showComments(for put up: Publish) {
let viewController = PostDetailsModule().buildDefault(with: put up, delegate: self)
self.viewController?.present(viewController, sender: nil)
}
}
extension PostsDefaultRouter: PostDetailsModuleDelegate {
func toggleBookmark(for put up: Publish) {
self.presenter?.toggleBookmark(for: put up)
}
}
protocol PostDetailsModuleDelegate: class {
func toggleBookmark(for put up: Publish)
}
class PostDetailsModule {
func buildDefault(with put up: Publish, delegate: PostDetailsModuleDelegate? = nil) -> UIViewController {
let view = PostDetailsDefaultView()
let interactor = PostDetailsDefaultInteractor(apiService: App.shared.apiService,
bookmarkService: App.shared.bookmarkService)
let presenter = PostDetailsDefaultPresenter(put up: put up)
return view
}
}
class PostDetailsDefaultRouter {
weak var presenter: PostDetailsPresenter?
weak var viewController: UIViewController?
weak var delegate: PostDetailsModuleDelegate?
}
extension PostDetailsDefaultRouter: PostDetailsRouter {
func toggleBookmark(for put up: Publish) {
self.delegate?.toggleBookmark(for: put up)
}
}
class PostDetailsDefaultPresenter {
var router: PostDetailsRouter?
var interactor: PostDetailsInteractor?
weak var view: PostDetailsView?
let put up: Publish
init(put up: Publish) {
self.put up = put up
}
}
extension PostDetailsDefaultPresenter: PostDetailsPresenter {
func reload() {
self.view?.setup(with: self.interactor!.bookmark(for: self.put up))
self.interactor?.feedback(for: self.put up)
.onSuccess(queue: .fundamental) { feedback in
self.view?.show(feedback)
}
.onFailure(queue: .fundamental) { error in
}
}
func toggleBookmark() {
self.router?.toggleBookmark(for: self.put up)
self.view?.setup(with: self.interactor!.bookmark(for: self.put up))
}
}
Within the builder methodology I can entry each element of the VIPER module so I can merely move across the variable to the designated place (identical applies for the delegate parameter). I normally set enter variables on the presenter and delegates on the router.
It is normally a presenter who wants knowledge from the unique module, and I prefer to retailer the delegate on the router, as a result of if the navigation sample modifications I haven’t got to vary the presenter in any respect. That is only a private desire, however I like the way in which it seems to be like in code. It is actually laborious to write down down these items in a single article, so I might suggest to obtain my completed pattern code from GitHub.
Abstract
As you possibly can see I am utilizing varied design patterns on this VIPER structure tutorial. Some say that there isn’t a silver bullet, however I consider that I’ve discovered a extremely wonderful methodology that I can activate my benefit to construct high quality apps in a short while.
Combining Guarantees, MVVM with assortment views on high of a VIPER construction merely places each single piece into the precise place. Over-engineered? Possibly. For me it is well worth the overhead. What do you consider it? Be at liberty to message me by way of twitter. You can even subscribe to my month-to-month e-newsletter beneath.