» » » iOS Storyboards: анализ плюсов и минусов, best practices

 

iOS Storyboards: анализ плюсов и минусов, best practices

Автор: admin от 14-06-2019, 16:30, посмотрело: 26

iOS Storyboards: анализ плюсов и минусов, best practices


Apple создала Storyboards, чтобы разработчики могли визуализировать экраны iOS-приложений и связи между ними. Не всем понравился этот инструмент, и на то есть разумные причины. Я встречал много статей с критикой Storyboards, однако так и не нашел подробного и непредвзятого анализа всех плюсов и минусов с учетом best practices. В итоге я решил написать такую статью сам.

SwiftGen и R.swift, а может даже Perform. Но так мы лишь избавляемся от строковых литералов и добавляем синтаксический сахар, а не решаем проблемы, которые возникают:




  • Как узнать, каким образом в примере выше настраивается DetailViewController? Если вы новый на проекте и не обладаете этими знаниями, вам придется открыть файл с описанием этого контроллера и изучить его.


  • Свойства DetailViewController устанавливаются уже после инициализации, значит они должны быть опциональными. Нужно обработать случаи, когда какое-либо свойство равно nil, иначе приложение может упасть в самый неподходящий момент. Можно пометить свойства как неявно развернутые опциональные (var object: Object!), но суть не изменится.


  • Свойства должны быть помечены как var, не let. А значит возможна ситуация, когда кто-то извне захочет их изменить. DetailViewController должен обрабатывать и такие ситуации.




Один из вариантов решения описан в этой статье.



4. По мере роста Storyboard навигация в нем становится все сложнее



Как мы уже отмечали ранее, не нужно помещать все в один Storyboard, лучше разбить его на несколько более мелких. С появлением Storyboard Reference это стало очень просто.

Добавляем Storyboard Reference из библиотеки объектов в Storyboard:

iOS Storyboards: анализ плюсов и минусов, best practices
Выставляем необходимые значения полей в Attributes Inspector – это имя Storyboard-файла и по необходимости Referenced ID, что соответствует Storyboard ID нужного экрана. По умолчанию будет загружен Initial View Controller:

iOS Storyboards: анализ плюсов и минусов, best practices
Если указать неверное имя в поле Storyboard или сослаться на несуществующий Storyboard ID, Xcode предупредит об этом на этапе компиляции.



5. Xcode тормозит при загрузке Storyboards



Если Storyboard содержит большое количество экранов с многочисленными constraints, то его загрузка действительно будет отнимать определенное время. Но опять же, лучше разбить большой Storyboard на более мелкие. По отдельности они грузятся значительно быстрее и с ними становится удобнее работать.



6. Storyboards хрупкие, ошибка может привести к падению приложения на этапе выполнения



Основные слабые места:




  • Ошибки в идентификаторах UITableViewCell и UICollectionViewCell.


  • Ошибки в идентификаторах segues.


  • Использование подкласса UIView, которого уже не существует.


  • Синхронизация IBActions и IBOutlets с кодом.




Все это и некоторые другие проблемы способны привести к падению приложения на этапе выполнения, а значит есть вероятность, что такие ошибки попадут в релизную сборку. Например, когда мы задаем идентификаторы ячеек или segues в Storyboard, они должны быть скопированы в код везде, где используются. Изменив идентификатор в одном месте, он должен быть изменен во всех остальных. Есть вероятность, что вы просто забудете об этом или сделаете опечатку, но узнаете об ошибке только во время работы приложения.



Можно снизить вероятность ошибки, если избавиться от строковых литералов в коде. Для этого идентификаторам UITableViewCell и UICollectionViewCell можно присваивать названия самих классов ячеек: например, идентификатором ItemTableViewCell будет строка «ItemTableViewCell». В коде достаем ячейку так:



let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ItemTableViewCell.self)) as! ItemTableViewCell


Можно добавить в UITableView соответствующую generic-функцию:



extension UITableView {
	
    open func dequeueReusableCell<T>()  T where T: UITableViewCell {
        return dequeueReusableCell(withIdentifier: String(describing: T.self)) as! T
    }
}


И тогда получить ячейку становится проще:



let cell: ItemTableViewCell = tableView.dequeueReusableCell()


