![Xcode: наверное, лучший способ работы со сторибордами - 1 Xcode: наверное, лучший способ работы со сторибордами - 1](https://www.pvsm.ru/images/2016/10/17/Xcode-navernoe-luchshii-sposob-raboty-so-storibordami.png)
Этот пост является вольным переводом статьи Xcode: A Better Way to Deal with Storyboards by Stan Ostrovskiy
Некоторые примеры кода в оригинальной статье устарели (ввиду выхода Swift 3) и в переводе были изменены.
Советы и рекомендации по работе с Interface Builder.
Apple серьезно улучшили Interface Builder в новом Xcode 8. Использование size classes стало более интуитивным, возможность масштабирования сториборда — очень удобной, а полное превью прям в Interface Builder — просто великолепным. Для тех у кого были сомнения насчет использования Interface Builder, это может стать хорошими плюсами.
С другой стороны, у многих разработчиков все еще есть некоторые проблемы с Interface Builder когда они создают большие многоэкранные приложения со сложной навигацией.
В этой статье я поделюсь некоторыми из лучших практик для работы со сторибордами в вашем проекте. Вы уже пользуетесь Interface Builder, или только делаете первые шаги в этом направлении? — в любом случае, эти советы будут полезны для вас.
1. Если вы работаете в команде, используйте отдельный сториборд для каждого экрана. Даже если вы работаете один — это наверняка станет хорошей привычкой.
В вашем проекте есть один файл main.storyboard, который выглядит вот так?
![Xcode: наверное, лучший способ работы со сторибордами - 2 Xcode: наверное, лучший способ работы со сторибордами - 2](https://www.pvsm.ru/images/2016/10/17/Xcode-navernoe-luchshii-sposob-raboty-so-storibordami-2.png)
С точки зрения дизайнера, все хорошо: полностью видно UI и навигацию. И это именно то, для чего Interface Builder и был создан.
Но для разработчика это несет множество проблем:
- Контроль версий: конфликты слияния сторибордов очень трудно решать, так что работа в отдельных сторибордах сделает жизнь вашей команды проще.
- Файл сториборда становится объемным и в нем сложно ориентироваться. Как часто вы случайно меняли constraint кликом мышки не в том вью-контроллере?
- Вам необходимо присваивать каждому вью-контроллеру свой storyboard ID и это может привести к ошибкам: вам нужно «хардкодить» этот ID каждый раз когда хотите использовать этот вью-контроллер в коде.
Как же связать различные сториборды в вашем проекте? Есть два способа.
-
Используйте ссылки на сториборды (storyboard referencing), которые появились в Xcode 7.
- Связывайте сториборды непосредственно в коде.
О первом способе вы можете почитать детальнее здесь.
Я расскажу о втором способе, так как он широко используется для сложных проектов.
2. Используйте одни и те же имена для файла со сторибордом и для связанного класса контроллера (наследника UIViewController).
Это упростит правила именования, а также даст некоторые "плюшки" о которых поговорим в пункте 3.
3. Инициализируйте сториборд непосредственно в классе контроллера.
Когда дело доходит до инициализации вью-контроллера через сториборд, я часто вижу следующий код:
let storyboard = UIStoryboard(name: “Main”, bundle: nil)
let homeViewController = storyboard.instantiateViewController(withIdentifier: “HomeViewController”)
Немного "грязновато": вам нужно назвать сториборд, вам нужен storyboard ID вью-контроллера, и вам необходимо использовать этот паттерн каждый раз, когда вы создаете HomeViewController.
Лучше перенести этот код в сам класс контроллера и использовать статический метод, чтоб инициализировать контроллер с помощью сториборда:
class HomeViewController: UIViewController {
static func storyboardInstance() -> HomeViewController? {
let storyboard = UIStoryboard(name: “HomeViewController”, bundle: nil)
return storyboard.instantiateInitialViewController() as? HomeViewController
}
}
Если вы последуете предыдущему совету (одинаковые имена файлов), то можете избежать "харкода" имени сториборда и воспользоваться String(describing:):
let storyboard = UIStoryboard(name: String(describing: self), bundle: nil)
Убедитесь, что у файла сториборда такое же имя как и у класса контроллера. Иначе ваше приложение будет «крэшится» когда вы попытаетесь создать ссылку на такой сториборд.
Это делает ваш код более читаемым и отказоустойчивым:
class HomeViewController: UIViewController {
static func storyboardInstance() -> HomeViewController? {
let storyboard = UIStoryboard(name: String(describing: self), bundle: nil)
return storyboard.instantiateInitialViewController() as? HomeViewController
}
}
Если вы хотите иметь доступ к вью-контроллеру через instantiateInitialViewController() убедитесь, что вы указали этот вью-контроллер как initialViewController в Interface Builder. Если у вас несколько вью-контроллеров на одном сториборде, вам придется использовать instantiateViewController(withIdentifier: _ )
Теперь, инициализация такого вью-контроллера займет одну строку:
let homeViewController = HomeViewController.storyboardInstance()
Просто и понятно, не так ли?
Вы можете использовать этот же подход для инициализации вью из nib:
class LoginView: UIView {
static func nibInstance() -> LoginView? {
let nib = Bundle.main.loadNibNamed(String(describing: self), owner: nil, options: nil)
return nib?.first as? LoginView
}
}
4. Не перегружайте свой проект переходами на сториборде.
У вас не будет переходов, если вы последуете совету из пункта 1. Но даже если у вас есть несколько вью-контроллеров в одном сториборде, использование переходов (segues) для навигации между ними — не очень хорошая идея:
- Вам нужно дать имя каждому переходу (segue), что само по себе может привести к ошибкам. «Хардкодить» строки с именами — плохая практика.
- Метод prepareForSegue будет просто нечитаем, когда вы будете работать в нем с несколькими segue, используя операторы ветвления if/else или switch.
Какова альтернатива? Когда вы хотите перейти к следующему вью-контроллеру по нажатию на кнопку, просто добавьте IBAction для этой кнопки и инициализируйте вью-контроллер в коде: это ведь всего одна строка, как вы помните из пункта 3.
@IBAction func didTapHomeButton(_ sender: AnyObject) {
if let nextViewController = NextViewController.storyboardInstance() {
// initialize all your class properties
// nextViewController.property1 = …
// nextViewController.property2 = …
// either push or present the nextViewController,
// depending on your navigation structure
// present(nextViewController, animated: true, completion: nil)
// or push
navigationController?.pushViewController(nextViewController, animated: true)
}
}
5. Unwind segue? Не, не слышал.
Иногда навигация предполагает возврат пользователя к предыдущему экрану.
Очень распространенная ошибка: использовать новый переход для навигации к предыдущему вью-контроллеру. Такой переход создает новый экземпляр вью-контроллера, который уже находится в стэке, вместо того, чтоб убрать текущий вью-контроллер и таким образом вернуться к предыдущему.
Начиная с iOS 7, Interface Builder дает вам возможность сделать "unwind" навигационного стэка.
![Xcode: наверное, лучший способ работы со сторибордами - 3 Xcode: наверное, лучший способ работы со сторибордами - 3](https://www.pvsm.ru/images/2016/10/17/Xcode-navernoe-luchshii-sposob-raboty-so-storibordami-3.png)
Unwind segue позволяет вам указать возврат на предыдущий экран. Это звучит довольно просто, но на практике это требует некоторых дополнительных действий и только сбивает с толку разработчика:
- Обычно, когда вы создаете действие для кнопки (action), Interface Builder создаст для вас код (IBAction). В этом же случае, ожидается, что код уже написан до того, как вы зажмете «Ctrl» и перетащите действие от вашей кнопки к «Exit».
- Обычно когда вы создаете действие для кнопки, код этого действия создается в том же классе, которому и принадлежит кнопка. Для Unwind Segues, вам нужно писать код в классе того вью-контроллера, в который этот переход произойдет.
- Метод prepareForUnwind будет иметь все те же недостатки, что и метод prepareForSegue (см. предыдущий пункт).
Каков же более простой способ?
Проще делать это в коде: вместо создания действия "unwind" для вашей кнопки, создайте обычный IBAction и используйте dismissViewController или popViewController (в зависимости от вашей навигации):
@IBAction func didTapBackButton(_ sender: AnyObject) {
// if you use navigation controller, just pop ViewController:
if let nvc = navigationController {
nvc.popViewController(animated: true)
} else {
// otherwise, dismiss it
dismiss(animated: true, completion: nil)
}
}
На сегодня это все. Я надеюсь, вы найдете что-то полезное для себя.
От переводчика:
Благодаря методу описанному в этой статье, я очень сильно упростил работу со сторибордами в своем текущем проекте. Пока я работал над ним один — все было прекрасно, но как только появились другие разработчики — работа со сторибордом превратилась в настоящий ад. От отчаянья мы практически перешли к "банановому методу" (можно почитать здесь в разделе "Pass the banana").
Конечно же, в идеале нужно будет рано или поздно прийти к VIPER. Но об этом будет уже другой пост. :)
Автор: s_suhanov