UICollectionView Around the Head: Mengubah Views on the Fly

Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel " Tutorial UICollectionView: Mengubah presentasi dengan cepat ".

Dalam artikel ini, kami akan mempertimbangkan penggunaan berbagai cara menampilkan elemen, serta penggunaan kembali dan perubahan dinamisnya. Di sini kita tidak akan membahas dasar-dasar bekerja dengan koleksi dan pembayaran otomatis.

Hasilnya, kami mendapat contoh:


Saat mengembangkan aplikasi seluler, sering ada situasi ketika tampilan tabel tidak cukup dan Anda perlu menunjukkan daftar elemen yang lebih menarik dan unik. Selain itu, kemampuan untuk mengubah cara elemen ditampilkan dapat menjadi "chip" dalam aplikasi Anda.

Semua fitur di atas cukup sederhana untuk diimplementasikan menggunakan UICollectionView dan berbagai implementasi protokol UICollectionViewDelegateFlowLayout.

Kode proyek lengkap.

Yang kita butuhkan pertama-tama untuk implementasi:

  • kelas FruitsViewController: UICollectionViewController.
  • Model Data Buah

    struct Fruit { let name: String let icon: UIImage } 
  • kelas FruitCollectionViewCell: UICollectionViewCell

Sel dengan UIImageView dan UILabel untuk menampilkan buah


Kami akan membuat sel dalam file terpisah dengan xib untuk digunakan kembali.

Secara desain, kita melihat bahwa ada 2 opsi sel yang mungkin - dengan teks di bawah ini dan teks di sebelah kanan gambar.



Mungkin ada jenis sel yang sama sekali berbeda, dalam hal ini Anda perlu membuat 2 kelas terpisah dan menggunakan yang diinginkan. Dalam kasus kami, tidak ada kebutuhan seperti itu dan 1 sel dengan UIStackView sudah cukup.



Langkah-langkah untuk membuat antarmuka untuk sel:

  1. Tambahkan UIView
  2. Di dalamnya tambahkan UIStackView (horizontal)
  3. Selanjutnya, tambahkan UIImageView dan UILabel ke UIStackView.
  4. Untuk UILabel, tetapkan nilai Prioritas Ketahanan Kompresi Konten = 1000 untuk horizontal dan vertikal.
  5. Tambahkan 1: 1 untuk Rasio Aspek UIImageView dan ubah prioritas menjadi 750.

Ini diperlukan untuk tampilan yang benar dalam mode horizontal.

Selanjutnya, kami menulis logika untuk menampilkan sel kami dalam mode horizontal dan vertikal.

Kriteria utama untuk tampilan horizontal adalah ukuran sel itu sendiri. Yaitu jika ada cukup ruang - tampilkan mode horizontal. Jika tidak, vertikal. Kami berasumsi bahwa ruang yang cukup adalah ketika lebarnya 2 kali lebih besar dari ketinggian, karena gambar harus persegi.

Kode Sel:

 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() } } } 

Mari kita beralih ke bagian utama - ke controller dan logika untuk menampilkan dan mengganti tipe sel.

Untuk semua status tampilan yang memungkinkan, buat enum PresentationStyle.
Kami juga menambahkan tombol untuk beralih antar status di bilah navigasi.

 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 } } 

Segala sesuatu tentang metode menampilkan elemen dalam koleksi dijelaskan dalam protokol UICollectionViewDelegateFlowLayout. Oleh karena itu, untuk menghapus implementasi apa pun dari pengontrol dan membuat elemen yang dapat digunakan kembali secara independen, kami akan membuat implementasi terpisah dari protokol ini untuk setiap jenis tampilan.

Namun, ada 2 nuansa:

  1. Protokol ini juga menjelaskan metode pemilihan sel (didSelectItemAt :)
  2. Beberapa metode dan logika adalah sama untuk semua metode pemetaan N (dalam kasus kami, N = 3).

Oleh karena itu, kami akan membuat protokol CollectionViewSelectableItemDelegate , memperluas protokol UICollectionViewDelegateFlowLayout standar, di mana kami menentukan penutupan pemilihan sel dan, jika perlu, setiap properti dan metode tambahan (misalnya, mengembalikan tipe sel jika jenis yang berbeda digunakan untuk representasi). Ini akan menyelesaikan masalah pertama.

 protocol CollectionViewSelectableItemDelegate: class, UICollectionViewDelegateFlowLayout { var didSelectItem: ((_ indexPath: IndexPath) -> Void)? { get set } } 

Untuk mengatasi masalah kedua - dengan duplikasi logika, kita akan membuat kelas dasar dengan semua logika umum:

 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 } } 

Dalam kasus kami, logika umum adalah memanggil penutupan saat memilih sel, serta mengubah latar belakang sel saat beralih ke keadaan yang disorot .

Selanjutnya, kami menjelaskan 3 implementasi dari representasi: tabular, 3 elemen di setiap baris dan kombinasi dari dua metode pertama.

Tabel :

 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 elemen di setiap baris:

 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 } } 

Kombinasi tabel dan 3x berturut-turut.

 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 } } 

Langkah terakhir adalah menambahkan data tampilan ke controller dan mengatur koleksi delegasi yang diinginkan.

Poin penting: karena delegasi koleksi lemah , Anda harus memiliki tautan yang kuat di controller ke objek tampilan.

Di controller, buat kamus dari semua tampilan yang tersedia mengenai jenis:

 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 }() 

Dan dalam metode updatePresentationStyle () , tambahkan perubahan animasi ke delegasi koleksi:

  collectionView.delegate = styleDelegates[selectedStyle] collectionView.performBatchUpdates({ collectionView.reloadData() }, completion: nil) 

Hanya itu yang diperlukan agar elemen kita dapat bergerak dengan animasi dari satu tampilan ke tampilan lain :)


Dengan demikian, kita sekarang dapat menampilkan elemen pada layar apa pun dengan cara apa pun yang kita suka, secara dinamis beralih di antara layar, dan yang paling penting, kode ini independen, dapat digunakan kembali, dan dapat diskalakan.

Kode proyek lengkap.

Source: https://habr.com/ru/post/id445708/


All Articles