Swift Subviews count is not working on iOS 14

Viewed 849

In the past, I customized the images of indicators of Page Control using some functions like the following code provided by @Politta.

class CustomPageControl: UIPageControl {

    @IBInspectable var currentPageImage: UIImage?

    @IBInspectable var otherPagesImage: UIImage?

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

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

    override func awakeFromNib() {
        super.awakeFromNib()
        pageIndicatorTintColor = .clear
        currentPageIndicatorTintColor = .clear
        clipsToBounds = false
    }

    private func updateDots() {

        for (index, subview) in subviews.enumerated() {
            let imageView: UIImageView
            if let existingImageview = getImageView(forSubview: subview) {
                imageView = existingImageview
            } else {
                imageView = UIImageView(image: otherPagesImage)
                // Modify image size
                imageView.frame = ....
                imageView.center = subview.center
                subview.addSubview(imageView)
                subview.clipsToBounds = false
            }
            imageView.image = currentPage == index ? currentPageImage : otherPagesImage
        }
    }

    private func getImageView(forSubview view: UIView) -> UIImageView? {
        if let imageView = view as? UIImageView {
            return imageView
        } else {
            let view = view.subviews.first { (view) -> Bool in
                return view is UIImageView
            } as? UIImageView

            return view
        }
    }
}

Now I found that Subviews count is not working on iOS 14 as Apple had introduced some new APIs for UIPageControll. Now when I try to use a function setIndicatorImage(image, index) provided by @Soumen, the image shows abnormally big. Modifying the size of page control doesn't help me. In the past, since I add image view to current view of page control, I can define its frame, but now the function setIndicatorImage() just takes image as its parameter. How do I solve the issue?

class CustomPageControl: UIPageControl {

    @IBInspectable var currentPageImage: UIImage?

    @IBInspectable var otherPagesImage: UIImage?

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

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

    override func awakeFromNib() {
        super.awakeFromNib()
        if #available(iOS 14.0, *) {
            defaultConfigurationForiOS14AndAbove()
        } else {
            pageIndicatorTintColor = .clear
            currentPageIndicatorTintColor = .clear
            clipsToBounds = false
        }
    }

    private func defaultConfigurationForiOS14AndAbove() {
        if #available(iOS 14.0, *) {
            for index in 0..<numberOfPages {
                let image = index == currentPage ? currentPageImage : otherPagesImage
                setIndicatorImage(image, forPage: index)
            }

            // give the same color as "otherPagesImage" color.
            pageIndicatorTintColor = .gray
            
            // give the same color as "currentPageImage" color.
            currentPageIndicatorTintColor = .red 
            /*
             Note: If Tint color set to default, Indicator image is not showing. So, give the same tint color based on your Custome Image.
            */
        }
    }

    private func updateDots() {
        if #available(iOS 14.0, *) {
            defaultConfigurationForiOS14AndAbove()
        } else {
            for (index, subview) in subviews.enumerated() {
                let imageView: UIImageView
                if let existingImageview = getImageView(forSubview: subview) {
                    imageView = existingImageview
                } else {
                    imageView = UIImageView(image: otherPagesImage)
                    // Modify image size
                    imageView.frame = ....
                    imageView.center = subview.center
                    subview.addSubview(imageView)
                    subview.clipsToBounds = false
                }
                imageView.image = currentPage == index ? currentPageImage : otherPagesImage
            }
        }
    }

    private func getImageView(forSubview view: UIView) -> UIImageView? {
        if let imageView = view as? UIImageView {
            return imageView
        } else {
            let view = view.subviews.first { (view) -> Bool in
                return view is UIImageView
            } as? UIImageView

            return view
        }
    }
}
2 Answers

For iOS 14, the hierarchy of Views has changed, so we cannot get subviews count of UIPageControl like we did before (in iOS < 14). To get them like before, you need to change your accessing method of dot subviews like below.
For accessing them in iOS 14,
Before:

for (index, subview) in subviews.enumerated() {
    //Your rest of the code
}

After:

var dotViews: [UIView] = subviews
if #available(iOS 14, *) {
    let pageControl = dotViews[0]
    let dotContainerView = pageControl.subviews[0]
    dotViews = dotContainerView.subviews
}

for (index, subview) in dotViews.enumerated() {
    //Your rest of the code
}

Your full code may look like this after modification:

class CustomPageControl: UIPageControl {

    @IBInspectable var currentPageImage: UIImage?

    @IBInspectable var otherPagesImage: UIImage?

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

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

    override func awakeFromNib() {
        super.awakeFromNib()
        pageIndicatorTintColor = .clear
        currentPageIndicatorTintColor = .clear
        clipsToBounds = false
    }

    private func updateDots() {

        var dotViews: [UIView] = subviews
        if #available(iOS 14, *) {
            let pageControl = dotViews[0]
            let dotContainerView = pageControl.subviews[0]
            dotViews = dotContainerView.subviews
        }

        for (index, subview) in dotViews.enumerated() {
            let imageView: UIImageView
            if let existingImageview = getImageView(forSubview: subview) {
                imageView = existingImageview
            } else {
                imageView = UIImageView(image: otherPagesImage)
                // Modify image size
                imageView.frame = ....
                imageView.center = subview.center
                subview.addSubview(imageView)
                subview.clipsToBounds = false
            }
            imageView.image = currentPage == index ? currentPageImage : otherPagesImage
        }
    }

    private func getImageView(forSubview view: UIView) -> UIImageView? {
        if let imageView = view as? UIImageView {
            return imageView
        } else {
            let view = view.subviews.first { (view) -> Bool in
                return view is UIImageView
            } as? UIImageView

            return view
        }
    }
}

In this way, you can access your dot views and proceed code like before (Customising the images of indicators, change background color etc.)

For iOS 14.0 you have to access pageControl.subviews[0].subviews[0].subviews in order to get the dots views of the pageControl. Instead, for iOS < 14.0 you'll get the dots views accessing pageControl.subviews

private func updatePageControlDots() {
    var currentDot = UIView()
    if #available(iOS 14, *) {
        let pageControlContent = pageControl.subviews[0]
        let dotContainerView = pageControlContent.subviews[0]
        currentDot = dotContainerView.subviews[currentPage]
    } else {
        currentDot = pageControl.subviews[currentPage]
    }
}
Related