Search code examples
iosswiftxcodecs193p

Having trouble plotting the x vs. y graph on Stanford CS193P Assignment 3 (2016)


I am a beginner in iOS development. I have done everything on Assignment 3 (Graphing Calculator) of the 2016 Stanford CS193P iOS development course on iTunes U (gestures, view controllers, views, segues etc.) apart from actually plotting the x vs. y graph. I am quite confused about where and how to put my code to do this; and where the drawRect is going to get the y value from. I have searched the Internet for a solution for ages. I'm assuming the model to the graph view controller is the CalculatorBrain or maybe the program of the brain but I'm not quite sure as to how the view is going to talk to this controller from the drawRect function to get the y value for a point x. If you could put me on the right track that would be very helpful. I'll paste my graph view controller and graph view code below. Thanks in advance.

//GraphViewController

import UIKit

class GraphViewController: UIViewController {

    private var brain = CalculatorBrain()

    var program: [AnyObject]?

    @IBOutlet weak var graphView: GraphingView! {
        didSet {
            graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector (GraphingView.changeScale(_:))))
            graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(GraphingView.panView(_:))))
            graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(GraphingView.doubleTapOrigin(_:))))
        }
    }
}




//GraphingView
import UIKit

@IBDesignable
class GraphingView: UIView {

    @IBInspectable var scale: CGFloat = 50 { didSet { setNeedsDisplay() } }
    @IBInspectable var linewidth: CGFloat = 2 { didSet { setNeedsDisplay() } }
    var origin: CGPoint! { didSet { setNeedsDisplay() } }

    /* Gesture Code

    func changeScale (recognizer: UIPinchGestureRecognizer) {
        switch recognizer.state {
        case .Changed,.Ended:
            scale *= recognizer.scale
            recognizer.scale = 1.0
        default:
            break
        }
    }

    func panView (recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .Changed,.Ended:
            origin.x += recognizer.translationInView(self).x
            origin.y += recognizer.translationInView(self).y
            recognizer.setTranslation(CGPoint(x:0,y:0), inView: self)
        default:
            break
        }
    }

    func doubleTapOrigin (recognizer: UITapGestureRecognizer) {
        recognizer.numberOfTapsRequired = 2
        switch recognizer.state {
        case .Ended :
            origin = CGPoint(x: recognizer.locationInView(self).x, y:recognizer.locationInView(self).y)
        default: break
        }
    }
    */

    var axesDrawer = AxesDrawer()

    override func drawRect(rect: CGRect) {

        if origin == nil {
            origin = CGPoint(x: bounds.midX, y: bounds.midY)
        }
        axesDrawer.drawAxesInRect(rect, origin: origin, pointsPerUnit: scale)

        var pixelX = 0

        while pixelX <= bounds.maxX {

            //do some code to get value of y for current x from the brain in the view controller and plot it with UIBezierPath

            pixelX += 1/contentScaleFactor
        }

    }

}

Solution

  • Updated Answer - Using a Function Pointer

    The previous answer is one way to do it using a data source. It is modeled after the way a UITableViewController gets its data.

    Reading though the assignment, it seems the Prof wants you to use an optional function pointer instead.

    Do the following:

    1. Create an optional variable in the GraphingView to hold a function pointer:

      var computeYForX: ((Double) -> Double)?
      
    2. Apply didSet to that variable and have it call self.setNeedsDisplay() when it is set. This will tell iOS that drawRect needs to be called.

    3. In your calculator when it is time to draw, set the computeYForX property of the GraphingView to a (Double) -> Double function in your GraphViewController. If you want to remove the function (for instance, when someone resets the calculator), set the property to nil.

    4. In drawRect, check to make sure computeYForX is not nil before using it to graph the function. Use guard or if let to safely unwrap the function before using it.

    Previous Answer - Using a DataSource

    You need to add a Data Source to your GraphingView. First define a protocol called GraphDataSource:

    protocol GraphDataSource: class {
        func computeYforX(x: Double) -> Double
    }
    

    Add a dataSource property to the GraphingView:

    class GraphingView: UIView {
        weak var dataSource: GraphDataSource?
    

    Have your GraphViewController implement that protocol by adding GraphDataSource to the class definition line, by implementing computeYForX(), and by setting itself as the dataSource in didSet for graphView:

    class GraphViewController: UIViewController, GraphDataSource {
    
        private var brain = CalculatorBrain()
    
        var program: [AnyObject]?
    
        @IBOutlet weak var graphView: GraphingView! {
            didSet {
                graphView.dataSource = self
                graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector (GraphingView.changeScale(_:))))
                graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(GraphingView.panView(_:))))
                graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(GraphingView.doubleTapOrigin(_:))))
            }
        }
    
        func computeYForX(x: Double) -> Double {
            // call the brain to calculate y and return it
        }
    }
    

    Then in drawRect, call computeYForX() on the dataSource when you need a y value:

    let y = dataSource?.computeYForX(x)