MKGeodesicPolyline not dynamically changing size of map - swiftui

I have a function that is adding a geodesic polyline on a great circle track between two points on a map. The issue I am facing is dynamically changing the size of the map to include both the start position and the end position.
Any tips on what I am missing here as I am setting the setVisibleMapRect???
Code below is for Melbourne Airport to Brisbane Airport with the result
var body: some View {
MapView()
.onAppear {
self.mapRoute.toggle()
self.updateMapOverlayViews()
}
.frame(height: 256)
.cornerRadius(25)
.padding()
}
func addRoute(latitudeStart: Double, longitudeStart: Double, latitudeEnd: Double, longitudeEnd: Double) {
let c1 = CLLocation(latitude: latitudeStart, longitude: longitudeStart)
let c2 = CLLocation(latitude: latitudeEnd, longitude: longitudeEnd)
let zoom = c1.distance(from: c2)
//Draws a MKGeodesicPolyline between the two points.
var coordinates = [c1.coordinate, c2.coordinate]
let geodesicPolyline = MKGeodesicPolyline(coordinates: &coordinates, count: 2)
mapView.addOverlay(geodesicPolyline)
let location = CLLocationCoordinate2D(latitude: (latitudeStart+latitudeEnd) * 0.5, longitude: (longitudeStart+longitudeEnd) * 0.5)
let region = MKCoordinateRegion(center: location, latitudinalMeters: zoom, longitudinalMeters: zoom)
let adjustRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustRegion, animated: true)
mapView.setVisibleMapRect(geodesicPolyline.boundingMapRect, edgePadding: UIEdgeInsets.init(top: 80.0, left: 100.0, bottom: 100.0, right: 100.0), animated: false)
}
func updateMapOverlayViews() {
mapView.removeAnnotations(mapView.annotations)
mapView.removeOverlays(mapView.overlays)
if mapRoute { addRoute(latitudeStart: -37.6690, longitudeStart: 144.8410, latitudeEnd: -27.3942, longitudeEnd: 153.1218) }
}
Result:

Related

Type ‘()’ cannot conform to ‘AccessibilityRotorContent’

