Только после победы соперник будет уязвим для атаки
Мы очень кратко коснулись их при разработке приложения для macOS. Теперь же мы расширим наш опыт, создав приложение с любыми местами отдыха наших милых дам - владельцев!
View Controller
- главный контроллер представленияResources
- сторибоарды приложения и бандл ассетовCells
- в которой мы будем создавать ячейкиData
- где будем хранить модели данных. Сюда перенесите файл places.plist
. Он содержит заранее подготовленные данные про девочек с их любимыми местами.Collection View Controller
- он содержит несколько полезных нам вещей, что сократит сложность разработки в разы.ViewController
на UICollectionViewController
Создадим модель данных для хранения мест:
name
imageName
Codable
class Place: Codable {
let name: String
let imageName: String
init(name: String, imageName: String) {
self.name = name
self.imageName = imageName
}
}
На его основе теперь мы можем создать модель девочки Girl
:
name
places
в виде опционального массиваclass Girl: Codable {
let name: String
var places: [Place]?
init(name: String) {
self.name = name
}
}
Теперь мы создадим источник данных, который будет отвечать за их извлечение из plist
-файла.
Обратите внимание, что древо файлов целиком соответствует структуре классов. Это даст нам возможность извлечь объекты с помощью обычного метода декодинга.
Начнем наше творчество:
class DataSource {
private init() {
}
static let shared = DataSource()
}
Особенность того, что мы сделали - у класса единственный приватный конструктор, а значит его объекты не могут быть созданы ниоткуда, кроме из него самого. А так же у него есть член shared
, который даёт доступ к единственному экземпляру класса. Вот мы и реализовали дизайн-паттерн синглтон.
Теперь создадим свойство для извлечения массива девочек:
var girls: [Girl]? {
}
Сперва мы получим ссылку на файл с ними, иначе вернём ничто:
guard let url = Bundle.main.url(forResource: "places", withExtension: "plist") else {
return nil
}
Теперь попробуем извлечь байт-данные из него:
guard let data = try? Data(contentsOf: url) else {
return nil
}
И наконец выполним декодинг и вернём результат:
return try? PropertyListDecoder().decode([Girl].self, from: data)
Min Spacing
, чтобы убрать минимальный отступ для ячеекУчтите, что финальный размер ячеек будет отличаться. Здесь он нужен лишь для создания дизайна
UIView
c нулевыми отступами от границAspect Fill
Clip To Bounds
Title
8
точек8
точек3
Cell
CollectionViewCell
, унаследовав его от UICollectionViewCell
и разместив в группе Cells
.imageView
titleLabel
Теперь мы добавим небольшой код, который позволит ячейке принимать в себя место, а на его основе инициализировать себя:
var place: Place? {
didSet {
if let place = place {
titleLabel.text = place.name
imageView.image = UIImage(named: place.imageName)!
}
}
}
По-моему просто чудесно. А Вы ещё не купили нашу подписку? Спешите успеть по скидке!
Добавим сюда последний метод для ячейки, который вызывается системой реиспользования и очищает её от лишнего:
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil
imageView.image = nil
}
В данном приложении Вы не заметите отличий от его использования или неиспользования, но запомните его на будущее - правильным будет очищать любое переиспользуемое представление, чтобы им можно было воспользоваться новь.
Так как мы использовать контроллер коллекции, то он уже реализует методы источники и делегата. Осталось перезаписать их.
Сперва добавим свойство контроллера, в которое будем извлекать девочек:
var girls = DataSource.shared.girls!
Так как наше представление будет делить любимые места девочек по ним самим, то у нас будет столько же секций, сколько девочек:
extension ViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return girls.count
}
}
А в каждой секции будет столько вещей, сколько там мест:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return girls[section].places?.count ?? 0
}
Так как любимых мест может не быть, то мы можем получить ничего, аз значит вернуть 0.
Схоже ведь с табличными представлениями, isn't it?
И похожим методом создаётся ячейка:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
//Сперва она извлекается из очереди
cell.place = girls[indexPath.section].places![indexPath.item]
//Мы передаём в неё место и возвращаем
return cell
}
Здесь интерес для нас также имеет тот факт, что мы получили не строку, а вещь из индекс-пути. Мы могли обратиться и к строке - она будет иметь то же значение, но для представлений-коллекций это немного неуместно.
Запустите приложение и Вы получите чёрный экран:
Дело в том, что представление коллекции не знает, как ему быть с ячейками - в отличие от табличного представления, представление-коллекция чуточку глупее и ему нужен специальный объект разметки - layout
. По-умолчанию выбран Flow Layout
. Который решает большую часть задач, но ему нужно немного нашей помощи, чтобы узнать размеры ячеек. Добавьте во viewDidLoad
:
let width = view.frame.width / 3
//Получили ширину кадра
let layout = collectionViewLayout as! UICollectionViewFlowLayout
//Извлекли layout и сделали его нужного типа
layout.itemSize = CGSize(width: width, height: width)
//Задали размер ячейки
Запустите приложение и Вы увидите -фоточки азиаток- любимые места наших милых дам!
Всё здорово, но текст слишком сливается с фоном, существует графический эффект, когда на часть объекта накладывается прозрачный градиент, получающий на конце часть белого цвета.
Создадим сам класс, унаследовав его от простого представления:
class GradientView: UIView {
}
Создадим в нём ленивое свойство градиентного уровня:
lazy fileprivate var gradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [#colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor, #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.75).cgColor]
//Цвета в градиенте
layer.locations = [0, 1]
//Точки для цветов градиента
return layer
}()
cgColor
, так как этот массив принимает любой объект, но работает только с цветами Core Graphics
Помимо задания промежуточных позиций можно создать линейный градиент через начальное и конечное значения
Сделайте так, чтобы при пронуждении из файла интерфейса:
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0)
layer.addSublayer(gradientLayer)
}
Теперь реализуем метод, который при изменении подвидов будет менять и размер слоя:
override func layoutSubviews() {
super.layoutSubviews()
CATransaction.begin()
//Начинаем анимацию
CATransaction.setDisableActions(true)
//Отключаем действия
gradientLayer.frame = bounds
//Приравниваем подслой к границам
CATransaction.commit()
//Подтверждаем анимацию
}
Нечто новенькое для нас - транзакция Core Animation
- на самом деле неявно они срабатывают постоянно при работе программы. В данном случае это работает как блок - все действия по анимациями между вызовом begin
и commit
накапливаются, а затем выполняются вместе.
Теперь доработаем наш холст:
check-circle-blank
22
точки8
точек8
точекUIView
с классом градиентного представления в ячейку таким образом, чтобы её отступы от границ кроме верхней были равны 0
, а между верхней его стороной и верхней стороной стэка было -8
точек.Вы можете создать ограничение на совпадение верхних сторон и затем изменить константу.
Вот и результат.
Теперь мы создадим секции, что позволит разделить любимые места по девочкам явно:
Section Headers
- это создаст нам заголовки секцийCollection Reusable View
Этот объект позволит нам создать наши заголовки. В табличных представлениях мы могли воспользоваться готовой системой, здесь придётся слегка замарать свои -крабовые клешни- лапки.
Задайте этому представлению цвет #EB6046
Поместите в это представление:
34
точкиAspect Fill
Clip To Bounds
Name
:
System Semibold 16
Count
:
System Light 14
Объедините их все в стэк:
Fill
Spacing
- 8
точек8
Теперь нам нужно решить проблемы сплющивания контента - задайте у Count
в инспекторе атрибутов Content Hugging Priority
в 252
.
Что же это за свойство? У объектов, у которых оно выше, стэк старается сохранить их Intrinsic Size
или же действительные размеры. У картинки ширина фиксированная, задав больший приоритет сохранности метки для числа, мы позволяем имени расширяться.
Теперь создадим новый класс, который будет отвечать за это представление:
import UIKit
class SectionHeaderView: UICollectionReusableView {
}
Установите его классом для нашего реиспользуемого заголовка и задайте ему идентификатор SectionHeader
.
Теперь создайте следующие аутлеты:
imageView
nameLabel
countLabel
А теперь по аналогии с ячейкой создайте хранимое свойство:
var girl: Girl? {
didSet {
if let girl = girl {
imageView.image = UIImage(named: girl.name)
nameLabel.text = girl.name
countLabel.text = String(girl.places?.count ?? 0)
}
}
}
Помните, что хорошим тоном будет создать метод очищения для реиспользуемого вида.
Теперь добавим в контроллер последний метод:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "SectionHeader", for: indexPath) as! SectionHeaderView
//Извлекаем реиспользуемое представление
view.girl = girls[indexPath.section]
//Добавлям в секцию девочку
return view
}
Этот метод очень схож с методом получения ячейки, отличие лишь в том, что извлекаются не основные, а вспомогательные виды.
Запустите программу:
Последним штрихом добавим прикрепление заголовка секции к экрану, пока идёт эта секция. Очень похоже на приложение Фото
.
Для этого во viewDidLoad
добавьте следующую строку:
layout.sectionHeadersPinToVisibleBounds = true
Давайте добавим всякие возможности для коллекции. Начнем мы с добавления контроллера навигации:
Bar Tint
) устнаовите в #EB6046Info.plist
сделайте статус-бар светлымSwift World Places
Tint
-цвет задайте в белыйВы получите следующее:
Прежде, чем двигаться дальше, попробуйте реализовать протокол NSCopying
для класса Place
:
extension Place: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return Place(name: name, imageName: imageName)
}
}
Также измените квалификатор имени с константы на переменную.
В правую часть добавьте Bar Button Item
с системным видом Add
.
Теперь создайте от неё действие addItem
:
Новое место
@IBAction func addItem(_ sender: Any?) {
let randomGirlNumber = GKRandomDistribution(lowestValue: 0, highestValue: 2).nextInt()
let girl = girls[randomGirlNumber]
let place = girl.places![GKRandomDistribution(lowestValue: 0, highestValue: girl.places!.count - 1).nextInt()].copy() as! Place
place.name = "Новое место"
girl.places?.append(place)
collectionView?.insertItems(at: [IndexPath(item: girl.places!.count - 1, section: randomGirlNumber)])
}
Теперь можно добавлять случайные новые места: