22

I want to add border color for dots in UIPageControl. Here is the small picture of it:

enter image description here

I am able to put second dot by configuring it from XCode but I cannot make the first and third circles' inside empty. Is there a simple way to achieve that?

Thanks :)

Emrah Akgül
  • 630
  • 2
  • 7
  • 18

13 Answers13

27

Edited- Swift 3 & 4 extension to achieve the same result-

extension UIPageControl {

    func customPageControl(dotFillColor:UIColor, dotBorderColor:UIColor, dotBorderWidth:CGFloat) {
        for (pageIndex, dotView) in self.subviews.enumerated() {
            if self.currentPage == pageIndex {
                dotView.backgroundColor = dotFillColor
                dotView.layer.cornerRadius = dotView.frame.size.height / 2
            }else{
                dotView.backgroundColor = .clear
                dotView.layer.cornerRadius = dotView.frame.size.height / 2
                dotView.layer.borderColor = dotBorderColor.cgColor
                dotView.layer.borderWidth = dotBorderWidth
            }
        }
    }

}

to use it write below code in viewDidLoad() or viewDidAppear()

pageControl.customPageControl(dotFillColor: .orange, dotBorderColor: .green, dotBorderWidth: 2)

In Objective-C use below code-

- (void) customPageControlWithFillColor:(UIColor*)dotFillColor borderColor:(UIColor*)dotBorderColor borderWidth:(CGFloat)dotBorderWidth {
    for (int pageIndex = 0; pageIndex < _pageControl.numberOfPages; pageIndex++) {
        UIView* dotView = [_pageControl.subviews objectAtIndex:pageIndex];
        if (_pageControl.currentPage == pageIndex) {
            dotView.backgroundColor = dotFillColor;
            dotView.layer.cornerRadius = dotView.frame.size.height / 2;
        } else {
            dotView.backgroundColor = [UIColor clearColor];
            dotView.layer.cornerRadius = dotView.frame.size.height / 2;
            dotView.layer.borderColor = dotBorderColor.CGColor;
            dotView.layer.borderWidth = dotBorderWidth;
        }
    }
}

Output-

enter image description here

Rohit Khandelwal
  • 1,778
  • 15
  • 23
  • 1
    Is there other way to achieve this? – Emrah Akgül Mar 07 '16 at 11:27
  • 4
    @EmrahAkgül if you are setting the page control in storyboard then try calling the `for` loop after `viewDidAppear`. It worked for me. It wasn't working when I called the loop in `viewDidLoad`. Thanks @RiosK – Rohan Sanap Feb 23 '17 at 05:23
  • 2
    The `borderColor` and `borderWidth` settings are enough. No need for the rest. `backgroundColor` for states can be set using `pageIndicatorTintColor` and `currentPageIndicatorTintColor`. Upvoting this because it helped. Make sure to call after view appears, like @TheRohanSanap mentioned above. – Avi Feb 23 '17 at 13:47
  • @iPeter it is showing in my case. Refer attachment. – Rohit Khandelwal May 16 '18 at 05:36
  • 1
    Iterating on subviews could break anytime if view hierarchy gets changed by an OS update. – Thibaud David Dec 26 '18 at 15:47
  • adding border width to pagecontrol not to dots, if background is other than white. – karthikeyan Jan 04 '21 at 06:50
20

Another approach would be to use a pattern image of the correct size (which currently is 7 points in diameter). Here's what the result looks like:

Pagination dots screenshot

And here's how it's done:

let image = UIImage.outlinedEllipse(size: CGSize(width: 7.0, height: 7.0), color: .darkGray)
self.pageControl.pageIndicatorTintColor = UIColor.init(patternImage: image!)
self.pageControl.currentPageIndicatorTintColor = .darkGray

Which uses this simple little extension to UIImage:

/// An extension to `UIImage` for creating images with shapes.
extension UIImage {

    /// Creates a circular outline image.
    class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {

        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        guard let context = UIGraphicsGetCurrentContext() else {
                return nil
        }

        context.setStrokeColor(color.cgColor)
        context.setLineWidth(lineWidth)
        // Inset the rect to account for the fact that strokes are
        // centred on the bounds of the shape.
        let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
        context.addEllipse(in: rect)
        context.strokePath()

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}

The downside of this is that if the dot size changes in an OS update, the image will look weird as it will be tiled or clipped.

Luke Rogers
  • 2,369
  • 21
  • 28
16

SWIFT 3 Version from @RiosK

func updatePageControl() {
    for (index, dot) in pageControl.subviews.enumerated() {
        if index == pageControl.currentPage {
            dot.backgroundColor = dotColor
            dot.layer.cornerRadius = dot.frame.size.height / 2;
        } else {
            dot.backgroundColor = UIColor.clear
            dot.layer.cornerRadius = dot.frame.size.height / 2
            dot.layer.borderColor = dotColor.cgColor
            dot.layer.borderWidth = dotBorderWidth
        }
    }
}
Jerome
  • 2,114
  • 24
  • 24
8

Swift 4. You can assign borderColor and then observe currentPage property to change dots' border:

class CustomPageControl: UIPageControl {

