Custom Drawing and AutoLayout

I am working on a custom UIView class called TrackerView that basically builds a FedEx style tracker to display the progress of a manufacturing procedure. This entire view is based on custom drawing code inside the drawRect method of TrackerView. The tracker is up and running and my next problem to solve is how to make my custom view work with AutoLayout to handle things like landscape and portrait orientations, etc.


I'm trying to learn this stuff, so if anyone has any resources or materials that they can point to on where to get started with AutoLayout as it pertains to custom drawn views in particular... that would be great! Any sample code or example code of how to handle custom drawing and layout changes would be an immense help as well!


Thanks.

QuinceyMorris, I just read some documentation from Apple on custom views and Bezier path... and its starting to make sense. I think I'm going to play around with UIBezierPath before making a decision, but so far I like it!

I have a question about UIBezierPath and UIImage. In my custom UIView class, my progress elements are actually UIImageViews like so:


/** Making a progress element */  
      let progressElement = UIImageView(frame: CGRect(x: x - progressElementWidth/2,  
                                                      y: self.bounds.midY - progressElementHeight/2,  
                                                      width: progressElementWidth,  
                                                      height: progressElementHeight))  


      /** Change the corner radius to make the imageView a circle */  
      progressElement.layer.cornerRadius = progressElementWidth/2 


The images are added in a later method from the server / JSON response. I am starting to use UIBezierPath to make a progress element that is basically a circle... something like this:


// Creates a circular progress element
// Still a work in progress...
func pathForCircleCenteredAt(midPoint: CGPoint, withRadius radius: CGFloat) -> UIBezierPath {
      
     let path = UIBezierPath(arcCenter: midPoint,
                             radius: radius,
                             startAngle: 0.0,
                             endAngle: CGFloat(2*Double.pi),
                             clockwise: false)
      
      
      path.lineWidth = 5.0
      return path
      
   }



My question is how would I go about adding a UIImage... I did some initial research and this is what I came accross this from the Apple Documentation:


"If you need to combine standard UI elements with custom drawing, consider using a Core Animation layer to superimpose a custom view with a standard view so that you draw as little as possible."


Is there an example of superimposing a UIImage into a UIBezierPath?

It's not hard to do what you're asking (draw an image into the interior of a bezier path), but there are a few moving parts, and you need to think systematically. Here's some (untested) code cut down from actual code that clips an image to a circle:


func makeImage(in contextRect: CGRect, isFlipped: Bool) -> UIImage {
    // Create a drawing context
    UIGraphicsBeginImageContextWithOptions(contextRect.size, false, 0)
    let context = UIGraphicsGetCurrentContext()!
    // Flip the context if necessary
    if isFlipped {
        context.translateBy(x: 0, y: contextRect.height)
        context.scaleBy(x: 1, y: -1)
    }
    // Clip to the bezier path
    let imageRect = contextRect.insetBy(dx: 1, dy: 1)
    UIBezierPath(ovalIn: imageRect).addClip()
    // Draw the image inside the bezier path, scaled if necessary
    let interiorImage = UIImage(named: "whatever")!
    let scale = max(imageRect.width / interiorImage.size.width, imageRect.height / interiorImage.size.height)
    let drawSize = CGSize(width: interiorImage.size.width * scale, height: interiorImage.size.height * scale)
    let drawRect = CGRect(origin: imageRect.origin, size: drawSize)
    interiorImage.draw(in: drawRect)
    // Create the image
    let contextImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return contextImage
}


If you study that code, you should be able to do what you want.

Thanks... Definetly going to study this.

QuinceyMorris... I have been studying UIBezierPaths and doing some experimentation and here is where I am so far with this:


1. Made a UIView class called ProgressTrackerView

2. I want the progress tracker to have a good amount of visual padding on the top, bottom, right, and left when it sits inside the view... for this I have some computed properties to give me the width and height of the view... I also have a struct called SizeRatios that allows me to resize the tracker and add appropriate padding.


3. I have code to generated the components of the tracker (circles and lines... starting with circles first though)

4. I want to layout the circles with equa spacing inside the red rectangle (see screenshot below)... my question is where should my layout code go... layoutSubviews? somewhere else?


