AxesDrawer cannot draw swift3 xcode 8

249 views Asked by At

I'm trying to make a graphing calculator app and I can't get AxesDrawer to work. This is from the stanford university course with swift 2 and I don't know how to draw this out using UIBezierPath etc

AxesDrawer.swift:

    import UIKit

class AxesDrawer
{
    private struct Constants {
        static let HashmarkSize: CGFloat = 6
    }

    var color = UIColor.blue
    var minimumPointsPerHashmark: CGFloat = 40
    var contentScaleFactor: CGFloat = 1 // set this from UIView's contentScaleFactor to position axes with maximum accuracy

    convenience init(color: UIColor, contentScaleFactor: CGFloat) {
        self.init()
        self.color = color
        self.contentScaleFactor = contentScaleFactor
    }

    convenience init(color: UIColor) {
        self.init()
        self.color = color
    }

    convenience init(contentScaleFactor: CGFloat) {
        self.init()
        self.contentScaleFactor = contentScaleFactor
    }

    // this method is the heart of the AxesDrawer
    // it draws in the current graphic context's coordinate system
    // therefore origin and bounds must be in the current graphics context's coordinate system
    // pointsPerUnit is essentially the "scale" of the axes
    // e.g. if you wanted there to be 100 points along an axis between -1 and 1,
    //    you'd set pointsPerUnit to 50

    func drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
    {
        UIGraphicsGetCurrentContext()!.saveGState()
        color.set()
        let path = UIBezierPath()
        path.move(to: CGPoint(x: bounds.minX, y: align(coordinate: origin.y)))
        path.addLine(to: CGPoint(x: bounds.maxX, y: align(coordinate: origin.y)))
        path.move(to: CGPoint(x: align(coordinate: origin.x), y: bounds.minY))
        path.addLine(to: CGPoint(x: align(coordinate: origin.x), y: bounds.maxY))
        path.stroke()
        drawHashmarksInRect(bounds: bounds, origin: origin, pointsPerUnit: abs(pointsPerUnit))
        UIGraphicsGetCurrentContext()!.restoreGState()
    }

    // the rest of this class is private

    private func drawHashmarksInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
    {
        if ((origin.x >= bounds.minX) && (origin.x <= bounds.maxX)) || ((origin.y >= bounds.minY) && (origin.y <= bounds.maxY))
        {
            // figure out how many units each hashmark must represent
            // to respect both pointsPerUnit and minimumPointsPerHashmark
            var unitsPerHashmark = minimumPointsPerHashmark / pointsPerUnit
            if unitsPerHashmark < 1 {
                unitsPerHashmark = pow(10, ceil(log10(unitsPerHashmark)))
            } else {
                unitsPerHashmark = floor(unitsPerHashmark)
            }

            let pointsPerHashmark = pointsPerUnit * unitsPerHashmark

            // figure out which is the closest set of hashmarks (radiating out from the origin) that are in bounds
            var startingHashmarkRadius: CGFloat = 1
            if !bounds.contains(origin) {
                let leftx = max(origin.x - bounds.maxX, 0)
                let rightx = max(bounds.minX - origin.x, 0)
                let downy = max(origin.y - bounds.minY, 0)
                let upy = max(bounds.maxY - origin.y, 0)
                startingHashmarkRadius = min(min(leftx, rightx), min(downy, upy)) / pointsPerHashmark + 1
            }

            // now create a bounding box inside whose edges those four hashmarks lie
            let bboxSize = pointsPerHashmark * startingHashmarkRadius * 2
            var bbox = CGRect(center: origin, size: CGSize(width: bboxSize, height: bboxSize))

            // formatter for the hashmark labels
            let formatter = NumberFormatter()
            formatter.maximumFractionDigits = Int(-log10(Double(unitsPerHashmark)))
            formatter.minimumIntegerDigits = 1

            // radiate the bbox out until the hashmarks are further out than the bounds
            while !bbox.contains(bounds)
            {

                let label = formatter.string(from: NSNumber(value: Int(origin.x-bbox.minX / pointsPerUnit)))
                if let leftHashmarkPoint = alignedPoint(x: bbox.minX, y: origin.y, insideBounds:bounds) {
                    drawHashmarkAtLocation(location: leftHashmarkPoint, .Top("-\(label)"))
                }
                if let rightHashmarkPoint = alignedPoint(x: bbox.maxX, y: origin.y, insideBounds:bounds) {
                    drawHashmarkAtLocation(location: rightHashmarkPoint, AnchoredText.Top(label!))
                }
                if let topHashmarkPoint = alignedPoint(x: origin.x, y: bbox.minY, insideBounds:bounds) {
                    drawHashmarkAtLocation(location: topHashmarkPoint, AnchoredText.Left(label!))
                }
                if let bottomHashmarkPoint = alignedPoint(x: origin.x, y: bbox.maxY, insideBounds:bounds) {
                    drawHashmarkAtLocation(location: bottomHashmarkPoint, .Left("-\(label)"))
                }
                bbox.insetBy(dx: -pointsPerHashmark, dy: -pointsPerHashmark)
            }
        }
    }

