1

I'm writing a ChartView using NSView with data obtained from a rest api using Combine. The struct PlotView is the SwiftUI View that displays the chart, ChartViewRepresentable is the bridge between the NSView with the chart and the SwiftUI world and ChartView is the view that I actually draw on.

RestRequest gets the data from the network correctly and PlotView has access to it with no issues. When the data is received a ChartViewRepresentable is created and it contains the data, and ChartViewRepresentable creates a ChartView with the data and the data is stored in its data property correctly.

There are two problems: 1) the view's draw method never gets called when the data is loaded, and 2) if the view is redrawn a new ChartViewRepresentable (with a new ChartView) is created by SwiftUI but with no data.

I have connected the RestRequest @StateObject in every possible way imaginable, using @Binding, using @State, with no luck so far, so I'm discounting it as the problem, but with SwiftUI who really knows. It doesn't matter how I load the data, even loading the data manually into ChartView, it never calls the draw method on its own when receiving the data, and then when I for example resize the window to force a draw call it does call the draw method but on a new ChartViewRepresentable struct with no data in it.

What am I doing wrong? This is all the code besides the RestRequest() struct which I know works because I have been using it reliably on other views until now. Any clue or even a hint would be greatly appreciated.

struct PlotView: View {
    @StateObject var request = RestRequest()
    
    var body: some View {
        Group {
            ChartViewRepresentable(data: ChartData(array: ChartData.createArray(from: request.response.data)))
                .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
        }
        .onAppear{
            let params: [String: String] = [
                "limit": "10",
            ]
            request.perform(endPoint: "http://localhost:4000/api/testdata", parameters: params)
        }
    }
}


struct ChartViewRepresentable: NSViewRepresentable {
    typealias NSViewType = ChartView
    
    var chart: ChartView
    
    init(data: ChartData) {
        chart = ChartView(data: data)
    }
    
    func makeNSView(context: Context) -> ChartView {
        return chart
    }
    func updateNSView(_ nsView: ChartView, context: Context) {
    }
}

class ChartView: NSView {
    
    private var data: ChartData
    
    init(data: ChartData) {
        self.data = data
        print("\(data)")
        super.init(frame: .zero)
        wantsLayer = true
        layer?.backgroundColor = .white
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ dirtyRect: NSRect) {
        print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
        
        super.draw(dirtyRect)
        
        guard let context = NSGraphicsContext.current else { return }
        context.saveGraphicsState()
        
        if data.array.count > 0 {
            //detect data present on ChartView
            let ctx = context.cgContext
            ctx.setFillColor(NSColor.green.cgColor)
            ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
        }
        
        context.restoreGraphicsState()
    }
}
Alejandro
  • 367
  • 3
  • 10

1 Answers1

3

Right now, in your ChartViewRepresentable, you set the data in init, and then never touch it again.

This means that your ChartView will have its data set before your onAppear API call ever runs and returns data.

To fix this, you'll need to make use of the updateNSView function.

struct ChartViewRepresentable: NSViewRepresentable {
    typealias NSViewType = ChartView
    
    var data: ChartData //store the data -- not the chart view
    
    func makeNSView(context: Context) -> ChartView {
        return ChartView(data: data)
    }
    
    func updateNSView(_ chart: ChartView, context: Context) {
        chart.data = data //update the chart view's data
    }
}

And, you'll need to respond to the update of that data and force a redraw:

class ChartView: NSView {
    
    var data: ChartData {
        didSet {
            self.needsDisplay = true //<-- Here
        }
    }
    
    init(data: ChartData) {
        self.data = data
        print("\(data)")
        super.init(frame: .zero)
        wantsLayer = true
        layer?.backgroundColor = .white
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ dirtyRect: NSRect) {
        print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
        
        super.draw(dirtyRect)
        
        guard let context = NSGraphicsContext.current else { return }
        context.saveGraphicsState()
        
        if data.array.count > 0 {
            //detect data present on ChartView
            let ctx = context.cgContext
            ctx.setFillColor(NSColor.green.cgColor)
            ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
        }
        
        context.restoreGraphicsState()
    }
}

Full working example with the API call mocked:

struct ChartData {
    var array : [Int]
}

struct ContentView: View {
    @State var chartData : ChartData = ChartData(array: [])
    
    var body: some View {
        Group {
            ChartViewRepresentable(data: chartData)
                .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
        }
        .onAppear{
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                print("New data!")
                chartData = ChartData(array: [1,2,3,4])
            }
        }
    }
}


struct ChartViewRepresentable: NSViewRepresentable {
    typealias NSViewType = ChartView
    
    var data: ChartData
    
    func makeNSView(context: Context) -> ChartView {
        return ChartView(data: data)
    }
    
    func updateNSView(_ chart: ChartView, context: Context) {
        chart.data = data
    }
}

class ChartView: NSView {
    
    var data: ChartData {
        didSet {
            self.needsDisplay = true
        }
    }
    
    init(data: ChartData) {
        self.data = data
        print("\(data)")
        super.init(frame: .zero)
        wantsLayer = true
        layer?.backgroundColor = .white
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ dirtyRect: NSRect) {
        print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
        
        super.draw(dirtyRect)
        
        guard let context = NSGraphicsContext.current else { return }
        context.saveGraphicsState()
        
        if data.array.count > 0 {
            //detect data present on ChartView
            let ctx = context.cgContext
            ctx.setFillColor(NSColor.green.cgColor)
            ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
        }
        
        context.restoreGraphicsState()
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94