Во многих приложениях вы можете столкнуться с прокруткой, которая никогда не переносится в противоположном направлении в конце контента. Эта техника стандартна уже в течение многих лет, на многих платформах. С другой стороны, есть много сторонних библиотек, чтобы получить этот эффект. НО вам не нужно никакой сторонней библиотеки. У этой техники очень простая логика.
Постраничная поддержка 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