Статьи

iOS с нуля с Swift: изучение контроллеров панели вкладок

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

UITabBarController — это еще UIViewController подкласс UIViewController . В то время как контроллеры навигации управляют стеком связанных контроллеров представления, контроллеры панели вкладок управляют массивом контроллеров представления, которые не имеют явного отношения друг к другу.

Приложения Clock и Music на iOS являются двумя яркими примерами контроллеров панели вкладок. Как и любой другой подкласс UIViewController , контроллер панели вкладок управляет экземпляром UIView .

Приложение Clock - отличный пример использования контроллера панели вкладок

Представление контроллера панели вкладок состоит из двух подпредставлений:

  • панель вкладок в нижней части представления
  • представление одного из контроллеров представления, которым управляет контроллер панели вкладок
Анатомия контроллера панели вкладок

При работе с контроллерами панели вкладок необходимо учитывать несколько предостережений. Даже если экземпляры UITabBar могут отображать только пять вкладок, UITabBarController может управлять несколькими контроллерами представления. Если контроллер панели вкладок управляет более чем пятью контроллерами представления, последняя вкладка панели вкладок называется Больше .

Контроллер панели вкладок может управлять неограниченным количеством контроллеров представления

Через эту вкладку можно получить доступ к дополнительным контроллерам представления, и даже можно редактировать положение вкладок на панели вкладок.

Доступ к дополнительным контроллерам представления можно получить на вкладке «Дополнительно».

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

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

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

Откройте XCode, создайте новый проект (« Файл»> «Создать»> «Проект …» ) и выберите шаблон приложения Single View .

Выбор шаблона приложения

Назовите проект « Библиотека с вкладками» , присвойте имя и идентификатор организации, установите « Язык» на Swift и « Устройства» на iPhone . Сообщите Xcode, где вы хотите сохранить проект, и нажмите « Создать» .

Конфигурирование проекта

Несмотря на то, что Xcode включает шаблон приложения с вкладками , я предпочитаю начать с базового шаблона приложения, чтобы вы понимали, как различные части головоломки сочетаются друг с другом. Вы заметите, что контроллеры панели вкладок не так сложны.

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

Загрузите исходный код из предыдущей статьи и откройте проект Xcode, который включен в исходные файлы в новом окне Finder. Найдите AuthorsViewController , BooksViewController и BookCoverViewController и перетащите их в новый проект. Обязательно скопируйте файлы в новый проект, установив флажок Копировать элементы, если это необходимо, и не забудьте добавить файлы в целевую библиотеку с вкладками .

Копировать файлы в проект

В дополнение к этим трем классам нам также необходимо скопировать папку ресурсов, содержащую Books.plist и файлы изображений, в наш новый проект. Перетащите папку с именем Resources в наш проект и используйте те же настройки, которые мы использовали для копирования файлов классов. Теперь мы готовы создать экземпляр контроллера панели вкладок приложения и заполнить его первым контроллером представления.

Если вы откроете Main.storyboard , вы заметите, что раскадровка содержит экземпляр класса ViewController . Выберите контроллер просмотра и нажмите «Удалить» или «Backspace». Откройте библиотеку объектов справа и перетащите контроллер панели вкладок в рабочую область.

Xcode автоматически добавляет два дочерних контроллера представления к контроллеру панели вкладок. Поскольку я хочу показать, как вы вручную добавляете дочерние контроллеры представления в контроллер панели вкладок, мы собираемся удалить те, которые Xcode создал для нас. Выберите дочерние контроллеры представления и нажмите delete или backspace, чтобы удалить их.

Выберите контроллер панели вкладок, откройте инспектор атрибутов и установите флажок « Начальный просмотр контроллера» . Если мы не установим контроллер панели вкладок в качестве начального контроллера представления, приложение будет аварийно завершать работу при запуске.

Добавление контроллера панели вкладок

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

Нам нужно добавить несколько контроллеров представления в раскадровку и добавить их в свойство viewControllers контроллера панели вкладок. Посмотрим, как это работает.

Перетащите экземпляр UITableViewController из библиотеки объектов в рабочую область и установите для его класса значение AuthorsViewController в Identity Inspector . Выберите табличное представление контроллера представления и установите Ячейки Прототипа в 0 в Инспекторе Атрибутов .

Добавление контроллера табличного представления

Чтобы добавить контроллер представления авторов в массив контроллеров представления панели вкладок, перетащите курсор с контроллера панели вкладок на контроллер представления авторов, удерживая клавишу « Control» . Выберите Отношение Segue> просмотреть контроллеры из меню, которое появляется.

Создание отношений Segue
Создание отношений Segue

