Split Controller без этих ваших сторибордов

в 13:13, , рубрики: iOS, ipad, swift, UISplitViewController, xcode, дизайн мобильных приложений, мультитаскинг, разработка под iOS

Раньше для поддержки iPad делали отдельный xib. Чтобы унифицировать лейаут, в 2014 году Apple представила Auto Layout и Size Classes, а для адаптивной навигации UISplitViewController.

Split Controller без этих ваших сторибордов - 1

Split-контроллер — это контейнер, который разместит два контроллера рядом. Слева будет навигационный контроллер (речь не про Navigation Controller), справа соответсвующий выбору в навигационном. Короче, как в Настройках.

Разберем как настроить UISplitViewController и его поведение на экранах.

Определяем контроллеры

Обозначим левый (навигационный) и правый контроллеры. Для обоих установим заголовок:

class MasterController: UITableViewController {
   override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.title = "Master"
        self.navigationController?.navigationBar.prefersLargeTitles = true
    }
}

class DetailController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.title = "Detail"
        self.navigationController?.navigationBar.prefersLargeTitles = true
    }
}

Split-контроллер это контейнер для двух контроллеров. Как я писал до ката, слева навигационный-главный контроллер, справа соответствующий выбору в навигационном, или детальный (Detail Controller). Master и Detail запоминайте, слова будут встречаться в документации и протоколах.

Добавляем Split

Перейдем в AppDelegate, в методе didFinishLaunchingWithOptions создаем новое окно, инициализируем Split-контроллер и два других, устанавливаем в Split:

let masterController = MasterController()
let masterNavigationController = UINavigationController(rootViewController: masterController)

let detailController = DetailController()
let detailNavigationController = UINavigationController(rootViewController: detailController)

let splitViewController =  UISplitViewController()
splitViewController.viewControllers = [masterNavigationController, detailNavigationController]

self.window = UIWindow(frame: UIScreen.main.bounds)
self.window!.rootViewController = splitViewController
self.window!.makeKeyAndVisible()

Запустим симулятор. Портретная и альбомная ориентации соответсвенно:

Split Controller без этих ваших сторибордов - 2

В портертной ориентации не видно Master-контроллера. Смахните у левого края, чтобы он появился. Настраивается с помощью режимов, их разберем дальше.

Добавим в Master-контроллер ячейки. Напомню, Master-контроллер это таблица, обернутая в UINavigationController.

Ты умеешь добавлять ячейки, смотреть код тебе не нужно

class MasterController: UITableViewController {

    override func viewDidLoad() {
        self.tableView = UITableView(frame: .zero, style: .insetGrouped)
        super.viewDidLoad()
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "id")
        self.navigationItem.title = "Master"
        self.navigationController?.navigationBar.prefersLargeTitles = true
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 4
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "id")!
        cell.textLabel?.text = "(indexPath)"
        return cell
    }
}

В iOS 13 появился новый стиль таблицы .insetGrouped, я установил его. Стиль доступен начиная с Xcode 11.

Запустим проект:

Split Controller без этих ваших сторибордов - 3

По нажатию на ячейку покажем детальный контроллер. Как же режут слух «‎детальные контроллеры»‎, надеюсь в коментах подскажите перевод лучше. Код выглядит так:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   let controller = DetailController()
   controller.navigationTitle = "(indexPath)"
   self.showDetailViewController(UINavigationController(rootViewController: controller), sender: nil)
}

Помните про нейминг мастер / детальный контроллеры? Вот пример использования Detail в методе showDetailViewController.

Внимательные заметят, что проперти navigationTitle у контроллера нет. Обновим класс Detail-контроллера:

class DetailController: UIViewController {

    var navigationTitle: String = "Detail"

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.title = navigationTitle
        self.view.backgroundColor = .white
    }
}

Запустите проект и нажмите на ячейку:

Split Controller без этих ваших сторибордов - 4

Айфоны

Навигация адаптивная, а значит всё готово. Ну почти. Выберите айфон и запустите:

Split Controller без этих ваших сторибордов - 5

Работает как обычный Navigation-контроллер. Это универсальность адаптивность — в зависимости от свободного пространства Split-контроллер размещает главный и детальный контроллеры.

На айфоне первым открылся не Master-контроллер, а Detail. Переход от отображения двух контроллеров к одному похожая ситуация, настраивается делегатом UISplitViewControllerDelegate. Возвращаемое значение определяет показывать Master-контроллер, или Detail:

extension AppDelegate: UISplitViewControllerDelegate {

    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        return true
    }
}

Этот метод не всегда должен возвращать true. Пример: при смене ориентации в компактную и уже открытом Detail контроллере, может потребоваться оставить Detail на экране. Планируйте это поведение.

Split Controller без этих ваших сторибордов - 6

Альбомная для iPhone

Альбомная ориентация для айфонов работает без Split по умолчанию. Это исправляется режимом отображения для Split-контроллера:

splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.allVisible

Задаем предпочтительный режим отображения, а именно показывать пару Master-Detail всегда, где это возможно. Возможность определяет API, настроить нельзя.

В портретной ориентации останется так же. А вот в альбомной (только для Xs Max и 8+):

Split Controller без этих ваших сторибордов - 7

Если в SDK изменятся условия для Split-контроллера, ваш проект по умолчанию реализует их.

Размеры

Можно настроить. Делается это отношением сторон:

splitViewController.preferredPrimaryColumnWidthFraction = 0.5
splitViewController.maximumPrimaryColumnWidth = 2000

Master и Detail будут одинаковых размеров. Обязательно установите maximumPrimaryColumnWidth. Опционально можно установить минимальную ширину. Скриншот добавлять не буду, и так много айпадов на туториал)

Прячем Master

Добавляем кнопку, которая открывает Detail на весь экран. Split-контроллер должен быть в режиме .allVisible. Вставьте код для Detail-контроллера в viewDidLoad:

if let splitController = self.splitViewController{
   if let navController = splitController.viewControllers.last as? UINavigationController {
      navController.topViewController?.navigationItem.leftBarButtonItem = splitController.displayModeButtonItem
   }
}

Описать поведение сложно, а гифка получается размером с бюджет Москвы. Поэтому скриншот:

Split Controller без этих ваших сторибордов - 8

Или у меня в твиттере видео.

Гайдлайны

AutoLayout размещает элементы, Split-контроллер определяет навигацию. Эпл настоятельно рекомендует использовать статический Master-контроллер. Я ради эксперимента сделал слева навигационный контроллер, по нажатию на ячейку пушил контроллеры (вместо обновления Detail-контроллера). Выглядит странно.

In general, restrict navigation to one side of a split view. Placing navigation in both panes of a split view makes it hard for people to stay oriented and discern the relationship between the two panes.

Выделяйте активный выбор в Master-контроллере. Хотя содержимое Detail-контроллера может изменяться, оно всегда должно соответствовать выделению на Master. Это поможет людям контролировать отношения между контроллерами. Контролировать контроллеры — каламбур какой-то.

Не нужно использовать Split везде. Но если в вашем приложении сильная навигация, а root-контроллер это Tab или Navigation, скорее всего Split полезен. Если у вас одноэкранное приложение-переводчик, Split вам не нужен.

Для ищущих

Ссылка на документацию, ссылка на гайдлайны.
Если вам удобнее смотреть видео, гляньте туториал:

Автор: IvanVorobei

Источник


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


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