macosnsviewios-charts

Charts MarkerView in MacOS won't display


This question is in reference to DanielGindi/Charts but I think some knowledge of NSView rendering might help even if you're not familiar with the Charts library.

I'm using Charts in a MacOS project, and having trouble with getting MarkerView to display correctly. I've copied RadarMarkerView from the ChartsDemo-iOS project to ChartsDemo-macOS in order to use it in the MacOS RadarDemoViewController. But the marker never appears in the view when I click on a value in the chart.

Here is the full implementation of RadarMarkerViewMac:

public class RadarMarkerViewMac: MarkerView {
  @IBOutlet var label: NSTextField!

  public override func awakeFromNib() {
    self.offset.x = -self.frame.size.width / 2.0
    self.offset.y = -self.frame.size.height - 7.0
  }

  public override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
    label.stringValue = String.init(format: "%.3f %%", (entry.y))
    _ = self // see note 2
    layout() // see note 1
  }
}

As you can see if you look at the original RadarMarkerView, there are only two notable changes:

  1. layoutIfNeeded() is changed to layout() because the former function doesn't exist in NSView.
  2. I put this line in there so that I could add a breakpoint to see if self was being drawn correctly (when paused for a breakpoint, hover mouse over self, then click the eyeball button to see the view).

The weird thing I discovered was this: Clicking on the chart highlights the selected value but doesn't show the marker. However, when I put a breakpoint on the layout() line, then viewed self with the preview popup, I saw the view just fine... and then once I continued the app running, the marker appeared as it should in the chart in all subsequent selections, even with the breakpoint disabled.

Also important to note, I think, is that if I put the breakpoint in and DIDN'T show the preview, the marker would not display on the chart.

I assume this has something to do with the NSView rendering, but I haven't been able to make the app behave correctly here. Can you help?


Solution

  • It is not sufficient to only copy RadarMarkerView.

    You need to create a corresponding xib for macOS. In RadarDemoViewController in viewDidLoad load the view from xib and add it as a subview outside of visible area, so that it can be rendered:

    let marker = RadarMarkerView.viewFromXib()!
    marker.chartView = radarChartView
    radarChartView.marker = marker
    
    self.view.addSubview(marker)
    marker.frame = NSRect(x: -100, y: -100, width: 64, height: 64)
    

    As one can see in MarkerView a property nsuiLayer is used, which basically corresponds to self.layer.

    Therefore you need to specify that you want the view to use a layer as its backing store.

    public required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        wantsLayer = true
    }
    

    You can define an offset (depending on the size of your view):

    open override func offsetForDrawing(atPoint point: CGPoint) -> CGPoint {
        return CGPoint(x: -36, y: -40)
    }
    

    And in refreshContent you specify that the view needs a layout pass before it can be drawn with

    needsLayout = true
    

    So the complete RadarMarkerView looks something like this:

    import AppKit
    import Charts
    
    public class RadarMarkerView: MarkerView {
        @IBOutlet var label: NSTextField!
    
        public required init?(coder decoder: NSCoder) {
            super.init(coder: decoder)
            wantsLayer = true
        }
    
        open override func draw(context: CGContext, point: CGPoint) {
    //        NSColor.red.setFill()
    //        NSRect(x: point.x - 32, y: point.y - 32, width: 64, height: 64).fill()
            super.draw(context: context, point: point)
        }
    
        open override func offsetForDrawing(atPoint point: CGPoint) -> CGPoint {
            return CGPoint(x: -36, y: -40)
        }
    
    
        public override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
            label.stringValue = String.init(format: "%d %%", Int(round(entry.y)))
            needsLayout = true
        }
    }
    

    Note the override of draw(context:point:). To make debugging easier, you can programmatically draw a red rectangle (just uncomment the two lines).

    Test

    marker test

    As you can see the marker is then shown.