Skip to content

Instantly share code, notes, and snippets.

@chanonly123
Last active September 9, 2022 21:26
Show Gist options
  • Save chanonly123/fd58b04b73e930fd0feef97d3aa732fe to your computer and use it in GitHub Desktop.
Save chanonly123/fd58b04b73e930fd0feef97d3aa732fe to your computer and use it in GitHub Desktop.

Efficient Scroll View (Vertical/Horizontal) for tik-tok like scrolling

It is a custom UIScrollView where subviews have same size as UIScrollView and only 3 subview will be created and reused efficiently.

Usage

// to reload data source
reloadAllData() 

// data source 
extension  ConnectVC: EfficientScrollViewDataSource {
    func numberOfPages() -> Int {
        return  self.podcast.liveAudiences?.count ?? 0
    }

    func onBind(view: UIView?, index: Int) -> UIView {
	 let holder: JoinReqCardView = view as? JoinReqCardView ?? JoinReqCardView.loadNib()

	 holder.podcast = podcast
	 holder.req = self.podcast.liveAudiences?[index]
	 holder.parentViewc = self
	 holder.relaodViews()
	 return holder
    }
}

Sample ViewController

let viewc = EfficientScrollViewVC()
self.show(viewc, sender: nil)
import UIKit
protocol EfficientScrollViewDataSource: class {
func numberOfPages() -> Int
func onBind(view: UIView?, index: Int) -> UIView
}
class EfficientScrollView: UIScrollView {
weak var dataSource: EfficientScrollViewDataSource?
private var pages: [UIView] = []
private var totalItemCount: Int = 0
var onPageChanged: ((Int, UIView?) -> Void)?
var currentPage: Int = -1
var vertical = true
override func didMoveToSuperview() {
super.didMoveToSuperview()
if superview == nil {
removeObserver(self, forKeyPath: "contentOffset", context: nil)
} else {
addObserver(self, forKeyPath: "contentOffset", options: [NSKeyValueObservingOptions.new], context: nil)
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
var page = 0
if vertical {
page = height == 0 ? 0 : Int(round(contentOffset.y / height))
} else {
page = width == 0 ? 0 : Int(round(contentOffset.x / width))
}
if page != currentPage {
currentPage = page
load(page: page)
load(page: page - 1)
load(page: page + 1)
onPageChanged?(currentPage, pages.first(where: { $0.tag == page }))
}
}
private func load(page: Int, force: Bool = false) {
if page < 0 || page >= totalItemCount { return }
if let view = pages.first(where: { $0.tag == page }) {
if force {
if let v = dataSource?.onBind(view: view, index: page) {
if v.superview == nil { addSubview(v) }
}
}
} else {
if let view = pages.first(where: { $0.tag < currentPage - 1 || $0.tag > currentPage + 1 }) {
view.tag = page
if let v = dataSource?.onBind(view: view, index: page) {
if v.superview == nil { addSubview(v) }
}
} else {
if let view = dataSource?.onBind(view: nil, index: page) {
view.tag = page
addSubview(view)
pages.append(view)
}
}
}
//print("pages size: \(pages.count)")
reloadContentSizeAndPosition()
}
func reloadAllData() {
totalItemCount = dataSource?.numberOfPages() ?? 0
//if totalItemCount < 3 {
pages.forEach({ $0.tag = -3; $0.removeFromSuperview() })
//}
if totalItemCount > 0 {
if currentPage == -1 {
currentPage = 0
}
load(page: currentPage - 1, force: true)
load(page: currentPage, force: true)
load(page: currentPage + 1, force: true)
} else {
currentPage = -1
}
}
// MARK: size fixing
private var height: CGFloat = 0
private var width: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
reloadContentSizeAndPosition()
}
private func reloadContentSizeAndPosition() {
height = bounds.height
width = bounds.width
contentSize = vertical ? CGSize(width: width, height: CGFloat(totalItemCount) * height) :
CGSize(width: CGFloat(totalItemCount) * width, height: height)
pages.forEach {
if tag >= 0 && tag < totalItemCount {
$0.frame = vertical ? CGRect(x: 0, y: CGFloat($0.tag) * height, width: width, height: height) :
CGRect(x: CGFloat($0.tag) * width, y: 0, width: width, height: height)
}
}
}
}
import UIKit
class EfficientScrollViewVC: UIViewController {
let scrollView = EfficientScrollView()
var listShow = [Int]()
var apiCallInProgress = false
let btn = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
automaticallyAdjustsScrollViewInsets = false
scrollView.contentInsetAdjustmentBehavior = .never
view.backgroundColor = .white
view.addSubview(scrollView)
scrollView.frame = view.bounds
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.dataSource = self
scrollView.isPagingEnabled = true
scrollView.layer.borderWidth = 3
scrollView.layer.borderColor = UIColor.black.cgColor
scrollView.clipsToBounds = false
scrollView.transform = .init(scaleX: 0.3, y: 0.3)
btn.setTitle("Resize", for: .normal)
btn.sizeToFit()
btn.addTarget(self, action: #selector(actionResize), for: .touchUpInside)
view.addSubview(btn)
callApiForData()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
btn.frame.origin = CGPoint(x: 20, y: view.safeAreaInsets.top + 20)
}
func callApiForData() {
if apiCallInProgress { return }
apiCallInProgress = true
getNextPageData { [weak self] list in
self?.listShow.append(contentsOf: list)
self?.scrollView.reloadAllData()
self?.apiCallInProgress = false
}
}
@objc func actionResize() {
let new: CGAffineTransform = scrollView.transform == .identity ? .init(scaleX: 0.3, y: 0.3) : .identity
UIView.animate(withDuration: 1.0) {
self.scrollView.transform = new
}
}
static var count = 0
func getNextPageData(completion: @escaping (([Int])->Void)) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let pageCount = 5
completion(Array(EfficientScrollViewVC.count...EfficientScrollViewVC.count + pageCount))
EfficientScrollViewVC.count += (pageCount + 1)
}
}
}
extension EfficientScrollViewVC: EfficientScrollViewDataSource {
func numberOfPages() -> Int {
return listShow.count
}
func onBind(view: UIView?, index: Int) -> UIView {
let view = (view as? SubView) ?? SubView(frame: self.view!.bounds)
view.lbl.text = "\(listShow[index])"
view.backgroundColor = index % 2 == 0 ? .red : .blue
view.backgroundColor = view.backgroundColor?.withAlphaComponent(0.5)
if index == listShow.count - 1 {
callApiForData()
}
return view
}
}
class SubView: UIView {
let lbl = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit() {
addSubview(lbl)
lbl.textColor = .black
lbl.font = UIFont.systemFont(ofSize: 100)
lbl.textAlignment = .center
lbl.frame = self.bounds
lbl.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
@csmac3144
Copy link

Also are you available for contract work?

@chanonly123
Copy link
Author

chanonly123 commented Jan 14, 2021

Right now I am doing full time job. So if I need to do any contract job I will definitely contact you. Thank you for showing interest.
Also added a sample view controller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment