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.

You don't have to do anything specific to make your custom drawing work with autolayout.


Instead, your code in drawRect needs to use self.bounds as the rectangle to draw into. Your drawing code should work correctly no matter what size or shape the bounds rect is.


Note that the rectangle passed into the drawRect call is the part of the bounds that needs redrawing. You shouldn't use this rect to determine the geometry of your view, only to limit which parts of it you choose (as an optimization) to draw.

Ah... I see, I think wasnt doing that part correctly... let me try that soon and see if it works.

Okay, I think I might be doing this incorrectly... When my progress tracker loads in portrait, it fine, but when it goes into landscape it relays out, but its getting draw twice.. please see screenshot below:


h ttps://drive.google.com/file/d/1sqqMa7_l7PPUJKgiKMWCH1XmrCBd6vfE/view?usp=sharing


Also... I'm not sure where to set the self.bounds property you were mentioning. I basicially have 2 files:


1. ProgressTrackerView.swift (a custom UIView class for a progress tracker view)

2. ViewController.swift


In my ViewController.swift file, I basically have an outlet called tracker that is of type ProgressTrackerView. Then in my viewDidLoad I have code that parses data from a JSON file and generates the correct number elements in the tracker and the appropriate lines and coloring logic. I'm pretty sure that issue in the ProgressTrackerView class...


The ProgressTrackerView file is quite large since it has several methods for logic, coloring, generating individial elements, etc. I dont know if it makes sense to put all that code here??

You don't set the bounds, you use the bounds to determine how to place the elements you're drawing in your drawRect: method.


In your screen shot, it looks like the content is drawn correctly for landscape mode (1-2-3-4 is approximately centered in the landscape view) but it's just drawn on top of the old content (which had been centered for portrait view). It may be that you just need to fill the drawRect: method's dirtyRect parameter with your background color before drawing the actual content.


IIRC it's configurable whether the view erases itself to the background color before calling drawRect:. You do need to check that you aren't drawing twice after the rotation, but I suspect you just need to add the background color fill to drawRect:.


I'd suggest that you add some debugging code to log whenever drawRect: is called, along with the value of self.bounds and the dirtyRect parameter at the time of the call. That should give you a clearer idea of what is going on.

Okay... I think you're right about debugging and thanks for clarifying the issue about the bounds. I will write the debugging code to log whenever drawRect is called as well as the self.bounds values. Two things:


1. What is dirtyRect parameter, never heard of this?

2. What is IIRC?

1. In your draw method:


https://developer.apple.com/documentation/uikit/uiview/1622529-draw


there is one parameter of type CGRect. For historical reasons, it's often know as "dirtyRect", because it represents the part of the view that has been "dirtied" (invalidated) since the last time the draw method was called, and so needs to be redrawn now. You can use it to limit redrawing to only those parts of the view that really need redrawing.


2. IIRC = "if I remember correctly". It's my way of weaseling out of being wrong, if I'm wrong.

Okay, didnt know about the dirtyRect concept before, thanks... and I'll have to remember to use IIRC sometime if its not under copyright 😉! It'll probably be a few days before I touch that part of the code for debugging, can I leave this thread open for the meantime or open a new one if I get issues after debugging..?

I did some debugging and it looks like in the storyboard file, the content mode attribute was set to redraw, i set it back to Scale to Fill and this is what i get now. It doesn't have the weired issue where its redrawing it the center and there is another one behind it to the left, however, its not centering, its just sticking to the left.


See Screenshot:

h ttps://drive.google.com/file/d/1sqqMa7_l7PPUJKgiKMWCH1XmrCBd6vfE/view?usp=sharing

I'm not sure what's going on there, but the contentMode really should be "redraw", because you actually want to redraw the contents when the size changes, not to re-use the existing content.


I don't think you have much choice but to dig in and debug your drawing code. You need to get an idea of when it's drawing, what it's drawing, and where. This should not be especially difficult.

