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