Если вдруг вы забудете указать значение идентификатора ячейки в Storyboard, Xcode выдаст предупреждение, поэтому не стоит их игнорировать.



Что касается идентификаторов segues, то для них можно использовать перечисления. Создадим специальный протокол:



protocol SegueHandler {
	
    associatedtype SegueIdentifier: RawRepresentable
}


UIViewController, поддерживающий этот протокол, должен будет определить вложенный тип с таким же именем. В нем перечисляются все идентификаторы segues, которые этот UIViewController может обработать:



extension StartViewController: SegueHandler {
	
    enum SegueIdentifier: String {
        case signIn, signUp
    }
}


Кроме того, в расширении протокола SegueHandler определим две функции: одна принимает UIStoryboardSegue и возвращает соответствующее ей значение SegueIdentifier, а другая просто вызывает performSegue, принимая на вход SegueIdentifier:



extension SegueHandler where Self: UIViewController, SegueIdentifier.RawValue == String {

    func performSegue(withIdentifier segueIdentifier: SegueIdentifier, sender: AnyObject?) {
        performSegue(withIdentifier: segueIdentifier.rawValue, sender: sender)
    }

    func segueIdentifier(for segue: UIStoryboardSegue)  SegueIdentifier {
        guard let identifier = segue.identifier, let identifierCase = SegueIdentifier(rawValue: identifier) else {
            fatalError("Invalid segue identifier (String(describing: segue.identifier)).")
        }
        return identifierCase
    }
}


И теперь в UIViewController, поддерживающем новый протокол, с prepare(for:sender:) можно работать следующим образом:



extension StartViewController: SegueHandler {
	
    enum SegueIdentifier: String {
        case signIn, signUp
    }
	
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        switch segueIdentifier(for: segue) {
        case .signIn:
            print("signIn")
        case .signUp:
            print("signUp")
        }
    }
}


А запускать segue так:



performSegue(withIdentifier: .signIn, sender: nil)


Если добавить новый идентификатор в SegueIdentifier, то Xcode обязательно заставит его обработать в switch/case.



Еще один вариант избавиться от строковых литералов типа идентификаторов segues и других – использовать инструменты кодогенерации наподобие R.swift.



7. Storyboards менее гибкие, в отличие от кода



Да, это действительно так. Если стоит задача создать сложный экран с анимацией и эффектами, с которыми Storyboard не справится, то нужно использовать код!



8. Storyboards не позволяют сменить тип специальных UIViewControllers



Например, когда нужно сменить тип UITableViewController на UICollectionViewController, приходится удалять объект, добавлять новый с другим типом и заново его перенастраивать. Хоть это и нечастый случай, но стоит отметить, что в коде такие изменения производятся быстрее.



9. Storyboards добавляют две дополнительные зависимости в проект. Они могут содержать ошибки, которые разработчик не в силах исправить



Это Interface Builder и парсер Storyboards. Такие случаи – редкость, и зачастую их можно обойти другими решениями.



10. Сложный code review



Нужно принять во внимание, что code review – это не совсем поиск багов. Да, их находят в процессе просмотра кода, но главной целью является выявление слабых мест, способных создать проблемы в долгосрочной перспективе. Для Storyboards это, в первую очередь, работа Auto Layout. Не должно быть никаких ambiguous и misplaced. Чтобы их найти, достаточно воспользоваться поиском в Storyboard XML по строкам «ambiguous=«YES»» и «misplaced=«YES»» или просто открыть Storyboard в Interface Builder и искать красные и желтые точки:

iOS Storyboards: анализ плюсов и минусов, best practices
Однако этого может быть недостаточно. Конфликты между constraints могут выявляться и во время работы приложения. Если подобная ситуация имеет место, информация об этом выводится в консоли. Такие случаи – не редкость, поэтому к их поиску тоже нужно отнестись серьезно.



Все остальное – соответствие положения и размеров элементов с дизайном, корректная привязка IBOutlets и IBActions – не для code review.



Кроме того, важно делать коммиты чаще, тогда рецензенту будет проще просматривать изменения небольшими кусками. Он сможет лучше вникнуть в детали, ничего не упустив. Это, в свою очередь, положительно скажется на качестве ревью кода.