    private func drawHashmarkAtLocation(location: CGPoint, _ text: AnchoredText)
    {
        var dx: CGFloat = 0, dy: CGFloat = 0
        switch text {
            case .Left: dx = Constants.HashmarkSize / 2
            case .Right: dx = Constants.HashmarkSize / 2
            case .Top: dy = Constants.HashmarkSize / 2
            case .Bottom: dy = Constants.HashmarkSize / 2
        }

        let path = UIBezierPath()
        path.move(to: CGPoint(x: location.x-dx, y: location.y-dy))
        path.addLine(to: CGPoint(x: location.x+dx, y: location.y+dy))
        path.stroke()

        text.drawAnchoredToPoint(location: location, color: color)
    }

    private enum AnchoredText
    {
        case Left(String)
        case Right(String)
        case Top(String)
        case Bottom(String)

        static let VerticalOffset: CGFloat = 3
        static let HorizontalOffset: CGFloat = 6

        func drawAnchoredToPoint(location: CGPoint, color: UIColor) {
            let attributes = [
                NSFontAttributeName : UIFont.preferredFont(forTextStyle: UIFontTextStyle.footnote),
                NSForegroundColorAttributeName : color
            ]
            var textRect = CGRect(center: location, size: text.size(attributes: attributes))
            switch self {
                case .Top: textRect.origin.y += textRect.size.height / 2 + AnchoredText.VerticalOffset
                case .Left: textRect.origin.x += textRect.size.width / 2 + AnchoredText.HorizontalOffset
                case .Bottom: textRect.origin.y -= textRect.size.height / 2 + AnchoredText.VerticalOffset
                case .Right: textRect.origin.x -= textRect.size.width / 2 + AnchoredText.HorizontalOffset
            }
            text.draw(in: textRect, withAttributes: attributes)
        }

        var text: String {
            switch self {
                case .Left(let text): return text
                case .Right(let text): return text
                case .Top(let text): return text
                case .Bottom(let text): return text
            }
        }
    }

    // we want the axes and hashmarks to be exactly on pixel boundaries so they look sharp
    // setting contentScaleFactor properly will enable us to put things on the closest pixel boundary
    // if contentScaleFactor is left to its default (1), then things will be on the nearest "point" boundary instead
    // the lines will still be sharp in that case, but might be a pixel (or more theoretically) off of where they should be

    private func alignedPoint(x x: CGFloat, y: CGFloat, insideBounds: CGRect? = nil) -> CGPoint?
    {
        let point = CGPoint(x: align(coordinate: x), y: align(coordinate: y))
        if let permissibleBounds = insideBounds, !permissibleBounds.contains(point) {
            return nil
        }
        return point
    }

    private func align(coordinate: CGFloat) -> CGFloat {
        return round(coordinate * contentScaleFactor) / contentScaleFactor
    }
}

extension CGRect
{
    init(center: CGPoint, size: CGSize) {
        self.init(x: center.x-size.width/2, y: center.y-size.height/2, width: size.width, height: size.height)
    }
}

ViewController.swift:

    import UIKit
var calculatorCount = 0
class CalculatorViewController: UIViewController {
    var graphl = GraphView()
    private var on = true
    @IBOutlet private var display: UILabel!

    private var userIsInTheMiddleOfTyping = false

    override func viewDidLoad() {
        super.viewDidLoad()
        calculatorCount +=  1
        //print("Loaded up a new Calculator (count = \(calculatorCount))")
        brain.addUnaryOperation(symbol: "Z") { [ weak weakSelf = self ] in
            weakSelf?.display.textColor = UIColor.red
            return sqrt($0)
        }
        graphl.print2()
    }


    deinit {
        calculatorCount -= 1
        //print(" Calculator left the heap (count = \(calculatorCount))")
    }


