Итак, вы начали новый проект в Xcode. Первое, что я предлагаю сделать, это удалить Main.storyboard. Почему? Потому что от него исходит много проблем.

Чем мне не угодил Storyboard


Не спорю, Storyboard?—?очень удобная вещь. Все контроллеры расположены в одном месте, причем, все они соединены переходами (segues). Можно сказать, что приложение будто находится у вас на ладонях. И это замечательно, ведь не всегда удается запомнить, к какому контроллеру мы перейдем, если нажмем на очередную кнопку или ячейку.
Но, это все? Есть ли еще какие-нибудь преимущества? На самом деле нет. Зато приходится мириться с многими неприятными вещами.

Неприятность первая


Все контроллеры расположены в одном месте

А так ли это хорошо? Возможно, если в приложении их не так много. Но что обычно происходит при увеличении их количества? А вот что:

Storygetti

Выглядит не очень, да и поддерживать такое вряд ли кому-то захочется.
(Ах да, еще оно тормозит)

Неприятность вторая


Да, держать все контроллеры в одном месте это ужасно. Может ли быть что-нибудь хуже? Может. Например, если над таким большим приложением работает не один человек. Если двое разработчика занимаются интерфейсом, который находится в одном Storyboard’е, в разных ветках, то как им потом объединить эти изменения? Ответ простой?—?никак. Скорее всего, кому-то придется слить себе чужие изменения и сделать свою работу заново.
И чем чаще будут происходить такие моменты, тем горячее вам будет сидеть на стуле.

Неприятность третья


А вот и самая большая проблема для меня на текущий момент: передача зависимостей. Приведу небольшой пример:
Если следовать MVVM, то у каждого контроллера должна быть ViewModel, причем породить ее должна родительская ViewModel. Вот как это может выглядеть при использовании Storyboard:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == Segues.MoviePreview {
        let controller = segue.destinationViewController as? MoviePreviewController
        controller?.movieViewModel = viewModel.movieViewModel()
    }
}

При этом, MoviePreviewController выглядит как-то так:
class MoviePreviewController: UIViewController {

    var movieViewModel: MovieViewModel?
    
    // ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let movieTitle = movieViewModel?.title {
            doSomethingWithMovieTitle(movieTitle)
        } else {
            // Report absence of title
        }
    }

}

Проблема в том, что movieViewModel у нас Optional, хотя мы знаем, что там обязательно должно быть значение. Это выливается в совсем ненужные проверки при попытке извлечь нужные нам данные.

Некоторые скажут, что это совсем не проблема, ведь мы можем насильно развернуть значение с помощью специального оператора ! или, еще лучше, объявить movieViewModel как MovieViewModel! , и они будут правы. Такая возможность есть, но она приносит с собой еще большую проблему: крэши в рантайме.

Например, если в методе prepareForSegue(_:sender:) вместо segue.identifier == Segues.MoviePreview написать segue.identifier == Segues.HahaYouHaveAProblem , то мы можем быть уверены, что в скором времени приложение упадет.

Что можно сделать


Первые две неприятности решаются без каких-либо особых усилий. Если приложение должно работать на iOS 8+, то можно без зазрения совести воспользоваться Storyboard Reference и разделить один большой Storyboard на множество (в разумных пределах) мелких.

Правда, для iOS 7+ решение не будет таким же бесшовным и придется дописывать руками что-то наподобии такого:
let storyboard = UIStoryboard(name: Storyboards.SomeStory, bundle: nil)
let viewController = storyboard.instantiateInitialViewController()
 
if let viewController = viewController {
    presentViewController(viewController, animated: true, completion: nil)
}

И опять же, это ведет к потенциальным крэшам при инициализации Storyboard’a с неправильным именем.
А вот чтобы решить третью проблему, придется, ни много ни мало, вернуться к старым добрым .xib’ам.
И, в таком случае, MoviePreviewController может выглядеть так:
class MoviePreviewController: UIViewController {

    var movieViewModel: MovieViewModel?
    
    // ...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let movieTitle = movieViewModel?.title {
            doSomethingWithMovieTitle(movieTitle)
        } else {
            // Report absence of title
        }
    }

}