Screenshot (The red rect will be not be visible... its just there for dev purposes, but I want to draw the tracker in the bounds of that rectnagle..."


h ttps://drive.google.com/file/d/1eOEfifpmQXfYqPFeihIbR2LkRE-b-Qs9/view?usp=sharing


Here is the code:


import UIKit


class ProgressTrackerView: UIView {

   
   
   
   /** Computes and returns the `bounds.width` value of the `ProgressTrackerView`. */
   var progressTrackerViewWidth: CGFloat {
      
      return bounds.width
   
   }
   
   
   
   /** Computes and returns the `bounds.height` value of the `ProgressTrackerView`. */
   var progressTrackerViewHeight: CGFloat {
      
      return bounds.height
      
   }
      

   
   
   
  
   
   private struct SizeRatios {
      
      
      /** Ratio to set the width of the `progressTrackerRect` to be proportional to the width of the `ProgressTrackerView` */
      static let progressTrackerRectWidthToViewWidth: CGFloat = 0.885
      
      
      /** Ratio to set the height of a progress element proportional to height of the `ProgressTrackerView` */
      static let progressElementHeightToViewHeight: CGFloat = 0.531
      
      
      /** Ratio to set the height of a line proportional to the height of a progress element */
      static let lineHeightToProgressElementHeight: CGFloat = 0.250
      
      
   }
   
   
   
   /** Configures and returns a rectangular bezier path. The progress tracker (composes of circular elements and lines) will be drawn in the bounds
       of this bezier path.*/
   func prepareRectForProgressTracker() -> UIBezierPath {
      
      
      /** Define the width and height of the progress tracker rectangle. */
      let progressTrackerRectWidth = progressTrackerViewWidth * SizeRatios.progressTrackerRectWidthToViewWidth
      let progressTrackerRectHeight = progressTrackerViewHeight * SizeRatios.progressElementHeightToViewHeight
      
      
      /** Define the x and y coordinates of the rectangle */
      let xRect = (progressTrackerViewWidth - progressTrackerRectWidth) / 2
      let yRect = (progressTrackerViewHeight - progressTrackerRectHeight) / 2
      
      
      /** The final rectangle to be returned */
      let progressTrackerRect = UIBezierPath(rect: CGRect(x: xRect, y: yRect, width: progressTrackerRectWidth, height: progressTrackerRectHeight))
      
      progressTrackerRect.lineWidth = 2.0
      
      return progressTrackerRect
      
      
   }
   
   
   
  
   
   
    override func draw(_ rect: CGRect) {
      
      let color = UIColor.MCRed
      color.setStroke()
      
     
 let progressTrackerRect = prepareRectForProgressTracker()
      progressTrackerRect.stroke()
      
   }

}

When you're drawing within a custom view, "layout" is something you do for yourself. Calculate the X and Y coordinates that you use in your bezier curves relative to the bottom left of the view. This point is given by the value of "bounds.origin" (which is a CGPoint).


So if you want to draw a circle of radius 15 points, inset 20 points from the bottom left corner, then you could use:


     CGPoint(x: bounds.origin.x + 20 + 15, y: bounds.origin.y + 20 + 15)


for the center of the circle.


FWIW, you can refer to bounds.origin.x as bounds.minX, and bounds.origin.y as bounds.minY — they mean the same thing. There is also midX/midY/maxY/maxY, which are often useful in this kind of code.


P.S. It might be a good time to start a new thread. The posts get narrower and narrower as the thread gets longer, which makes code harder to read.

Okay... I will definetly start a new thread, I was also starting to think like this thread was getting to big 🙂. And just for clarification, are UIBezierPaths "subviews" that are added to a view hiearchy?


Also, whats FWIW? Thanks.. I'm new to these acronyms.. 😉

For What It's Worth.... Used when you are providing information and you do not know if it is useful or not. AKA 'you get what you pay for'. aka is Also Known As.


Google/Urban Dictionary knows these, if you don't want to waste time waiting for explanations 😉 - helps to shorten threads, and good reading in your spare time.


When a thread pushes too far right visually, just reply to the very first post and it will move to the left again. Starting another thread may remove context and confuse anyone just coming in that wants to follow and/or add comments. If you do create another thread, pls. try to add a link to it in the first one, thanks.


GLWT

Custom Drawing and AutoLayout
 
 
Q