    @IBAction func off(_ sender: UIButton) {
        on = false
    }

    @IBAction func on(_ sender: UIButton) {
        on = true
    }

    @IBAction private func tocuhDigit(_ sender: UIButton) {
        if on {
            let digit = sender.currentTitle!
            if userIsInTheMiddleOfTyping {
                let textCurrentlyInDisplay = display.text!
                display.text =  textCurrentlyInDisplay + digit
            } else {
                display.text = digit
            }
            userIsInTheMiddleOfTyping = true
        }
    }
    private var displayValue: Double {
        get {
            return Double(display.text!)!
        }
        set {
            display.text = String(newValue)

        }
    }
    var savedProgram: CalculatorBrain.PropertyList?
    @IBAction func save() {
        savedProgram = brain.program

    }

    @IBAction func restore() {
        if savedProgram != nil {
            brain.program = savedProgram!
            displayValue = brain.result
        }
    }

    private var brain = CalculatorBrain()

    @IBAction func Reset(_ sender: UIButton) {
        if on {

            displayValue = 0
        }
    }
    @IBAction private func performOperation(_ sender: UIButton) {
        if userIsInTheMiddleOfTyping && on {
            brain.setOperand(operand: displayValue)
            userIsInTheMiddleOfTyping = false
        }

        if let mathematicalSymbol = sender.currentTitle {
            brain.perofrmOperation(symbol: mathematicalSymbol)

        }
        displayValue = brain.result
    }

}

CalculatorBrain.swift:

    import Foundation


class CalculatorBrain {

    private var accumulator = 0.0
    private var internalProgram = [AnyObject]()

    func setOperand(operand: Double) {
        accumulator = operand
        internalProgram.append(operand as AnyObject)

    }
    func addUnaryOperation(symbol: String, operation: @escaping (Double) -> Double) {
        operations[symbol] = Operation.UnaryOperation(operation)
    }

    private var operations: Dictionary<String, Operation> = [
        "π" :Operation.Constant(M_PI),
        "e" : Operation.Constant(M_E),
        "±" : Operation.UnaryOperation({ -$0 }),
        "∓" : Operation.UnaryOperation({+$0}),
        "√" : Operation.UnaryOperation(sqrt),  //sqrt,
        "cos" : Operation.UnaryOperation(cos),
        "×" : Operation.BinaryOperation({ $0 * $1 }),
        "-" : Operation.BinaryOperation({ $0 - $1 }),
        "+" : Operation.BinaryOperation({ $0 + $1 }),
        "÷" : Operation.BinaryOperation({ $0 / $1 }),
        "=" : Operation.Equals,
        "i" : Operation.Constant(sqrt(-1)),
        "x2" : Operation.UnaryOperation({$0 * $0}),
        "xb" : Operation.BinaryOperation2({pow($0, $1)})
    ]

    private enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double)
        case BinaryOperation((Double, Double) -> Double)
        case Equals
        case BinaryOperation2((Double, Double) -> Double)

    }

    func perofrmOperation(symbol: String) {
        internalProgram.append(symbol as AnyObject)
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value): accumulator = value
            case .UnaryOperation(let function): accumulator = function(accumulator)
            case .BinaryOperation(let function):  executePendingBinaryOperation()
            pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
            case .Equals:
                executePendingBinaryOperation()

            case .BinaryOperation2(let function):
                pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)




            }
        }


    }
    private func executePendingBinaryOperation() {
        if pending != nil {
            accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
        }
    }

    private var pending: PendingBinaryOperationInfo?

    private struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double
        var firstOperand: Double

    }

    typealias PropertyList = AnyObject
    var program: PropertyList {
        get {
            return internalProgram as CalculatorBrain.PropertyList
        }
        set {
            clear()
            if let arrayOfOps = newValue as? [AnyObject] {
                for op in arrayOfOps {
                    if let operand = op as? Double {
                        setOperand(operand: operand)
                    } else if let operation = op as? String {
                        perofrmOperation(symbol: operation)
                    }
                }
            }
        }
    }
    func clear() {
        accumulator = 0.0
        pending = nil
        internalProgram.removeAll()
    }

    var result: Double {
        get {
            return accumulator

        }
    }

}

AppDelegate.swift:

    import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

Main.storyboard:
[buttons are in stack a stack view and created a UIView for the graph][1]


  [1]: https://i.stack.imgur.com/oxwmw.png
2

There are 2 answers