Yeah, I'm going to go down the road of debugging this week. Thanks

Okay... so to simplify the problem and for me to understand whats going on here, i made a test app with a simple custom view class that draws a square UIView. Here is my code for that:


TestView Class

import UIKit

class TestView: UIView {

   
    override func draw(_ rect: CGRect) {
      
      print("We're in drawRect")
      
      drawSquare()
      print("Draw Square was called")
      
      
      
      if UIDevice.current.orientation == UIDeviceOrientation.portrait {
         
         print("Portrait")
      
      
      } else if UIDevice.current.orientation == UIDeviceOrientation.portraitUpsideDown {
         
         print("Portrait Upsidedown")
         
      } else if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
         
          print("Landscape Left")
         
      } else {
         
         print("Landscape Right")
      }
      
      
   }
   
   
   func drawSquare() {
      
      
      let square = UIView(frame: CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 25, height: 25))
      
      square.backgroundColor = UIColor.orange
      
      addSubview(square)
      
      
   }
   

}



In ny storyboard, I dragged out a UIView from the library and used autolayout to position it in my superview. Then I created an outlet to it in my ViewController.swift file and set its content mode to redraw in viewDidLoad like so. For good measure, I also set the content mode attribute in the IB to redraw as well:


import UIKit

class ViewController: UIViewController {

   @IBOutlet weak var customTestView: TestView!
   
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      customTestView.contentMode = .redraw
      self.view.addSubview(customTestView)
      
   }

  

}



I am encountering that same problem where there is a square to the left and when the orientation changes, another square is drawn in the center of the new bounds. Here is my debugg statements as well and a link to my screenshot for this project:


Initial loading of app

We're in drawRect

Draw Square was called

Portrait


Rotate phone landscape left

We're in drawRect

Draw Square was called

Landscape Left



Rotate phone back to portrait

We're in drawRect

Draw Square was called

Portrait


Rotate phone back to landscape left

We're in drawRect

Draw Square was called

Landscape Left


Rotate phone back to landscape left

We're in drawRect

Draw Square was called

Portrait



h ttps://drive.google.com/file/d/1sqqMa7_l7PPUJKgiKMWCH1XmrCBd6vfE/view?usp=sharing



Thanks for help in advance!

Oh no! Don't do that!


You absolutely should not change the view hierarchy (by adding a squre subview) in your "draw" method. All you're allowed to do is draw.


That also explains why your "old" drawing persists on the left side of the view. After you added the subview (which, incidentally, didn't actually draw the orange background when you added it, that happened at the next iteration of the draw cycle, but you wouldn't really notice the difference by looking at the screen), you never removed it.


That means you're adding a subview every time "draw" is called. Until you rotate, you won't notice because all of the subviews are drawn on top of each other. You won't notice, that is, until there are so many views that everything slows down to a crawl and your app crashes.


If you want to draw a square, you can use the UIRectFill/UIRectFrame family of UIKit functions. If you want to draw circles, arcs, or irregular shapes, you can use the UIBezier class.

Okay... so I probably might have done this all wrong in my code then. For my ProgressTracker, I made a custom UIView class called ProgressTrackerView. At the basic level, a progress tracker has the following components:


1. Progress Element (A circular UIImageView with an image for each "stage" of a multi step task)

2. Progress Line (A UIView that connects two progress elements together)


For my progress element, I have the following method:

func makeProgressElementAt(x: CGFloat, forDirection : Direction) {
      
      
      /** 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
      
      
      /** Add the newly made line subview */
      addSubview(progressElement)
      
      
      
       /** TODO: Documentation */
      if (direction == .left) {
         
         progressElements.insert(progressElement, at: 0)
         
         
      } else {
         
         
         progressElements.append(progressElement)
         
         
      }
      
      
      
      
   }


For my progress lines, I have a method like this:


