skip to Main Content

So I am currently trying to add a image inside the circularShapeLayer in the below code.

Here is the full code:

import Foundation
import UIKit

@IBDesignable
class PlainHorizontalProgressBar: UIView {
    @IBInspectable var color: UIColor = .gray {
        didSet { setNeedsDisplay() }
    }

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

    var circularShapeColor: UIColor = .blue {
        didSet { setNeedsDisplay() }
    }

    private let progressLayer = CALayer()
    private let backgroundMask = CAShapeLayer()
    private let circularShapeLayer = CAShapeLayer()
    
    // Create an UIImageView for the system image
    private let systemImageView = UIImageView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        circularShapeColor = UIColor.systemOrange
        setupLayers()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        circularShapeColor = UIColor.systemOrange
        setupLayers()
    }

    private func setupLayers() {
        layer.addSublayer(progressLayer)
        layer.addSublayer(circularShapeLayer)
        
        // Add the system image view to the circularShapeLayer
        circularShapeLayer.addSublayer(systemImageView.layer)
    }

    override func draw(_ rect: CGRect) {
        backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.75).cgPath
        layer.mask = backgroundMask
        
        let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
        
        progressLayer.frame = progressRect
        progressLayer.backgroundColor = color.cgColor
        
        // Calculate the position and size of the circular shape
        let circularSize = CGSize(width: rect.height, height: rect.height)
        let circularOrigin = CGPoint(x: progressRect.maxX - circularSize.width / 2.0, y: (rect.height - circularSize.height) / 2.0)
        let circularPath = UIBezierPath(ovalIn: CGRect(origin: circularOrigin, size: circularSize))
        circularShapeLayer.path = circularPath.cgPath
        
        // Set the frame and image for the system image view
        systemImageView.frame = circularShapeLayer.bounds
        systemImageView.image = UIImage(systemName: "bolt.fill") // Set the system image
        systemImageView.tintColor = .white // Set the image color
    }
}

But somehow the image is not showing. I have tried changing the tintColor of the image, and tried with other images also. What am I doing wrong? Any suggestions or ideas on how can I accomplish this?

2

