لانهائي UIScrollView

صورة

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

دعم الصفحة يتيح UIScrollView للمستخدم عرض صفحة المحتويات الخاصة به حسب الصفحة. يمكّن UIScrollView هذا التأثير عن طريق ضبط إزاحة scrollView عندما ينتهي المستخدم من السحب. عندما يقوم المستخدم بالتمرير إلى نهاية الصفحات (على اليمين) ، يحد عرض التمرير من فائض محتوياته عن طريق تحريك الإزاحة في الاتجاه المعاكس باستخدام رسوم متحركة جميلة.

صورة

نحن نريد أن لا تحدّ صفحة التمرير من إزاحة المحتوى عندما يريد المستخدم تجاوز عددها. لذلك ، نحتاج إلى إضافة صفحتين أخريين إلى UIScrollView. ستتم إضافة الصفحة الأخيرة إلى فهرس الصفر ، وستتم إضافة الصفحة الأولى إلى الفهرس (numberOfItems + 1). ثم ، إذا شاهد المستخدم صفحة "numberOfItems" ، فسيتم تعيين إزاحة محتوى التمرير x على 0. إذا قام المستخدم بعرض الفهرس 0 ، فسيتم تعيين إزاحة scrollView x على "pageSize * numberOfItems".

صورة

أول شيء فعله هو إنشاء فئة جديدة موروثة من UIView.

صورة

يجب أن يكون BannerView كما يلي:

import UIKit class BannerView: UIView { override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } 

لا يوجد شيء غير عادي هنا. نحتاج الآن إلى إضافة رمز التمرير وعرض setUp لـ BannerView:

 import UIKit class BannerView: UIView { private let scrollView:UIScrollView = { let sc = UIScrollView(frame: .zero) sc.translatesAutoresizingMaskIntoConstraints = false sc.isPagingEnabled = true return sc }() // BannerView DataSources (1) private var itemAtIndex:((_ bannerView:BannerView , _ index:Int)->(UIView))! private var numberOfItems:Int = 0 override init(frame: CGRect) { super.init(frame: frame) setUpUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setUpUI() { scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height) scrollView.delegate = self self.addSubview(scrollView) scrollView.showsHorizontalScrollIndicator = false } func reloadData(numberOfItems:Int , itemAtIndex:@escaping ((_ bannerView:BannerView , _ index:Int)->(UIView)) ) { self.itemAtIndex = itemAtIndex self.numberOfItems = numberOfItems reloadScrollView() } private func reloadScrollView() { guard self.numberOfItems > 0 else { return } if self.numberOfItems == 1 { let firstItem:UIView = self.itemAtIndex(self , 0) addViewToIndex(view: firstItem, index: 0) scrollView.isScrollEnabled = false return } let firstItem:UIView = self.itemAtIndex(self , 0) addViewToIndex(view: firstItem, index: numberOfItems+1) let lastItem:UIView = self.itemAtIndex(self , numberOfItems-1) addViewToIndex(view: lastItem, index: 0) for index in 0..<self.numberOfItems { let item:UIView = self.itemAtIndex(self , index) addViewToIndex(view: item, index: index+1) } scrollView.contentSize = CGSize(width: CGFloat(numberOfItems+2)*scrollView.frame.size.width, height: scrollView.frame.size.height) scrollView.contentOffset = CGPoint(x: self.scrollView.frame.size.width, y: self.scrollView.contentOffset.y) } private func addViewToIndex(view:UIView, index:Int) { view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(view) view.frame = CGRect(x: CGFloat(index)*scrollView.frame.size.width, y: 0, width: scrollView.frame.size.width, height: scrollView.frame.size.height) } } 

لقد استخدمت الإطارات بدلاً من التخطيط التلقائي للبساطة. بالإضافة إلى ذلك ، استخدمت عمليات الإغلاق بدلاً من المندوبين. هذا يساعد على تجنب الأوساخ في ViewController. مع عمليات الإغلاق ، يمكنك ببساطة استخدام bannerView على النحو التالي:

 // ViewController.swift bannerView = BannerView(frame: CGRect(x: 0, y: 64, width: self.view.frame.size.width, height: 200)) self.view.addSubview(bannerView) bannerView.reloadData(numberOfItems: 5) { (bannerView, index) -> (UIView) in let view = UIView() view.backgroundColor = UIColor.red return view } 

لتفويض UIScrollView ، سأستخدم scrollViewDidEndDecelerating (_ scrollView: UIScrollView) بدلاً من scrollViewDidScroll (_ scrollView: UIScrollView). لأننا لسنا بحاجة لحساب موقف المبادلة مع كل حركة عرض التمرير.

 func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let currentPage:Int = Int(scrollView.contentOffset.x / scrollView.frame.size.width) if currentPage == 0 { self.scrollView.contentOffset = CGPoint(x: scrollView.frame.size.width * CGFloat(numberOfItems), y: scrollView.contentOffset.y) } else if currentPage == numberOfItems { self.scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y) } } 

