Emrah Akgül
Emrah Akgül

Reputation: 630

Add border for dots in UIPageControl

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 :)

Upvotes: 22

Views: 23567

Answers (13)

Somi Malik
Somi Malik

Reputation: 1

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

Upvotes: 0

Sunny Summer
Sunny Summer

Reputation: 41

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
                }
            }
        }
    }
}

Upvotes: 2

Anton Tropashko
Anton Tropashko

Reputation: 5806

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
    }
    
}

Upvotes: -1

Elvis Rudonja
Elvis Rudonja

Reputation: 419

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;
}

Upvotes: -1

Edgar Georgel
Edgar Georgel

Reputation: 602

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
    }
}

Upvotes: 0

Ivan Zinovyev
Ivan Zinovyev

Reputation: 146

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
            }
        }
    }

}

Upvotes: 8

Arnab Ghoshal
Arnab Ghoshal

Reputation: 49

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")!)

Upvotes: 2

Rohit Khandelwal
Rohit Khandelwal

Reputation: 1778

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

Upvotes: 27

Alex_Burla
Alex_Burla

Reputation: 810

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;
        }
    }

Upvotes: -1

Luke Rogers
Luke Rogers

Reputation: 2418

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.

Upvotes: 20

Jerome
Jerome

Reputation: 2152

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
        }
    }
}

Upvotes: 16

David Seek
David Seek

Reputation: 17122

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.

Upvotes: 3

Hemang
Hemang

Reputation: 27050

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.

Upvotes: 7

Related Questions