I currently have this custom UIView to show 3 dots. The selected dot being the biggest. Here's how it looks:
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:
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.
If this is the pagination effect you are looking for, read on for a different approach...
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.