Xcode: наверное, лучший способ работы со сторибордами

в 4:08, , рубрики: storyboard, swift, xcode, разработка под iOS
Xcode: наверное, лучший способ работы со сторибордами - 1

Этот пост является вольным переводом статьи 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

С точки зрения дизайнера, все хорошо: полностью видно UI и навигацию. И это именно то, для чего Interface Builder и был создан.

Но для разработчика это несет множество проблем:

  • Контроль версий: конфликты слияния сторибордов очень трудно решать, так что работа в отдельных сторибордах сделает жизнь вашей команды проще.
  • Файл сториборда становится объемным и в нем сложно ориентироваться. Как часто вы случайно меняли constraint кликом мышки не в том вью-контроллере?
  • Вам необходимо присваивать каждому вью-контроллеру свой storyboard ID и это может привести к ошибкам: вам нужно «хардкодить» этот ID каждый раз когда хотите использовать этот вью-контроллер в коде.

Как же связать различные сториборды в вашем проекте? Есть два способа.

  1. Используйте ссылки на сториборды (storyboard referencing), которые появились в Xcode 7.

  2. Связывайте сториборды непосредственно в коде.

О первом способе вы можете почитать детальнее здесь.

Я расскажу о втором способе, так как он широко используется для сложных проектов.

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

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js