Если можешь не спать — не спи. Сон для слабых.
Мы научимся работать с сетью, а также ещё немного расширим наши знания о табличных представлениях.
В данном приложении мы получим часть карт популярной карточной игры Hearthstone и выведем их в таблице.
assets
в качестве иконок приложения, а шрифт ttf
в группу Resources
Model
, View
, Controlle
ViewController
в папку с Controller
HSCards
TableView
(с нулевыми отступами от границ)tableView
Вы можете открыть редактор помошник не только через верхнее меню, но и нажав с зажатым ALT по нужному файлу.
Мы хотим изменить отображение нашего навигационного бара во всех случаях. Для этого есть очень удобный способ - метод appearance()
класса UINavigationBar
- он возвращает так навязываемый прокси-объект, который никогда не инстанцируется, но обращение к которому меняет все остальные объекты класса.
Давайте извлечём этот объект в методе application(_:didFinishLaunchingWithOptions:)
класса ApplicationDelegate
:
let navigationBarProxy = UINavigationBar.appearance()
//Это раскрасит навигационный бар во всех контроллерах
navigationBarProxy.barTintColor = #colorLiteral(red: 0.9178599119, green: 0.6678383946, blue: 0.1624581814, alpha: 1)
//А это сделает элементы навигации белыми
navigationBarProxy.tintColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
Мы всегда ранее пользовались готовыми шрифтами, но в этот раз мы добавим свой собственный шрифт в приложение. Мы уже добавили в папку с ресурсами наш кастомный шрифт Belwe-Bold
. Теперь добавим его в приложение:
Info.plist
добавьте строчку Fonts provided by application
, что создаст новый массив.Item 0
Belwe-Bold.ttf
. Учтите, что нужно указывать точное название файла. При этом добавить можно сколько угодно шрифтовВернёмся в тот же метод, где мы задавали вид навигации:
//Пытаемся получить наш новый шрифт
if let barFont = UIFont(name: "Belwe-Bold", size: 24.0) {
//Если это вышло, то задаём атрибуты для заголовка навигации
navigationBarProxy.titleTextAttributes = [
.foregroundColor: #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0), //через цвет
.font: barFont //и шрифт
]
}
И последним штрихом мы сделаем так, чтобы наш статус-бар был белым:
UIApplication.shared.statusBarStyle = .lightContent
Что же такое
UIApplication
? Это специальный класс, объект которого создаётся всего один раз для каждого приложения и позволяет приложениям взаимодействовать.
Но этого не достаточно - нужно также прописать в файле Info.plist
, что статус-бар не должен адаптироваться к контроллерам вида:
View controller-based status bar appearance
Со значением NO
.
Можно задать для каждого контроллера свой стиль статус-бара, переписав свойство
preferredStatusBarStyle
. Либо его можно можно вообще скрыть, задав свойствоprefersStatusBarHidden
.
Раньше мы сохраняли данные в plist
, теперь же мы будем получать их по сети. Данные в REST-сервисах чаще всего передаются в виде строк в формате JSON
(JavaScript Object Notation). Swift позволяет кодировать объекты в этот формат и декодировать так же просто, как и в случае со списками свойств.
Сам по себе данный формат выглядит примерно так;
[
{
"cardId": "EX1_572",
"name": "Ysera",
"cardSet": "Classic",
"type": "Minion",
"faction": "Neutral",
"rarity": "Legendary",
"cost": 9,
"attack": 4,
"health": 12,
"text": "At the end of your turn, add a Dream Card to your hand.",
"flavor": "Ysera rules the Emerald Dream. Which is some kind of green-mirror-version of the real world, or something?",
"artist": "Gabor Szikszai",
"collectible": true,
"elite": true,
"race": "Dragon",
"img": "http://wow.zamimg.com/images/hearthstone/cards/enus/original/EX1_572.png",
"imgGold": "http://wow.zamimg.com/images/hearthstone/cards/enus/animated/EX1_572_premium.gif",
"locale": "enUS"
}
]
REST-сервисы унифицируют передачу данных по протоколу HTTP(s) между сервером и клиентом.
Вы можете получить доступ к указанному API по этому адресу Hearthstone API. Вы можете создать здесь свой ключ для доступа к ресурсам, или же воспользоваться тем, что мы предоставим в процессе создания приложения.
Создайте класс Card
в новом одноимённом файле.
Наша модель карты будет достаточно простой:
class Card: Codable {
//Имя карты
let name: String
//Цена в мане
let manaCost: Int
//Текст карты
let text: String
//Путь до картинки
let imagePath: String
init(name: String, manaCost: Int, text: String, imagePath: String) {
self.name = name
self.manaCost = manaCost
self.text = text
self.imagePath = imagePath
}
}
Мы сразу же объявили класс соответствующим протоколу Codable
.
Сверху же однако виден пример результата... Что будет с теми ключами, которых нет в классе? Ничего - они просто будут отброшены. Но допустим нам нужно сделать так, чтобы img
из ответа сервера превращался на нашей стороне в свойство imagePath
. Что же делать?
Для этого протокол Codable
позволяет определить вложенное перечисление enum CodingKeys: String, CodingKey
, каждый кейс которого будет иметь название поля типа в Swift, а значением - название поля в ответе сервера. Проблема состоит в том, что мы не можем переодпрелеить только какие-то ключи - придётся переопределять все:
enum CodingKeys: String, CodingKey {
case name = "name"
case manaCost = "cost"
case text = "text"
case imagePath = "img"
}
В этот раз создадим ячейку отдельно от остального вида. Для этого в папке View
создайте класс CardTableViewCell
, унаследовав его от UITableViewCell
, при этом галочка Also create xib
должна быть установлена, что создаст отдельное представление для ячейки таблицы - причём иметь оно будет то же название, что и класс. Только расширение будет .xib
.
ImageView
(аутлет cardImageView
)Label
с именем Name
(аутлет nameLabel
)Label
с именем Text
(аутлет descriptionLabel
)Name
шрифт Headline
(мы узнаем совсем скоро, что это нам дастText
тонкий (thin
) шрифт размером 11. Ему же задайте число линий в 0 (то есть бесконечность).Stack View
:
Alignment
и Distribution
установите в Fill
Spacing
в 5 точекAlignment
- Center
Distribution
- Fill
Spacing
- 10Content Mode
- Aspect Fit
Content View
Вы получите примерно следующее:
Если Вы запутаетесь в иерархии видов, то вместо подробной иерархии можно получить краткую, нажав правой кнопкой манипулятора вместе с SHIFT по виду:
Теперь перейдите в раскадровку и в инспекторе размеров табличного представления задайте:
Row Height
- 100Estimate
- 100Выделяют несколько типов запросов и соответсвующих путей для получения ресурсов:
Метод | URL | Действие | Маршрут | Описание |
GET | /cards | index | cards.index | Получение всех карт |
GET | /cards/create | create | cards.create | Запрос на создание карты (например, получение веб-формы) |
POST | /cards | store | cards.store | Запрос на сохрание карты |
GET | /cards/{card} | show | cards.show | Получение отдельной карты, причём карта специфицируемая как аргумент пути в виде `{card}`, то есть вместо этой надписи будет находиться уникальный идентификатор карты |
GET | /cards/{card}/edit | edit | cards.edit | Запрос на изменение карты |
PUT/PATCH | /cards/{card} | update | cards.update | Запрос на сохранение изменений в карте |
DELETE | /cards/{card} | delete | cards.delete | Удаление карты |
В этом задании мы будем использовать только получение одной текущей карты. Добавьте ключ API в качестве свойства константы в контроллер:
let kAPIKey = "sJoVbrkZRPmshSXRb5TUbAyYEpMZp1I6ntujsnJvZskUFsFRkW"
А также предопределённый массив скрытых карт:
private let cardNames = [
"Ysera",
"Ancient Watcher",
"Argent Protector",
"Cruel Taskmaster",
"Defias Ringleader",
"Dire Wolf Alpha",
"Doomsayer",
"Eviscerate"
]
Теперь создадим свойство для хранения карт и навесим на него обсервер:
var cards = [Card]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
При каждом изменении массива будет происходить обновление таблицы. Почему мы обернули его в асинхронный вызов в главном потоке? Дело в том, что изменение будет происходить в вспомогательной очереди - сетевые запросы чаще всего выполняются асинхронно, чтобы сократить время выполнения программы, но потом будет требоваться вернуть изменения в главный поток, так как только в нём разрешено манипулировать интерфейсом.
Во viewDidLoad
будем заполнять этот массив для каждого названия карты:
for name in cardNames {
//Создаётся строка запроса, причём она кодируется в строку, пригодную для запросов
let urlString = "https://omgvamp-hearthstone-v1.p.mashape.com/cards/\(name)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
//Из неё создаётся объект запроса
var request = URLRequest(url: URL(string: urlString!)!)
//Можно было создать и обычную ссылку, но объект запроса позволяет изменить его тип - мы явно укажем, что это GET
request.httpMethod = "GET"
//А также добавим заголовок со значением ключа API
request.addValue(kAPIKey, forHTTPHeaderField: "X-Mashape-Key")
//Заголовки, это дополнительные сведения передеаваемые с запросом. Назване заголовка придумали авторы.
//Для выполнения запрос необходимо создать задачу в URL-сессии. Для большинства простых задач можно воспользоваться готовым объектом класса:
URLSession.shared.dataTask(with: request) { (data, respone, netError) in
//В результате выполнения задачи придут
// data - данные, если запрос был успешен
// netError - если запрос был неудачен
// response - заголовок ответа сервера в любом случае
// Учтите, что это замыкание выпонляется асинхронно
//Если данные не пришли, то гарантированно произошла ошибка, а мы её выведем
guard let cardsData = data else {
print(netError)
return
}
do {
//Пытаемся извлечь карту
//Заметьте, что метод декодера для JSON поход на `plist`
let card = try JSONDecoder().decode([Card].self, from: cardsData).first!
//Мы декодировали массив и извлекали из него первый объект, а не просто объект, так как в этом странном API ответ оборачивается в массив
self.cards.append(card)
//Добавление карты в массив приводит к срабатыванию наблюдателя
} catch {
//Если не вышло декодировать, то выводим ошибку
print(error)
}
}.resume()
//Созданную задачу ешё необходимо запустить
}
Заметьте, как легко мы получили с сервера объект! В будущем мы научимся не только получать объекты с сервера, но и отправлять их на него.
Мы не создавали ячеек прототипов, а значит и не регистрировали наш класс ячейки в табличном представлении. Однако есть и программный способ сделать это (код добавляется во viewDidLoad
):
tableView.register(CardTableViewCell.self, forCellReuseIdentifier: "Cell")
Данный метод регистрирует класс для ячейки с указанным идентификатором.
Теперь так же зарегистрируем представление для ячейки:
tableView.register(UINib(nibName: "CardTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
И напоследок зададим динамическую высоту ячеек таблицы:
tableView.rowHeight = UITableViewAutomaticDimension
Это позволяет ячейкам иметь разную высоту, а табличному представлению самому высчитывать эти размеры.
В расширении класса реализуйте соответствие протоколам делегата и источника данных табличного представления. Причём число строк сделайте равным количеству карт:
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cards.count
}
}
Последним штрихом будет реализация метода, дающего нам конкретную ячейку:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CardTableViewCell
let card = cards[indexPath.row]
cell.nameLabel.text = card.name
cell.descriptionLabel.text = card.text
do {
cell.cardImageView?.image = try UIImage(data: Data(contentsOf: URL(string: card.imagePath)!))!
} catch {
print(error)
}
return cell
}
Новинкой в данной реализации было то, что мы загрузили картинку по ссылке в набор байт, а затем уже отобразили её. Нам даже уже не нужно хранить картинки на устройстве!
По умолчанию данная программа не будет отображать картинки. Это связано с тем, что они расположены по адресу с протоколом
http
, а неhttps
. Последний предполагает шифрование, а в первом данные передаются в открытом виде. По-умолчанию Apple запрещает Вам использовать доступ без шифрования. Вы можете добавить адреса и разрешить загрузку с определенных или любых доменов черезInfo.plist
. Для этого нужно добавить словарьApp Transport Security Settings
, а в нём значениеAllow Arbitrary Loads
с величинойYES
.Учтите, что не стоит делать такого в настоящих приложениях, если на то нет крайней необходимости - любой участник сети сможет перехватить такие данные. Добавление SSL-сертификата в наши дни занимает считанные минуты и может быть выполнено бесплатно.
Запустите программу и Вы получите примерно следующее:
Помните, как мы добавили стиль шрифта Headline
в наше чудо инженерной мысли?
Оно было нужно для динамического шрифта - приложения, реализующие такой функционал, позволяют пользователю самостоятельно менять размеры шрифта. Чтобы воспользоваться таким функционалом, убейте приложения и перейдите в:
Larger Accessability Sizes