وأخيرًا ، سيكون رمزنا مثل هذا لـ BannerView.swift:

 // BannerView.swift import UIKit class BannerView: UIView , UIScrollViewDelegate{ private let scrollView:UIScrollView = { let sc = UIScrollView(frame: .zero) sc.translatesAutoresizingMaskIntoConstraints = false sc.isPagingEnabled = true return sc }() private var itemAtIndex:((_ bannerView:BannerView , _ index:Int)->(UIView))! private var numberOfItems:Int = 0 override init(frame: CGRect) { super.init(frame: frame) setUpUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func reloadData(configuration:BannerViewConfiguration? , numberOfItems:Int , itemAtIndex:@escaping ((_ bannerView:BannerView , _ index:Int)->(UIView)) ) { self.itemAtIndex = itemAtIndex self.numberOfItems = numberOfItems reloadScrollView() } private func reloadScrollView() { guard self.numberOfItems > 0 else { return } if self.numberOfItems == 1 { let firstItem:UIView = self.itemAtIndex(self , 0) addViewToIndex(view: firstItem, index: 0) scrollView.isScrollEnabled = false return } let firstItem:UIView = self.itemAtIndex(self , 0) addViewToIndex(view: firstItem, index: numberOfItems+1) let lastItem:UIView = self.itemAtIndex(self , numberOfItems-1) addViewToIndex(view: lastItem, index: 0) for index in 0..<self.numberOfItems { let item:UIView = self.itemAtIndex(self , index) addViewToIndex(view: item, index: index+1) } scrollView.contentSize = CGSize(width: CGFloat(numberOfItems+2)*scrollView.frame.size.width, height: scrollView.frame.size.height) scrollView.contentOffset = CGPoint(x: self.scrollView.frame.size.width, y: self.scrollView.contentOffset.y) } private func addViewToIndex(view:UIView, index:Int) { view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(view) view.frame = CGRect(x: CGFloat(index)*scrollView.frame.size.width, y: 0, width: scrollView.frame.size.width, height: scrollView.frame.size.height) } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let currentPage:Int = Int(scrollView.contentOffset.x / scrollView.frame.size.width) if currentPage == 0 { self.scrollView.contentOffset = CGPoint(x: scrollView.frame.size.width * CGFloat(numberOfItems), y: scrollView.contentOffset.y) } else if currentPage == numberOfItems { self.scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y) } } private func setUpUI() { scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height) scrollView.delegate = self self.addSubview(scrollView) scrollView.showsHorizontalScrollIndicator = false } } 

صورة

يؤدي


وبالتالي ، قدمنا ​​مكونًا قابلاً لإعادة الاستخدام يمكن إعادة استخدامه مع القليل من المنطق. بالمناسبة ، مع وجود كميات هائلة من البيانات ، من الأفضل استخدام UICollectionView ، لأنه يحتوي على أداء أفضل وإدارة ذاكرة أفضل من UIScrollView. بالإضافة إلى ذلك ، يمكنك تمديد InfiniteScrollView باستخدام خيارات المزامنة أو التمرير ثنائي الاتجاه. مع تحسن طفيف ، ستكون أداة قابلة لإعادة الاستخدام حقًا لتطبيقاتك.

→ شفرة المصدر الكامل يمكن العثور عليها على جيثب

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


All Articles