On this tutorial I’ll present you a whole information about how one can construct a VIPER based mostly iOS software, written totally in Swift.
VIPER
This put up is somewhat bit outdated, please count on a brand new model coming quickly…
Getting began with VIPER
Initially, it’s 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 somewhat bit, final week. ⭐️
The issue with that article nonetheless was that I have not present you the true deal, aka. the Swift code for implementing VIPER. Now after a full 12 months 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 mission, use the one view app template, title the mission (VIPER finest practices), use Swift and now you are able to take the subsequent step of creating 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’s best to give attention to extra vital issues than making boilerplate code. You should use my light-weight module generator referred to as:
VIPERA
Simply obtain or clone the repository from github. You’ll be able to set up the binary instrument by operating swift run set up --with-templates
. It will set up the vipera
app below /usr/native/bin/
and the fundamental templates below the ~/.vipera
listing. You should use your personal templates too, however for now I am going to work with the default one. 🔨
I normally begin with a module referred to as Predominant
that is the basis view of the appliance. You’ll be able to generate it by calling vipera Predominant
within the mission listing, so the generator can use the right mission title for the header feedback contained in the template information.
Clear up the mission construction somewhat bit, by making use of my conventions for Xcode, which means assets goes to an Property
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) below the Sources
listing and transfer the newly generated Predominant
module below that group. Now repair the mission points, by deciding on the Information.plist
file from the Property
folder for the present goal. Additionally do take away the Predominant Interface, and after which you can safely delete the Predominant.storyboard
and the ViewController.swift
information, as a result of we’re not going to wish them in any respect.
Contained in the AppDelegate.swift file, you need to set the Predominant module’s view controller as the basis 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.primary.bounds)
self.window?.rootViewController = MainModule().buildDefault()
self.window?.makeKeyAndVisible()
return true
}
}
Congratulations, you have created your very first VIPER module! 🎉
UITabBarController & VIPER
I’ve an excellent easy answer for utilizing a tab bar controller in a VIPER module. First let’s generate just a few new modules, these are going to be the tabs. I’ll use the JSONPlaceholder service, so lets say a separate tab for every of those assets: posts, albums, photographs, todos (with the identical module title). Generate all of them, and transfer them into the modules folder.
Now, let’s generate yet one more module referred to as House
. It will implement our tab bar controller view. If you’d like you should use the Predominant module for this function, however I wish to preserve that for animation functions, to have a neat transition between the loading display screen and my House module (all of it relies on your wants).
So the principle logic that we will implement is that this: the principle view will notify the presenter concerning the viewDidAppear occasion, and the presenter will ask the router to show the House module. The House module’s view might be a subclass of a UITabBarController, it will additionally notify it is presenter about viewDidLoad, and the presenter will ask for the right tabs, through the use of 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 extra line contained in the House module builder perform that triggers the presenter to setup correct view controllers. That is simply because the UITabBarController viewDidLoad methodology will get referred to as earlier than the init course of finishes. This behaviour is kind of undocumented however I assume it is an UIKit hack in an effort to keep the view references (or only a easy bug… is anybody from Apple right here?). 😊
Anyway, now you have got a correct tab bar contained in the mission built-in as a VIPER module. It is time to get some information from the server and right here comes one other vital lesson: not every part is a VIPER module.
Providers and entities
As you would possibly seen there isn’t a such factor as an Entity inside my modules. I normally wrap APIs, CoreData and lots of extra information 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 via the protocol definition with out realizing the underlying implementation.
One other factor is that I at all times use my promise library if I’ve to take care of async code. The rationale behind it’s fairly easy: it is far more elegant than utilizing callbacks and elective consequence 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: Put up) -> Promise<[Comment]>
func albums() -> Promise<[Album]>
func photographs(for album: Album) -> Promise<[Photo]>
func todos() -> Promise<[Todo]>
}
struct Put up: 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
}
personal 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) { information, 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 information = information else {
promise.reject(Error.emptyData)
return
}
do {
let mannequin = strive JSONDecoder().decode(T.self, from: information)
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 take a look at out every part I need. How do I swap between these providers? Nicely, there’s a shared (singleton – do not hate me it is fully wonderful 🤪) 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 go round correct service objects for the VIPER modules.
class App {
static let shared = App()
personal 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 at present desire this method. This fashion interactors can instantly name the service with some additional particulars, like filters, order, kind, and many others. 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 hundreds some content material asynchronously and shows the outcomes or the error (plus a loading indicator) is only a few strains lengthy. I am at all times attempting to implement the three primary UI stack components (loading, information, error) through the use of the identical protocol naming conventions on the view. 😉
On the view facet I am utilizing my good previous assortment view logic, which considerably reduces the quantity of code I’ve to put in writing. You’ll be able to go along with the normal method, implementing just a few information supply & delegate methodology for a desk or assortment view shouldn’t be a lot code in any case. Right here is my view instance:
extension PostsDefaultPresenter: PostsPresenter {
func viewDidLoad() {
self.view?.displayLoading()
self.interactor?.posts()
.onSuccess(queue: .primary) { posts in
self.view?.show(posts)
}
.onFailure(queue: .primary) { 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 carried out")
}
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 likely to dedicate an App folder for the customized software particular views, extensions, view fashions, and many others.
class PostCell: CollectionViewCell {
@IBOutlet weak var textLabel: UILabel!
}
class PostViewModel: CollectionViewViewModel<PostCell, Put up> {
override func config(cell: PostCell, information: Put up, indexPath: IndexPath, grid: Grid) {
cell.textLabel.textual content = information.title
}
override func dimension(information: Put up, 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’s 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’m going with easy variables – and delegates if I’ve to ship again some type of information to the unique module – that I go round contained in the construct strategies. I’ll 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: Put up) {
let viewController = PostDetailsModule().buildDefault(with: put up, delegate: self)
self.viewController?.present(viewController, sender: nil)
}
}
extension PostsDefaultRouter: PostDetailsModuleDelegate {
func toggleBookmark(for put up: Put up) {
self.presenter?.toggleBookmark(for: put up)
}
}
protocol PostDetailsModuleDelegate: class {
func toggleBookmark(for put up: Put up)
}
class PostDetailsModule {
func buildDefault(with put up: Put up, 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: Put up) {
self.delegate?.toggleBookmark(for: put up)
}
}
class PostDetailsDefaultPresenter {
var router: PostDetailsRouter?
var interactor: PostDetailsInteractor?
weak var view: PostDetailsView?
let put up: Put up
init(put up: Put up) {
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: .primary) { feedback in
self.view?.show(feedback)
}
.onFailure(queue: .primary) { 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 go across the variable to the designated place (similar 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 information from the unique module, and I wish to retailer the delegate on the router, as a result of if the navigation sample adjustments I haven’t got to vary the presenter in any respect. That is only a private desire, however I like the best way it seems to be like in code. It is actually onerous to put in writing down this stuff in a single article, so I would advocate to obtain my completed pattern code from github.
Abstract
As you may see I am utilizing varied design patterns on this VIPER structure tutorial. Some say that there isn’t a silver bullet, however I imagine 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 correct place. Overengineered? Possibly. For me it is definitely worth the overhead. What do you concentrate on it? Be happy to message me via twitter. You too can subscribe to my month-to-month publication beneath.