В этом уроке мы разработаем более сложное приложение-справочник, которое уже не будет состоять из одного экрана и минимума кода. Сделаем упор на работу с UITableViewController, но также не забудем и об уже изученных контроллерах навигации и создадим еще один класс. По-мимо этого, также рассмотрим как работать с asset каталогами. Наше приложение будет отображать небольшой список устройств от компании Apple. По-нажатию на один из продуктов — приложение откроет его более подробное описание.
Создадим новый проект под названием Apple Gallery со следующими параметрами:
- Project template: Single View Application;
- Product name: Apple Gallery;
- Language: Swift.
- Devices: iPhone.
Интерфейс
Сразу же откроем сториборд, отключим опцию Use Size Classes и приступим к созданию интерфейса. Xcode уже любезно создал контроллер для нас, но он нам не понадобится. Удалите его и перетащите UINavigationController из Object library. Как мы уже знаем, Xcode автоматически добавит контроллер навигации с вложенным UITableViewController. Выделите первый контроллер и в Attributes inspector включите опцию Is Initial View Controller:
Включение опции Is Initial View Controller
Запустите приложение и убедитесь, что приложение отображает пустую таблицу.
Мы еще не научились делать приложения с «гибким» ннтерфейсом, но в будущем обязательно это сделаем. Но на данный момент, в качестве модели симулятора, используйте iPhone 5 или 5s.
Теперь выделим ячейку в контроллере Root View Controller и всё в том же Attributes inspector зададим полю Identifier значение «Cell». Также в опции Style выберите опцию Subtitle. В результате ячейка должна иметь следующий вид:
Ячейка со стилем Subtitle
Также изменим title контроллера UITableViewController с Root View Controller на Apple Gallery.
Добавим один стандартный контроллер представления справа от UITableViewController. Поместим на него:
- UIImageView с параметрами:
- Attributes inspector:
- Mode: Aspect fill.
- Size inspector:
- X: 20.
- Y: 76.
- Width: 80.
- Height: 80.
- Attributes inspector:
- UILabel:
- Attributes inspector:
- Font: System Light 21.0.
- Lines: 0.
- Size inspector:
- X: 108.
- Y: 76.
- Width: 192.
- Height: 80.
- Attributes inspector:
- UITextView:
- Attributes inspector:
- Font: System 17.0.
- Editable: No.
- Selectable: No.
- Size inspector:
- X: 20.
- Y: 164.
- Width: 280.
- Height: 384.
- Attributes inspector:
В результате контроллер должен иметь следующий вид:
Теперь нужно связать между собой UITableViewController и только что созданный контроллер представления. Нам нужно, чтобы приложение открывало UIViewController при нажатии на ячейку в Root View Controller, поэтому связь должна быть не от контроллера к контроллеру, а от ячейки к контроллеру:
Добавление связи между ячейкой и контроллером
В качестве последнего штриха в построении интерфейса, нам осталось дать идентификатор связи. Выделите segue и в Attributes inspector в поле Identifier укажите «goDetail». Также изменим title контроллера представления на Info.
Заметьте, что при выделении segue, Xcode подсветит его источник для Вас. В нашем случае это ячейка таблицы.
Архитектура приложения
Завершив построение интерфейса, мы можем перейти к написанию архитектуры самого приложения. У нас есть два новых контроллера: UITableViewController и UIViewController, значит необходимо создать им классы и связать их между собой. Добавьте два новых класса так, как мы это делали в прошлых уроках:
- ListTVC (Subclass of: UITableViewController).
- DetailVC (Subclass of: UIViewController).
Перед тем как мы начнем имплементацию работы интерфейса, добавим еще один класс: Product (Subclass of: NSObject). Он будет выступать в качестве модели данных, которую мы будем использовать для отображения данных о продуктах. Пусть Product будет содержать следующие атрибуты:
1 2 3 4 |
var name: String! var type: String! var info: String! var imagePath: String! |
Также для удобства добавим еще два метода, один из которых поможет быстро инициализировать класс со всеми атрибутами, а второй будет возвращать нам объект UIImage согласно значению переменной imagePath:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
convenience init(name: String, type: String, imagePath: String, info: String) { self.init() self.name = name self.type = type self.info = info self.imagePath = imagePath } func getImage() -> UIImage? { return UIImage(named: self.imagePath) } |
Модель готова, так что можем двигаться дальше.
Asset каталог
Помните, что каждый продукт должен отображать определенную картинку? Каждое изображение нужно подготовить, поэтому загрузите из интернета по одной картинке для каждого продукта:
- iPhone 6S;
- iPad Pro;
- iMac;
- Macbook Pro;
- Apple Watch.
Постарайтесь подобрать изображения равные по пропорциям (1:1, 4:3 и т.д.)
Добавим картинки в проект, вот только на этот раз сделаем это правильно. Начиная с iOS 7 у нас имеется возможность хранить все изображения в специальном каталоге. Xcode уже создал его для Вас. В навигаторе откройте Assets.xcassets. Здесь имеется возможность хранить все иконки приложения, сплэш скрины и изображения. Главной особенностью является не только каталогизация всех изображений в одном месте, но и возможность указывать для какого девайса с каким размером экрана данная картинка будет использоваться.
Добавьте новый набор изображений, нажав на «+» в нижней части экрана и выбрав вариант New image set:
Добавление нового набора изображений (image set)
Теперь в правой части asset-каталога мы видим группу Image. В данную группу можно задать 3 изображения — для каждой резолюции экрана. При разработке приложений, в качестве якорей размеров выступают точки (pt), в то время как само изображение на экране реального устройства измеряется в пикселях (px). Поэтому все устройства Apple условно делятся на 3 вида согласно типу экрана:
- 1x — устройства со стандартным экраном. Здесь 1pt = 1px.
- 2х — устройства с Retina-дисплеем (iPhone 4+, The New iPad+ и т.д.). У этих девайсов 1pt = 2px.
- 3х — к данным устройствам, на момент написания урока, относятся iPhone 6(s) Plus и iPad Pro.
Соответственно, для каждого девайса стоит подготовить изображения с правильной резолюцией. Но из личного опыта могу сказать, что в последние года об 1х-изображениях никто уже не заботиться, поэтому всегда обязательно заготавливайте изображения с 2х разрешением. iOS сама сожмет 2х изображение в 1х.
Переименуйте только что созданный набор в im_iphone. Выделите его, нажмите Enter или дважды медленно кликните и введите нужное название. Теперь перетащите загруженное изображение из Finder в ячейку 2х. Повторите то же самое с оставшимися картинками. Для избежания расхождения в коде, назовите их следующим образом:
- im_iphone;
- im_ipad;
- im_imac;
- im_macbook;
- im_apple_watch.
UITableViewDataSource & UITableViewDelegate
Откроем ListTVC в Editor’е и посмотрим на его содержимое. Здесь мы видим гараздо большее количество методов, чем при создании класса UIViewController. При рассмотрении всех основных контроллеров представления в iOS, мы упоминали о том, что работа UITableViewController строится на таких понятиях как DataSource и Delegate.
По-сути, UITableViewController — это класс, который стоит за имплементацией работы UITableView. Класс весьма «гибкий» и у него нет какой-либо привязки к типу данных, поэтому в таблице Вы можете отображать всё что душе угодно. Что и как — зависит только от Вас. Соответственно, эти данные необходимо передавать из класса в таблицу. Здесь на помощь и приходит UITableViewDataSource — набор методов, с помощью которых UITableView получает эти самые данные. Весь дата сурс таблицы «стоит на трёх китах» — трёх обязательных методах:
- numberOfSectionsInTableView: — запрашивает количество секций, которые должны быть отображены;
- tableView:numberOfRowsInSection: — запрашивает количество ячеек, которые должны отображаться у каждой секции;
- tableView:cellForRowAtIndexPath: — запрашивает саму ячейку, для конкретной секции.
Первые два метода вызываются таблицей только при загрузке данных: при инициализации контроллера и при вызове функций типа reloadData(). Последний метод из этого списка вызывается каждый раз, когда какая-либо ячейка должна отобразиться: при загрузке данных, при скролле и т.д.
Метод tableView:cellForRowAtIndexPath: закомментирован по-умолчанию, поэтому раскомментируйте его (удалите символы /* перед методом, и символы */ после).
Помимо Data Source, упоминалось и такое понятие как делегирование. Делегирование используется при работе с событиями и, в своей простейшей форме, это просто механизм обратного вызова (callback). UITableViewDelegate позволяет «отлавливать» различные события, которые происходят с таблицей. К примеру, вот тройка из них:
- tableView:didSelectRowAtIndexPath: — вызывается когда юзер совершил нажатие на ячейку таблицы;
- tableView:willBeginEditingRowAtIndexPath: — вызывается в тот момент, когда ячейка только переходит в режим редактирования;
- tableView:heightForRowAtIndexPath: — вызывается когда таблице нужно знать высоту ячейки.
UITableViewController
Теперь, когда все ресурсы на месте и мы знаем что такое Data Source и Delegate — перейдем непосредственно к реализации класса ListTVC. Создадим источник данных. Объявим массив объектов Product под названием allData:
1 |
var allData = [Product]() |
В методе viewDidLoad() заполним allData контентом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
allData += [ Product(name: "iPhone 6S", type: "Phone", imagePath: "im_iphone", info: "The original iPhone introduced the world to Multi-Touch, forever changing the way people experience technology. With 3D Touch, you can do things that were never possible before. It senses how deeply you press the display, letting you do all kinds of essential things more quickly and simply."), Product(name: "iPad Pro", type: "Tablet", imagePath: "im_ipad", info: "iPad Pro is more than the next generation of iPad — it’s an uncompromising vision of personal computing for the modern world. It puts incredible power that leaps past most portable PCs at your fingertips. It makes even complex work as natural as touching, swiping, or writing with a pencil."), Product(name: "Apple Watch", type: "Watch", imagePath: "im_apple_watch", info: "Instantly receive and respond to your favorite notifications. Get the motivation you need to stay active and healthy. Express your personal style in a whole new way. From the way it works to the way it looks, Apple Watch isn’t just something you wear. It’s an essential part of who you are."), Product(name: "iMac", type: "Computer", imagePath: "im_imac", info: "The idea behind iMac has never wavered: to craft the ultimate desktop experience. The best display, paired with high-performance processors, graphics, and storage — all within an incredibly thin, seamless enclosure. And that commitment continues with the all-new 21.5‑inch iMac with Retina 4K display."), Product(name: "Macbook Pro", type: "Computer", imagePath: "im_macbook", info: "A groundbreaking Retina display. A new force-sensing trackpad. All-flash architecture. Powerful dual-core and quad-core Intel processors. Together, these features take the notebook to a new level of performance. And they will do the same for you in everything you create."), ] |
Обратите внимание, что для атрибута imagePath мы указываем имя, которое дали наборам изображений в asset каталоге.
Вся информация для атрибутов info взята с официального сайта Apple.
Заполним методы UITableViewDataSource следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { // Говорим таблице, что нам нужна только одна секция return 1 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // Даём знать таблицы, что нам необходимо количество ячеек равное количеству объектов в массиве return allData.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { // Инициализируем объект ячейки с идентификатором "Cell" // Это тот самый идентификатор, который мы задали ячейке в сториборде let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) // Получаем объект Product под индексом равным значению indexPath.row let product: Product = self.allData[indexPath.row] // Заполняем ячейку данными cell.imageView?.image = product.getImage() cell.textLabel?.text = product.name cell.detailTextLabel?.text = product.type // Отдаём таблице ячейку return cell } |
Вы могли заметить, что при работе с таблицами мы сталкиваемся с объектом indexPath. Его классом является NSIndexPath, который хранит в себе два значения: section (номер секции) и row (номер) ячейки в таблице. Нумерация начинается с 0. Т.е. 0 = 1, 1 = 2, 2 = 3 и т.д.
Запустите приложение и посмотрите всё ли у Вас работает. В результате апп должен отображать 5 продуктов с краткой информацией:
Список продуктов Apple Gallery
Кликнув на одну из ячеек мы перейдем на второй контроллер представления. Наша задача передать информацию о выбранном продукте в этот контроллер и отобразить её.
Откройте DetailVC и добавьте связи между контроллером и добавленными объектами интерфейса (UILabel, UITextView, UIImage) следующим образом:
1 2 3 |
@IBOutlet weak var imageViewPhoto: UIImageView! @IBOutlet weak var labelTitle: UILabel! @IBOutlet weak var textViewInfo: UITextView! |
Объявите переменную Product:
1 |
var product: Product! |
Теперь добавим метод контроллера представления viewWillAppear() и напишем в нем отображение данных в интерфейсе:
1 2 3 4 5 6 7 8 9 10 11 |
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) if product != nil { self.imageViewPhoto.image = product.getImage() self.labelTitle.text = product.name self.textViewInfo.text = product.info; } } |
На этом контроллере всё готово, теперь имплементируем саму передачу объекта Product из ListTVC в DetailVC. Откройте контроллер с таблицей и опуститесь вниз к методу prepareForSegue:. Раскомментируйте его. Данный метод срабатывает в тот момент, когда юзер осуществляет переход с одной сцены на другую с помощью segue. Здесь мы и передадим нужный объект Product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // Проверяем что сработал segue с нужным идентификатором // Это тот идентификатор, который мы указали в сториборде if segue.identifier == "goDetail" { // Здесь объектом sender является ячейка, на которую нажимает юзер // Получаем indexPath выбранной ячейки с помощью метода indexPathForCell: let indexPath = self.tableView.indexPathForCell((sender as! UITableViewCell)) // Получаем объект Product под нужным индексом let product = self.allData[indexPath!.row] // Получаем контроллер, на который юзер попадёт с этим segue let detailVC: DetailVC = segue.destinationViewController as! DetailVC // Задаём атрибут Product в DetailVC detailVC.product = product } } |
Запустите приложение и проверьте всё ли работает правильно:
Подробная информация о выбранном продукте
Поздравляем! Вы только что написали своё первое iOS приложение-справочник для iPhone и iPod Touch с использованием UITableViewController. Для того чтобы закрепить результат, попробуйте модифицировать приложение, добавив больше продуктов и больше полей с информацией.