Making a multicolor bar in swift - ios

I'm trying to make a line (so basically UIView) that has fixed height and width and is divided to nine segments. I want to be able to control the height of each segment and its color. E.g. I want the first segment be yellow and 30% of the total height of the line, the second to be red and 8% of the total height etc.
I'm not really skilled in Swift, so my solution would be to make 9 UIViews, stack them on top of each other on my storyboard and then manually set the height and background color of every view, so they'd seem like a one multicolored line. Is there cleaner and less bulky solution? Thanks

I would highly recommend using Core Graphics for this.
As the drawing is dead simple (you just want to stack some colored lines within a view), you can easily achieve this by subclassing UIView and overriding drawRect() and drawing them in Core Graphics.
It's certainly a much cleaner solution than adding 9 subviews!
Something like this should achieve the desired result:
class LineView : UIView {
let colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor()]
let values:[CGFloat] = [0.35, 0.45, 0.2]
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]) // set fill color to the given color
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Going further...
You could allow the colors and values properties to be changed from outside the LineView class, allowing for much greater flexibility. You just have to override the didSet to trigger the view to be redrawn when the properties change.
For example:
class LineView : UIView {
/// An array of optional UIColors (clearColor is used when nil is provided) defining the color of each segment.
var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
/// An array of CGFloat values to define how much of the view each segment occupies. Should add up to 1.0.
var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]?.CGColor ?? UIColor.clearColor().CGColor) // set fill color to the given color if it's provided, else use clearColor
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Usage:
let lineView = LineView(frame: CGRectMake(50, 50, 20, view.bounds.size.height-100))
lineView.colors = [
UIColor(red: 1.0, green: 31.0/255.0, blue: 73.0/255.0, alpha: 1.0), // red
UIColor(red:1.0, green: 138.0/255.0, blue: 0.0, alpha:1.0), // orange
UIColor(red: 122.0/255.0, green: 108.0/255.0, blue: 1.0, alpha: 1.0), // purple
UIColor(red: 0.0, green: 100.0/255.0, blue: 1.0, alpha: 1.0), // dark blue
UIColor(red: 100.0/255.0, green: 241.0/255.0, blue: 183.0/255.0, alpha: 1.0), // green
UIColor(red: 0.0, green: 222.0/255.0, blue: 1.0, alpha: 1.0) // blue
]
lineView.values = [0.15, 0.1, 0.35, 0.15, 0.1, 0.15]
view.addSubview(lineView);
(I've only added 6 colors here, but you can add as many as you want).
Full project: https://github.com/hamishknight/Color-Segment-Line-View

I've just realized that this was not what you needed.
I leave the answer anyway so that maybe could be helpful to somebody else in the future.
Make sure your line view has it's own UIView subclass, so that we can override drawRect and achieve your goal.
Then a simple implementation would be:
class BarLine: UIView {
override func drawRect(rect: CGRect) {
//Height of each segment, in percentage
var heights : [CGFloat] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
//Lets create 9 rects and set each rect width to be 1/9th of the view size, then add them to the array
let width : CGFloat = rect.size.width / 9.0
var i : Int = Int()
//Loop to generate 9 segmnets
for (i = 0; i < 9; i++) {
//Each rect origin must be translated by i * width
let origin = CGPointMake(CGFloat(i) * width, rect.height)
//Generate a random color
let color = UIColor(red: heights[i], green: 0.5, blue: 0.5, alpha: 1)
let segment = CGRect(x: origin.x, y: origin.y, width: width, height: -heights[i] * rect.height)
//Set the color
color.set()
//Add the segment to the view by drawing it
UIRectFill(segment)
}
}
}
This will produce something like :
(Remember to set your UIView class to you custom class in IB)
I hope this helped

To make #Hamish code compatible for Swift 5, here the LineView class (and invert width & height in draw->fill to make it horizontal):
import UIKit
public class ColorLineView : UIView {
public var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
public var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func draw(_ rect: CGRect) {
let r = self.bounds
let numberOfSegments = values.count
guard let ctx = UIGraphicsGetCurrentContext() else { return }
var cumulativeValue:CGFloat = 0
for i in 0..<numberOfSegments {
ctx.setFillColor(colors[i]?.cgColor ?? UIColor.clear.cgColor)
ctx.fill(CGRect(x: 0, y: cumulativeValue*r.size.height, width: r.size.width, height: values[i]*r.size.height))
cumulativeValue += values[i]
}
}
}

Related

How to properly generate a random gradient background?

I've been trying to create a function that generates a random gradient background every time a new view controller is presented, as shown with the code provided below, yet I'm unable to get it to properly work. The issue being that the "random" colors are always the same and are generated in the same order.
import Foundation
import UIKit
extension UIView {
func setGradientBackground() {
let redValue = CGFloat(drand48())
let greenValue = CGFloat(drand48())
let blueValue = CGFloat(drand48())
let redValue2 = CGFloat(drand48())
let greenValue2 = CGFloat(drand48())
let blueValue2 = CGFloat(drand48())
let random = UIColor(red: redValue, green: greenValue, blue: blueValue, alpha: 1).cgColor
let random2 = UIColor(red: redValue2, green: greenValue2, blue: blueValue2, alpha: 1).cgColor
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [random, random2]
return layer.insertSublayer(gradient, at: 0)
}
}
If you use drand48, you need to seed it (e.g. with srand48). The quick and dirty solution would be to seed it with some value derived from time from the number of seconds passed since some reference date or the like.
Even better, I’d suggest using CGFloat.random(in: 0...1) instead, which doesn’t require any seeding.
extension UIView {
func addGradientBackground() {
let random = UIColor.random().cgColor
let random2 = UIColor.random().cgColor
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [random, random2]
layer.insertSublayer(gradient, at: 0)
}
}
extension UIColor {
static func random() -> UIColor {
let red = CGFloat.random(in: 0...1)
let green = CGFloat.random(in: 0...1)
let blue = CGFloat.random(in: 0...1)
return UIColor(red: red, green: green, blue: blue, alpha: 1)
}
}
For what it’s worth, we should appreciate that this technique of adding a sublayer is problematic if the view changes size (e.g., you rotate the device, etc.). You’d have to manually hook into viewDidLayoutSubviews or layoutSubviews and adjust this gradient layer frame. I might instead suggest actually having a UIView subclass that does this gradient layer.
#IBDesignable
class RandomGradientView: UIView {
override static var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
}
}
private extension RandomGradientView {
func configure() {
let random = UIColor.random().cgColor
let random2 = UIColor.random().cgColor
gradientLayer.colors = [random, random2]
}
}
Because this UIView subclass actually specifies the layer class to be used for the main backing layer, and because that main layer always resizes automatically as the view changes size, then it will gracefully handle any size changes (including animated ones).
All of the having been said, I appreciate the convenience of the extension (you can apply it to existing UIView subclasses), but you have to offset that with the more cumbersome process of handling frame size changes. It’s your choice, but I wanted to provide the alternative.