/**
    * Makes a new line according to the direction passed in
    *
    * - Note: This method will become private in a future code review.
    *
    */
    func makeLine(forDirection direction: Direction) {
      
      
      var x: CGFloat = 0
      
      
      /** TODO: Documentation */
     // A line can be drawn to the left of an element or to the right of an element or a line
    // can be in the center and elements are drawn on either side of it... 
      switch direction {
         
      case.left:
         x = leftLineAnchor - lineWidth
         leftLineAnchor = x
         break
         
      case.right:
         x = rightLineAnchor
         rightLineAnchor = x + lineWidth
         break
         
      default:
         x = self.bounds.midX - lineWidth/2
         leftLineAnchor = x
         rightLineAnchor = self.bounds.midX + lineWidth/2
         
         
      }
      
      
      /** Making a line */
      let line = UIView(frame: CGRect(x: x,
                                      y: self.bounds.midY - lineHeight/2,
                                      width: lineWidth,
                                      height: lineHeight))
      
      
      /** Color the line */
      line.backgroundColor = defaultLineColor
      
      /** Add the newly made line subview */
      addSubview(line)
      
      /** TODO: Documentation */
      if (direction == .left) {
         
         lines.insert(line, at: 0)
         
         
      } else {
         
         lines.append(line)
         
         
      }
      
      
      
   }


Then I have several methods, but the main is is drawProgressTracker in which the elements are generated based on the number of stages for task, the appropriate images are placed, etc, after which I calle my draw method below:


override  func draw(_ rect: CGRect) {
      
      
      progressElements = []
      lines = []
      
      drawProgressTracker()
   
      
      
   }


Would this probably be better to discuss over email @QuinceyMorris...I've written a fair bit of code, it's pretty well documented, but its a lot... I dont know how much I would have to re-write or tweak to make this correctly work.

You can create a subview hierarchy to represent your progress lines and stages. However, you cannot create this hierarchy in your "draw" method. If you want to take this approach, you have to add the subviews at some other time — when the stages are actually created, for example.


There are, however, a couple of long-term problems with this approach.


First, when the top level view changes size (e.g. when you rotate the device), you need to recalculate the positions of all of the existing subviews. IIRC, the correct place to do that would be an override of "layoutSubviews", not of "draw".


Second, views are relatively resource-heavy. If you get more than a few stages, you're going to have a lot of subviews, and your app is going to get slower (and use more battery).


The alternative approach is not to use any subviews at all, but simply draw everything directly in the main ProgressTrackerView. This likely involves learning how to use UIBezierCurve objects, which give you a way of drawing shapes in a view.

I first explored UIBezierCurve, but I couldnt wrap my brain around it... making a custom UIView Class seemed more easier (and tbh more fun 😀), but I will take a look at both suggestions about modifying the existing method and checking out UIBezierCurve. Also to your point about views and resource intensity, for version 1.0 of the tracker, our use case will be a maximum of 4 stages, thats another reason I'm thinking about sticking with my current method.


On the question of UIBezierCurver, I want to know if it would be possible to do things that I am currently doing with my custom class:


Color Every Line and Element Independently

Line and element colors convey information like if the stage is currently passed, pending, or a serious issue is causing delay. Currently, my class has the following methods for color lines and elements that are in a lines or elements array respectively. Would this be something I can do with Bezier curves?


/**
    * Sets the color of a line at a specific index in the `lines` array
    *
    * - Parameters:
    *    - color: The desired color of the line
    *    - index: The index of the line to color
    */
    func set(color: UIColor, forLineAtIndex index: Int) {
      
      lines[index].backgroundColor = color
      
   }


/**
    * Sets the background color of a progress element at a specific index
    * `progressElements` array
    *
    * - Parameters:
    *    - color: The desired color of the progress element
    *    - index: The index of the progress element to color
    */
    func set(color: UIColor, forProgressElementAtIndex index: Int) {
      
      progressElements[index].backgroundColor = color
      
   }
Custom Drawing and AutoLayout
 
 
Q