И его инициализация:
@IBAction func buttonTapped(sender: AnyObject) {
    let controller = MoviePreviewController(movieViewModel: viewModel.movieViewModel())
    presentViewController(controller, animated: true, completion: nil)
}

Теряем ли мы что-нибудь, отказываясь от Storyboard в пользу .xib’ов? Ничего, кроме вышеперечисленного.

Ссылки:
Storyboard from hell
Изначальная статья

Комментарии (32)


  1. Lonkly
    16.01.2016 22:12
    +3

    Есть еще RBStoryboardLink для pre-iOS9. На ксибы все же не очень удобно возвращаться


    1. devnikor
      16.01.2016 22:35
      -2

      Видел когда-то давно, но не пользовался. Лишняя зависимость, притом не такая нужная. На ксибы я пока тоже не перешел) Собираюсь в следующем проекте попробовать (начиная с day one в карьере, пользовался исключительно сторибордом)


  1. egormerkushev
    16.01.2016 23:00
    +3

    Вам роутер нужен, у вас должен быть опыт в написании подобного, странно, что вы жалуетесь на сторибоарды.
    В последнем своём проекте (iOS 7+) отлично поработал со сторибордами, их было аж восемь штук, никакой проблемы с навигацией за счет констант с именами сторибоардов и контроллеров и отдельного роутера, который знает что, как и куда. Клеить паутину из segue в сторибоардах — глупо, это ж очевидно было сразу как сториборды появились. Максимум для чего segue годятся — прямая связь навконтроллера и контроллера, таббара с контроллерами, вложенные контроллеры и одноуровневые переходы на другие экраны.


    1. devnikor
      16.01.2016 23:50
      +1

      К сожалению, опыта в написании роутера нет. Начинаю засматриваться на то, как реализуется роутер viper, чтобы перенести что-то похожее в mvvm

      Оффтоп
      если у вас есть полезные ссылки, буду рад принять их в личке :)


      1. Agent_Smith
        22.01.2016 12:27

        Писал свой роутер поверх JLRoutes, очень удобная, гибкая и легковесная штука без лишних наворотов.


  1. complexityclass
    16.01.2016 23:24

    Storyboard Reference работают для iOS 8+


    1. devnikor
      16.01.2016 23:42

      Судя по документации — нет:

      Compatibility: Storyboard references required an app targeting at least iOS 9.0, OS X 10.11, or WatchKit 2.0 for watchOS.

      Линк


      1. complexityclass
        16.01.2016 23:50

        В release notes Xcode 7.0. Мы юзаем, работает.
        Storyboard References may now be deployed to iOS 8, OS X 10.10, and watchOS 1.


        1. devnikor
          16.01.2016 23:52

          Спасибо за инфу, приму к сведению и поправлюсь)


  1. rule
    17.01.2016 03:36
    +4

    Почему многие думаю что сториборд должен быть один? У нас обычно 5-8 сторибордов в приложении. Всё очень хорошо и особых проблем нет.


    1. devnikor
      17.01.2016 10:09
      -3

      Не знаю, почему. Но один раз я видел в сторибоарде ~30-40 контроллеров и это был ад


      1. rule
        17.01.2016 10:30
        +4

        Ну я когда-то видел развернутый цикл на 40 итераций. Это тоже не прелесть была. Но это же не значит, что теперь нельзя пользоваться циклами.


        1. devnikor
          17.01.2016 13:27

          Я нигде не говорю, что ими нельзя пользоваться. Я говорю про проблемы, которые они привносят. Особенно с di в свифте. Конечно, эта проблема тоже решаема, но хотелось бы что-нибудь вроде init(contex:), как в WatchKit


  1. bartleby
    17.01.2016 19:44
    +3

    Ух… Удивлен таким статьям на хабре, а я сюда еще и из дайджеста перешел… Ваша проблема растет из неверной, изначально, архитектуры проекта, все эти проблемы решаются разделением на юзер стори и роутером, я выработал за время работы нечто свое где разделяю каждую историю, например логин, одна история, профиль другая, в контроллер через di инжектится роутер и все необходимые сервисы, у каждой истории свой роутер, получается очень чистенько, кстати и от мввм использую вм, так же для списков отдельные датасорсы, ну и сервисы по работе с сетью например. Такая архитектура легко расширяема, тестируема, если работает команда то лучше и не придумать, ведь юзер стори это отдельный модуль, над которым работает один человек, вот структура стандартного юзер стори — interface (сториборда), router, vc, view, network (web service, у каждой юс свой), вм и ассемблай (от тайфуна для инъекций), это проще и легче вайпера по которому все так сходят с ума в последнее время, а можно и мвц хорошо и красиво реализовать :)


    1. aspcartman
      18.01.2016 03:20

      Подробнее можно где почитать про раздирание сторибордов на стори?

      Если в двух сторибордах один и тот же VC фигурирует, что тогда? (не пользуюсь IB, совсем ничего не знаю по теме, но в моих фантазиях получается, что прийдется дублировать, либо выносить в отдельный ксиб)


      1. devnikor
        18.01.2016 07:52

        Про storyboard reference можно почитать тут
        Если нужна поддержка iOS 7, то тут вполне неплохой туториал

        Контроллеры можно не дублировать, а использовать storyboard.instantiateViewControllerWithIdentifier(_:)
        То есть, каждому контроллеру можно присвоить идентификатор и инстанциировать по нему в любом месте


        1. aspcartman
          18.01.2016 07:56

          Про референс известно и iOS7 тоже
          Про инстантиирование сториборда тоже, что происходит в коде, а не в IB.

          Я к ответу bartleby вопрос задал: как в двух отдельных сторибордах описать «стори», затрагивающую один и тот же VC? Мы же про сториборды говорим?


        1. devnikor
          18.01.2016 07:57

          Пишу с телефона, ссылки почему-то не вставляются
          Первая: developer.apple.com/library/ios/recipes/xcode_help-IB_storyboard/Chapters/RefactorStoryboard.html

          Вторая: timdietrich.me/blog/swift-multiple-storyboards/


    1. devnikor
      18.01.2016 07:41

      Спасибо за комментарий! Решение фабрика контроллеров + роутер достаточно неплохое. Собираюсь попробовать в текущем проекте. Что касается остальной архитектуры, использую нечто похожее, правда, без фреймворка для di. Пока инжектирую вручную.


    1. rsi
      20.01.2016 09:23

      А вы не могли бы показать пример своего проекта, в частности интересует именно роуетр. Не могу найти толковой статьи или примера в интернете. Можно в личку.


  1. usgleb
    18.01.2016 00:07
    +1

    Я вот когда прихожу в ресторан, так сразу же выбрасываю ножик. Не понимаю зачем его придумали? Он же острый — можно запросто себе руку отрезать или еще чего. А мясушко я так, руками кусочки отрываю и кушаю. Удобно же!


  1. aspcartman
    18.01.2016 02:55

    Я бы советовал быть более радикальным и вообще полностью отказаться от IB.

    - (void) loadView {
            UIView *layout = [UIView new];
            self.view = layout;
    
            XXButton *button = [XXButton new];
            button.label.text = @"Привет Хабр";
            [layout addSubview:button];
            button.keepTopInset.equal = 20;
            button.keepLeftInset.equal = 20;
    }
    


    За плечами не один огромный проект и ни в одном из них нет ни единого .xib.
    Пример кода — самый простой. Со временем паттерны построения интерфейса обрастают вспомогательными классами, чтобы печатать еще меньше. Уже на iOS7 я имплементировал (весьма и весьма тривиально) свой UIStackView и активно использовал.

    Плюсы очевидны:
    1. Динамическое построение дается проще (что, если вы разрабатываете элемент в IB, внутри которого может быть N подэлементов? Решается не очень сложно, но приседаний больше, чем ноль.),
    2. Никаких IBOutlet и IBAction
    3. Никаких непоняток с Autolayout (google 'KeepLayout github'), субьективно код печатается быстрее, чем в IB мучаться
    4. Изменения в интерфейсе можно применять без перекомпиляции\перезапуска (google 'ios xcode\appcode injection plugin') — поменял код, хоткей нажал и в симуляторе интерфейс перестроился
    5. Субьективно нравится :)
    6. Неучтенный вариант.

    Минусы:
    1. Нельзя без запуска глазами увидеть ничего
    2. Подсказывайте, не пользуюсь IB :)


    1. aspcartman
      18.01.2016 03:10

      Пример из реального кода:

      __unused XSBattlesListView *battlesView = [XSBattlesListView with:^(XSBattlesListView *o) {
      		o.delegate = _s;
      		[_layoutMain addSubview:o];
      		o.keepInsets.equal      = 0;
      
      		[_reloads addObject:^(XSUser *user) {
      			o.battles = user.battles;
      		}];
      	}];
      


      -1. Это кусок метода -loadView. Выше этих строк просто [super loadView];

      0. __unused XSBattlesListView *battlesView — синтаксический сахар, переменная не используется, но при чтении кода важно, чтобы человек понимал, за что какой кусок отвечает.

      1. with: — просто метод, добавленный к NSObject, вызывающий переданный блок у [Class new]. Синтаксический сахар (вспомнил детство). Этот блок IDE автоматически мне сворачивает, весь -loadView в свернутом состоянии имеет столько строк, сколько на вьюхе элементов высшего уровня.

      2. Тк scope у with: четко обозначен и метод используется в каждом -loadView по всему приложению, то по аналогии с for i=0… для аргумента берется n-буквенное имя, где n — глубина вложенности. Если этого не делать, то становится сильно труднее читать куски очень глубокой вложенности: не понятно, что используется, а что нет.

      3. Заполняем параметры конкретной вьюхи. _s = __weak self

      4. _layout* — уже созданный отцовским классом (от которого наследую все VC) вью. Пихаем вьюху туда.

      5. o.keep* — выставляет констрейнты

      6. *пустое место* — тут мы обычно идем вглубь и создаем еще элементы

      7. _reloads — массив блоков, которые нужно вызвать для обновления элементов на экране при обновлении отображаемых данных (этакий аналог MVVM или ReactiveCocoa, эпически легковесный. Противоборствует созданию огромного количества iVar'ов в классе и распуханию -Reload метода, как и хорошо действует против использования KVO). Как видно, на этом экране отображаемым ресурсом является XSUser и при обновлении данных нужно для этой вьюхи просто выдрать из него все battles.


    1. devnikor
      18.01.2016 08:06

      Думаю, к минусам можно отнести лишнюю зависимость, увеличение сложности и необходимого времени. Если этот проект будет поддерживать кто-то другой, то ему, скорее всего, будет не весело)
      Говорю за себя, по крайней мере.
      Да и такая гибкость не везде нужна


      1. aspcartman
        18.01.2016 08:48

        Оговорка: я все это пишу не для понта, а из любопытства к IB.

        1. Зависимость.
        Это плохо, это хорошо? В чем разница проекта, в котором 10 подов в cocoapods и проектом, в котором 20 подов в cocoapods? Время компиляции, генерации дебаг символов и линковки опуская.
        Зависимость — большая проблема в мире С++, например, или Java. Но по стечению обстоятельств, у нас это проблемой являться перестало, как мне до этого разговора казалось. Я ошибался? Проблемы появляются при мажорном обновлении зависимости, что не то, чтобы очень плохо: если лень, можно зафиксировать версии.

        2. Сложность.
        М? :) Я думаю, что это понятие субьективное и я уверен, что если человеку показать, как делать вещи в коде и как делать тоже самое в IB, то предсказать, что он в итоге предпочтет, не так уж и легко, и сильно зависит от предпочтений. ИМХО: IB впечатлял (сильно) меня лет шесть назад (OSX dev), но со временем я сталкивался с интерфейсными задачами, выполнение которых в IB требовало все больше и больше приседаний. Потом появились сториборды. Потом проекты, в которых сториборды превращались в описанное в статье мессиво (правда, количество связей было у меня на порядок меньше). И потом я наткнулся на KeepLayout и как камень с плеч. С тех пор я пытаюсь изредка вернуться к IB, чисто из интереса, и хотябы в xib собрать что-нибудь, и каждый раз для меня в коде оказывается проще.

        3. Время
        Простой пример. Есть элемент интерфейса, допустим поле из звездочек, аля рейтниг. Только вот условие: звездочек не константное количество, а N штук. Звезды отцентрованы, должны находится на неком минимальном расстоянии и становиться меньше, если не помещаются. Очевидно autolayout, но как бы это имплементировать в IB? В коде решение straight-forward -> создаем отцентрованное подвью и по мере необходимости, подпихиваем в него звезды, аля

        lastStar.keepRightInset.equal = KeepNone // убираем прибитие последней звезды к правому краю
        newStar.keepLeftOffsetTo(lastStart).equal = minOffset // втыкаем новую звезду справа от последней
        newStar.keepHorizontalAlignTo(lastStar).equal = 0 // отцентровываем новую звезду по предыдущей
        // newStar.keepVerticalCenter.equal = 0.5 // Ну либо так, просто фиксируем ее в центре по вертикали
        newStar.keepRigthInset.equal = 0 // прибиваем последнюю звезду к правому краю
        newStar.keepInsets.min = 0 // не даем ей выйти за пределы отцентрованной вьюхи
        newStar.keepSizeTo(lastStar).equal = 1 // "будь такого-же размера, что и предыдущая звезда"
        


        Так же там имеется сахар, чтобы все это писать короче. Код — добавление новой звезды. Если сразу известно, сколько их будет в конкретно этом инстансе, то все становится еще кароче. И это я написал сейчас без IDE. ИМХО — это называется быстро. Кстати, KeepLayout все констрейнты называет максимально адекватно и если что-то идет не так, то очень легко найти косяк. Стандартные констрейнты, создаваемые UIKit'ом я прочитать могу в большинстве случаев только с гуглом на пару.

        Как я вижу ситуацию: xib — просто xml'ка, storyboard — набор этих xml'ек и связей между ними.
        Мне быстрее написать руками эти xml'ки, чем в UI искать нужные мне галки. Да и лишний раз запускать проклятый xcode совсем не хочется. А тк есть возможность писать эти xml'ки на родном и горячо любимом obj-c в AppCode и сразу видеть результат в симуляторе по хоткею — то для меня это бомба, которую очень тяжело покрыть. И если IB может это превзойти, то огонь.

        Говорю за себя, по крайней мере.
        Да и такая гибкость не везде нужна


        Почему-то заметил эту приписку только после того, как накатал текст выше. Упс.
        В общем-то я согласен с тем, что не всегда требуется гибкость, однако не только же в ней дело.


        1. devnikor
          18.01.2016 09:13

          Выглядит, конечно, вкусно)
          Возможно, стоит как-нибудь попробовать такой подход
          Спасибо за такой подробный комментарий!


          1. aspcartman
            18.01.2016 10:52

            Да не за что :)
            А все же есть идеи, как звездочки можно в IB сделать? Интересно-ж


            1. devnikor
              18.01.2016 13:19

              Можно через uistackview
              Потом в коде добавлять туда звёздочки n раз
              Вполне добротный вариант


              1. devnikor
                18.01.2016 16:43

                Хоть и не совсем ib


            1. PapaBubaDiop
              18.01.2016 22:39

              Никак. ИБ — для статических макетов с элементами, числом не более 10, типа слайда в презентации. Все прочее — гиковство… Сториборд мне просто отвратителен, не люблю скроллить по всем направлениям.


  1. AlexRoch
    19.01.2016 09:09

    Умиляет как люди, пытаются прогнуть целые технологии под свои умозаключения…
    «Это годы исследований, а ты вот так берешь и просто шлешь все нах*р».
    Сначала наговнокодил в Storyboard, а теперь пытаешься оправдаться.
    — Stroyboard это зло, я вот прямо сейчас даже статью на хабре напишу и меня поддержат. Мое исследование послужит людям…


    1. SPetruk
      21.01.2016 18:17

      Мне кажется сториборды сделаны в первую очередь чтобы проще было новичкам набросать свой проект.
      И по поводу решений эпла, вот тут паренек пишет что Apple MVC, это немного другой MVC) И все знают как его расшифровывают Massive View Controller.
      medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.srej8sisa