Привет, Хабр! Представляю вашему вниманию перевод статьи "UICollectionView Tutorial: Changing presentation on the fly".
В данной статье мы рассмотрим использование различный способов отображение элементов, а также их переиспользование и динамическое изменение. Здесь мы не будем рассматривать основы работы с коллекциями и autolayout.
В результате мы получим такой пример:
При разработке мобильных приложений часто встречаются ситуации, когда табличного представления недостаточно и нужно показывать список элементов более интересно и уникально. Кроме того, возможность изменить способ отображения элементов может стать “фишкой” вашего приложения.
Все вышеперечисленные возможности достаточно просто реализовать при помощи UICollectionView и различных реализаций протокола UICollectionViewDelegateFlowLayout.
Полный код проекта.
Что нам потребуется в первую очередь для реализации:
Ячейку мы создадим в отдельном файле с xib ом для возможности переиспользования.
По дизайну мы видим, что возможны 2 варианта ячейки — с текстом снизу и текстом справа от изображения.
Могут быть абсолютно отличающиеся виды ячеек, в таком случае нужно создавать 2 отдельных класса и использовать нужный. В нашем случае такой необходимости нет и достаточно 1 ячейки с UIStackView.
Шаги создания интерфейса для ячейки:
Это нужно для корректного отображения в горизонтальном режиме.
Далее напишем логику отображения нашей ячейки как в горизонтальном, так и в вертикальном режиме.
Основным критерием отображения по горизонтали будем считать размер самой ячейки. Т.е. если места достаточно — отображаем горизонтальный режим. Если нет — вертикальный. Будем считать что достаточно места — это когда ширина в 2 раза больше высоты, так как изображение должно быть квадратным.
Код ячейки:
Перейдем к основной части — к контроллеру и логике отображения и переключения видов ячеек.
Для всех возможных состояний отображения создадим enum PresentationStyle.
Также добавим кнопку для переключения между состояниями в навигейшн бар.
Все что касается способа отображения элементов в коллекции описано в протоколе UICollectionViewDelegateFlowLayout. Следовательно, чтобы убрать какие либо реализации из контроллера и создать независимые переиспользуемые элементы — создадим под каждый вид отображения отдельную реализацию этого протокола.
Однако, есть 2 нюанса:
По этому, создадим протокол CollectionViewSelectableItemDelegate, расширять стандартный протокол UICollectionViewDelegateFlowLayout, в котором определим замыкание выбора ячейки и в случае необходимости, какие либо дополнительные свойства и методы (например, возвращение типа ячейки, если используются различные типы для представлений). Это решит первую проблему.
Для решения второй проблемы — с дублированием логики, создадим базовый класс со всей общей логикой:
В нашем случае общей логикой является вызов замыкания при выборе ячейки, а также изменение фона ячейки при переходе в состояние highlighted.
Далее опишем 3 реализации представлений: табличная, по 3 элемента в каждом ряде и комбинация первых двух способов.
Табличная:
3 элемента в каждом ряде:
Комбинация табличной и 3х в ряд.
Последний этап — это добавить данные представления в контроллер и устанавливать коллекции нужный делегат.
Важный момент: так как делегат коллекции weak, то необходимо иметь strong ссылку в контроллере на объект представления.
Создадим в контроллере словарь всех доступных представлений относительно типа:
И в метод updatePresentationStyle() добавим анимированное изменение делегат коллекции:
Вот и все что необходимо, чтобы наши элементы анимировано переходили от одного вида к другому :)
Таким образом, мы теперь можем на любом экране отображать элементы каким угодно способом, динамически переключаться между отображениями и самое главное — код получился независимый, переиспользуемый и масштабируемый.
Полный код проекта.
В данной статье мы рассмотрим использование различный способов отображение элементов, а также их переиспользование и динамическое изменение. Здесь мы не будем рассматривать основы работы с коллекциями и autolayout.
В результате мы получим такой пример:
При разработке мобильных приложений часто встречаются ситуации, когда табличного представления недостаточно и нужно показывать список элементов более интересно и уникально. Кроме того, возможность изменить способ отображения элементов может стать “фишкой” вашего приложения.
Все вышеперечисленные возможности достаточно просто реализовать при помощи UICollectionView и различных реализаций протокола UICollectionViewDelegateFlowLayout.
Полный код проекта.
Что нам потребуется в первую очередь для реализации:
- class FruitsViewController: UICollectionViewController.
- Модель данных Fruit
struct Fruit { let name: String let icon: UIImage }
- class FruitCollectionViewCell: UICollectionViewCell
Ячейка с UIImageView и UILabel для отображения фруктов
Ячейку мы создадим в отдельном файле с xib ом для возможности переиспользования.
По дизайну мы видим, что возможны 2 варианта ячейки — с текстом снизу и текстом справа от изображения.
Могут быть абсолютно отличающиеся виды ячеек, в таком случае нужно создавать 2 отдельных класса и использовать нужный. В нашем случае такой необходимости нет и достаточно 1 ячейки с UIStackView.
Шаги создания интерфейса для ячейки:
- Добавляем UIView
- Внутрь нее добавляем UIStackView (horizontal)
- Далее добавляем UIImageView и UILabel в UIStackView.
- Для UILabel устанавливаем значения Content Compression Resistance Priority = 1000 для горизонтали и для вертикали.
- Добавляем для UIImageView Aspect Ratio 1:1 и изменяем приоритет на 750.
Это нужно для корректного отображения в горизонтальном режиме.
Далее напишем логику отображения нашей ячейки как в горизонтальном, так и в вертикальном режиме.
Основным критерием отображения по горизонтали будем считать размер самой ячейки. Т.е. если места достаточно — отображаем горизонтальный режим. Если нет — вертикальный. Будем считать что достаточно места — это когда ширина в 2 раза больше высоты, так как изображение должно быть квадратным.
Код ячейки:
class FruitCollectionViewCell: UICollectionViewCell {
static let reuseID = String(describing: FruitCollectionViewCell.self)
static let nib = UINib(nibName: String(describing: FruitCollectionViewCell.self), bundle: nil)
@IBOutlet private weak var stackView: UIStackView!
@IBOutlet private weak var ibImageView: UIImageView!
@IBOutlet private weak var ibLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = .white
clipsToBounds = true
layer.cornerRadius = 4
ibLabel.font = UIFont.systemFont(ofSize: 18)
}
override func layoutSubviews() {
super.layoutSubviews()
updateContentStyle()
}
func update(title: String, image: UIImage) {
ibImageView.image = image
ibLabel.text = title
}
private func updateContentStyle() {
let isHorizontalStyle = bounds.width > 2 * bounds.height
let oldAxis = stackView.axis
let newAxis: NSLayoutConstraint.Axis = isHorizontalStyle ? .horizontal : .vertical
guard oldAxis != newAxis else { return }
stackView.axis = newAxis
stackView.spacing = isHorizontalStyle ? 16 : 4
ibLabel.textAlignment = isHorizontalStyle ? .left : .center
let fontTransform: CGAffineTransform = isHorizontalStyle ? .identity : CGAffineTransform(scaleX: 0.8, y: 0.8)
UIView.animate(withDuration: 0.3) {
self.ibLabel.transform = fontTransform
self.layoutIfNeeded()
}
}
}
Перейдем к основной части — к контроллеру и логике отображения и переключения видов ячеек.
Для всех возможных состояний отображения создадим enum PresentationStyle.
Также добавим кнопку для переключения между состояниями в навигейшн бар.
class FruitsViewController: UICollectionViewController {
private enum PresentationStyle: String, CaseIterable {
case table
case defaultGrid
case customGrid
var buttonImage: UIImage {
switch self {
case .table: return imageLiteral(resourceName: "table")
case .defaultGrid: return imageLiteral(resourceName: "default_grid")
case .customGrid: return imageLiteral(resourceName: "custom_grid")
}
}
}
private var selectedStyle: PresentationStyle = .table {
didSet { updatePresentationStyle() }
}
private var datasource: [Fruit] = FruitsProvider.get()
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.register(FruitCollectionViewCell.nib,
forCellWithReuseIdentifier: FruitCollectionViewCell.reuseID)
collectionView.contentInset = .zero
updatePresentationStyle()
navigationItem.rightBarButtonItem = UIBarButtonItem(image: selectedStyle.buttonImage, style: .plain, target: self, action: #selector(changeContentLayout))
}
private func updatePresentationStyle() {
navigationItem.rightBarButtonItem?.image = selectedStyle.buttonImage
}
@objc private func changeContentLayout() {
let allCases = PresentationStyle.allCases
guard let index = allCases.firstIndex(of: selectedStyle) else { return }
let nextIndex = (index + 1) % allCases.count
selectedStyle = allCases[nextIndex]
}
}
// MARK: UICollectionViewDataSource & UICollectionViewDelegate
extension FruitsViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datasource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FruitCollectionViewCell.reuseID,
for: indexPath) as? FruitCollectionViewCell else {
fatalError("Wrong cell")
}
let fruit = datasource[indexPath.item]
cell.update(title: fruit.name, image: fruit.icon)
return cell
}
}
Все что касается способа отображения элементов в коллекции описано в протоколе UICollectionViewDelegateFlowLayout. Следовательно, чтобы убрать какие либо реализации из контроллера и создать независимые переиспользуемые элементы — создадим под каждый вид отображения отдельную реализацию этого протокола.
Однако, есть 2 нюанса:
- В этом протоколе также описан метод выбора ячейки (didSelectItemAt:)
- Некоторые методы и логика одинаковы для всех N методов отображения (в нашем случаем N = 3).
По этому, создадим протокол CollectionViewSelectableItemDelegate, расширять стандартный протокол UICollectionViewDelegateFlowLayout, в котором определим замыкание выбора ячейки и в случае необходимости, какие либо дополнительные свойства и методы (например, возвращение типа ячейки, если используются различные типы для представлений). Это решит первую проблему.
protocol CollectionViewSelectableItemDelegate: class, UICollectionViewDelegateFlowLayout {
var didSelectItem: ((_ indexPath: IndexPath) -> Void)? { get set }
}
Для решения второй проблемы — с дублированием логики, создадим базовый класс со всей общей логикой:
class DefaultCollectionViewDelegate: NSObject, CollectionViewSelectableItemDelegate {
var didSelectItem: ((_ indexPath: IndexPath) -> Void)?
let sectionInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 20.0, right: 16.0)
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
didSelectItem?(indexPath)
}
func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.clear
}
func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.white
}
}
В нашем случае общей логикой является вызов замыкания при выборе ячейки, а также изменение фона ячейки при переходе в состояние highlighted.
Далее опишем 3 реализации представлений: табличная, по 3 элемента в каждом ряде и комбинация первых двух способов.
Табличная:
class TabledContentCollectionViewDelegate: DefaultCollectionViewDelegate {
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let paddingSpace = sectionInsets.left + sectionInsets.right
let widthPerItem = collectionView.bounds.width - paddingSpace
return CGSize(width: widthPerItem, height: 112)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 10
}
}
3 элемента в каждом ряде:
class DefaultGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate {
private let itemsPerRow: CGFloat = 3
private let minimumItemSpacing: CGFloat = 8
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1)
let availableWidth = collectionView.bounds.width - paddingSpace
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return minimumItemSpacing
}
}
Комбинация табличной и 3х в ряд.
class CustomGriddedContentCollectionViewDelegate: DefaultCollectionViewDelegate {
private let itemsPerRow: CGFloat = 3
private let minimumItemSpacing: CGFloat = 8
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemSize: CGSize
if indexPath.item % 4 == 0 {
let itemWidth = collectionView.bounds.width - (sectionInsets.left + sectionInsets.right)
itemSize = CGSize(width: itemWidth, height: 112)
} else {
let paddingSpace = sectionInsets.left + sectionInsets.right + minimumItemSpacing * (itemsPerRow - 1)
let availableWidth = collectionView.bounds.width - paddingSpace
let widthPerItem = availableWidth / itemsPerRow
itemSize = CGSize(width: widthPerItem, height: widthPerItem)
}
return itemSize
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return sectionInsets
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return minimumItemSpacing
}
}
Последний этап — это добавить данные представления в контроллер и устанавливать коллекции нужный делегат.
Важный момент: так как делегат коллекции weak, то необходимо иметь strong ссылку в контроллере на объект представления.
Создадим в контроллере словарь всех доступных представлений относительно типа:
private var styleDelegates: [PresentationStyle: CollectionViewSelectableItemDelegate] = {
let result: [PresentationStyle: CollectionViewSelectableItemDelegate] = [
.table: TabledContentCollectionViewDelegate(),
.defaultGrid: DefaultGriddedContentCollectionViewDelegate(),
.customGrid: CustomGriddedContentCollectionViewDelegate(),
]
result.values.forEach {
$0.didSelectItem = { _ in
print("Item selected")
}
}
return result
}()
И в метод updatePresentationStyle() добавим анимированное изменение делегат коллекции:
collectionView.delegate = styleDelegates[selectedStyle]
collectionView.performBatchUpdates({
collectionView.reloadData()
}, completion: nil)
Вот и все что необходимо, чтобы наши элементы анимировано переходили от одного вида к другому :)
Таким образом, мы теперь можем на любом экране отображать элементы каким угодно способом, динамически переключаться между отображениями и самое главное — код получился независимый, переиспользуемый и масштабируемый.
Полный код проекта.