Status Bar clock has been truncated on iPhoneX

After adding a gradient layer to the statusbar and make it light or dark for different view controllers, at some random point in navigation, my clock in the status bar of notch iPhones has become truncated. like 1... or ...
I have tried these two solutions for make statusbar content white; But this has no effect on this random clock behaviour.
this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barStyle = .blackOpaque
self.setNeedsStatusBarAppearanceUpdate()
}
or this:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Both of them changes my status bar color of content. Any idea about what is cause of this behavior???
and this is how I make the status bar gradient:
extension UIViewController {
func makeStatusBarGradient(){
let statusBarView = UIView(frame: UIApplication.shared.statusBarFrame)
let gradientLayer1 = CAGradientLayer()
gradientLayer1.frame = statusBarView.frame
gradientLayer1.colors = [UIColor.APColors.redGradient.cgColor, UIColor.APColors.orangeGradient.cgColor]
gradientLayer1.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer1.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer1.cornerRadius = 0
gradientLayer1.zPosition = -10
gradientLayer1.name = "gradient"
//Change status bar color
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
statusBar.layer.addSublayer(gradientLayer1)
}
setNeedsStatusBarAppearanceUpdate()
}
func clearStatusBarGradient(){
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
if let layers = statusBar.layer.sublayers?.filter({$0.name == "gradient"}){
if (layers.count > 0){
layers.first?.removeFromSuperlayer()
}
}
}
setNeedsStatusBarAppearanceUpdate()
}
}
and, the APColors are:
extension UIColor {
struct APColors {
static var redGradient : UIColor { return UIColor(red: 1.00, green: 0.42, blue: 0.24, alpha: 1) }
static var orangeGradient : UIColor { return UIColor(red: 0.95, green: 0.19, blue: 0.42, alpha: 1)}
}
}
You are using extension on UILabel, which override it's intrinsicContentSize. (or maybe you swizzling this method) So status bar clock label trigger it and got wrong intrinsicContentSize. Try to fix textSize calculation in this overrided method, or avoid overriding it if possible.
I can guess you're using UILabel+Padding from popular SO answer, so check if you've got this:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
}
if you do, then simply change it:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
} else {
return contentSize
}
As i do not know what's the APColor, so I changed the line with below & everything is working fine.
gradientLayer1.colors = [UIColor.red.cgColor, UIColor.orange.cgColor]
Try to change the same line and check your issue is solved or not. Most probably, APColor does not caused the issue.
Check is their any multiple stuffs is in you statusbar is available or not. e.g. indicator etc.
If you added anything else in your status bar then let me know.
Output

