Core Graphics Tutorial Part 2: Gradients and Contexts

Hi Caroline!
Thank you for the amazing tutorial!
Iā€™m taking it and improving it a bit for my code, but Iā€™m facing some issues.
Basically Iā€™m using the graphics inside a UiViewCollection cell, but I want different Graphs, so Iā€™m adding the graph programmatically like the code below:

It works just fine, except for the fact that the Clipping areas arenā€™t working (image below).

What do I need to do to make it work ?
Thanks in advance.

@rixd - have you created a clipping path and added it?

path.addClip()?

Yes I did, that root view controller image its with the path created and added, I have managed to get the results that I wanted doing this:

    let rect = CGRect(x: 0, y: 0, width: 308, height: 228)                        
    let customView = GraphView(frame: rect)
    customView.layer.cornerRadius = 20.0
    customView.clipsToBounds = true

I just wanted to add an annotation, that when creating the gradient for the graph:

CGContextDrawLinearGradient

will have changed with Swift 3? maybe somewhere before. Use

context?.drawLinearGradient

Chris

1 Like

Thank you @christopherkmoo - Core Graphics went though a number of changes in Swift 3.

Hi, I downloaded your completed project and it appears a function declaration may be missing?
Line 74 of GraphView.Swift reads: let maxValue = maxElement(graphPoints); however, I donā€™t see a function defined for maxElement and the compiler is throwing an unresolved identifier error on it. Itā€™s also referenced in ViewController.swift and has the same error. Am I missing something?

Thanks,
Evan

@roth - try this

    let maxValue = graphPoints.max()!
1 Like

Hi, againā€”had another question that Iā€™m hoping you might be able to help with! Iā€™ve added a tap touch gesture to the graph and would like to update the color of the graph circle that Iā€™ve tapped on (the UIBezierPath). Iā€™m having problems doing this. I have an array of UIBezierPath called graphCircles which stores them when theyā€™re set up in -drawRect:, and I pull out the specific one I want to update in my tap gesture handler when the touch falls inside the graph circle point. Iā€™m trying to update the color like this:

  UIColor.white.setFill()
  self.graphCircles[idx].fill() // UIBezierpath that was tapped

However, I get this error and nothing happens: ā€œCGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.ā€

Can I update the UIBezierPath outside of -drawRect:? Iā€™ve tried saving a reference to the CGContext I have in drawRect: and then loading it in my tap gesture handler method using UIGraphicsPushContext(self.savedContext!) but that doesnā€™t work. This is the only thing I want to update when this happens, so Iā€™m hoping I can do it in a standalone fashion and not have to call -setNeedsDisplay to redraw the whole view. Iā€™ve searched all over and canā€™t seem to find an easy or working way to do this.

Any help would be appreciated! Thanks, Evan

@roth

You donā€™t have access to the drawing context outside of draw(_:)

However, you donā€™t need to draw the whole thing using setNeedsDisplay() - you can use setNeedsDisplayInRect(_:)

Apple Developer Documentation

This will only redraw the specified rect, so you only need to draw the rect containing your circle.

Ah, that explains a lot! Thank you! Should I go off a bool in draw(_:) that I set in my tap gesture handling method so that when I pass in the rect for my circle that I want to change the color on in setNeedsDisplayInRect(_:) it doesnā€™t go through the rest of the regular graph drawing code that you have in draw(_:)? Like:

override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    if self.redrawGraphCircle {
        // Update graph circle color
    } else {
        // Regular graph drawing code that we don't need to go through again
    }

@roth - On reflection, I think I was wrong. Youā€™d still have to draw the background gradient if you used setNeedsDisplayInRect(_:), as the whole rect needs to be redrawn.

If you are interacting with circles, then you could make them views. That would make the touch code easy as well as the draw code.

So you could have seven circular custom views. When one is tapped, call the viewā€™s delegate to deselect the other views and select this one.

Yes, I think I saw that behavior occur when I tried to implement your suggestionā€”I could get the UIBezierPath circle to change color but the edges of its rect were white, which was probably, as you said, because the background gradient for it would need to get redrawn. Doing UIView circles was going to be my backup if I couldnā€™t get this to work. Thanks for all your help!

I tried to implement your code but its not working on simulator (6 and above ),i am implementing graph View on scrollview and 120+ graph points over graph view.its showing on 3D view and iOS Device but does not visible on simulator.

Please help!!!

Hi @snehasen -

Iā€™m afraid I have no idea why it would work on the device and not the simulator.

Is the simulator running the same iOS version as the device?

Is it all simulator sizes? (Could be auto layout)

Hi Caroline, and thank you for this amazing tutorial.

There is only one thing I canā€™t figure out : how are set the colors replacing the green for the gradient under the graph?

Thank you again.

Hi @robbeyroad - the only green in the graph view is the initial IBOutlet setting for endColor so I assume thatā€™s what you mean?

The class is IBDesignable and the property is IBInspectable, so this can be changed in the storyboard. Have a look at the storyboard and find the graph view. Thatā€™s where the endColor is actually set.

Hi again,

I am sorry for the confusion, I was talking about the green that you were temporarily using under the graph before replacing it (in the section ā€œA Gradient Graphā€). I just realized that you are actually using the same gradient.

Thanks a lot again!

1 Like

Hi @caroline,

Could you please clarify this piece of code? Instead of your code, which was written, I assume for Swift 1:

  //4 - get today's day number
  let dateFormatter = NSDateFormatter()
  let calendar = NSCalendar.currentCalendar()
  let componentOptions:NSCalendarUnit = .CalendarUnitWeekday
  let components = calendar.components(componentOptions, 
                                       fromDate: NSDate())
  var weekday = components.weekday
 
  let days = ["S", "S", "M", "T", "W", "T", "F"]
 
  //5 - set up the day name labels with correct day
  for i in reverse(1...days.count) {
    if let labelView = graphView.viewWithTag(i) as? UILabel {
      if weekday == 7 {
        weekday = 0
      }
      labelView.text = days[weekday--]
      if weekday < 0 {
        weekday = days.count - 1
      }
    }
  }

I am using this (Swift 3):

    // 4 - get today's day number
    let dateFormatter = DateFormatter()
    let calendar = Calendar.current
    let componentOptions:NSCalendar.Unit = .weekday
    let components = (calendar as NSCalendar).components(componentOptions,
                                                         from: Date())
    
    var weekday = components.weekday
    
    let days = ["S", "S", "M", "T", "W", "T", "F"]
    
    // 5 - set up the day name labels with correct day
    for i in (1...days.count).reversed() {
      if let labelView = graphView.viewWithTag(i) as? UILabel {
        if weekday == 7 {
          weekday = 0
        }
        labelView.text = days[i-1] // instead of days[weekday--]
        if weekday! < 0 {
          weekday = days.count - 1
        }
      }
    }

So, what confuses me here is the weekday variable, because I donā€™t see how this would change the value inside the for loop??? When I use labelView.text = days[weekday-1] I get the same value for all the weekday labels.:pensive:

I would appreciate if you could clarify this, thank you very much!

@ljubinkovicd,

Looks like the following line is your issue:

labelView.text = days[i-1] // instead of days[weekday--]

Try this:

labelView.text = days[weekday]
weekday -= 1
2 Likes