Итог



В списке недостатков Storyboards я оставил 4 пункта (в порядке убывания их значения):




  1. В Storyboards тяжело править конфликты при слиянии изменений.


  2. Storyboards менее гибкие, в отличие от кода.


  3. Storyboards хрупкие, ошибка может привести к падению приложения на этапе выполнения.


  4. Нельзя использовать кастомные инициализаторы для UIViewControllers, созданных в Storyboard.




Преимущества



1. Визуализация пользовательского интерфейса и constraints



Даже если вы новичок и только взялись за незнакомый проект, то легко найдете точку входа в приложение и как из нее перейти к нужному экрану. Вы знаете, как будет выглядеть каждая кнопка, метка или текстовое поле, какую позицию они будут занимать, как на них влияют constraints, как они взаимодействуют с другими элементами. С помощью нескольких щелчков мыши вы можете легко создать новый UIView, настроить его внешний вид и поведение. Auto Layout позволяет нам работать с UIView естественно, как если бы мы сказали: «Вот эта кнопка должна быть слева от той метки и иметь одинаковую с ней высоту». Такая работа с пользовательским интерфейсом интуитивно понятна и эффективна. Можно попытаться привести примеры, когда грамотно написанный код экономит больше времени при создании каких-то элементов UI, но глобально это мало что меняет. Storyboard хорошо справляется со своей задачей.



Отдельно отметим Auto Layout. Это очень мощный и полезный инструмент, без которого трудно было бы создать приложение, поддерживающее все множество различных размеров экрана. Interface Builder позволяет увидеть результат работы с Auto Layout без запуска приложения, и если какие-то constraints не вписываются в общую схему, Xcode сразу же предупредит об этом. Конечно, существуют случаи, когда Interface Builder не способен обеспечить нужное поведение какого-то очень динамичного и сложного интерфейса, тогда приходится полагаться на код. Но даже в таких ситуациях можно сделать большую часть в Interface Builder и дополнить это лишь парой строчек кода.



Рассмотрим несколько примеров, демонстрирующих полезные возможности Interface Builder.



Динамичные таблицы на основе UIStackView



Создаем новый UIViewController, добавляем UIScrollView на весь экран:

iOS Storyboards: анализ плюсов и минусов, best practices
В UIScrollView добавляем вертикальный UIStackView, привязываем его к краям и устанавливаем высоту и ширину, равную UIScrollView. При этом высоте присвоим priority = Low (250):

iOS Storyboards: анализ плюсов и минусов, best practices
Далее создаем все необходимые ячейки и добавляем их в UIStackView. Может это будут обычные UIView в единственном экземпляре, а может и переиспользуемые UIView, для которых мы создали свой Xib-файл. В любом случае, весь UI этого экрана – в Storyboard, а благодаря правильно настроенному Auto Layout прокрутка будет работать идеально, подстраиваясь под содержимое:



iOS Storyboards: анализ плюсов и минусов, best practices



Мы также можем сделать, чтобы ячейки адаптировались под размеры своего контента. Добавим в каждую ячейку по UILabel, привяжем их к краям:

iOS Storyboards: анализ плюсов и минусов, best practices
Уже понятно, как это все будет выглядеть на этапе выполнения. К ячейкам можно привязать любые действия, например, переход на другой экран. И все это без единой строчки кода.

Более того, если установить hidden = true для UIView из UIStackView, то оно не только скроется, но еще и не будет занимать пространства. UIStackView автоматически пересчитает свои размеры:



iOS Storyboards: анализ плюсов и минусов, best practices


Self-sizing ячейки



В Size inspector таблицы устанавливаем Row Height = Automatic, а Estimate – в какое-нибудь среднее значение:

iOS Storyboards: анализ плюсов и минусов, best practices
Чтобы это работало, в самих ячейках должны быть верно настроены constraints и позволять точно вычислить высоту ячейки на основе содержимого на этапе выполнения. Если не понятно, о чем идет речь, очень хорошее объяснение есть в официальной документации.



В итоге, запустив приложение, мы увидим, что все корректно отображается:

iOS Storyboards: анализ плюсов и минусов, best practices

Self-sizing таблица



