I'm currently working on a SwiftUI project, and in order to detect intersections/collisions, I need real-time coordinates, which SwiftUI animations cannot offer. After doing some research, I came across a wonderful question by Kike regarding how to get the real-time coordinates of a view when it is moving/transitioning. And Pylyp Dukhov's answer to that topic recommended utilizing CADisplayLink to calculate the position for each frame and provided a workable solution that did return the real time values when transitioning.
But I'm so unfamiliar with CADisplayLink and creating custom animations that I'm not sure I'll be able to bend it to function the way I want it to.
So this is the animation I want to achieve using CADisplayLink that animates the orange circle view in a circular motion using its position coordinates and repeats forever:
Here is the SwiftUI code:
struct CircleView: View {
#Binding var moveClockwise: Bool
#Binding var duration: Double // Works as speed, since it repeats forever
let geo: GeometryProxy
var body: some View {
ZStack {
Circle()
.stroke()
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
//MARK: - What I have with SwiftUI animation
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
.offset(x: -CGFloat(geo.size.width / 2))
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(
.linear(duration: duration)
.repeatForever(autoreverses: false), value: moveClockwise
)
//MARK: - What I need with CADisplayLink
// Circle()
// .fill(.orange)
// .frame(width: 35, height: 35, alignment: .center)
// .position(CGPoint(x: pos.realTimeX, y: realTimeY))
Button("Start Clockwise") {
moveClockwise = true
// pos.startMovement
}.foregroundColor(.orange)
}.fixedSize()
}
}
struct ContentView: View {
#State private var moveClockwise = false
#State private var duration = 2.0 // Works as speed, since it repeats forever
var body: some View {
VStack {
GeometryReader { geo in
CircleView(moveClockwise: $moveClockwise, duration: $duration, geo: geo)
}
}.padding(20)
}
}
This is what I have currently with CADisplayLink, I added the coordinates to make a circle and that’s about it & it doesn’t repeat forever like the gif does:
Here is the CADisplayLink + real-time coordinate version that I’ve tackled and got lost:
struct Point: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
}
}
struct ContentView: View {
#StateObject var P: Position = Position()
var body: some View {
VStack {
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
}.onAppear() {
P.startMovement()
}
}
}
class Position: ObservableObject, Equatable {
struct AnimationInfo {
let startDate: Date
let duration: TimeInterval
let startPoint: CGPoint
let endPoint: CGPoint
func point(at date: Date) -> (point: CGPoint, finished: Bool) {
let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
return (
point: CGPoint(
x: startPoint.x + (endPoint.x - startPoint.x) * progress,
y: startPoint.y + (endPoint.y - startPoint.y) * progress
),
finished: progress == 1
)
}
}
#Published var realtimePosition = CGPoint.zero
private var mainTimer: Timer = Timer()
private var executedTimes: Int = 0
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
return displayLink
}()
private let animationDuration: TimeInterval = 0.1
private var animationInfo: AnimationInfo?
private var coordinatesPoints: [CGPoint] {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// great progress haha
let radius: Double = Double(screenWidth / 2)
let center = CGPoint(x: screenWidth / 2, y: screenHeight / 2)
var coordinates: [CGPoint] = []
for i in stride(from: 1, to: 360, by: 10) {
let radians = Double(i) * Double.pi / 180 // raiments = degrees * pI / 180
let x = Double(center.x) + radius * cos(radians)
let y = Double(center.y) + radius * sin(radians)
coordinates.append(CGPoint(x: x, y: y))
}
return coordinates
}
// Conform to Equatable protocol
static func ==(lhs: Position, rhs: Position) -> Bool {
// not sure why would you need Equatable for an observable object?
// this is not how it determines changes to update the view
if lhs.realtimePosition == rhs.realtimePosition {
return true
}
return false
}
func startMovement() {
mainTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(movePoint),
userInfo: nil,
repeats: true
)
}
#objc func movePoint() {
if (executedTimes == coordinatesPoints.count) {
mainTimer.invalidate()
return
}
animationInfo = AnimationInfo(
startDate: Date(),
duration: animationDuration,
startPoint: realtimePosition,
endPoint: coordinatesPoints[executedTimes]
)
displayLink.isPaused = false
executedTimes += 1
}
#objc func displayLinkAction() {
guard
let (point, finished) = animationInfo?.point(at: Date())
else {
displayLink.isPaused = true
return
}
realtimePosition = point
if finished {
displayLink.isPaused = true
animationInfo = nil
}
}
}
Inside Position you're calculating position related to whole screen. But .position modifier requires value related to the parent view size.
You need to make your calculations based on the parent size, you can use such sizeReader for this purpose:
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry in
Color.clear
.onAppear {
block(geometry.size)
}
.onChange(of: geometry.size, perform: block)
}
)
}
}
Usage:
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
.sizeReader { size in
P.containerSize = size
}
Also CADisplayLink is not used in the right way. The whole point of this tool is that it's already called on each frame, so you can calculate real time position, so your animation is gonna be really smooth, and you don't need a timer or pre-calculated values for only 180(or any other number) positions.
In the linked answer timer was used because a delay was needed between animations, but in your case the code can be greatly simplified:
class Position: ObservableObject {
#Published var realtimePosition = CGPoint.zero
var containerSize: CGSize?
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
displayLink.isPaused = true
return displayLink
}()
private var startDate: Date?
func startMovement() {
startDate = Date()
displayLink.isPaused = false
}
let animationDuration: TimeInterval = 5
#objc func displayLinkAction() {
guard
let containerSize = containerSize,
let timePassed = startDate?.timeIntervalSinceNow,
case let progress = -timePassed / animationDuration,
progress <= 1
else {
displayLink.isPaused = true
startDate = nil
return
}
let frame = CGRect(origin: .zero, size: containerSize)
let radius = frame.midX
let radians = CGFloat(progress) * 2 * .pi
realtimePosition = CGPoint(
x: frame.midX + radius * cos(radians),
y: frame.midY + radius * sin(radians)
)
}
}
I've tried to make more simplified the implementation, here is the SwiftUI code,
struct RotatingDotAnimation: View {
#State private var moveClockwise = false
#State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: moveClockwise
)
}
.onAppear {
self.moveClockwise.toggle()
}
}
}
It'll basically create animation like this,
enter image description here
I am using part of the following repo: https://github.com/pd95/CS193p-EmojiArt
Have modified some of the code, as I am using images instead of Emojies, but the size part I can't figure out.
The Emojies use size with a same number for width and height, but with my images, I use a different for the width and height (all images have the same width and height).
When I zoom the page, the images do not resize.
From the mentioned repo, I haven't changed the size part and zoom part.
Somebody has an idea how I can fix that?
Updating with example code
Size is set as int:
var size: Int
There is a scaleInstruments function:
func scaleInstrument(_ instrument: StageManager.Instrument, by scale: CGFloat) {
if let index = stageManager.instruments.firstIndex(matching: instrument) {
stageManager.instruments[index].size = Int((CGFloat(stageManager.instruments[index].size) * scale).rounded(.toNearestOrEven))
}
}
And the zoomScale / zoomGesture functions:
#GestureState private var gestureZoomScale: CGFloat = 1.0
private var zoomScale: CGFloat {
document.steadyStateZoomScale * (hasSelection ? 1 : gestureZoomScale)
}
private func zoomScale(for instrument: StageManager.Instrument) -> CGFloat {
if isInstrumentSelected(instrument) {
return document.steadyStateZoomScale * gestureZoomScale
}
else {
return zoomScale
}
}
private func zoomGesture() -> some Gesture {
MagnificationGesture()
.updating($gestureZoomScale, body: { (latestGestureScale, gestureZoomScale, transaction) in
gestureZoomScale = latestGestureScale
})
.onEnded { finalGestureScale in
if self.hasSelection {
self.selectedInstrumentIDs.forEach { (instrumentId) in
if let instrument = self.document.instruments.first(where: {$0.id == instrumentId }) {
self.document.scaleInstrument(instrument, by: finalGestureScale)
}
}
}
else {
self.document.steadyStateZoomScale *= finalGestureScale
}
}
}
Hope this sufficient to explain the issue I have.
I managed to do this, using the following code change:
From:
.frame(width: 140, height: 70)
To:
.frame(width: 140 * self.zoomScale(for: instrument), height: 70 * self.zoomScale(for: instrument))
Now the images will resize according the zoom.
I would like to build a simple view that allows me to show an image in a scroll view and let the user pinch to zoom on the image, and pan.
I've looked around and started with thisScrollView:
struct TestView: View {
var body: some View {
ScrollView {
Image("test")
.border(Color(.yellow))
}
.border(Color(.red))
}
}
That would not handle zooming.
I then did this:
struct TestView: View {
/// https://wp.usatodaysports.com/wp-content/uploads/sites/88/2014/03/sunset-in-the-dolos-mikadun.jpg
var image = UIImage(named: "test")!
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
var body: some View {
GeometryReader { geo in
ScrollView([.vertical, .horizontal], showsIndicators: false){
ZStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, height: geo.size.width)
.scaleEffect(scale)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
var newScale = self.scale * delta
if newScale < 1.0 {
newScale = 1.0
}
scale = newScale
}.onEnded{val in
lastScaleValue = 1
})
}
}
.frame(width: geo.size.width, height: geo.size.width)
.border(Color(.red))
.background(Color(.systemBackground).edgesIgnoringSafeArea(.all))
}
}
}
This allows me to zoom in and out, however, I cannot pan the image:
How can I code up things so I can support zoom and panning?
in order to get the panning functionality you will have to change the size of your Image container, in this case the ZStack.
So first we need a variable to save the current latest scale value.
#State var scaledFrame: CGFloat = 1.0
Then just change the size of the container each time the gesture ends.
ZStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, height: geo.size.width )
.scaleEffect(scale)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
var newScale = self.scale * delta
if newScale < 1.0 {
newScale = 1.0
}
scale = newScale
}.onEnded{val in
scaledFrame = scale//Update the value once the gesture is over
lastScaleValue = 1
})
.draggable()
}
.frame(width: geo.size.width * scaledFrame, height: geo.size.width * scaledFrame)
//change the size of the frame once the drag is complete
This is due to the way ScrollView works, when you were zooming in, the real size of the container was not changing, therefore the ScrollView was only moving accordingly.
I am trying to following this discussion. The suggested solution was written for Swift 2. I have updated it to Swift 3 and got an error "Ambiguous use of init((CGImage: scale: orientation:)" for the line:
images.append(UIImage(CGImage: tileCgImage, scale: image.scale, orientation: image.imageOrientation))
Have you any idea how to repair it? Here is the code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func slice(image: UIImage, into howMany: Int) -> [UIImage] {
let width: CGFloat
let height: CGFloat
switch image.imageOrientation {
case .left, .leftMirrored, .right, .rightMirrored:
width = image.size.height
height = image.size.width
default:
width = image.size.width
height = image.size.height
}
let tileWidth = Int(width / CGFloat(howMany))
let tileHeight = Int(height / CGFloat(howMany))
let scale = Int(image.scale)
var images = [UIImage]()
let cgImage = image.cgImage!
var adjustedHeight = tileHeight
var y = 0
for row in 0 ..< howMany {
if row == (howMany - 1) {
adjustedHeight = Int(height) - y
}
var adjustedWidth = tileWidth
var x = 0
for column in 0 ..< howMany {
if column == (howMany - 1) {
adjustedWidth = Int(width) - x
}
let origin = CGPoint(x: x * scale, y: y * scale)
let size = CGSize(width: adjustedWidth * scale, height: adjustedHeight * scale)
let tileCgImage = cgImage.cropping(to: CGRect(origin: origin, size: size))!
images.append(UIImage(CGImage: tileCgImage, scale: image.scale, orientation: image.imageOrientation))
x += tileWidth
}
y += tileHeight
}
return images
}
}
Just wanted to make sure that Rob's comment gets highlighted since that seems to be the correct answer. To add to it, as of Swift 4, method signature stays what Rob has mentioned.
Rob:
"In Swift 3, the first label to that function is now cgImage:, not CGImage:. See init(cgImage:scale:orientation:)."
For e.g.:
let resultUIImg = UIImage(cgImage: someCGImg!, scale: origUIImg.scale, orientation: origUIImg.imageOrientation)
In Swift 3
You can using like this:
let image:UIImag = UIImage( cgImage: cgImage )
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:
Create a new project on Xcode.
Add "AxesDrawer.swift" file to your project (File -> Add Files to "ProjectName"...
Create a new Cocoa Touch Class file using subclass UIView on the main folder of your project.
Add a View on Main.storyboard and set the Class Name of this view equal to the file created above.
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
}
//
// 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)
}
}