Search code examples
iosswiftuikit

properly aligning dots in swift


I currently have this custom UIView to show 3 dots. The selected dot being the biggest. Here's how it looks:

First Image

But the issue is, sometimes when the currentPage is 0 it covers the second dot and only shows two dots, but in the view hierarchy debugger I can see that the first dot (big one) is covering the second and only the third is visible.

like this:

Second Image

My code:

import UIKit

class ExpandedCustomLoopedPageControl: UIView {

// MARK: - start flow in pagenumber  set
    public var numberOfPages: Int = 0 {
        didSet {

            dotViewArray.forEach { $0.removeFromSuperview() }
            dotViewArray.removeAll()

            for _ in 0..<numberOfPages {
                let dotView = UIView()
                dotView.layer.cornerRadius = cornerRadius
                addSubview(dotView)
                dotViewArray.append(dotView)
            }

            isInitialize = true
            setNeedsLayout()
        }
    }

    public var currentPage: Int {
        set {
            if newValue < 0 ||
                newValue >= dotViewArray.count ||
                dotViewArray.count == 0 ||
                newValue == currentPage ||
                inAnimating {
                return
            }

            var animationDuration = 0.1
            if ((  currentPage == (numberOfPages - 1) ) && ( newValue == 0))
                || (currentPage == 0 && (newValue == (numberOfPages - 1))) {
                animationDuration = 0
            }
// MARK: - Shift to the right
            if newValue > currentPage {
                let currentView = dotViewArray[currentPage]
                bringSubviewToFront(currentView)
                inAnimating = true
                UIView.animate(withDuration: animationDuration, animations: {
                    // For the currently selected dot, the x is
                    // unchanged, the width is increased, and several dots
                    // and gap distances are added.
                    let x = currentView.frame.origin.x
                    let y = currentView.frame.origin.y
                    let w = self.currentDotWidth + (self.dotSpace + self.otherDotWidth) * CGFloat(newValue - self.currentPage)
                    let h = currentView.frame.size.height
                    currentView.frame = CGRect(x: x, y: y, width: w, height: h)
                }) { (_) in
                    let endView = self.dotViewArray[newValue]
                    endView.backgroundColor = currentView.backgroundColor
                    endView.frame = currentView.frame
                    self.bringSubviewToFront(endView)

                    currentView.backgroundColor = self.otherDotColor

                    // Shift left one by one
                    let start_X = currentView.frame.origin.x
                    for i in 0..<(newValue - self.currentPage) {

                        let dotView = self.dotViewArray[self.currentPage + i]
                        // shift left
                        let x = start_X + (self.otherDotWidth + self.dotSpace) * CGFloat(i)
                        let y = dotView.frame.origin.y
                        let w = self.otherDotWidth
                        let h = self.dotHeight
                        dotView.frame = CGRect(x: x, y: y, width: w, height: h)
                    }
                    UIView.animate(withDuration: 0.1, animations: {
                        let w = self.currentDotWidth
                        let x = endView.frame.maxX - self.currentDotWidth
                        let y = endView.frame.origin.y
                        let h = endView.frame.size.height
                        endView.frame = CGRect(x: x, y: y, width: w, height: h)
                    }) { (_) in
                        self.currentPageInner = newValue
                        self.inAnimating = false
                    }
                }
            }
 // MARK: - Shift to the Left
            else {
                let currentView = self.dotViewArray[self.currentPage]
                bringSubviewToFront(currentView)
                inAnimating = true

                UIView.animate(withDuration: animationDuration, animations: {
                    // For the currently selected dot, move x to the left, increase the width, increase a few dots and the gap distance
                    let x = currentView.frame.origin.x - (self.dotSpace + self.otherDotWidth) * CGFloat(self.currentPage - newValue)
                    let y = currentView.frame.origin.y
                    let w = self.currentDotWidth + (self.dotSpace + self.otherDotWidth) * CGFloat(self.currentPage - newValue)
                    let h = currentView.frame.size.height
                    currentView.frame = CGRect(x: x, y: y, width: w, height: h)
                }) { (_) in
                    let endView = self.dotViewArray[newValue]
                    endView.backgroundColor = currentView.backgroundColor
                    endView.frame = currentView.frame
                    self.bringSubviewToFront(endView)

                    currentView.backgroundColor = self.otherDotColor

                    // Move the view to the right one by one
                    let start_X = currentView.frame.maxX
                    for i in 0..<(self.currentPage - newValue) {
                        let dotView = self.dotViewArray[self.currentPage - i]
                        let tempFrame = dotView.frame
                        // move right
                        let x = start_X - self.otherDotWidth - (self.otherDotWidth + self.dotSpace) * CGFloat(i)
                        let y = tempFrame.origin.y
                        let w = self.otherDotWidth
                        let h = tempFrame.size.height
                        dotView.frame = CGRect(x: x, y: y, width: w, height: h)
                    }

                    UIView.animate(withDuration: 0.1, animations: {
                        let x = endView.frame.origin.x
                        let y = endView.frame.origin.y
                        let w = self.currentDotWidth
                        let h = endView.frame.size.height
                        endView.frame = CGRect(x: x, y: y, width: w, height: h)
                    }) { (_) in
                        self.currentPageInner = newValue
                        self.inAnimating = false
                    }
                }
            }
        }
        get {
            currentPageInner
        }
    }

// MARK: - properties of pageControl