Нужно реализовать такое поведение таблицы:



iOS Storyboards: анализ плюсов и минусов, best practices



Как добиться подобного динамического изменения высоты? В отличие от UILabel, UIButton и других подклассов UIView, с таблицей это сделать немного сложнее, так как Intrinsic Content Size не зависит от размеров ячеек внутри нее. Она не может вычислить свою высоту на основе содержимого, но есть возможность ей в этом помочь.



Заметим, что на видео в какой-то момент высота таблицы перестает меняться, достигая определенного максимального значения. Этого можно добиться, установив у таблицы height constraint со значением Relation = Less Than Or Equal:

iOS Storyboards: анализ плюсов и минусов, best practices
На данном этапе Interface Builder еще не знает, какой высоты будет таблица, ему лишь известно ее максимальное значение, равное 200 (из height constraint). Как было отмечено ранее, Intrinsic Content Size не равен содержимому таблицы. Однако у нас есть возможность установить placeholder в поле Intrinsic Size:

iOS Storyboards: анализ плюсов и минусов, best practices
Это значение действительно только на время работы с Interface Builder. Безусловно, Intrinsic Content Size не обязан быть равен этому значению во время выполнения. Мы лишь сказали Interface Builder, что все под контролем.



Далее, создаем новый подкласс таблицы CustomTableView:



final class CustomTableView: UITableView {

    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        return contentSize
    }
}


Один из тех случаев, когда код необходим. Здесь мы вызываем invalidateIntrinsicContentSize всегда, когда меняется contentSize таблицы. Это позволит системе принять новое значение Intrinsic Content Size. Оно, в свою очередь, возвращает contentSize, заставляя таблицу динамически регулировать свою высоту и отображать определенное количество ячеек без прокрутки. Прокрутка появляется в тот момент, когда мы достигаем предела height constraint.



Все эти три возможности Interface Builder можно комбинировать друг с другом. Они добавляют больше гибкости в вариантах организации содержимого без необходимости дополнительной настройки constraints или каких-либо UIView.



2. Возможность мгновенно увидеть результат своих действий



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



Использование @IBInspectable раскрывает это преимущество еще интереснее. Добавим к RedView два UILabel и два свойства:



final class RedView: XibView {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!

    @IBInspectable var title: String = "" { didSet { titleLabel.text = title } }
    @IBInspectable var subtitle: String = "" { didSet { subtitleLabel.text = subtitle } }
}


В Attributes Inspector для RedView появится два новых поля – Title и Subtitle, которые мы пометили как @IBInspectable:

iOS Storyboards: анализ плюсов и минусов, best practices
Если мы попробуем ввести значения в эти поля, то сразу же увидим, как все будет выглядеть на этапе выполнения:



iOS Storyboards: анализ плюсов и минусов, best practices


Можно управлять чем угодно: cornerRadius, borderWidth, borderColor. Например, расширим базовый класс UIView:



extension UIView {

    @IBInspectable var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue }
        get { return layer.cornerRadius }	
    }

    @IBInspectable var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }

    @IBInspectable var borderColor: UIColor? {
        set { layer.borderColor = newValue?.cgColor }
        get { return layer.borderColor != nil ? UIColor(cgColor: layer.borderColor!) : nil }
    }

    @IBInspectable var rotate: CGFloat {
        set { transform = CGAffineTransform(rotationAngle: newValue * .pi/180) }
        get { return 0 }
    }
}


Видим, что Attributes Inspector объекта RedView обзавелся еще 4-мя новыми полями, с которыми теперь тоже можно поиграться:



iOS Storyboards: анализ плюсов и минусов, best practices


3. Предварительный просмотр всех размеров экрана одновременно



Вот мы закинули необходимые элементы на экран, настроили их вид и добавили нужные constraints. Как нам выяснить, будет ли содержимое корректно отображаться на разных размерах экрана? Конечно, можно запустить приложение на каждом симуляторе, но это займет немало времени. Существует вариант получше: у Xcode есть режим предварительного просмотра, он позволяет увидеть сразу несколько размеров экрана одновременно без запуска приложения.



Вызываем Assistant editor, в нем нажимаем на первый сегмент панели переходов, выбираем Preview –> Settings.storyboard (как пример):

