UIPageControllerの選択しているドットを大きくする

環境

iOS12.0
Xcode Version 10.0 (10A255)
Swift4.2

やりたいこと

こんな感じにUIPageControllerの選択中のドットを大きくしたい

f:id:y_sumida:20181215231319p:plain

カスタマイズしたUIPageCotrollクラス

class CustomPageControl: UIPageControl {
    private static let defaultDotSize: CGFloat = 7.0 // UIPageControllerのドットのサイズ

    private lazy var currentPageIndicator: UIImage = UIImage.dotImage(color: .gray, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
    private lazy var pageIndicator: UIImage = UIImage.dotImage(color: .gray, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))

    private lazy var currentPageIndicatorOffset: CGFloat = calcIndicatorOffset(size: CustomPageControl.defaultDotSize)
    private lazy var pageIndicatorOffset: CGFloat = calcIndicatorOffset(size: CustomPageControl.defaultDotSize)

    var currentPageIndicatorSize: CGFloat = defaultDotSize {
        didSet {
            currentPageIndicatorOffset = calcIndicatorOffset(size: currentPageIndicatorSize)
            currentPageIndicator = UIImage.dotImage(color: currentPageIndicatorTintColor ?? .gray, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
            updateDots()
        }
    }
    var pageIndicatorSize: CGFloat = defaultDotSize {
        didSet {
            pageIndicatorOffset = calcIndicatorOffset(size: pageIndicatorSize)
            pageIndicator = UIImage.dotImage(color: pageIndicatorTintColor ?? .gray, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))
            updateDots()
        }
    }

    override var numberOfPages: Int {
        didSet {
            updateDots()
        }
    }

    override var currentPage: Int {
        didSet {
            updateDots()
        }
    }

    override var currentPageIndicatorTintColor: UIColor? {
        didSet {
            if let color = currentPageIndicatorTintColor {
                currentPageIndicator = UIImage.dotImage(color: color, size: CGSize(width: currentPageIndicatorSize, height: currentPageIndicatorSize))
            }
        }
    }

    override var pageIndicatorTintColor: UIColor? {
        didSet {
            if let color = pageIndicatorTintColor {
                pageIndicator = UIImage.dotImage(color: color, size: CGSize(width: pageIndicatorSize, height: pageIndicatorSize))
            }
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        clipsToBounds = false
        updateDots()

        addTarget(self, action: #selector(didChangeValue(sender:)), for: .valueChanged)
    }
}

extension CustomPageControl {
    private func updateDots() {
        var i = 0
        let currentPageIndicatorRect = CGRect(x: 0, y: 0, width: currentPageIndicatorSize, height: currentPageIndicatorSize)
        let pageIndicatorRect = CGRect(x: 0, y: 0, width: pageIndicatorSize, height: pageIndicatorSize)

        for view in subviews {
            if let imageView = imageForSubview(view) {
                if i == currentPage {
                    imageView.image = currentPageIndicator
                    imageView.frame = currentPageIndicatorRect
                    imageView.frame.origin.y = imageView.frame.origin.y - currentPageIndicatorOffset
                    imageView.frame.origin.x = imageView.frame.origin.x - currentPageIndicatorOffset
                } else {
                    imageView.image = pageIndicator
                    imageView.frame = pageIndicatorRect
                    imageView.frame.origin.y = imageView.frame.origin.y - pageIndicatorOffset
                    imageView.frame.origin.x = imageView.frame.origin.x - pageIndicatorOffset
                }
                i += 1
            } else {
                var dotImage = pageIndicator
                if i == currentPage {
                    dotImage = currentPageIndicator
                }
                view.clipsToBounds = false
                let addedImageView: UIImageView = UIImageView(image: dotImage)
                if dotImage == currentPageIndicator {
                    addedImageView.frame = currentPageIndicatorRect
                    addedImageView.frame.origin.y = addedImageView.frame.origin.y - currentPageIndicatorOffset
                    addedImageView.frame.origin.x = addedImageView.frame.origin.x - currentPageIndicatorOffset
                } else {
                    addedImageView.frame.origin.y = addedImageView.frame.origin.y - pageIndicatorOffset
                    addedImageView.frame.origin.x = addedImageView.frame.origin.x - pageIndicatorOffset
                }
                view.addSubview(addedImageView)
                i += 1
            }
        }
    }

    private func imageForSubview(_ view:UIView) -> UIImageView? {
        var dot: UIImageView?
        if let dotImageView = view as? UIImageView {
            dot = dotImageView
        } else {
            for foundView in view.subviews {
                if let imageView = foundView as? UIImageView {
                    dot = imageView
                    break
                }
            }
        }
        return dot
    }

    private func calcIndicatorOffset(size: CGFloat) -> CGFloat {
        return abs(size - CustomPageControl.defaultDotSize) / 2
    }

    @objc private func didChangeValue(sender: UIPageControl){
        currentPage = sender.currentPage
    }
}

使い方

  • StoryBoard上でUIPageController置いてこのクラスを指定する。コード上で生成も可。
  • viewDidLoad() とかで、currentPageIndicatorSize でカレントのドットサイズを指定する
  • カレント以外のドットのサイズも変えたい場合は、 pageIndicatorSize を指定する

ポイント

  • ドット用のUIImageを用意して、カレントページが変わるたびに差し替え
    • 塗りつぶした画像生成用のextensionをUIImageに生やしてます
  • ドットのサイズに合わせて位置調整をして、元のドットの中心と合わせるようにした
    • 単純にサイズ変えてるサンプルが多かった
  • タップアクションも忘れずに受けるようにした
    • けど、このやり方でいいのかは不明

その他

  • UIPageControllerをカスタマイズできるライブラリの多くは、UIPageControllerのカスタマイズじゃなくて独自のクラスで実現していた
  • でもドットの大きさかえるためだけにライブラリ入れたくないので良し

リポジトリ

GitHub - y-sumida/CustomDotPageControlSample: UIPageControlのドットの色やサイズを変えるサンプル

参考

stackoverflow.com