// // CarouselScrollbar.swift // VDS // // Created by Kanamarlapudi, Vasavi on 12/03/24. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// A carousel scrollbar is a control that allows to navigate between items in a carousel. /// It's also a status indicator that conveys the relative amount of content in a carousel and a location within it. @objc(VDSCarouselScrollbar) open class CarouselScrollbar: View { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Used to set total number of slides within carousel open var numberOfSlides: Int? { get { return _numberOfSlides } set { if let newValue { _numberOfSlides = newValue } else { _numberOfSlides = 1 } setThumbWidth() setNeedsUpdate() } } /// The amount of slides visible in the carousel container at one time. open var selectedLayout: Layout? { get { return _selectedLayout } set { if let newValue { _selectedLayout = newValue } else { _selectedLayout = .oneUP } setThumbWidth() setNeedsUpdate() } } /// Enum used to describe the amount of slides visible in the carousel container at one time. public enum Layout: String, CaseIterable { case oneUP = "1UP" case twoUP = "2UP" case threeUP = "3UP" case fourUP = "4UP" case fiveUP = "5UP" case sixUP = "6UP" case eightUP = "8UP" var value: Int { switch self { case .oneUP: 1 case .twoUP: 2 case .threeUP: 3 case .fourUP: 4 case .fiveUP: 5 case .sixUP: 6 case .eightUP: 8 } } } /// Used to set the position of the thumb(scrubber). This is used when the carousel container changes position, it will align the position of thumb(scrubber). open var position: Int? { didSet { setNeedsUpdate() } } /// Allows a unique id to be passed into the thumb and track of the thumb(scrubber). open var scrubberId: Int? { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal var containerSize: CGSize { CGSize(width: 45, height: 44) } private let trackViewWidth = 96 private let trackViewHeight: CGFloat = 4 private let minThumbWidth: Float = 16.0 private var thumbWidth: Float = 16.0 private var actualThumbWidth: Float = 0.0 private var _selectedLayout: Layout = .oneUP private var _numberOfSlides: Int? = 1 internal var cornerRadius: CGFloat = 4.0 internal var heightConstraint: NSLayoutConstraint? /// Track view with fixed width internal var trackView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() /// Left Active Track overlay with variable width internal var leftActiveOverlay: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.tag = 2 } }() /// Right Active Track overlay with variable width internal var rightActiveOverlay: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.tag = 3 } }() /// Thumb view with variable width internal var thumbView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- private var thumbColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrollthumbOnlight , VDSColor.interactiveScrollthumbOndark) private var trackColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveScrolltrackOnlight , VDSColor.interactiveScrolltrackOndark) private var activeOverlayColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteWhite) //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func initialSetup() { super.initialSetup() } open override func setup() { super.setup() accessibilityLabel = "Carousel Scrollbar" //create the wrapping view heightConstraint = self.heightAnchor.constraint(equalToConstant: containerSize.height) heightConstraint?.priority = .defaultHigh heightConstraint?.isActive = true //Trackview trackView.frame = CGRectMake(20, 20, CGFloat(trackViewWidth), trackViewHeight) trackView.layer.cornerRadius = cornerRadius addSubview(trackView) ///Left active overlay leftActiveOverlay.frame = CGRectMake(trackView.frame.origin.x, 20, CGFloat(trackViewWidth), trackViewHeight) leftActiveOverlay.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) leftActiveOverlay.isUserInteractionEnabled = true leftActiveOverlay.layer.cornerRadius = cornerRadius addSubview(leftActiveOverlay) ///Right active overlay rightActiveOverlay.frame = CGRectMake(thumbView.frame.origin.x + thumbView.frame.size.width, 20, CGFloat(trackViewWidth), trackViewHeight) rightActiveOverlay.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) rightActiveOverlay.isUserInteractionEnabled = true rightActiveOverlay.layer.cornerRadius = cornerRadius addSubview(rightActiveOverlay) //Thumbview thumbView.frame = CGRectMake(20, 20, CGFloat(thumbWidth), trackViewHeight) thumbView.layer.cornerRadius = cornerRadius thumbView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action:(#selector(self.handleGesture(_:))))) addSubview(thumbView) updateFrames() } open override func updateView() { super.updateView() trackView.backgroundColor = trackColorConfiguration.getColor(surface) thumbView.backgroundColor = thumbColorConfiguration.getColor(surface) } open override func updateAccessibility() { super.updateAccessibility() } open override func reset() { for subview in subviews { for recognizer in subview.gestureRecognizers ?? [] { subview.removeGestureRecognizer(recognizer) } } super.reset() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { // handling code let tag = gestureRecognizer.view?.tag switch tag! { case 2 : thumbView.frame.origin.x = thumbView.frame.origin.x - CGFloat(actualThumbWidth) updateFrames() case 3 : thumbView.frame.origin.x = thumbView.frame.origin.x + CGFloat(actualThumbWidth) updateFrames() default: print("default") } } @objc func handleGesture(_ sender: UIPanGestureRecognizer) { switch sender.state { case .began: print("began") break case .changed: print("changed") break case .cancelled: print("cancelled") break case .ended: print("ended") break default: break } } private func setThumbWidth() { let width = Float (trackViewWidth / (numberOfSlides ?? 1) * _selectedLayout.value) actualThumbWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width thumbWidth = (width < Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth) thumbView.frame.size.width = CGFloat(thumbWidth) thumbView.frame.origin.x = trackView.frame.origin.x updateFrames() } private func updateFrames() { //left active overlay origin - x, width leftActiveOverlay.frame.size.width = thumbView.frame.origin.x - trackView.frame.origin.x //right active overlay origin - x, width let position1 = thumbView.frame.origin.x + thumbView.frame.size.width let position2 = trackView.frame.origin.x + trackView.frame.size.width rightActiveOverlay.frame.origin.x = position1 rightActiveOverlay.frame.size.width = position2 - position1 } }