Answers


  1. A couple of things:

    Don’t override draw(_:) if you’re constructing a view out of layers.

    You can’t install a view’s content layer as a sublayer of another layer. Use primitive layers. You can install an image as the contents of a "regular" CALayer, and then install that layer as a sublayer of your shape layer. That would work.

    Login or Signup to reply.
  2. Instead of overriding draw(_:), let’s manipulate layers.

    First note… we probably don’t want to mask the "base layer" (the entire view) because we might want to – at some point – "fancy it up" a little bit.

    And, for example, we would want:

    enter image description here

    So let’s use:

    private let backgroundLayer = CALayer()
    private let backgroundMaskLayer = CAShapeLayer()
    private let progressLayer = CALayer()
    
    // Create an UIImageView for the system image
    private let systemImageView = UIImageView()
    

    and we will add progressLayer as a sublayer of backgroundLayer, then mask the backgroundLayer.

    We’ll also add systemImageView as a subview instead of a layer.

    We’ll do the main setup like this:

    private func setupLayers() {
        
        layer.addSublayer(backgroundLayer)
        backgroundLayer.addSublayer(progressLayer)
        backgroundLayer.mask = backgroundMaskLayer
    
        backgroundLayer.backgroundColor = baseColor.cgColor
        
        systemImageView.image = UIImage(systemName: "bolt.fill") // Set the system image
        systemImageView.tintColor = .white // Set the image color
        
        addSubview(systemImageView)
        
        backgroundColor = .clear
        
    }
    

    Instead of overriding draw(_:) we’ll update everything in layoutSubviews():

    override func layoutSubviews() {
        super.layoutSubviews()
        
        // colors may have been changed after init
        backgroundLayer.backgroundColor = baseColor.cgColor
        progressLayer.backgroundColor = color.cgColor
        systemImageView.backgroundColor = circularShapeColor
    
        backgroundLayer.frame = bounds
        backgroundMaskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height * 0.75).cgPath
        
        let progressRect = CGRect(origin: .zero, size: CGSize(width: bounds.width * progress, height: bounds.height))
        progressLayer.frame = progressRect
        
        // Calculate the position and size of the circular shape
        
        let w: CGFloat = bounds.height
        let circularSize = CGSize(width: w, height: w)
        
        let circularOrigin = CGPoint(x: progressRect.maxX - w / 2.0, y: progressRect.midY - (w / 2.0))
        
        // Set the frame and image for the system image view
        systemImageView.frame = .init(origin: circularOrigin, size: circularSize)
        systemImageView.layer.cornerRadius = w * 0.5
    }
    

    That will give us this:

    enter image description here

    and we’re looking pretty good.

    Except… if we’re close to 0% or 100% the position of the circular-view won’t look great:

    enter image description here

    We’ll "fix" that…

    Let’s calculate the "progress range" – inset on each end by one-half of the circular-view width:

    enter image description here

    and modify our positioning calculations:

        let w: CGFloat = bounds.height
        let circularSize = CGSize(width: w, height: w)
        
        // we want 0% to have the circle-image center at leading Plus one-half circle-width
        // we want 100% to have the circle-image center at trailing Minus one-half circle-width
        let progressRange: CGFloat = (bounds.width - w)
        
        // calculate the circle-image centerX
        let circleCenterX: CGFloat = w * 0.5 + progressRange * progress
        
        let progressRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: circleCenterX, height: bounds.height))
        
        progressLayer.frame = progressRect
        
        let circularOrigin = CGPoint(x: circleCenterX - w / 2.0, y: progressRect.midY - (w / 2.0))
        
        // Set the frame for the system image view
        systemImageView.frame = CGRect(origin: circularOrigin, size: circularSize)
        
    

    and it’s looking better:

    enter image description here

    Here’s your complete class, with all of those modifications:

    @IBDesignable
    class PlainHorizontalProgressBar: UIView {
        
        @IBInspectable
        // default: very light gray
        var baseColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {
            didSet { setNeedsLayout() }
        }
        
        @IBInspectable
        // default: gray
        var color: UIColor = .gray {
            didSet { setNeedsLayout() }
        }
        
        @IBInspectable
        // default: orange
        var circularShapeColor: UIColor = .orange {
            didSet { setNeedsLayout() }
        }
        
        @IBInspectable
        // default: 0.0
        var progress: CGFloat = 0.0 {
            didSet { setNeedsLayout() }
        }
        
        private let backgroundLayer = CALayer()
        private let backgroundMaskLayer = CAShapeLayer()
        
        private let progressLayer = CALayer()
        
        // Create an UIImageView for the system image
        private let systemImageView = UIImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupLayers()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setupLayers()
        }
        
        private func setupLayers() {
            
            layer.addSublayer(backgroundLayer)
            backgroundLayer.addSublayer(progressLayer)
            backgroundLayer.mask = backgroundMaskLayer
            
            backgroundLayer.backgroundColor = baseColor.cgColor
            
            systemImageView.image = UIImage(systemName: "bolt.fill") // Set the system image
            systemImageView.tintColor = .white // Set the image color
            
            addSubview(systemImageView)
            
            backgroundColor = .clear
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            // colors may have been changed after init
            backgroundLayer.backgroundColor = baseColor.cgColor
            progressLayer.backgroundColor = color.cgColor
            systemImageView.backgroundColor = circularShapeColor
            
            backgroundLayer.frame = bounds
            backgroundMaskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height * 0.75).cgPath
            
            // Calculate the position and size of the circular shape
            
            let w: CGFloat = bounds.height
            let circularSize = CGSize(width: w, height: w)
            
            // we want 0% to have the circle-image center at leading Plus one-half circle-width
            // we want 100% to have the circle-image center at trailing Minus one-half circle-width
            let progressRange: CGFloat = (bounds.width - w)
            
            // calculate the circle-image centerX
            let circleCenterX: CGFloat = w * 0.5 + progressRange * progress
            
            let progressRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: circleCenterX, height: bounds.height))
            
            progressLayer.frame = progressRect
            
            let circularOrigin = CGPoint(x: circleCenterX - w / 2.0, y: progressRect.midY - (w / 2.0))
            
            // Set the frame for the system image view
            systemImageView.frame = CGRect(origin: circularOrigin, size: circularSize)
            
            // let's use .layer.cornerRadius instead of another layer mask
            systemImageView.layer.cornerRadius = systemImageView.frame.width * 0.5
            
        }
    
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search