Бесконечный UIScrollView +11


image

Во многих приложениях вы можете столкнуться с прокруткой, которая никогда не переносится в противоположном направлении в конце контента. Эта техника стандартна уже в течение многих лет, на многих платформах. С другой стороны, есть много сторонних библиотек, чтобы получить этот эффект. НО вам не нужно никакой сторонней библиотеки. У этой техники очень простая логика.

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

image

Мы хотим, чтобы scrollview не ограничивал смещение содержимого, когда пользователь захочет превысить их количество. Поэтому нам нужно добавить еще две страницы в UIScrollView. Последняя страница будет добавлена ??в нулевой индекс, а первая страница будет добавлена ??в индекс (numberOfItems + 1). Затем, если пользователь просматривает страницу «numberOfItems», смещение содержимого прокрутки x устанавливается на 0. Если пользователь просматривает индекс 0, тогда смещение содержимого scrollView x будет установлено на «pageSize * numberOfItems».

image

Первое, что надо делать, — это создать новый класс, унаследованный от UIView.

image

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

image

Итог


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

> Полный исходный код можно найти на GitHub




К сожалению, не доступен сервер mySQL