    var borderColor: UIColor = .clear

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

    func updateBorderColor() {
        subviews.enumerated().forEach { index, subview in
            if index != currentPage {
                subview.layer.borderColor = borderColor.cgColor
                subview.layer.borderWidth = 1
            } else {
                subview.layer.borderWidth = 0
            }
        }
    }

}
Ivan Zinovyev
  • 146
  • 1
  • 4
7

That is not possible with the current properties available for UIPageControl. But you can do by integrating any third party page control which mimic the functionality of iOS UIPageControl.

Other answer has applied a patch. I highly disagreed with that solution.

Hemang
  • 26,840
  • 19
  • 119
  • 186
3

I am using SMPageControl. It's a really awesome Framework written in Objective-C, so it's capable with Swift 2 and Swift 3.

enter image description here

The usage is completely simple:

pod 'SMPageControl'

Then in your PageViewController:

import SMPageControl

class MyController: UIPageViewController {

     var pageControl = SMPageControl()

     override func viewDidLoad() {
        super.viewDidLoad()

        stylePageControl()
    }

private func stylePageControl() {

    pageControl = SMPageControl(frame: CGRect(x: 0, y: self.view.frame.size.height - 50, width: self.view.frame.size.width, height: 50))
    pageControl.numberOfPages = yourPageControllerArray.count

//  the first (first) picture is the item in the bar, that is unused
//  the second (currentFirst) is an item that we use, when this is the current active page
//  in this example, we don't have dots, but we use "pictues" as dots

    let first = UIImage(named: "pageHome")?.imageWithColor(UIColor.grayColor())
    let currentFirst = first?.imageWithColor(UIColor.whiteColor())

    pageControl.setImage(first, forPage: 0)
    pageControl.setCurrentImage(currentFirst, forPage: 0)

    let second = UIImage(named: "pageMusic")?.imageWithColor(UIColor.grayColor())
    let currentSecond = second?.imageWithColor(UIColor.whiteColor())

    pageControl.setImage(second, forPage: 1)
    pageControl.setCurrentImage(currentSecond, forPage: 1)

    pageControl.indicatorMargin = 30.0 // this is the space between the dots

    self.view.addSubview(pageControl)
}

UIImage Extension I've used:

extension UIImage {

    func imageWithColor(color1: UIColor) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
        color1.setFill()

        let context = UIGraphicsGetCurrentContext()! as CGContextRef
        CGContextTranslateCTM(context, 0, self.size.height)
        CGContextScaleCTM(context, 1.0, -1.0);
        CGContextSetBlendMode(context, CGBlendMode.Normal)

        let rect = CGRectMake(0, 0, self.size.width, self.size.height) as CGRect
        CGContextClipToMask(context, rect, self.CGImage!)
        CGContextFillRect(context, rect)

        let newImage = UIGraphicsGetImageFromCurrentImageContext()! as UIImage
        UIGraphicsEndImageContext()

        return newImage
    }

}

The result looks like this:

enter image description here

Now we could of course use colored dots as the images, (blank colored as unused and color-filled as used) then we would have the asked result.

David Seek
  • 16,783
  • 19
  • 105
  • 136
3

Just add this two lines and add the desired image !!

pageControl.currentPageIndicatorTintColor  = UIColor.init(patternImage: UIImage(named: "slider_selected")!)

pageControl.pageIndicatorTintColor =  UIColor.init(patternImage: UIImage(named: "slider")!)
Uma Madhavi
  • 4,851
  • 5
  • 38
  • 73
2

Please try this custom subclassing of UIPageControl. iOS 14 allows setting indicator image with SFSymbol.

class BorderedPageControl: UIPageControl {

    var selectionColor: UIColor = .black
    
    override var currentPage: Int {
        didSet {
            updateBorderColor()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        currentPageIndicatorTintColor = selectionColor
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func updateBorderColor() {
        if #available(iOS 14.0, *) {
            let smallConfiguration = UIImage.SymbolConfiguration(pointSize: 8.0, weight: .bold)
            let circleFill = UIImage(systemName: "circle.fill", withConfiguration: smallConfiguration)
            let circle = UIImage(systemName: "circle", withConfiguration: smallConfiguration)
            for index in 0..<numberOfPages {
                if index == currentPage {
                    setIndicatorImage(circleFill, forPage: index)
                } else {
                    setIndicatorImage(circle, forPage: index)
                }
            }
            pageIndicatorTintColor = selectionColor
        } else {
            subviews.enumerated().forEach { index, subview in
                if index != currentPage {
                    subview.layer.borderColor = selectionColor.cgColor
                    subview.layer.borderWidth = 1
                } else {
                    subview.layer.borderWidth = 0
                }
            }
        }
    }
}
0

Swift 5 version

func customPageControl(dotFillColor: UIColor, dotBorderColor: UIColor, dotBorderWidth: CGFloat) {
    for (pageIndex, dotView) in self.subviews.enumerated() {
        dotView.backgroundColor = currentPage == pageIndex ? dotFillColor : .clear
        dotView.layer.cornerRadius = dotView.frame.size.height / 2
        dotView.layer.borderColor = dotBorderColor.cgColor
        dotView.layer.borderWidth = dotBorderWidth
    }
}
Edgar Georgel
  • 602
  • 1
  • 7
  • 17
0

Using @Luke Rogers answer. I came up with solution for latest iOS versions.

-> Extend UIImage and add these functions:

extension UIImage {

/// Creates a circular outline image.
class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {

    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    guard let context = UIGraphicsGetCurrentContext() else {
            return nil
    }