Контроллер панели вкладок с одной вкладкой не так полезен, поэтому давайте добавим еще один контроллер представления в микс. Перетащите другой экземпляр UITableViewController из библиотеки объектов , установите для его класса значение BooksViewController и установите для ячеек прототипа значение 0 . Создайте отношения, как мы делали для контроллера представления авторов.

Добавление другого контроллера табличного представления

Добавьте экземпляр UIViewController в рабочую область и установите его класс в BookCoverViewController в Identity Inspector . Добавьте экземпляр UIImageView в контроллер представления, подключите его к bookCoverView контроллера представления и примените необходимые ограничения макета. Создайте связь с контроллером панели вкладок, как мы это сделали для контроллеров табличного представления.

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

Создание сегментов между контроллерами представления

Создайте и запустите приложение. На данный момент панель вкладок содержит три вкладки. При нажатии на вторую или третью вкладку происходит сбой приложения. Это почему? Пришло время для некоторой отладки.

Когда вы нажимаете на имя автора в контроллере представления авторов, приложение вылетает. Первое, что вы должны сделать, когда столкнулись с сбоем, это проверить консоль Xcode. Это то, что он говорит мне.

1
Tabbed Library[1141:54864] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Receiver (<Tabbed_Library.AuthorsViewController: 0x7fde3943d050>) has no segue with identifier ‘BooksViewController»

Сообщение, которое Xcode отображает в консоли, не сложно расшифровать. Мы говорим контроллеру представления выполнить переход с идентификатором BooksViewController , но этот переход не существует. Результатом является сбой.

Эту ошибку легко исправить, встраивая контроллер представления авторов в контроллер навигации, создавая переход к экземпляру BooksViewController и называя segue BooksViewController . Я оставляю это на ваше усмотрение, поскольку мы уже делали это в предыдущей статье. Исходные файлы этого руководства содержат решение.

Более важной проблемой в этом примере является понимание того, что три контроллера представления, которые связаны с контроллером панели вкладок, не взаимодействуют друг с другом. Идея контроллера панели вкладок и контроллера навигации очень отличается. Контроллер навигации поддерживает массив контроллеров представления, стек навигации. Контроллеры представления в стеке навигации имеют неявную связь друг с другом в том смысле, что они являются частью одного и того же стека навигации.

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

Приложения Clock и Music на iOS являются хорошим примером того, как следует использовать контроллер панели вкладок. Контроллеры представления, которыми управляет контроллер панели вкладок в приложении Music, не имеют отношения друг к другу, кроме того, что они показывают песни.

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

При нажатии на вторую вкладку контроллера панели вкладок приложение также вылетает. Это то, что я вижу в консоли XCode.

1
fatal error: unexpectedly found nil while unwrapping an Optional value

Это тоже не удивительно, так как мы не устанавливаем свойство author контроллера представления книг. Давайте обновим контроллер представления книг, чтобы он отображал каждую книгу в Books.plist .

Нам нужно сделать три изменения. Давайте начнем с самого простого изменения в первую очередь. Откройте BooksViewController.swift и измените тип свойства author на [String: AnyObject]? , Свойство автора теперь является необязательным, а не принудительным развернутым необязательным.

1
var author: [String: AnyObject]?

Для второго изменения нам нужно обновить реализацию viewDidLoad() . Мы показываем имя автора, если author не nil . Если автор равен nil , мы устанавливаем заголовок "Books" .

01
02
03
04
05
06
07
08
09
10
11
override func viewDidLoad() {
    super.viewDidLoad()
     
    if let author = author, let name = author[«Author»] as?
        title = name
    } else {
        title = «Books»
    }
     
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
}

Последнее изменение немного сложнее. Мы превращаем свойство computed books свойство с отложенным хранением. Ленивое сохраненное свойство инициализируется при первом обращении к нему. Другими словами, свойство не инициализируется при инициализации контроллера представления. Вот как выглядит реализация.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lazy var books: [AnyObject] = {
    // Initialize Books
    var buffer = [AnyObject]()
     
    if let author = self.author, let books = author[«Books»] as?
        buffer += books
         
    } else {
        let filePath = NSBundle.mainBundle().pathForResource(«Books», ofType: «plist»)
         
        if let path = filePath {
            let authors = NSArray(contentsOfFile: path) as!
             
            for author in authors {
                if let books = author[«Books»] as?
                    buffer += books
                }
            }
        }
    }
     
    return buffer
}()

Ленивые сохраненные свойства помечены lazy ключевым словом. Вместо того, чтобы назначать обычное значение свойству, мы назначаем замыкание. Но обратите внимание, что закрытие заканчивается двумя круглыми скобками () . Это означает, что закрытие выполняется при первом обращении к books . Это очень похоже на анонимные функции в других языках, таких как JavaScript.

