Implement a Image Loader with CAShapeLayer

Posted By : Aditya Kumar Sharma | 04-May-2016
  • Open Xcode and create a new Project, choose the template Single View Application, enter the ProjectName( ImageLoaderExample) and click Next.
  • Now add an UIImageView to viewcontroller through Storyboard.
  • Create a file CustomImageDisplayView with subclass UIImageView.
  • Now go to storyboard and  click ImageView, In attribute Inspector choose custom class and assign it - CustomImageDisplayView name.
  • Add ImageIO framework and SDWebImage files in the project.
  • Open CustomImageDisplayView file and add following lines:
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let url = NSURL(string: "")
    sd_setImageWithURL(url, placeholderImage: nil, options: .CacheMemoryOnly, progress: {
      [weak self]
      (receivedSize, expectedSize) -> Void in
        //Update progress here 
      }) {
        [weak self]
        (image, error, _, _) -> Void in
        //Reveal image here
  • Now create a file (DisplayingCircularLoaderView) with subclass of UIView and  add following code:
    let circularPathLayer = CAShapeLayer()
    let circleRadius: CGFloat = 20.0
    override init(frame: CGRect) {
        super.init(frame: frame)
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    func configure() {
        progress = 0

        circularPathLayer.frame = bounds
        circularPathLayer.lineWidth = 3
        circularPathLayer.fillColor = UIColor.clearColor().CGColor
        circularPathLayer.strokeColor = UIColor.blueColor().CGColor
        circularPathLayer.lineCap = kCALineCapRound;
        backgroundColor = UIColor.whiteColor()

Section A - circularPathLayer is the circularPath and cirleRadius defines the radius of loader which will be shown.

Section B -  Both initializers are having configure(). configure() sets up the line width of 3 , clear fill color , gives blue stroke color and lineCap adds rounded progress line. Then it add shape layer as a sublayer to the main view and make the background white so that when loader is running the background is blanked out.

  • Now add following line of code in DisplayingCircularLoaderView:
    func circularFrame() -> CGRect {
        var circularFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
        circularFrame.origin.x = CGRectGetMidX(circularPathLayer.bounds) - CGRectGetMidX(circularFrame)
        circularFrame.origin.y = CGRectGetMidY(circularPathLayer.bounds) - CGRectGetMidY(circularFrame)
        return circularFrame
    func circularPath() -> UIBezierPath {
        return UIBezierPath(ovalInRect: circularFrame())
    override func layoutSubviews() {
        circularPathLayer.frame = bounds
        circularPathLayer.path = circularPath().CGPath

Section C - The circularFrame() returns the CGRect which bounds the frame of  indicator's path.  Width and height of circularFrame is twice the circleRadius and lies in center of the view.

Section D - This returns UIBezierPath which is bounded by the circularFrame () 

Section E - The layoutSubviews() update the circlePathLayers frame as the view size changes.

  • Now open CustomImageDisplayView and add following codes:
   let progressIndicatorView = DisplayingCircularLoaderView(frame: CGRectZero)
  • Add these lines in init(coder: ) before the url is defined:
    progressIndicatorView.frame = bounds
    progressIndicatorView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] 

This will add progressIndicator view as a subview to the customImageview

  • Now add following code in DisplayingCircularLoaderView which has custom getter and setter, the getter returns circlePathLayer.strokeEnd and setter changes between o and 1 and sets the strokeEnd accordingly.
   var progress: CGFloat {
        get {
            return circularPathLayer.strokeEnd
        set {
            if (newValue > 1) {
                circularPathLayer.strokeEnd = 1
            } else if (newValue < 0) {
                circularPathLayer.strokeEnd = 0
            } else {
                circularPathLayer.strokeEnd = newValue
  • Open CustomImageDisplayView and replace the comment Update progress here with:
     self!.progressIndicatorView.progress = CGFloat(receivedSize)/CGFloat(expectedSize)

This calculates the size of image downloaded and accordingly the progress indicator view is managed.

  • Add following code in DisplayingCircularLoaderView :
  func reveal() {
        backgroundColor = UIColor.clearColor()
        progress = 1
        superview?.layer.mask = circularPathLayer

This method clears the view's background so that image behind the view is not remain hidden, removes the implicit animation for strokeEnd and remove circularPathLayer from its layer.

  • Now add this line in the CustomImageDisplayView :
  • Open reveal() in DisplayingCircularLoaderView and add following lines of code to it:
        let center = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds))
        let finalRadius = sqrt((center.x*center.x) + (center.y*center.y))
        let radiusInset = finalRadius - circleRadius
        let outerRect = CGRectInset(circularFrame(), -radiusInset, -radiusInset)
        let toPath = UIBezierPath(ovalInRect: outerRect).CGPath
        let fromPath = circularPathLayer.path
        let fromLineWidth = circularPathLayer.lineWidth
        CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
        circularPathLayer.lineWidth = 2*finalRadius
        circularPathLayer.path = toPath
        let lineWidthBasicAnimation = CABasicAnimation(keyPath: "lineWidth")
        lineWidthBasicAnimation.fromValue = fromLineWidth
        lineWidthBasicAnimation.toValue = 2*finalRadius
        let pathBasicAnimation = CABasicAnimation(keyPath: "path")
        pathBasicAnimation.fromValue = fromPath
        pathBasicAnimation.toValue = toPath
        let groupAnimation = CAAnimationGroup()
        groupAnimation.duration = 1
        groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        groupAnimation.animations = [pathBasicAnimation, lineWidthBasicAnimation]
        groupAnimation.delegate = self
        circularPathLayer.addAnimation(groupAnimation, forKey: "strokeWidth") 

Section H - It determine the radius of circle and the calculate the area of view in which that circle would fully bound .

Section I - lineWidth and path initial values are set for match values of the layer.

Section J - Hhen animation completes lineWidth and path final values are set. kCATransactionDisableActions are set to true for disabling layers animations. 

Section K - Two instances are created for path and lineWidth. lineWidth should increase with double rate as the radius increases so that it can expand inward as well as outward. Now both instances are added to animation group to layer. 

  • Now at last add this method to DisplayingCircularLoaderView :
    override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
        superview?.layer.mask = nil

This will remove the circle completely from the view



Request for Proposal

Recaptcha is required.

Sending message..