Adding border with width to UIView show small background outside

I'm trying to add circle border to a UIView with green background, I created simple UIView subclass with borderWidth, cornerRadius and borderColor properties and I'm setting it from storyboard.
#IBDesignable
class RoundedView: UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable var borderColor: UIColor {
get {
if let color = layer.borderColor {
return UIColor(cgColor: color)
} else {
return UIColor.clear
}
}
set {
layer.borderColor = newValue.cgColor
}
}
}
But when I compile and run an app or display it in InterfaceBuilder I can see a line outside the border that is still there (and is quite visible on white background).
This RoundedView with green background, frame 10x10, corner radius = 5 is placed in corner of plain UIImageView (indicates if someone is online or not). You can see green border outside on both UIImageView and white background.
Can you please tell me what's wrong?
What you are doing is relying on the layer to draw your border and round the corners. So you are not in charge of the result. You gave it a green background, and now you are seeing the background "stick out" at the edge of the border. And in any case, rounding the corners is a really skanky and unreliable way to make a round view. To make a round view, make a round mask.
So, the way to make your badge is to take complete charge of what it is drawn: you draw a green circle in the center of a white background, and mask it all with a larger circle to make the border.
Here is a Badge view that will do precisely what you're after, with no artifact round the outside:
class Badge : UIView {
class Mask : UIView {
override init(frame:CGRect) {
super.init(frame:frame)
self.isOpaque = false
self.backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fillEllipse(in: CGRect(origin:.zero, size:rect.size))
}
}
let innerColor : UIColor
let outerColor : UIColor
let innerRadius : CGFloat
var madeMask = false
init(frame:CGRect, innerColor:UIColor, outerColor:UIColor, innerRadius:CGFloat) {
self.innerColor = innerColor
self.outerColor = outerColor
self.innerRadius = innerRadius
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.setFillColor(outerColor.cgColor)
con.fill(rect)
con.setFillColor(innerColor.cgColor)
con.fillEllipse(in: CGRect(
x: rect.midX-innerRadius, y: rect.midY-innerRadius,
width: 2*innerRadius, height: 2*innerRadius))
if !self.madeMask {
self.madeMask = true // do only once
self.mask = Mask(frame:CGRect(origin:.zero, size:rect.size))
}
}
}
I tried this with a sample setting as follows:
let v = Badge(frame: CGRect(x:100, y:100, width:16, height:16),
innerColor: .green, outerColor: .white, innerRadius: 5)
self.view.addSubview(v)
It looks fine. Adjust the parameters as desired.
I solved this by using a UIBezierPath and adding to the view's layer:
let strokePath = UIBezierPath(roundedRect: view.bounds, cornerRadius: view.frame.width / 2)
let stroke = CAShapeLayer()
stroke.frame = bounds
stroke.path = strokePath.cgPath
stroke.fillColor = .green.cgColor
stroke.lineWidth = 1.0
stroke.strokeColor = .white.cgColor
view.layer.insertSublayer(stroke, at: 2)
I solved this problem with gradients.
Just seting the backgroundColor of your circle as gradient.
let gradientLayer = CAGradientLayer()
//define colors
gradientLayer.colors = [<<your_bgc_color>>>>, <<border__bgc__color>>]
//define locations of colors as NSNumbers in range from 0.0 to 1.0
gradientLayer.locations = [0.0, 0.7]
//define frame
gradientLayer.frame = self.classView.bounds
self.classView.layer.insertSublayer(gradientLayer, at: 0)
MyImage
An easier fix might be to just mask it like this:
let mask = UIView()
mask.backgroundColor = .black
mask.frame = yourCircleView.bounds.inset(by: UIEdgeInsets(top: 0.1, left: 0.1, bottom: 0.1, right: 0.1))
mask.layer.cornerRadius = mask.height * 0.5
yourCircleView.mask = mask