    context.setStrokeColor(color.cgColor)
    context.setLineWidth(lineWidth)
    // Inset the rect to account for the fact that strokes are
    // centred on the bounds of the shape.
    let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
    context.addEllipse(in: rect)
    context.strokePath()

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}

class func ellipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {

    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    guard let context = UIGraphicsGetCurrentContext() else {
            return nil
    }

    context.setFillColor(color.cgColor)
    context.setLineWidth(lineWidth)
    // Inset the rect to account for the fact that strokes are
    // centred on the bounds of the shape.
    let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
    context.addEllipse(in: rect)
    context.fillPath()

    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}}

Then Extend UIPageControl and add these functions:

extension UIPageControl {
@available(iOS 16.0, *)
func outlinedPageIndicaor(color: UIColor, outlinedColor: UIColor? = nil, count: Int) {
    self.pageIndicatorTintColor =
    outlinedColor != nil ? outlinedColor : color
    self.currentPageIndicatorTintColor = color
    let image = UIImage.outlinedEllipse(size: CGSize(width: 8.0, height: 8.0), color: .red)
    let ellipse = UIImage.ellipse(size: CGSize(width: 9.0, height: 9.0), color: .red)
    self.preferredIndicatorImage = image
    for i in 0..<count {
        self.setCurrentPageIndicatorImage(ellipse, forPage: i)
    }
}

func outlinedPageIndicaor(color: UIColor, outlinedColor: UIColor? = nil, count: Int, index: Int) {
    self.pageIndicatorTintColor =
    outlinedColor != nil ? outlinedColor : color
    self.currentPageIndicatorTintColor = color
    let image = UIImage.outlinedEllipse(size: CGSize(width: 8.0, height: 8.0), color: .red)
    let ellipse = UIImage.ellipse(size: CGSize(width: 9.0, height: 9.0), color: .red)
    self.preferredIndicatorImage = image
    for i in 0..<count {
        if i != index {
            self.setIndicatorImage(image, forPage: i)
        } else {
            self.setIndicatorImage(ellipse, forPage: index)
        }
    }
}}

You can use this if you are using iOS 16 or above in viewDidLoad

pageControl.outlinedPageIndicaor(color: .systemMint, count: count)

// pass same count as number of pages of pageControl

if iOS version is below 16, then use second method:

in viewDidLoad:

pageControl.outlinedPageIndicaor(color: .systemMint, count: count, index: 0)

//initial index is passed as 0 for first page

and when current page changes:

pageControl.outlinedPageIndicaor(color: .systemMint, count: count, index: index)

//pass current page in index

-1

Need to add this in viewDidAppear

 for (int i = 0; i < _pageControl.numberOfPages; i++) {
        UIView* dot = [_pageControl.subviews objectAtIndex:i];
        if (i == _pageControl.currentPage) {
            dot.backgroundColor = [UIColor whiteColor];
            dot.layer.cornerRadius = dot.frame.size.height / 2;
        } else {
            dot.backgroundColor = [UIColor clearColor];
            dot.layer.cornerRadius = dot.frame.size.height / 2;
            dot.layer.borderColor = [UIColor whiteColor].CGColor;
            dot.layer.borderWidth = 1;
        }
    }
Alex_Burla
  • 800
  • 1
  • 9
  • 9
-1

Luke Rogers's solution, written in objective-c:

-(UIImage *) outlinedEllipse:(CGSize)size color: (UIColor*) lineColor width:(CGFloat) lineWidth {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0);

        CGContextRef context = UIGraphicsGetCurrentContext();
        if (context == NULL) {
            return NULL;
        }

        CGContextSetStrokeColorWithColor(context, lineColor.CGColor);
        CGContextSetLineWidth(context, lineWidth);
        // Inset the rect to account for the fact that strokes are
        // centred on the bounds of the shape.

        CGRect rect = CGRectInset(CGRectMake(0, 0, 7.0f, 7.0f), lineWidth * 0.5, lineWidth * 0.5);
        CGContextAddEllipseInRect(context, rect);
        CGContextStrokePath(context);

        UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
}
Elvis Rudonja
  • 389
  • 3
  • 5
-1

Modern Luke Rogers (elk_cloner says it's not modern enough for ios14)

extension UIImage
{
// https://stackoverflow.com/questions/35842040/add-border-for-dots-in-uipagecontrol
    // modernized Luke Rogers
    class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {

        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { context in

            color.setFill()
            
            context.cgContext.setLineWidth(lineWidth)
            // Inset the rect to account for the fact that strokes are
            // centred on the bounds of the shape.
            let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
            context.cgContext.strokeEllipse(in: rect)
        }
        return image
    }
    
}
Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66