    public var currentDotWidth: CGFloat = 20
    public var otherDotWidth: CGFloat = 5
    public var dotHeight: CGFloat = 5
    public var dotSpace: CGFloat = 7
    public var cornerRadius: CGFloat = 2.5
    public var currentDotColor: UIColor = Color.lines.color
    public var otherDotColor: UIColor = Color.slider_indicator_bubble.color

    private var currentPageInner: Int = 0
    private var dotViewArray = [UIView]()
    private var isInitialize: Bool = false
    private var inAnimating: Bool = false

// MARK: - init()
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        setupUI()
    }
// MARK: - Setup UI
    private func setupUI() {
        if dotViewArray.isEmpty || isInitialize == false {
            return
        }
        self.isInitialize = false

        let totalWidth = CGFloat(numberOfPages) * (currentDotWidth + dotSpace) - dotSpace
        var currentX = (frame.size.width - totalWidth) / 2

        for (index, dotView) in dotViewArray.enumerated() {
            // Update location
            let width = (index == currentPage ? currentDotWidth : otherDotWidth)
            let height = dotHeight
            let y = (frame.size.height - height) / 2
            dotView.frame = CGRect(x: currentX, y: y, width: width, height: height)

            // Update color
            dotView.backgroundColor = (index == currentPage) ? currentDotColor : otherDotColor

            currentX += width + dotSpace
        }
    }
}

The first dot when selected should not cover second and third.


Solution

  • If this is the pagination effect you are looking for, read on for a different approach...

    paginated dots

    class DotsViewController: UIViewController {
        
        @IBOutlet private weak var redDot: DotView!
        @IBOutlet private weak var dotMover: UIStepper!
        @IBOutlet private weak var dotStack: UIStackView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            
            configureStepper()
            redDot.layer.zPosition = 10 //ensures the red dot stays on top during animations
        }
        
        private func configureStepper() {
            dotMover.setDecrementImage(UIImage(systemName: "chevron.left"), for: .normal)
            dotMover.setIncrementImage(UIImage(systemName: "chevron.right"), for: .normal)
            
            dotMover.maximumValue = Double(dotStack.subviews.count - 1) //because we start at index 0
        }
    
        @IBAction
        private func didModifyStepper(_ sender: UIStepper) {
            let currentValue = Int(sender.value)
            UIView.animate(withDuration: 0.4, animations: { [weak self] in
                guard let strongSelf = self else { return }
                strongSelf.dotStack.removeArrangedSubview(strongSelf.redDot)
                strongSelf.dotStack.insertArrangedSubview(strongSelf.redDot, at: currentValue)
                strongSelf.dotStack.layoutIfNeeded()
            })
        }
    }
    

    where DotView is:

    @IBDesignable
    class DotView: UIView {
        @IBInspectable
        var cornerRadius: CGFloat = .zero {
            didSet {
                self.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMaxXMinYCorner]
                self.layer.cornerRadius = self.cornerRadius
                self.clipsToBounds = true
            }
        }
    }
    

    Custom pagination controls are achieved by UIStackView and bunch of UIViews with rounded corners. Source code on github.

    Layout details