session-ios/Session/Shared/SessionCarouselView.swift

203 lines
7.0 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
final class SessionCarouselView: UIView, UIScrollViewDelegate {
private let slicesForLoop: [UIView]
private let info: SessionCarouselView.Info
var delegate: SessionCarouselViewDelegate?
// MARK: - UI
private lazy var scrollView: UIScrollView = {
let result: UIScrollView = UIScrollView()
result.delegate = self
result.isPagingEnabled = true
result.showsHorizontalScrollIndicator = false
result.showsVerticalScrollIndicator = false
result.contentSize = CGSize(
width: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count),
height: self.info.sliceSize.height
)
result.layer.cornerRadius = self.info.cornerRadius
result.layer.masksToBounds = true
return result
}()
private lazy var pageControl: UIPageControl = {
let result: UIPageControl = UIPageControl()
result.numberOfPages = self.info.sliceCount
result.currentPage = 0
result.isHidden = !self.info.shouldShowPageControl
result.transform = CGAffineTransform(
scaleX: self.info.pageControlStyle.size.rawValue,
y: self.info.pageControlStyle.size.rawValue
)
return result
}()
private lazy var arrowLeft: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToPreviousSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
private lazy var arrowRight: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.right")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToNextSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
// MARK: - Lifecycle
init(info: SessionCarouselView.Info) {
self.info = info
if self.info.sliceCount > 1,
let copyOfFirstSlice: UIView = self.info.copyOfFirstSlice,
let copyOfLastSlice: UIView = self.info.copyOfLastSlice
{
self.slicesForLoop = [copyOfLastSlice]
.appending(contentsOf: self.info.slices)
.appending(copyOfFirstSlice)
} else {
self.slicesForLoop = self.info.slices
}
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
set(.width, to: self.info.sliceSize.width + Values.largeSpacing + 2 * self.info.arrowsSize.width)
set(.height, to: self.info.sliceSize.height)
let stackView: UIStackView = UIStackView(arrangedSubviews: self.slicesForLoop)
stackView.axis = .horizontal
stackView.set(.width, to: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count))
stackView.set(.height, to: self.info.sliceSize.height)
addSubview(self.scrollView)
scrollView.center(in: self)
scrollView.set(.width, to: self.info.sliceSize.width)
scrollView.set(.height, to: self.info.sliceSize.height)
scrollView.addSubview(stackView)
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * (self.info.sliceCount > 1 ? 1 : 0),
y: 0
),
animated: false
)
addSubview(self.pageControl)
self.pageControl.center(.horizontal, in: self)
self.pageControl.pin(.bottom, to: .bottom, of: self)
addSubview(self.arrowLeft)
self.arrowLeft.pin(.leading, to: .leading, of: self)
self.arrowLeft.center(.vertical, in: self)
addSubview(self.arrowRight)
self.arrowRight.pin(.trailing, to: .trailing, of: self)
self.arrowRight.center(.vertical, in: self)
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex: Int = {
let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/self.info.sliceSize.width))
if self.info.sliceCount > 1 {
if maybeCurrentPageIndex == 0 {
return pageControl.numberOfPages - 1
}
if maybeCurrentPageIndex == self.slicesForLoop.count - 1 {
return 0
}
return maybeCurrentPageIndex - 1
}
return maybeCurrentPageIndex
}()
pageControl.currentPage = pageIndex
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
private func setCorrectCotentOffsetIfNeeded(_ scrollView: UIScrollView) {
if pageControl.currentPage == 0 {
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * 1,
y: 0
),
animated: false
)
}
if pageControl.currentPage == pageControl.numberOfPages - 1 {
let realLastIndex: Int = self.slicesForLoop.count - 2
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * realLastIndex,
y: 0
),
animated: false
)
}
}
// MARK: - Interaction
@objc func scrollToNextSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x + self.info.sliceSize.width,
y: 0
),
animated: true
)
}
@objc func scrollToPreviousSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x - self.info.sliceSize.width,
y: 0
),
animated: true
)
}
}