суббота, 18 ноября 2017 г.

iOS. Swift. Router with protocol composition

В данной статье хочу поделится своим вариантов как вынести переходы между экранами в iOS приложении в отдельный класс ( Router ). Как дополнять возможности router-a переходить на большее количество экранов при этом не копируя код и избегая наследования.

Если у нас приложение построено на MVC ( да и любой другой ) архитектуре, то мы часто можем столкнуться в коде ViewController-a со следующим вариантом показать экран ( пример перейти на экран деталей пользователя ):

func showUserProfile() {
    let profile = UserProfileViewController()

    self.show(profile, sender: nil)
}

Теперь нам нужно перейти на экран деталей пользователя например из списка контактов, результатов поиска или из пин-а на карте. Итого получается что нам нужно открыть экран деталей пользователя из нескольких ViewController-ов.

Как вынести метод showUserProfile в одно место?

Для решение задачи можно создать отдельный класс ( в нашем варианте UserRouter ) и скопировать туда метод. Получаем:

class UserRouter {

    func showUserProfile(from controller: UIViewController) {
        let profile = UserProfileViewController()

        controller.show(profile, sender: nil)
    }
}

Теперь нам нужно подключить наш роутер во все controller-ы где мы будем использовать метод showUserProfile. Итого получаем:

UsersListViewController.swift
class UsersListViewController {

    var router: UserRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}

SearchResultViewController.swift
class SearchResultViewController {

    var router: UserRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}

Вроде все хорошо и мы получили что хотели.

Но теперь нам потребовалось добавить переход на экран добавления нового пользователя из экрана UsersListViewController. Можно просто добавить метод showAddUserScreen метод в UserRouter, но SearchResultViewController-у этот метод не нужен и не хотелось бы видеть лишние метод при использовании router-a. Простой вариант создаем новый роутер UsersListRouter наследуемся от UserRouter и добавляем метод showAddUserScreen. Получаем:

class UsersListRouter: UserRouter {

    func showAddUserScreen(from controller: UIViewController) {
        let add = AddUserViewController()

        controller.show(add, sender: nil)
    }
}

Итого наш UsersListController может открыть экран деталей пользователи и экран добавления нового пользователя. В свою очередь SearchResultController может только открыть экран деталей пользователя. В результате мы не нарушили наше правило НЕ дублировать код.

Наш проект не стоит на месте и добавление нового функционала влечет за собой изменения в нашем коде.
Пришло новое требование: добавить возможность отравления сообщения из UsersListViewController и из UserProfileViewController.

Сейчас нам нужно сделать так чтобы
UserListViewController мог переходить на экраны: UserProfile, AddUser, SendMessage,
SearchResultViewController на экраны: UserProfile
UserProfileViewController на экраны: SendMessage

Наследование здесь не поможет потому что так или иначе какой то класс будет видеть методы ( возможность ) перехода на экран, на который ему переходить не надо :)

В данном случае нам помогут protocol и protocol extension. Изменим наш UserRouter:

UserRouter.swift
protocol UserRouter {

    func showUserProfile(from controller: UIViewController)
}

extension UserRouter {

    func showUserProfile(from controller: UIViewController) {
        let profile = UserProfileViewController()

        controller.show(profile, sender: nil)
    }
}

Добавим новый роутер SystemToolsRouter:

SystemToolsRouter.swift
protocol SystemToolsRouter {

    func showSendMessageScreen(from controller: UIViewController)
}

extension SystemToolsRouter {

    func showSendMessageScreen(from controller: UIViewController) {
        let sendMessage = SendMessageViewController()

        controller.show(sendMessage, sender: nil)
    }
}

И добавим изменения в наши роутеры экранов:

UsersListRouter.swift
class UsersListRouter: SystemToolsRouter, UserRouter {

    func showAddUserScreen(from controller: UIViewController) {
        let add = AddUserViewController()

        controller.show(add, sender: nil)
    }
}

SearchResultRouter.swift
class SearchResultRouter: UserRouter {

}

UsersProfileRouter.swift
class UserProfileRouter: SystemToolsRouter {

}

В результате наши контроллеры будут выглядеть:

UsersListViewController.swift
class UsersListViewController {

    var router: UsersListRouter?

    ...

    router?.showUserProfile(from: self)
    router?.showSendMessageScreen(from: self)
    router?.showAddUserScreen(from: self)

    ...
}


SearchResultViewController.swift
class SearchResultViewController {

    var router: SearchResultRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}


UserProfileViewController.swift
class UserProfileViewController {

    var router: UserProfileRouter?

    ...

    router?.showSendMessageScreen(from: self)

    ...
}

Итого при использовании protocol composition, protocol extension мы легко можем добавлять переходы на другие экраны не используя наследования и дублирования кода.

P.S. Оставляем комментарии, темы для рассмотрения. И кликаем +1, f, в, t, что располагаются чуть ниже статьи.