Making UIProgressView Rounded corners

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.
But if progress value is less then 1. Corners are not rounded as it should be.
Am I missing something ? How can I make it rounded corner ?
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.
Just do this in init
layer.cornerRadius = *desired_corner_radius*
clipsToBounds = true
It's very late to answer but actually I had the same problem.
Here my simplest solution (no code needed !) :
Add a container to embed your progress view
Round corner for your container (10 = height of container / 2)
The result :)
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.
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.
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
}
}
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/
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()
}
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.
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
//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()
}
}
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
}()
Set line cap :
.lineCap = kCALineCapRound;

Creating circle and using it as "bar graph"

I want to create a circle with an inner circle that looks like the image below. I'm having trouble with the inner circle and I don't know how to create it so it's easy to adjust percentage (like the image is showing).
So far I have this CircleGraph class which can draw the ouster circle and an inner circle which can only draw 50 %.
import Foundation
import UIKit
class CircleGraph: UIView
{
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
super.drawRect(rect)
// Outer circle
Colors().getMainColor().setFill()
let outerPath = UIBezierPath(ovalInRect: rect)
outerPath.fill()
// inner circle so far
let percentage = 0.5
UIColor.whiteColor().setFill()
let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.height/2,y: rect.height/2), radius: CGFloat(rect.height/2), startAngle: CGFloat(-M_PI_2), endAngle:CGFloat(M_PI * 2 * percentage - M_PI_2), clockwise: true)
circlePath.fill()
}
}
Can anyone assist me?
What I want is something simliar to the image below:
I would go for the easy solution and create a UIView with a UIView and UILabel as subviews. If you use something like:
// To make it round
let width = self.frame.width
self.view.layer.cornerRadius = width * 0.5
self.view.layer.masksToBounds = true
for each of the sublayers. If you have set the background colour of the UIView's background layer to something like Red and the UIView layer above to have a whiteish background colour with alpha 0.5 than you already achieve this effect.
If you do not know how to proceed with this tip ill try to provide a code sample.
-- EDIT --
Here is the code sample:
import UIKit
class CircleView: UIView {
var percentage : Int?
var transparency : CGFloat?
var bottomLayerColor : UIColor?
var middleLayerColor : UIColor?
init(frame : CGRect, percentage : Int, transparency : CGFloat, bottomLayerColor : UIColor, middleLayerColor : UIColor) {
super.init(frame : frame)
self.percentage = percentage
self.transparency = transparency
self.bottomLayerColor = bottomLayerColor
self.middleLayerColor = middleLayerColor
viewDidLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
viewDidLoad()
}
func viewDidLoad() {
let width = self.frame.width
let height = self.frame.height
let textFrame = CGRectMake(0, 0, width, height)
guard let percentage = self.percentage
else {
print("Error")
return
}
let newHeight = (CGFloat(percentage)/100.0)*height
let middleFrame = CGRectMake(0,height - newHeight, width, newHeight)
// Set Background Color
if let bottomLayerColor = self.bottomLayerColor {
self.backgroundColor = bottomLayerColor
}
// Make Bottom Layer Round
self.layer.cornerRadius = width * 0.5
self.layer.masksToBounds = true
// Create Middle Layer
let middleLayer = UIView(frame: middleFrame)
if let middleLayerColor = self.middleLayerColor {
middleLayer.backgroundColor = middleLayerColor
}
if let transparency = self.transparency {
middleLayer.alpha = transparency
}
// The Label
let percentageLayer = UILabel(frame: textFrame)
percentageLayer.textAlignment = NSTextAlignment.Center
percentageLayer.textColor = UIColor.whiteColor()
if let percentage = self.percentage {
percentageLayer.text = "\(percentage)%"
}
// Add Subviews
self.addSubview(middleLayer)
self.addSubview(percentageLayer)
}
}
To use in a View Controller:
let redColor = UIColor.redColor()
let blueColor = UIColor.blueColor()
let frame = CGRectMake(50, 50, 100, 100)
// 50% Example
let circleView = CircleView(frame: frame, percentage: 50, transparency: 0.5, bottomLayerColor: redColor, middleLayerColor: blueColor)
self.view.addSubview(circleView)
// 33% Example
let newFrame = CGRectMake(50, 150, 120, 120)
let newCircleView = CircleView(frame: newFrame, percentage: 33, transparency: 0.7, bottomLayerColor: UIColor.redColor(), middleLayerColor: UIColor.whiteColor())
self.view.addSubview(newCircleView)
This will yield something like this:

Resources