Skip to content

Instantly share code, notes, and snippets.

@trilliwon
Created July 4, 2020 05:43
Show Gist options
  • Save trilliwon/4331183e82f5644a2399f73b6aa89f6b to your computer and use it in GitHub Desktop.
Save trilliwon/4331183e82f5644a2399f73b6aa89f6b to your computer and use it in GitHub Desktop.
import UIKit
final class ScrollingPageControl: UIView {
// The number of dots
var pages: Int = 0 {
didSet {
guard pages != oldValue else { return }
pages = max(0, pages)
invalidateIntrinsicContentSize()
makeDotViews()
}
}
private func makeDotViews() {
dotViews = (0..<pages).map { index in
Circle(frame: CGRect(origin: .zero, size: CGSize(width: dotSize, height: dotSize)))
}
}
// The index of the currently selected page
var currentPage: Int = 0 {
didSet {
guard currentPage != oldValue else { return }
currentPage = max(0, min (currentPage, pages - 1))
updateColors()
if (0..<centerDots).contains(currentPage - pageOffset) {
centerOffset = currentPage - pageOffset
} else {
pageOffset = currentPage - centerOffset
}
}
}
// The maximum number of dots that will show in the control
var maxDots = 7 {
didSet {
maxDots = max(3, maxDots)
if maxDots % 2 == 0 {
maxDots += 1
print("maxPages has to be an odd number")
}
invalidateIntrinsicContentSize()
updatePositions()
}
}
// The number of dots that will be centered and full-sized
var centerDots = 3 {
didSet {
centerDots = max(1, centerDots)
if centerDots % 2 == 0 {
centerDots += 1
print("centerDots has to be an odd number")
}
updatePositions()
}
}
// The duration, in seconds, of the dot slide animation
var slideDuration: TimeInterval = 0.15
private var centerOffset = 0
private var pageOffset = 0 {
didSet {
UIView.animate(
withDuration: slideDuration,
delay: 0.15,
options: [.curveEaseInOut],
animations: { [weak self] in
self?.updatePositions()
})
}
}
private var dotViews: [UIView] = [] {
didSet {
oldValue.forEach { $0.removeFromSuperview() }
dotViews.forEach(addSubview)
updateColors()
updatePositions()
}
}
// The color of all the unselected dots
var dotColor = UIColor(white: 224 / 255, alpha: 1)
// The color of the currently selected dot
var selectedColor = UIColor(red: 4 / 255, green: 166 / 255, blue: 225 / 255, alpha: 1)
// The size of the dots
var dotSize: CGFloat = 6
// The space between dots
var spacing: CGFloat = 6
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false
}
private var lastSize = CGSize.zero
override func layoutSubviews() {
super.layoutSubviews()
guard bounds.size != lastSize else { return }
lastSize = bounds.size
updatePositions()
}
private func updateColors() {
dotViews.enumerated().forEach { page, dot in
dot.tintColor = page == currentPage ? selectedColor : dotColor
}
}
private func updatePositions() {
if pages > 5 {
let sidePages = (maxDots - centerDots) / 2
let horizontalOffset = CGFloat(-pageOffset + sidePages) * (dotSize + spacing) + (bounds.width - intrinsicContentSize.width) / 2
let centerPage = centerDots / 2 + pageOffset
dotViews.enumerated().forEach { page, dot in
let center = CGPoint(x: horizontalOffset + bounds.minX + dotSize / 2 + (dotSize + spacing) * CGFloat(page), y: bounds.midY)
let scale: CGFloat = {
let distance = abs(page - centerPage)
if distance > (maxDots / 2) { return 0 }
return [1, 0.66, 0.33, 0.16][max(0, min(3, distance - centerDots / 2))]
}()
dot.frame = CGRect(origin: .zero, size: CGSize(width: dotSize * scale, height: dotSize * scale))
dot.center = center
}
} else {
dotViews.enumerated().forEach { page, dot in
dot.frame = CGRect(origin: .zero, size: CGSize(width: dotSize, height: dotSize))
let center = CGPoint(x: bounds.minX + dotSize / 2 + (dotSize + spacing) * CGFloat(page), y: bounds.midY)
dot.center = center
}
}
}
override var intrinsicContentSize: CGSize {
let pages = min(maxDots, self.pages)
let width = CGFloat(pages) * dotSize + CGFloat(pages - 1) * spacing
let height = dotSize
return CGSize(width: width, height: height)
}
class Circle: UIView {
override func tintColorDidChange() {
self.backgroundColor = tintColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
override var frame: CGRect {
didSet {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment