21

I have created a UIProgressView with following properties

progressView.progressTintColor = UIColor.appChallengeColorWithAlpha(1.0)
progressView.trackTintColor = UIColor.clearColor()
progressView.clipsToBounds = true
progressView.layer.cornerRadius = 5

I am using a UIView for border. It appears like his progress = 1, which is exactly the way I want.

enter image description here

But if progress value is less then 1. Corners are not rounded as it should be.

enter image description here

Am I missing something ? How can I make it rounded corner ?

Umair Afzal
  • 4,947
  • 5
  • 25
  • 50

13 Answers13

30

UIProgressView has two part, progress part and track part. If you use Reveal, you can see it only has two subviews. The progress view hierarchy is very simple. so...

Objective-C

- (void)layoutSubviews
{
    [super layoutSubviews];
    [self.progressView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        obj.layer.masksToBounds = YES;
        obj.layer.cornerRadius = kProgressViewHeight / 2.0;
    }];
}

Swift (3, 4 and 5+)

override func layoutSubviews() {
    super.layoutSubviews()
    subviews.forEach { subview in
        subview.layer.masksToBounds = true
        subview.layer.cornerRadius = kProgressViewHeight / 2.0
    }
}

I admit subclass or extend progressView is the recommended way. In case of you don't want to do that for such a simple effect, this may do the trick. Keep the situation that Apple will change the view hierarchy, and something may go wrong in mind.

Jamil Hasnine Tamim
  • 4,389
  • 27
  • 43
ooops
  • 761
  • 8
  • 18
7

Just do this in init

    layer.cornerRadius = *desired_corner_radius*
    clipsToBounds = true
Manicek
  • 71
  • 1
  • 3
7

It's very late to answer but actually I had the same problem.

Here my simplest solution (no code needed !) :

  1. Add a container to embed your progress view

Container

  1. Round corner for your container (10 = height of container / 2)

RoundCorner

  1. The result :)

Tada!

Jordan Montel
  • 8,227
  • 2
  • 35
  • 40
6

After searching and trying I decided to create my own custom progress view. Here is the code for anyone who may find them selevs in same problem.

import Foundation
import UIKit

class CustomHorizontalProgressView: UIView {
var progress: CGFloat = 0.0 {

    didSet {
        setProgress()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)

    setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    setup()
}

func setup() {
    self.backgroundColor = UIColor.clearColor()
}

override func drawRect(rect: CGRect) {
    super.drawRect(rect)

    setProgress()
}

func setProgress() {
    var progress = self.progress
    progress = progress > 1.0 ? progress / 100 : progress

    self.layer.cornerRadius = CGRectGetHeight(self.frame) / 2.0
    self.layer.borderColor = UIColor.grayColor().CGColor
    self.layer.borderWidth = 1.0

    let margin: CGFloat = 6.0
    var width = (CGRectGetWidth(self.frame) - margin)  * progress
    let height = CGRectGetHeight(self.frame) - margin

    if (width < height) {
        width = height
    }

    let pathRef = UIBezierPath(roundedRect: CGRect(x: margin / 2.0, y: margin / 2.0, width: width, height: height), cornerRadius: height / 2.0)

    UIColor.redColor().setFill()
    pathRef.fill()

    UIColor.clearColor().setStroke()
    pathRef.stroke()

    pathRef.closePath()

    self.setNeedsDisplay()
}
}

Just put above code in a swift file and drag drop a UIView in IB and give it class CustomHorizontalProgressView. and That is it.

Umair Afzal
  • 4,947
  • 5
  • 25
  • 50
  • It's wrong to call setNeedsDisplay in drawRect (now it's done indirectly). What's needed - call setNeedsDisplay which triggers drawRect. – Valeriy Van Oct 19 '17 at 15:44
  • Great Solution, but gives error saying Invalid context. You should put the code in setProgress in drawRect() method instead. And call setNeedsDisplay() In didSet progress. setNeedsDisplay will call drawRect() method. – Sagar D Mar 12 '18 at 12:02
5

Another answer to throw in the mix, super hacky but very quick to use.

You can just grab the sublayer and set its radius. No need to write your own UIProgressView or mess with clip paths.

progressView.layer.cornerRadius = 5
progressView.layer.sublayers[1].cornerRadius = 5
progressView.subviews[1]. clipsToBounds = true
progressView.layer.masksToBounds = true

So you round the corner of your overall UIProgressView (no need for ClipsToBounds) Then the fill bar is the 2nd sublayer, so you can grab that and round its Corners, but you also need to set the subview for that layer to clipsToBounds.

Then set the overall layer to mask to its bounds and it all looks good.

Obviously, this is massively reliant on the setup of UIProgressView not changing and the 2nd subview/layer being the fill view.

But. If you're happy with that assumption, super easy code wise to use.

Iain Stanford
  • 606
  • 1
  • 6
  • 18
  • Actually, I take this answer back a bit - didn't realise rgerioda suggested essentially the same thing, missed that answer when I first started looking here – Iain Stanford Apr 16 '20 at 16:11
4

Basically progress view's (Default Style) subviews consist of 2 image view. One for the "progress", and one for the "track". You can loop the subviews of progress view, and set the each of image view's corner radius.

for let view: UIView in self.progressView.subviews {
    if view is UIImageView {
        view.clipsToBounds = true
        view.layer.cornerRadius = 15
    }
}
rgerioda
  • 51
  • 4
2

Yes ,one thing is missed...corner radius is set to progressview and it is reflecting as expected..

But if you want your track image to be rounded you have to customise your progressview. You have to use image with rounded corner.

[progressView setTrackImage:[UIImage imageNamed:@"roundedTrack.png"]];
//roundedTrack.png must be of rounded corner