В закрытии мы создаем пустой массив типа [AnyObject] . Если author не равен nil , мы загружаем книги из свойства author . Если у author нет значения, мы загружаем Books.plist и добавляем каждую книгу каждого автора в buffer . Обратите внимание, что мы возвращаем массив книг в конце закрытия. Запустите приложение, чтобы увидеть, исправляет ли это сбой, вызванный контроллером просмотра книг.

Если вы откроете вторую вкладку приложения и коснитесь книги из списка, приложение вылетает по той же причине, по которой контроллер представления авторов раньше вызывал сбой. Это можно исправить, встроив контроллер представления книг в контроллер навигации и создав последовательность с идентификатором BookCoverViewController для экземпляра BookCoverViewController .

При нажатии на третью вкладку приложение также вылетает. Решение аналогично тому, которое мы применили к классу BooksViewController . Начните с того, что BookCoverViewController свойство BookCoverViewController необязательным.

1
var book: [String: String]?

Мы объявляем другое ленивое хранимое свойство, bookCoverImage типа UIImage? , В bookCoverImage мы выбираем случайную книгу из книг, хранящихся в Books.plist, если свойство book не имеет значения. Если book имеет значение, мы отображаем обложку этой книги.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lazy var bookCoverImage: UIImage?
    var image: UIImage?
     
    if self.book == nil {
        // Initialize Buffer
        var buffer = [AnyObject]()
         
        let filePath = NSBundle.mainBundle().pathForResource(«Books», ofType: «plist»)
         
        if let path = filePath {
            let authors = NSArray(contentsOfFile: path) as!
             
            for author in authors {
                if let books = author[«Books»] as?
                    buffer += books
                }
            }
        }
         
        if buffer.count > 0 {
            let random = Int(arc4random()) % buffer.count
             
            if let book = buffer[random] as?
                self.book = book
            }
        }
    }
     
    if let book = self.book, let fileName = book[«Cover»] {
        image = UIImage(named: fileName)
    }
     
    return image
}()

В viewDidLoad() мы используем свойство bookCoverImage для обновления представления изображения контроллера представления обложки книги.

1
2
3
4
5
6
7
8
override func viewDidLoad() {
    super.viewDidLoad()
     
    if let bookCoverImage = bookCoverImage {
        bookCoverView.image = bookCoverImage
        bookCoverView.contentMode = .ScaleAspectFit
    }
}

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

Причина этой странной причуды на самом деле довольно проста. В общем случае представление не загружается в память до тех пор, пока оно не станет абсолютно необходимым. Обычно это означает, что представление загружается в память, когда оно собирается быть показано пользователю.

При запуске приложения « Библиотека с вкладками» по умолчанию выбирается первая вкладка. Пока вторая или третья вкладка не выбрана пользователем или программно, нет необходимости загружать представление этого контроллера представления. В результате viewDidLoad() не вызывается, пока не выбрана вкладка. Это означает, что заголовок не устанавливается до тех пор, пока не будет выбрана вкладка.

Решение простое. Каждый экземпляр UIViewController (или его подкласс) имеет свойство UITabBarItem! типа UITabBarItem! , Это свойство используется контроллером панели вкладок для настройки вкладок в его панели вкладок. Название вкладки контроллера представления tabBarItem свойства tabBarItem контроллера представления.

Чтобы устранить вышеуказанную проблему, нам нужно установить свойство title свойства tabBarItem когда инициализируются контроллеры представления контроллера панели вкладок. Давайте посмотрим, как это работает для класса BooksViewController .

Откройте BooksViewController.swift и добавьте следующий инициализатор. Прежде чем мы взглянем на реализацию, я бы хотел отметить три интересных момента об инициализаторе. В Swift инициализаторы — это особый тип функций.

1
2
3
4
5
6
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
     
    // Initialize Tab Bar Item
    tabBarItem = UITabBarItem(title: «Books», image: UIImage(named: «icon-books»), tag: 1)
}
  • Обязательное ключевое слово указывает, что этот инициализатор должен быть реализован каждым подклассом. Однако нет необходимости реализовывать требуемый инициализатор, если подкласс удовлетворяет требованиям с инициализатором, который он наследует своим родительским классом. Инициализация — довольно сложная тема в Swift, которая вызывает путаницу среди разработчиков. Из-за его сложности я не буду подробно останавливаться на этом в этой серии.
  • Знак вопроса после ключевого слова init указывает, что инициализация может завершиться неудачно. Это больше известно как сбой инициализатора . Если контроллер представления не может быть создан по той или иной причине, инициализатор возвращает nil вместо экземпляра класса.
  • Вы также, возможно, заметили, что инициализатор не использует ключевое слово func . Ключевое слово func не используется для инициализаторов.

