Infinite UIScrollView

image

Dans de nombreuses applications, vous pouvez rencontrer un défilement qui ne s'enroule jamais dans la direction opposée à la fin du contenu. Cette technique est standard depuis de nombreuses années sur de nombreuses plateformes. D'un autre côté, il existe de nombreuses bibliothèques tierces pour obtenir cet effet. MAIS vous n'avez besoin d'aucune bibliothèque tierce. Cette technique a une logique très simple.

Prise en charge des pages UIScrollView permet à l'utilisateur de visualiser son contenu page par page. UIScrollView active cet effet en ajustant le décalage scrollView lorsque l'utilisateur a terminé de faire glisser. Lorsque l'utilisateur fait défiler jusqu'à la fin des pages (à droite), scrollview limite l'excès de son contenu en déplaçant son décalage dans la direction opposée avec une belle animation.

image

Nous voulons que scrollview ne limite pas l'offset de contenu lorsque l'utilisateur veut dépasser son nombre. Par conséquent, nous devons ajouter deux pages supplémentaires à UIScrollView. La dernière page sera ajoutée à l'index zéro et la première page sera ajoutée à l'index (numberOfItems + 1). Ensuite, si l'utilisateur affiche la page «numberOfItems», le décalage du contenu x offset est défini sur 0. Si l'utilisateur affiche l'index 0, alors le décalage du contenu scrollView x sera défini sur «pageSize * numberOfItems».

image

La première chose à faire est de créer une nouvelle classe héritée d'UIView.

image

BannerView devrait être comme ci-dessous:

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

Il n'y a rien d'inhabituel ici. Maintenant, nous devons ajouter le code scrollView et setUp pour 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) } } 

J'ai utilisé des cadres au lieu de la mise en page automatique pour plus de simplicité. De plus, j'ai utilisé des fermetures au lieu de délégués. Cela permet d'éviter la saleté dans le ViewController. Avec les fermetures, vous pouvez simplement utiliser bannerView comme suit:

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

Pour déléguer UIScrollView, j'utiliserai scrollViewDidEndDecelerating (_ scrollView: UIScrollView) au lieu de scrollViewDidScroll (_ scrollView: UIScrollView). Parce que nous n'avons pas besoin de calculer la position de swap à chaque mouvement scrollView.

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

Et enfin, notre code sera comme ceci pour 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 } } 

image

Résumé


Ainsi, nous avons créé un composant scrollview réutilisable avec un peu de logique. Soit dit en passant, avec d'énormes quantités de données, il est préférable d'utiliser UICollectionView, car il a de meilleures performances et une meilleure gestion de la mémoire que UIScrollView. De plus, vous pouvez étendre InfiniteScrollView à l'aide des options de synchronisation ou du défilement bidirectionnel. Avec une petite amélioration, ce sera un outil vraiment réutilisable pour vos applications.

→ Le code source complet peut être trouvé sur GitHub

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


All Articles