
Во многих приложениях вы можете столкнуться с прокруткой, которая никогда не переносится в противоположном направлении в конце контента. Эта техника стандартна уже в течение многих лет, на многих платформах. С другой стороны, есть много сторонних библиотек, чтобы получить этот эффект. НО вам не нужно никакой сторонней библиотеки. У этой техники очень простая логика.
Постраничная поддержка UIScrollView позволяет пользователю просматривать его содержимое постранично. UIScrollView включает этот эффект, регулируя смещение scrollView, когда пользователь заканчивает перетаскивание. Когда пользователь прокручивает до конца страниц (справа), scrollview ограничивает превышение его содержимого, перемещая его смещение в противоположном направлении с красивой анимацией.

Мы хотим, чтобы 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") } }
Здесь нет ничего необычного. Теперь мы должны добавить код scrollView и 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). Потому что нам не нужно вычислять позицию подкачки при каждом движении 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) } }
И, наконец, наш код будет таким для 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 } }

Итог
Таким образом, мы сделали многократно используемый компонент scrollview с небольшой логикой. Кстати, с огромными объемами данных лучше использовать UICollectionView, т.к он имеет лучшую производительность и лучшее управление памятью, чем UIScrollView. Кроме того, вы можете расширить InfiniteScrollView с помощью параметров синхронизации или двунаправленной прокрутки. С небольшим улучшением, это будет действительно многократно используемый инструмент для ваших приложений.
→ Полный исходный код можно найти на GitHub