Driving me nuts… I have a class called TrackPiece2 with a bunch of var’s in it… I have an array of these and I want to change a value in each TrackPiece2 of the array…
but I am getting the error “Type ‘()’ cannot conform to ‘AccessibilityRotorContent’” on the ForEach line of func changeZoom below
class TrackPiece2: Identifiable {
var id: UUID = UUID()
var centerPnt: CGPoint = CGPoint.zero
var radius: CGFloat = 0.0
var width: CGFloat = 0.0
var startAngle: Angle = Angle.zero
var angleArc: Angle = Angle.zero
var color: Color = Color.clear
var zoom: CGFloat = 1.0
.
.
.
class TrackLayout2 {
var trackPieces: [TrackPiece2] = []
var drawOuterLines: Bool = false
func addTrackPiece(newTrkPc: TrackPiece2) {
self.trackPieces.append(newTrkPc)
}
func changeZoom(newZoom: CGFloat) {
ForEach(trackPieces) { <——— Error occurs here
trkPc in
trkPc.zoom = newZoom
}
}
func loadTrackPieces() {
let centerPoint = CGPoint(x: 160, y:160)
addTrackPiece(newTrkPc: TrackPiece2(centerPnt: centerPoint, radius: 160.0, width: 10.0, startAngle: Angle(degrees: 0), angleArc: Angle(degrees: 45), color: Color.pink, zoom: 0.5))
}
}
Interesting… but if I replace the entire ForEach {} in func changeZoom with a specific member of the array, ‘trackPieces[0].zoom = newZoom’, it works fine…
Also, TrackPiece2 is Identifiable… I also made it Hashable as well… but that didn’t change anything so I left it as Identifiable only
Thanks for any advice/help… TJ
Ugh ! Thanks vadian… changing
ForEach(trackPieces) {
trkPc in
to
for trkPc in trackPieces {
fixed the problem.

How to animate a view in a circular motion using its real-time position coordinates?

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

SwiftUI Button if Statement

I have the following problem:
I built an app that has 6 buttons and each of these buttons is assigned a CL region.
I have a GPS track file that provides the app with locations and simulates a walk.
Now buttons should turn yellow when the region assigned to them is reached. It works so far, but the problem is that I don't know where to ask whether or not to reach the Region of Button.
I tried .onAppear () {if ...}, but that is only triggered once and I need something that monitors the If statement all the time.
Is there anything?
Thanks in advance
here my code:
import MapKit
import SwiftUI
import CoreLocation
var region1 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505088, longitude: 13.333574), radius: 20, identifier: "region1")
var region2 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504829, longitude: 13.336798), radius: 20, identifier: "region2")
var region3 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504733, longitude: 13.341834), radius: 20, identifier: "region3")
var region4 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.503312, longitude: 13.347718), radius: 20, identifier: "region4")
var region5 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505699, longitude: 13.352198), radius: 20, identifier: "region5")
var region6 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.513517, longitude: 13.350253), radius: 20, identifier: "region6")
class LocationEnviroment: NSObject, CLLocationManagerDelegate, ObservableObject{
var locationManager = CLLocationManager()
#Published var currentPosition = ""
var Waypoints = [region1, region2, region3, region4, region5, region6];
#Published var RegionIndex: Int = 6
func initPositionService(){
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
func isWayPoint(mylocation : CLLocation) -> Int{
for i in self.Waypoints{
if(i == mylocation){
if(i.identifier == "region1"){
return 0
}else if(i.identifier == "region2"){
return 1
}else if(i.identifier == "region3"){
return 2
}else if(i.identifier == "region4"){
return 3
}else if(i.identifier == "region5"){
return 4
}else if(i.identifier == "region6"){
return 5
}
}
}
return 6 // <- nothing found
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let myLocation = locations.last
let lat = myLocation?.coordinate.latitude
let lon = myLocation?.coordinate.longitude
self.currentPosition = "lat: \(lat ?? 0) lon: \(lon ?? 0)"
self.RegionIndex = isWayPoint(mylocation: myLocation!)
}
}
struct ContentView: View {
#StateObject var locationEnviroment = LocationEnviroment()
var Todo = ["Friseur / Haare schneiden", "Dönerbude / zu Mittag essen", "Bank / Geld abheben", "Supermarkt / einkaufen", "Blumeneladen /\n Blumen kaufen", " Lisa's Wohnung /\n Lisa Blumen überreichen" ]
#State private var disabled = Array(repeating: true, count: 6)
#State private var red = 255.0
#State private var blue = 0.0
#State private var green = 0.0
var body: some View {
VStack(alignment: .center, spacing: 10){
Text(locationEnviroment.currentPosition)
.padding()
ForEach(0..<6) { row in
Button(action : {
if(disabled[row] == false){
red = 0.0
green = 255.0
blue = 0.0
}
}) {
Text(Todo[row])
.frame(width : 200, height: 40, alignment: .center)
.clipShape(Rectangle())
.padding()
.border(Color.black, width: 1)
.font(.system(size: 15))
.foregroundColor(.black)
}
.background(Color(red: red/255, green: green/255, blue: blue/255))
.lineLimit(nil)
.cornerRadius(8.0)
.disabled(disabled[row])
.onAppear(){
if(locationEnviroment.RegionIndex == row){
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
}
}
.onAppear(){
locationEnviroment.initPositionService()
}
}
}
You can use onChange to monitor the status of RegionIndex. It might look like this (replacing your onAppear):
.onChange(of: locationEnviroment.RegionIndex) { regionIndex in
if regionIndex == row {
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
A couple of side notes:
Generally in Swift, variable and property names start with lowercase letters
You may want to investigate using computed properties to change/determine the colors. I started writing a solution with those but then realized I didn't totally understand what the full logic of the color system was, especially with the interaction between pressing and disabling the buttons. But, in general, your view will re-render any time a #Published property on your #StateObject changes, so you don't necessarily have to use something like onChange to trigger an update.

How can i rotate an image in SwiftUI with one finger only?

I have an image in a view and i want to be able to rotate it with one finger only, how can i do that in SwiftUI?
I checked the RotationGesture but it works only with two fingers...
Thanks
Ok, i got it with this code :
https://gist.github.com/ts95/9f8e05380824c6ca999ab3bc1ff8541f
Ok fixed it with this code :
struct RotationGesture: View {
#State var totalRotation = CGSize.zero
#State var currentRotation = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-totalRotation.width)))
.gesture(
DragGesture()
.onChanged { value in
totalRotation.width = value.translation.width + currentRotation.width
}
.onEnded { value in
currentRotation = totalRotation
}
)
}
}
But now we have to fixed the vertical movement because this solution is only working when you move around the X axis...
I want to solution to work when you make circle movement around the view you want to rotate...
one finger rotation
struct RotationGesture: View {
#State var gestureValue = CGSize.zero
var body: some View {
Text("Hello, World!")
.frame(width: 150, height: 60)
.padding()
.background(Color.orange)
.cornerRadius(15)
.rotationEffect(Angle(degrees: Double(-gestureValue.width)))
.gesture(
DragGesture().onChanged({ (value) in
self.gestureValue = value.translation
}).onEnded({ (value) in
self.gestureValue = .zero
})
)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
}
}
Using the solutions from above this is what I have for rotating with one finger. This will keep the position when you finish rotating as well as starting the next rotation where the last one left off.
struct SwiftUIView: View {
#State private var rotation: Angle = Angle(degrees: 0)
#State private var previousRotation: Angle?
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.frame(width: 760, height: 760)
.rotationEffect(rotation, anchor: .center)
.gesture(DragGesture()
.onChanged{ value in
if let previousRotation = self.previousRotation {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
let angle = fingerAngle - previousRotation
rotation += angle
self.previousRotation = fingerAngle
} else {
let deltaY = value.location.y - (760 / 2)
let deltaX = value.location.x - (760 / 2)
let fingerAngle = Angle(radians: Double(atan2(deltaY, deltaX)))
previousRotation = fingerAngle
}
}
.onEnded{ _ in
previousRotation = nil
})
}
}
A dragGesture will give us the value of a location where user has dragged on screen (in our case, the circle). Here, the circle frame will be equal height and equal width.
Capturing the location point where the user tapped and subtracting height/2 from y point and width/2 from x point results in deltaX and deltaY. Once we have deltaX and deltaY, we can convert it into radians using the atan2 function (which is provided by the Swift Standard Library).
struct SwiftUIView: View {
#State var angle: Angle = .zero
var circleFrame: CGRect = CGRect(x: 0, y: 0, width: 300, height: 300)
var body: some View {
Circle()
.fill(AngularGradient(gradient: Gradient(colors: [Color.red, Color.blue]), center: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, angle: .degrees(90)))
.padding()
.rotationEffect(angle)
.gesture(
DragGesture()
.onChanged { value in
let deltaY = value.location.y - (circleFrame.height / 2)
let deltaX = value.location.x - (circleFrame.width / 2)
angle = Angle(radians: Double(atan2(deltaY, deltaX)))
}
)
}
}

AxesDrawer cannot draw swift3 xcode 8

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)
}
}