В инициализаторе мы вызываем инициализатор родительского класса. Ключевое слово super относится к суперклассу. Затем мы создаем элемент панели вкладок и назначаем его tabBarItem . Чтобы создать элемент панели вкладок, мы вызываем init(title:image:tag:) , передавая заголовок элемента панели вкладок, экземпляр UIImage и тег. Тег используется для идентификации элемента панели вкладок.

Обратите внимание, что вы можете опустить расширение имени файла. Даже если мы используем изображение с именем icon -hors.png , значение, которое мы UIImage инициализатору UIImage : "icon-authors" . Изображения, которые я использовал, включены в исходные файлы на GitHub, и вы также можете найти их в GraphicRiver, если хотите использовать их в своих собственных проектах.

Для каждого изображения есть два файла: icon -hors.png и icon-authors@2x.png . Операционная система выбирает первый файл для устройств, которые не имеют экрана сетчатки. Второй файл используется для устройств с экраном сетчатки. Обратите внимание, что вам не нужно включать спецификатор @ 2x в строку, которую вы передаете инициализатору UIImage . Операционная система достаточно умна, чтобы понять это.

Вы можете включить третий файл со спецификатором @ 3x для iPhone 6 / 6S Plus. Плотность пикселей в этом файле должна быть в три раза больше, чем у первого. Взгляните на следующий пример, чтобы лучше понять, как это работает на практике.

  • icon-авторы.png: 30px x 30px
  • icon-authors@2x.png: 60px x 60px
  • icon-authors@3x.png: 90px x 90px

Давайте также создадим элемент AuthorsViewController BookCoverViewController классов AuthorsViewController и BookCoverViewController .

1
2
3
4
5
6
7
// Authors View Controller
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
     
    // Initialize Tab Bar Item
    tabBarItem = UITabBarItem(title: «Authors», image: UIImage(named: «icon-authors»), tag: 0)
}
1
2
3
4
5
6
7
// Book Cover View Controller
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
     
    // Initialize Tab Bar Item
    tabBarItem = UITabBarItem(title: «Cover», image: UIImage(named: «icon-cover»), tag: 2)
}

Элементы панели вкладок также могут иметь значок. Например, если вы создаете почтовый клиент, вы можете добавить вкладку с количеством непрочитанных писем. Вы можете добавить значок к элементу панели вкладок, установив его свойство badgeValue . Обновите инициализатор контроллера представления книг, как показано ниже.

1
2
3
4
5
6
7
8
9
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
     
    // Initialize Tab Bar Item
    tabBarItem = UITabBarItem(title: «Books», image: UIImage(named: «icon-books»), tag: 1)
     
    // Configure Tab Bar Item
    tabBarItem.badgeValue = «8»
}

Вот как должен выглядеть результат при запуске приложения в симуляторе.

Обозначение предмета панели вкладок

В предыдущем уроке я писал, что каждый контроллер представления в стеке навигации содержит ссылку на контроллер навигации, управляющий стеком. То же самое верно для контроллеров представления, которыми управляет контроллер панели вкладок. Контроллер представления, управляемый контроллером панели вкладок, сохраняет ссылку на контроллер панели вкладок в свойстве tabBarController , которое имеет тип UITabBarController? ,

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

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

Откройте основную раскадровку и добавьте два экземпляра UITableViewController и один экземпляр UIViewController . В Identity Inspector установите класс этих контроллеров представления в AuthorsViewController , BooksViewController и BookCoverViewController соответственно. Запустите приложение, чтобы увидеть результат. Если вы вкладываете первую вкладку справа, вы должны увидеть два контроллера вида, которые не помещаются на панели вкладок.

Больше элементов панели вкладок

Вы даже можете изменить положение элементов панели вкладок, нажав кнопку « Редактировать» в правом верхнем углу.

Изменить положение элементов панели вкладок

Добавленные нами дополнительные контроллеры представления не очень полезны, но они показывают, как контроллер панели вкладок управляет более чем пятью дочерними контроллерами представления.

Важно понимать, что UITabBarController и UINavigationController представляют собой уникальную парадигму пользовательского интерфейса. Из этой статьи вы узнали, что освоить контроллеры панели вкладок несложно, если разобраться в задействованных компонентах. В следующей статье мы рассмотрим постоянство данных на iOS и варианты, которые вы имеете в качестве разработчика.

Если у вас есть какие-либо вопросы или комментарии, вы можете оставить их в комментариях ниже или обратиться ко мне в Twitter .