0
Renato R. B. Monteiro On

I know there is a lot of time since the question has been made, but I made the same question today and I hope this answer can help others:

You can follow these steps:

  1. Create a new project on Xcode.
  2. Add "AxesDrawer.swift" file to your project (File -> Add Files to "ProjectName"...
  3. Create a new Cocoa Touch Class file using subclass UIView on the main folder of your project.
  4. Add a View on Main.storyboard and set the Class Name of this view equal to the file created above.
  5. Use the following code to override draw function in file created on step 3:

    override func draw(_ rect: CGRect) {

    //Draw axes
    let axes: AxesDrawer = AxesDrawer.init(color: UIColor.black, contentScaleFactor: CGFloat(1))
    
    axes.drawAxes(in: CGRect(origin: CGPoint(x: bounds.midX, y: bounds.midY),
                             size: CGSize(width: 1000, height: -1000)),
                  origin: CGPoint(x: bounds.midX, y: bounds.midY),
                  pointsPerUnit: CGFloat(2))
    
    axes.drawAxes(in: CGRect(origin: CGPoint(x: bounds.midX, y: bounds.midY),
                             size: CGSize(width: -1000, height: 1000)),
                  origin: CGPoint(x: bounds.midX, y: bounds.midY),
                  pointsPerUnit: CGFloat(2))
    //End Draw axes
    

    }

1
csk On
//
//  AxesDrawer.swift
//  Calculator
//
//  Created by CS193p Instructor.
//  Copyright © 2015-17 Stanford University.
//  All rights reserved.
//

import UIKit

struct AxesDrawer
{
    var color: UIColor
    var contentScaleFactor: CGFloat             // set this from UIView's contentScaleFactor to position axes with maximum accuracy
    var minimumPointsPerHashmark: CGFloat = 40  // public even though init doesn't accommodate setting it (it's rare to want to change it)

    init(color: UIColor = UIColor.blue, contentScaleFactor: CGFloat = 1) {
        self.color = color
        self.contentScaleFactor = contentScaleFactor
    }

    // this method is the heart of the AxesDrawer
    // it draws in the current graphic context's coordinate system
    // therefore origin and bounds must be in the current graphics context's coordinate system
    // pointsPerUnit is essentially the "scale" of the axes
    // e.g. if you wanted there to be 100 points along an axis between -1 and 1,
    //    you'd set pointsPerUnit to 50

    func drawAxes(in rect: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
    {
        UIGraphicsGetCurrentContext()?.saveGState()
        color.set()
        let path = UIBezierPath()
        path.move(to: CGPoint(x: rect.minX, y: origin.y).aligned(usingScaleFactor: contentScaleFactor)!)
        path.addLine(to: CGPoint(x: rect.maxX, y: origin.y).aligned(usingScaleFactor: contentScaleFactor)!)
        path.move(to: CGPoint(x: origin.x, y: rect.minY).aligned(usingScaleFactor: contentScaleFactor)!)
        path.addLine(to: CGPoint(x: origin.x, y: rect.maxY).aligned(usingScaleFactor: contentScaleFactor)!)
        path.stroke()
        drawHashmarks(in: rect, origin: origin, pointsPerUnit: abs(pointsPerUnit))
        UIGraphicsGetCurrentContext()?.restoreGState()
    }

    // the rest of this class is private

    private struct Constants {
        static let hashmarkSize: CGFloat = 6
    }

    private let formatter = NumberFormatter() // formatter for the hashmark labels

    private func drawHashmarks(in rect: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
    {
        if ((origin.x >= rect.minX) && (origin.x <= rect.maxX)) || ((origin.y >= rect.minY) && (origin.y <= rect.maxY))
        {
            // figure out how many units each hashmark must represent
            // to respect both pointsPerUnit and minimumPointsPerHashmark
            var unitsPerHashmark = minimumPointsPerHashmark / pointsPerUnit
            if unitsPerHashmark < 1 {
                unitsPerHashmark = pow(10, ceil(log10(unitsPerHashmark)))
            } else {
                unitsPerHashmark = floor(unitsPerHashmark)
            }

            let pointsPerHashmark = pointsPerUnit * unitsPerHashmark

            // figure out which is the closest set of hashmarks (radiating out from the origin) that are in rect
            var startingHashmarkRadius: CGFloat = 1
            if !rect.contains(origin) {
                let leftx = max(origin.x - rect.maxX, 0)
                let rightx = max(rect.minX - origin.x, 0)
                let downy = max(origin.y - rect.minY, 0)
                let upy = max(rect.maxY - origin.y, 0)
                startingHashmarkRadius = min(min(leftx, rightx), min(downy, upy)) / pointsPerHashmark + 1
            }

            // pick a reasonable number of fraction digits
            formatter.maximumFractionDigits = Int(-log10(Double(unitsPerHashmark)))
            formatter.minimumIntegerDigits = 1

            // now create a bounding box inside whose edges those four hashmarks lie
            let bboxSize = pointsPerHashmark * startingHashmarkRadius * 2
            var bbox = CGRect(center: origin, size: CGSize(width: bboxSize, height: bboxSize))

            // radiate the bbox out until the hashmarks are further out than the rect
            while !bbox.contains(rect)
            {
                let label = formatter.string(from: (origin.x-bbox.minX)/pointsPerUnit)!
                if let leftHashmarkPoint = CGPoint(x: bbox.minX, y: origin.y).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
                    drawHashmark(at: leftHashmarkPoint, label: .top("-\(label)"))
                }
                if let rightHashmarkPoint = CGPoint(x: bbox.maxX, y: origin.y).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
                    drawHashmark(at: rightHashmarkPoint, label: .top(label))
                }
                if let topHashmarkPoint = CGPoint(x: origin.x, y: bbox.minY).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
                    drawHashmark(at: topHashmarkPoint, label: .left(label))
                }
                if let bottomHashmarkPoint = CGPoint(x: origin.x, y: bbox.maxY).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
                    drawHashmark(at: bottomHashmarkPoint, label: .left("-\(label)"))
                }
                bbox = bbox.insetBy(dx: -pointsPerHashmark, dy: -pointsPerHashmark)
            }
        }
    }

    private func drawHashmark(at location: CGPoint, label: AnchoredText)
    {
        var dx: CGFloat = 0, dy: CGFloat = 0
        switch label {
            case .left: dx = Constants.hashmarkSize / 2
            case .right: dx = Constants.hashmarkSize / 2
            case .top: dy = Constants.hashmarkSize / 2
            case .bottom: dy = Constants.hashmarkSize / 2
        }

        let path = UIBezierPath()
        path.move(to: CGPoint(x: location.x-dx, y: location.y-dy))
        path.addLine(to: CGPoint(x: location.x+dx, y: location.y+dy))
        path.stroke()

        label.draw(at: location, usingColor: color)
    }

    private enum AnchoredText
    {
        case left(String)
        case right(String)
        case top(String)
        case bottom(String)

        static let verticalOffset: CGFloat = 3
        static let horizontalOffset: CGFloat = 6

        func draw(at location: CGPoint, usingColor color: UIColor) {
            let attributes = [
                NSFontAttributeName : UIFont.preferredFont(forTextStyle: .footnote),
                NSForegroundColorAttributeName : color
            ]
            var textRect = CGRect(center: location, size: text.size(attributes: attributes))
            switch self {
                case .top: textRect.origin.y += textRect.size.height / 2 + AnchoredText.verticalOffset
                case .left: textRect.origin.x += textRect.size.width / 2 + AnchoredText.horizontalOffset
                case .bottom: textRect.origin.y -= textRect.size.height / 2 + AnchoredText.verticalOffset
                case .right: textRect.origin.x -= textRect.size.width / 2 + AnchoredText.horizontalOffset
            }
            text.draw(in: textRect, withAttributes: attributes)
        }

        var text: String {
            switch self {
                case .left(let text): return text
                case .right(let text): return text
                case .top(let text): return text
                case .bottom(let text): return text
            }
        }
    }
}

private extension CGPoint
{
    func aligned(inside bounds: CGRect? = nil, usingScaleFactor scaleFactor: CGFloat = 1.0) -> CGPoint?
    {
        func align(_ coordinate: CGFloat) -> CGFloat {
            return round(coordinate * scaleFactor) / scaleFactor
        }
        let point = CGPoint(x: align(x), y: align(y))
        if let permissibleBounds = bounds, !permissibleBounds.contains(point) {
            return nil
        }
        return point
    }
}

private extension NumberFormatter
{
    func string(from point: CGFloat) -> String? {
        return string(from: NSNumber(value: Double(point)))
    }
}

private extension CGRect
{
    init(center: CGPoint, size: CGSize) {
        self.init(x: center.x-size.width/2, y: center.y-size.height/2, width: size.width, height: size.height)
    }
}