import UIKit public final class LNCyclePageControl: UIControl { public var numberOfPages: Int = 0 { didSet { guard numberOfPages != oldValue else { return } if currentPage >= numberOfPages { currentPage = 0 } updateIndicatorViews() if !indicatorViews.isEmpty { setNeedsLayout() } } } public var currentPage: Int = 0 { didSet { guard oldValue != currentPage, currentPage < indicatorViews.count else { return } if currentPage < 0 { currentPage = 0; return } if !currentPageIndicatorSize.equalTo(pageIndicatorSize) { setNeedsLayout() } updateIndicatorViewsBehavior() if isUserInteractionEnabled { sendActions(for: .valueChanged) } } } public var hidesForSinglePage = false public var pageIndicatorSpacing: CGFloat = 10 { didSet { guard oldValue != pageIndicatorSpacing else { return } if !indicatorViews.isEmpty { setNeedsLayout() } } } public var contentInset: UIEdgeInsets = .zero { didSet { setNeedsLayout() } } public var contentSize: CGSize { let count = indicatorViews.count let width = CGFloat(max(count - 1, 0)) * (pageIndicatorSize.width + pageIndicatorSpacing) + pageIndicatorSize.width + contentInset.left + contentInset.right let height = currentPageIndicatorSize.height + contentInset.top + contentInset.bottom return CGSize(width: width, height: height) } public var pageIndicatorTintColor: UIColor? = UIColor(white: 0.5, alpha: 1) { didSet { updateIndicatorViewsBehavior() } } public var currentPageIndicatorTintColor: UIColor? = .white { didSet { updateIndicatorViewsBehavior() } } public var pageIndicatorImage: UIImage? { didSet { updateIndicatorViewsBehavior() } } public var currentPageIndicatorImage: UIImage? { didSet { updateIndicatorViewsBehavior() } } public var indicatorImageContentMode: UIView.ContentMode = .center { didSet { indicatorViews.forEach { $0.contentMode = indicatorImageContentMode } } } public var pageIndicatorSize: CGSize = CGSize(width: 6, height: 6) { didSet { guard !pageIndicatorSize.equalTo(oldValue) else { return } if currentPageIndicatorSize == .zero || (currentPageIndicatorSize.width < pageIndicatorSize.width && currentPageIndicatorSize.height < pageIndicatorSize.height) { currentPageIndicatorSize = pageIndicatorSize } if !indicatorViews.isEmpty { setNeedsLayout() } } } public var currentPageIndicatorSize: CGSize = CGSize(width: 6, height: 6) { didSet { guard !currentPageIndicatorSize.equalTo(oldValue) else { return } if !indicatorViews.isEmpty { setNeedsLayout() } } } public var animateDuration: TimeInterval = 0.3 private var indicatorViews: [UIImageView] = [] private var forceUpdate = false public override init(frame: CGRect) { super.init(frame: frame) configure() } public required init?(coder: NSCoder) { super.init(coder: coder) configure() } private func configure() { isUserInteractionEnabled = false currentPageIndicatorSize = pageIndicatorSize } public override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) if newSuperview != nil { forceUpdate = true updateIndicatorViews() forceUpdate = false } } public func setCurrentPage(_ page: Int, animate: Bool) { if animate { UIView.animate(withDuration: animateDuration) { self.currentPage = page self.layoutIfNeeded() } } else { currentPage = page } } public override func layoutSubviews() { super.layoutSubviews() layoutIndicatorViews() } private func updateIndicatorViews() { if superview == nil, !forceUpdate { return } if indicatorViews.count < numberOfPages { for _ in indicatorViews.count.. numberOfPages { for idx in stride(from: indicatorViews.count - 1, through: numberOfPages, by: -1) { indicatorViews[idx].removeFromSuperview() indicatorViews.remove(at: idx) } } updateIndicatorViewsBehavior() } private func updateIndicatorViewsBehavior() { guard !indicatorViews.isEmpty else { return } if superview == nil, !forceUpdate { return } if hidesForSinglePage, indicatorViews.count == 1 { indicatorViews[0].isHidden = true return } for (idx, view) in indicatorViews.enumerated() { if let pageIndicatorImage { view.contentMode = indicatorImageContentMode view.image = (idx == currentPage) ? currentPageIndicatorImage : pageIndicatorImage view.backgroundColor = .clear } else { view.image = nil view.backgroundColor = (idx == currentPage) ? currentPageIndicatorTintColor : pageIndicatorTintColor } view.isHidden = false } } private func layoutIndicatorViews() { guard !indicatorViews.isEmpty else { return } var originX: CGFloat = 0 var centerY: CGFloat = 0 var spacing = pageIndicatorSpacing switch contentHorizontalAlignment { case .center: originX = (bounds.width - CGFloat(indicatorViews.count - 1) * (pageIndicatorSize.width + pageIndicatorSpacing) - currentPageIndicatorSize.width) * 0.5 case .left: originX = contentInset.left case .right: originX = bounds.width - (CGFloat(indicatorViews.count - 1) * (pageIndicatorSize.width + pageIndicatorSpacing) + currentPageIndicatorSize.width) - contentInset.right case .fill: originX = contentInset.left if indicatorViews.count > 1 { spacing = (bounds.width - contentInset.left - contentInset.right - pageIndicatorSize.width - CGFloat(indicatorViews.count - 1) * pageIndicatorSize.width) / CGFloat(indicatorViews.count - 1) } default: break } switch contentVerticalAlignment { case .center: centerY = bounds.height * 0.5 case .top: centerY = contentInset.top + currentPageIndicatorSize.height * 0.5 case .bottom: centerY = bounds.height - currentPageIndicatorSize.height * 0.5 - contentInset.bottom case .fill: centerY = (bounds.height - contentInset.top - contentInset.bottom) * 0.5 + contentInset.top default: break } for (idx, view) in indicatorViews.enumerated() { let size = (idx == currentPage) ? currentPageIndicatorSize : pageIndicatorSize view.layer.cornerRadius = (pageIndicatorImage == nil) ? (size.height * 0.5) : 0 view.clipsToBounds = true view.frame = CGRect(x: originX, y: centerY - size.height * 0.5, width: size.width, height: size.height) originX += size.width + spacing } } }