Storyboard или интерфейс из кода?

Довольно продолжительное время я был адептом исключительно Storyboard и презирал использование кода в качестве основного способа построения интерфейса. Все изменилось. Для чего может понадобиться построение интерфейса из кода, как основной способ?

Путь с UIStoryboard

Начать нужно с того, как был выстроен процесс работы со Storyboard. Все экране делились на секции (Регистрация, Профиль, Новости и пр) все это относится к разным секциям. Каждая секция реализуется в своем Storyboard, чтобы не пихать все контроллеры в один. Все связанные с секцией классы располагаются в одной папке секции. Все, что относится к UI расположено в отдельной папке. Таким образом весь интерфейс выделен в отдельный слой

Все директории разделены

Для безопасности работы с контроллерами и Storyboard были приняты следующие соглашения:

Controller Init

Все контроллеры наследуются от базового который имеет следующие важные функции

/// Init controller with data. Should return false if data is incorrect or controller could not be initialized
///
/// - Parameter data: dictionary with data
open func initController(_ data: [String:Any]?) -> Bool {
    return true
}

public class func getController(_ data: [String:Any]? = nil) -> OXViewController? {
   let controller = getViewController()
   if !controller.initController(data) {
       return nil
   }
   return controller
}

Таким образом создаются все экземпляры контроллеров. Внутрь getController передаются необходимые параметры. initController проверяет входящие параметры, и, если его все устраивает создается контроллер. Это позволяет сделать типобезопасную инициализацию. За то, чтобы правильно достать контроллер из Storyboard есть следующий метод. Для всех контроллеров необходимо переопределить storyboardName: String и этот метод создаст контроллер с именем равным имени класса. На мой взгляд это сильно упрощает конвенцию имен и позволяет абстрагироваться от задачи создания экземпляра контроллера.

Router

В качестве управляющего звена для навигации используется механизм роутера. Роутер представляет из себя набор функций, осуществляющих подготовку к переходу и сам переход. Примеры приведены ниже

func presentLogs(parent: UIViewController) {
   if let controller = DebugLogsViewController.getController() {
       parent.navigationController?.pushViewController(controller, animated: true  )
   }
}

func presentMatchDetails(parent: UIViewController,
                                         match: Match) {
   if let controller = MatchDetailsViewController.getController(["match": match]) {
       parent.navigationController?.pushViewController(controller, animated: true)
   }
}

Используя эти 2 соглашения я добился высокого процента CrashFree в зоне ответственности контроллеров и окружения. Почему же возник вопрос, заданный в самом начале?

Code

Рассмотрим основные моменты, с которыми я столкнулся, чтобы начать перевод построения интерфейсов со Storyboard в код. Я не до конца уверен, что это единственный правильный путь, но единственный известный мне, который позволяет получить меньше проблем, чем используя Storyboard.

1. Изменения дизайна

Хорошо, если при небольшом изменении дизайна меняется один контроллер. Без вопросов проще и быстрее все обновить в Storyboard. Но, когда дело касается 3, 5, 10 а то и почти всех контроллеров это конец света. На деле такое бывает достаточно часто. Меняется общий стиль для всех таблиц, шрифт, цвета кнопок. Большинство описанных проблем решается каким-либо классом Style в котором у каждого уважающего себя iOS разработчика будет основной цвет, шрифты и другие атрибуты для построения интерфейсов.

Как только создание нового экрана начинается с того, что весь он или часть копируется из другого - повод задуматься. Гораздо дешевле уже тут использовать и переиспользовать View. Вот мы подошли ко второму пункту

2. Переиспользование

Нет никакой возможности переиспользовать Storyboard. На помощь приходят Xib, комбинации и макароны код. Эти подходы позволяют переиспользовать код. Но теряется визуальная составляющая в Storyboard. Необходимо запоминать где лежат те или иные части интерфейса.

Все!

Действительно. Все остальные доводы, за или против не имеют никаких преимуществ и разбиваются в пух и прах при правильном подходе к архитектуре приложения. Разделения интерфейсов, бизнес логики, модели на слои.

Если после прочтения статьи вам показалось, что я рекомендую избавиться от Storyboard и начать все дизайнить в коде - это не так. Из кода стоит делать верстку только в двух случаях. А именно: точно известно, что дизайн будет меняться или множественное переиспользование. Есть еще возможность скомбинировать Storyboard и код в случае динамического разнопланового контента.

Update

Как указали в комментариях getController и initController не панацея. Вот уже пару недель я использую другой подход, который пока полностью не опробовал не публиковал тут.

convenience init(player: UserModel?) {
    self.init()
    
    self.player = player
}

Вот такая инициализация контроллера оказалась удобнее. Более того, нет трансформации данных в словарь и обратно с приведением типов.

Router по прежнему нужен, он делает работу по созданию объекта контроллера. Но больше он отвечает за переходы, анимацию.

func presentScheduleEdit(parent: ViewControllerBase,
                         event: YouthEvent?,
                         delegate: UpdatableViewController) {
    let controller = ScheduleEditEventViewController(event: event, delegate: delegate)
    let masterNC = MasterNavigationController.makeMasterNC(rootViewController: controller)
    parent.present(masterNC, animated: true, completion: nil)
}

Комментарии

comments powered by Disqus