Чем мне не угодил Storyboard
Не спорю, Storyboard?—?очень удобная вещь. Все контроллеры расположены в одном месте, причем, все они соединены переходами (segues). Можно сказать, что приложение будто находится у вас на ладонях. И это замечательно, ведь не всегда удается запомнить, к какому контроллеру мы перейдем, если нажмем на очередную кнопку или ячейку.
Но, это все? Есть ли еще какие-нибудь преимущества? На самом деле нет. Зато приходится мириться с многими неприятными вещами.
Неприятность первая
Все контроллеры расположены в одном месте
А так ли это хорошо? Возможно, если в приложении их не так много. Но что обычно происходит при увеличении их количества? А вот что:
Выглядит не очень, да и поддерживать такое вряд ли кому-то захочется.
(Ах да, еще оно тормозит)
Неприятность вторая
Да, держать все контроллеры в одном месте это ужасно. Может ли быть что-нибудь хуже? Может. Например, если над таким большим приложением работает не один человек. Если двое разработчика занимаются интерфейсом, который находится в одном 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)
egormerkushev
16.01.2016 23:00+3Вам роутер нужен, у вас должен быть опыт в написании подобного, странно, что вы жалуетесь на сторибоарды.
В последнем своём проекте (iOS 7+) отлично поработал со сторибордами, их было аж восемь штук, никакой проблемы с навигацией за счет констант с именами сторибоардов и контроллеров и отдельного роутера, который знает что, как и куда. Клеить паутину из segue в сторибоардах — глупо, это ж очевидно было сразу как сториборды появились. Максимум для чего segue годятся — прямая связь навконтроллера и контроллера, таббара с контроллерами, вложенные контроллеры и одноуровневые переходы на другие экраны.devnikor
16.01.2016 23:50+1К сожалению, опыта в написании роутера нет. Начинаю засматриваться на то, как реализуется роутер viper, чтобы перенести что-то похожее в mvvm
Оффтопесли у вас есть полезные ссылки, буду рад принять их в личке :)Agent_Smith
22.01.2016 12:27Писал свой роутер поверх JLRoutes, очень удобная, гибкая и легковесная штука без лишних наворотов.
complexityclass
16.01.2016 23:24Storyboard Reference работают для iOS 8+
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.
Линк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.
rule
17.01.2016 03:36+4Почему многие думаю что сториборд должен быть один? У нас обычно 5-8 сторибордов в приложении. Всё очень хорошо и особых проблем нет.
devnikor
17.01.2016 10:09-3Не знаю, почему. Но один раз я видел в сторибоарде ~30-40 контроллеров и это был ад
rule
17.01.2016 10:30+4Ну я когда-то видел развернутый цикл на 40 итераций. Это тоже не прелесть была. Но это же не значит, что теперь нельзя пользоваться циклами.
devnikor
17.01.2016 13:27Я нигде не говорю, что ими нельзя пользоваться. Я говорю про проблемы, которые они привносят. Особенно с di в свифте. Конечно, эта проблема тоже решаема, но хотелось бы что-нибудь вроде init(contex:), как в WatchKit
bartleby
17.01.2016 19:44+3Ух… Удивлен таким статьям на хабре, а я сюда еще и из дайджеста перешел… Ваша проблема растет из неверной, изначально, архитектуры проекта, все эти проблемы решаются разделением на юзер стори и роутером, я выработал за время работы нечто свое где разделяю каждую историю, например логин, одна история, профиль другая, в контроллер через di инжектится роутер и все необходимые сервисы, у каждой истории свой роутер, получается очень чистенько, кстати и от мввм использую вм, так же для списков отдельные датасорсы, ну и сервисы по работе с сетью например. Такая архитектура легко расширяема, тестируема, если работает команда то лучше и не придумать, ведь юзер стори это отдельный модуль, над которым работает один человек, вот структура стандартного юзер стори — interface (сториборда), router, vc, view, network (web service, у каждой юс свой), вм и ассемблай (от тайфуна для инъекций), это проще и легче вайпера по которому все так сходят с ума в последнее время, а можно и мвц хорошо и красиво реализовать :)
aspcartman
18.01.2016 03:20Подробнее можно где почитать про раздирание сторибордов на стори?
Если в двух сторибордах один и тот же VC фигурирует, что тогда? (не пользуюсь IB, совсем ничего не знаю по теме, но в моих фантазиях получается, что прийдется дублировать, либо выносить в отдельный ксиб)devnikor
18.01.2016 07:52Про storyboard reference можно почитать тут
Если нужна поддержка iOS 7, то тут вполне неплохой туториал
Контроллеры можно не дублировать, а использовать storyboard.instantiateViewControllerWithIdentifier(_:)
То есть, каждому контроллеру можно присвоить идентификатор и инстанциировать по нему в любом местеaspcartman
18.01.2016 07:56Про референс известно и iOS7 тоже
Про инстантиирование сториборда тоже, что происходит в коде, а не в IB.
Я к ответу bartleby вопрос задал: как в двух отдельных сторибордах описать «стори», затрагивающую один и тот же VC? Мы же про сториборды говорим?
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/
devnikor
18.01.2016 07:41Спасибо за комментарий! Решение фабрика контроллеров + роутер достаточно неплохое. Собираюсь попробовать в текущем проекте. Что касается остальной архитектуры, использую нечто похожее, правда, без фреймворка для di. Пока инжектирую вручную.
rsi
20.01.2016 09:23А вы не могли бы показать пример своего проекта, в частности интересует именно роуетр. Не могу найти толковой статьи или примера в интернете. Можно в личку.
usgleb
18.01.2016 00:07+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 :)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.
devnikor
18.01.2016 08:06Думаю, к минусам можно отнести лишнюю зависимость, увеличение сложности и необходимого времени. Если этот проект будет поддерживать кто-то другой, то ему, скорее всего, будет не весело)
Говорю за себя, по крайней мере.
Да и такая гибкость не везде нужна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 может это превзойти, то огонь.
Говорю за себя, по крайней мере.
Да и такая гибкость не везде нужна
Почему-то заметил эту приписку только после того, как накатал текст выше. Упс.
В общем-то я согласен с тем, что не всегда требуется гибкость, однако не только же в ней дело.devnikor
18.01.2016 09:13Выглядит, конечно, вкусно)
Возможно, стоит как-нибудь попробовать такой подход
Спасибо за такой подробный комментарий!aspcartman
18.01.2016 10:52Да не за что :)
А все же есть идеи, как звездочки можно в IB сделать? Интересно-жPapaBubaDiop
18.01.2016 22:39Никак. ИБ — для статических макетов с элементами, числом не более 10, типа слайда в презентации. Все прочее — гиковство… Сториборд мне просто отвратителен, не люблю скроллить по всем направлениям.
AlexRoch
19.01.2016 09:09Умиляет как люди, пытаются прогнуть целые технологии под свои умозаключения…
«Это годы исследований, а ты вот так берешь и просто шлешь все нах*р».
Сначала наговнокодил в Storyboard, а теперь пытаешься оправдаться.
— Stroyboard это зло, я вот прямо сейчас даже статью на хабре напишу и меня поддержат. Мое исследование послужит людям…SPetruk
21.01.2016 18:17Мне кажется сториборды сделаны в первую очередь чтобы проще было новичкам набросать свой проект.
И по поводу решений эпла, вот тут паренек пишет что Apple MVC, это немного другой MVC) И все знают как его расшифровывают Massive View Controller.
medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.srej8sisa
Lonkly
Есть еще RBStoryboardLink для pre-iOS9. На ксибы все же не очень удобно возвращаться
devnikor
Видел когда-то давно, но не пользовался. Лишняя зависимость, притом не такая нужная. На ксибы я пока тоже не перешел) Собираюсь в следующем проекте попробовать (начиная с day one в карьере, пользовался исключительно сторибордом)