This above code will help you to change image of trackView for your progressview.

You may face the inappropriate stretching of image. You have to make your image resizable. May be the link below will be useful if issue arise https://www.natashatherobot.com/ios-stretchable-button-uiedgeinsetsmake/

Devang Tandel
  • 2,988
  • 1
  • 21
  • 43
2
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let v = ProgessView(frame: CGRect(x: 20, y: 200, width: 100, height: 10))
        view.addSubview(v)

        //v.progressLayer.strokeEnd = 0.8

    }
}

class ProgessView: UIView {

    lazy var progressLayer: CAShapeLayer = {
        let line = CAShapeLayer()
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 5, y: 5))
        path.addLine(to: CGPoint(x: self.bounds.width - 5, y: 5))
        line.path = path.cgPath
        line.lineWidth = 6
        line.strokeColor = UIColor(colorLiteralRed: 127/255, green: 75/255, blue: 247/255, alpha: 1).cgColor
        line.strokeStart = 0
        line.strokeEnd = 0.5
        line.lineCap = kCALineCapRound
        line.frame = self.bounds
        return line
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.white
        layer.cornerRadius = 5
        layer.borderWidth = 1
        layer.borderColor = UIColor(colorLiteralRed: 197/255, green: 197/255, blue: 197/255, alpha: 1).cgColor
        layer.addSublayer(progressLayer)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Test my codes. You can design the height and the width as your want. You can use strokeEnd to change the progress of the progressView. You can add an animation to it. But actually, it is already animatable, you can change the value of the strokeEnd to see its primary effect. If you want to design your own animation. Try CATransaction like below.

func updateProgress(_ progress: CGFloat) {
        CATransaction.begin()
        CATransaction.setAnimationDuration(3)
        progressLayer.strokeEnd = progress
        CATransaction.commit()
    }
jhd
  • 1,243
  • 9
  • 21
0

I had this exact same problem, which is what led me to your question after googling like crazy. The problem is two-fold. First, how to make the inside of the progress bar round at the end (which 季亨达's answer shows how to do), and secondly, how to make the round end of the CAShapeLayer you added match up with the square end of the original progress bar underneath (the answer to this other StackOverflow question helped with that How to get the exact point of objects in swift?) If you replace this line of code in 季亨达's answer:

path.addLine(to: CGPoint(x: self.bounds.width - 5, y: 5))

with this:

path.addLine(to: CGPoint(x: (Int(self.progress * Float(self.bounds.width))), y: 5))

you will hopefully get the result you're looking for.

Community
  • 1
  • 1
0

With swift 4.0 I'm doing in this way:

let progressViewHeight: CGFloat = 4.0

// Set progress view height
let transformScale = CGAffineTransform(scaleX: 1.0, y: progressViewHeight)
self.progressView.transform = transformScale

// Set progress round corners
self.progressView.layer.cornerRadius = progressViewHeight
self.progressView.clipsToBounds = true
Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146
0

//Updated for swift 4

import Foundation
import UIKit

class CustomHorizontalProgressView: UIView {
var progress: CGFloat = 0.0 {

    didSet {
        setProgress()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)

    setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    setup()
}

func setup() {
    self.backgroundColor = UIColor.clear
}

override func draw(_ rect: CGRect) {
    super.draw(rect)

    setProgress()
}

func setProgress() {
    var progress = self.progress
    progress = progress > 1.0 ? progress / 100 : progress

    self.layer.cornerRadius = self.frame.height / 2.0
    self.layer.borderColor = UIColor.gray.cgColor
    self.layer.borderWidth = 1.0

    let margin: CGFloat = 6.0
    var width = (self.frame.width - margin)  * progress
    let height = self.frame.height - margin

    if (width < height) {
        width = height
    }

    let pathRef = UIBezierPath(roundedRect: CGRect(x: margin / 2.0, y: margin / 2.0, width: width, height: height), cornerRadius: height / 2.0)

    UIColor.red.setFill()
    pathRef.fill()

    UIColor.clear.setStroke()
    pathRef.stroke()

    pathRef.close()

    self.setNeedsDisplay()
 }
}
Robert
  • 5,278
  • 43
  • 65
  • 115
Amit
  • 1
0

Swift 4.2 version from Umair Afzal's solution

class CustomHorizontalProgressView: UIView {

var strokeColor: UIColor?

var progress: CGFloat = 0.0 {
    didSet {
        setNeedsDisplay()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
}

func setup() {
    self.backgroundColor = UIColor.clear
}

override func draw(_ rect: CGRect) {
    super.draw(rect)
    setProgress()
}

func setProgress() {

    var progress = self.progress
    progress = progress > 1.0 ? progress / 100 : progress

    self.layer.cornerRadius = frame.size.height / 2.0
    self.layer.borderColor = UIColor.gray.cgColor
    self.layer.borderWidth = 1.0

    let margin: CGFloat = 6.0
    var width = (frame.size.width - margin)  * progress
    let height = frame.size.height - margin

    if (width < height) {
        width = height
    }

    let pathRef = UIBezierPath(roundedRect: CGRect(x: margin / 2.0, y: margin / 2.0, width: width, height: height), cornerRadius: height / 2.0)

    strokeColor?.setFill()

    pathRef.fill()
    UIColor.clear.setStroke()
    pathRef.stroke()
    pathRef.close()
}
}

And to use it

var progressView: CustomHorizontalProgressView = {
    let view = CustomHorizontalProgressView()
    view.strokeColor = UIColor.orange
    view.progress = 0.5
    return view
}()
Kelvin Fok
  • 621
  • 7
  • 9
-1

Set line cap :

.lineCap = kCALineCapRound;

sashi_bhushan
  • 394
  • 3
  • 16