UICollectionView حول الرأس: تغيير وجهات النظر على الطاير

مرحبا يا هبر! أقدم لكم ترجمة المقال " UICollectionView Tutorial: Changing presentation on the fly ".

في هذه المقالة ، سننظر في استخدام طرق مختلفة لعرض العناصر ، وكذلك إعادة استخدامها والتغيير الديناميكي. هنا لن نناقش أساسيات العمل مع المجموعات و autolayout.

نتيجة لذلك ، حصلنا على مثال:


عند تطوير تطبيقات الهاتف المحمول ، غالبًا ما تكون هناك مواقف عندما يكون عرض الجدول غير كافٍ وتحتاج إلى إظهار قائمة بالعناصر الأكثر إثارة للاهتمام وفريدة من نوعها. بالإضافة إلى ذلك ، يمكن أن تصبح القدرة على تغيير طريقة عرض العناصر "شريحة" في التطبيق الخاص بك.

جميع الميزات المذكورة أعلاه هي بسيطة للغاية لتنفيذ باستخدام UICollectionView والتطبيقات المختلفة لبروتوكول UICollectionViewDelegateFlowLayout.

رمز المشروع الكامل.

ما نحتاج إليه أولاً وقبل كل شيء للتنفيذ:

  • الفئة FruitsViewController: UICollectionViewController.
  • نموذج بيانات الفاكهة

    struct Fruit { let name: String let icon: UIImage } 
  • فئة FruitCollectionViewCell: UICollectionViewCell

خلية مع UIImageView و UILabel لعرض الفواكه


سنقوم بإنشاء الخلية في ملف منفصل مع xib لإعادة استخدامها.

حسب التصميم ، نرى أن هناك خيارين ممكنين للخلية - مع النص أدناه والنص على يمين الصورة.



قد يكون هناك أنواع مختلفة تمامًا من الخلايا ، وفي هذه الحالة تحتاج إلى إنشاء فئتين منفصلتين واستخدام الفئة المطلوبة. في حالتنا ، لا توجد مثل هذه الحاجة وخلية واحدة مع UIStackView كافية.



خطوات إنشاء واجهة لخلية:

  1. أضف UIView
  2. بداخله أضف UIStackView (أفقي)
  3. بعد ذلك ، أضف UIImageView و UILabel إلى UIStackView.
  4. بالنسبة إلى UILabel ، عيّن قيمة أولوية مقاومة ضغط المحتوى = 1000 للأفقي والرأسي.
  5. أضف 1: 1 لنسبة العرض إلى الارتفاع في UIImageView وقم بتغيير الأولوية إلى 750.

هذا ضروري للعرض الصحيح في الوضع الأفقي.

بعد ذلك ، نكتب المنطق لعرض خليتنا في الوضع الأفقي والرأسي.

سيكون المعيار الرئيسي للعرض الأفقي هو حجم الخلية نفسها. أي إذا كان هناك مساحة كافية - عرض الوضع الأفقي. إن لم يكن ، العمودي. نفترض أن المساحة الكافية تكون عندما يكون العرض أكبر مرتين من الارتفاع ، لأن الصورة يجب أن تكون مربعة.

كود الخلية:

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

دعنا ننتقل إلى الجزء الرئيسي - إلى وحدة التحكم والمنطق لعرض أنواع الخلايا وتبديلها.

بالنسبة لجميع حالات العرض الممكنة ، قم بإنشاء التعداد 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 الفروق الدقيقة:

  1. يصف هذا البروتوكول أيضًا طريقة تحديد الخلية (didSelectItemAt :)
  2. بعض الطرق والمنطق هي نفسها لجميع أساليب رسم الخرائط 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 } } 

في حالتنا ، فإن المنطق العام هو الدعوة للإغلاق عند اختيار خلية ، وكذلك تغيير خلفية الخلية عند التبديل إلى الحالة المميزة.

بعد ذلك ، نقوم بوصف 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 x على التوالي.

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

الخطوة الأخيرة هي إضافة بيانات العرض إلى وحدة التحكم وتعيين المفوض المطلوب إلى المجموعة.

نقطة مهمة: نظرًا لأن مفوض المجموعة ضعيف ، يجب أن يكون لديك رابط قوي في وحدة التحكم لكائن العرض.

في وحدة التحكم ، قم بإنشاء قاموس لجميع طرق العرض المتاحة فيما يتعلق بالنوع:

 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) 

هذا كل ما يلزم لعناصرنا للانتقال بشكل متحرك من عرض إلى آخر :)


وبالتالي ، يمكننا الآن عرض العناصر على أي شاشة بالطريقة التي نحبها ، والتبديل بشكل حيوي بين شاشات العرض ، والأهم من ذلك ، أن الكود مستقل وقابل لإعادة الاستخدام وقابل للتوسعة.

رمز المشروع الكامل.

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


All Articles