iOS Storyboards: анализ плюсов и минусов, best practices
Сначала мы видим лишь один экран, но можем добавить столько, сколько нужно, нажав «+» в левом нижнем углу и выбрав необходимые устройства из списка:

iOS Storyboards: анализ плюсов и минусов, best practices
Кроме того, если Storyboard поддерживает несколько языков, можно посмотреть, как будет выглядеть выбранный экран с каждым из них:

iOS Storyboards: анализ плюсов и минусов, best practices
Язык можно выбрать как для всех экранов сразу, так и для каждого по отдельности.



4. Удаление шаблонного UI кода



Создание пользовательского интерфейса без Interface Builder сопровождается либо большим количеством шаблонного кода, либо суперклассами и расширениями, которые влекут за собой дополнительную работу по обслуживанию. Этот код может проникать в другие части приложения, затрудняя чтение и поиск. Использование Storyboards и Xibs позволяет разгрузить код, благодаря чему он становится более сфокусированным на логике.



5. Size classes



Каждый год появляются новые устройства, под которые нужно адаптировать пользовательский интерфейс. В этом помогают концепции trait variations и, в частности, size classes, которые позволяют создавать UI под любые варианты размеров и ориентации экрана.



Size classes классифицируют высоту (h) и ширину (w) экранов устройств в терминах compact и regular (C и R). Например, iPhone 8 имеет size class (wC hR) в портретной ориентации и (wC hC) – в альбомной, а iPhone 8 Plus – (wC hR) и (wR hC) соответственно. По остальным девайсам можно узнать здесь.



В одном Storyboard или Xib для каждого из size classes можно хранить свой набор данных, а приложение уже на этапе выполнения будет использовать подходящий в зависимости от устройства и ориентации экрана, идентифицируя таким образом текущий size class. Если какие-то параметры макета одинаковы для всех size classes, то их можно настроить в категории «Any», которые уже выбрана по умолчанию.



Например, настроим размер шрифта в зависимости от size class. Выберем для просмотра в Storyboard устройство iPhone 8 Plus в портретной ориентации и добавим новое условие для font: если width – Regular (все остальное устанавливаем в «Any»), то размер шрифта должен быть равен 37:

iOS Storyboards: анализ плюсов и минусов, best practices
Теперь, если мы поменяем ориентацию экрана, размер шрифта увеличится – сработает новое условие, так как в альбомной ориентации iPhone 8 Plus имеет size class (wR hC). В Storyboard в зависимости от size class можно также скрывать Views, включать/отключать constraints, менять их значение constant и многое другое. Подробнее о том, как все это делать, можно прочитать здесь.



На скриншоте выше стоит еще отметить нижнюю панель с выбором устройства для отображения макета. Она позволяет быстро проверить адаптивность UI на любом устройстве и при любой ориентации экрана, а также показывает size class текущей конфигурации (рядом с названием устройства). Помимо прочего, справа есть кнопка «Vary for Traits». Ее цель в том, чтобы включить trait variations только для определенной категории ширины, высоты или ширины и высоты одновременно. Например, выбрав iPad с size class (wR hR), нажимаем «Vary for Traits» и ставим галочки напротив width и height. Теперь все последующие изменения макета будут применяться только к устройствам с (wR hR), пока мы не нажмем «Done Varying».



Заключение
































#

Недостатки

Преимущества

1

Тяжело править конфликты

Визуализация UI и constraints

2

Не такие гибкие, как код

Возможность мгновенно увидеть результат своих действий

3

Ошибка может привести к падению на этапе выполнения

Preview всех размеров экрана одновременно

4

Нельзя использовать кастомные инициализаторы для UIViewControllers

Удаление шаблонного UI кода

5

Size classes

Мы увидели, что Storyboards имеют свои сильные и слабые стороны. Мое мнение – не стоит полностью отказываться от их использования. При правильном применении они приносят огромную пользу и помогают эффективно решать поставленные задачи. Нужно лишь научиться расставлять приоритеты и забыть аргументы типа «мне не нравятся Storyboards» или «я привык так делать».

Источник: Хабр / Интересные публикации

Категория: iOS

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

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *