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
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:
Use the following code to override draw function in file created on step 3:
override func draw(_ rect: CGRect) {
}