Swift 3 / current CLLocation inside MKPolygon without CGPath or Mapview - swift

Full disclosure - I'm new here, but I've dug through everything and can't seem to parse together what I need. I'm at the point where Google is only returning purple links.
Is there a way to check user's current CLLocation lat/lon against several MKPolygon's without operating a MapView? Basically just in the background until lat/lon is inside MKPolygon then return a BOOL.
Here's what I have currently.
var points: [CLLocationCoordinate2D] = [CLLocationCoordinate2D]()
points.append(CLLocationCoordinate2DMake(43.8138, -78.8884))
points.append(CLLocationCoordinate2DMake(43.8074, -78.8768))
points.append(CLLocationCoordinate2DMake(43.8130, -78.8688))
points.append(CLLocationCoordinate2DMake(43.8210, -78.8806))
let zoneA: MKPolygon = MKPolygon(coordinates: &points, count: 4)
let point1 = CLLocationCoordinate2D(latitude: 43.8146, longitude: -78.8784)
func contained(by vertices: [CLLocationCoordinate2D]) -> Bool {
let path = CGMutablePath()
for vertex in vertices {
if path.isEmpty
{
path.move(to: CGPoint(x: vertex.longitude, y: vertex.latitude))
}
else
{
path.addLine(to: CGPoint(x: vertex.longitude, y: vertex.latitude))
}
}
let point = CGPoint(x: 43.7146, y: -78.8784)
return path.contains(point)
}

Related

Custom MKOverlay & Renderer draw not working

I'm attempting to create a custom overlay for MKMapView which varies the colour of the polyline according to certain criteria. However, none of my drawing code works, & I can't for the life of me figure out why. I'm using CoreGPX for trackPoints, but I don't think that's relevant to the problem as the overlay's coordinates are set in the normal way.
The subclasses are -
class FlightOverlay: MKPolyline {
enum Route {
case coordinates([CLLocationCoordinate2D])
case gpx(GPXRoot)
}
var trackPoints: [GPXTrackPoint]?
convenience init?(route: Route) {
switch route {
case .coordinates(let coordinates):
self.init(coordinates: coordinates, count: coordinates.count)
case .gpx(let gpxRoot):
guard let track = gpxRoot.tracks.first, let segment = track.segments.first else {
return nil
}
var trackPoints: [GPXTrackPoint] = []
let coordinates: [CLLocationCoordinate2D] = segment.points.compactMap { (point) -> CLLocationCoordinate2D? in
guard let latitude = point.latitude, let longitude = point.longitude else {
return nil
}
trackPoints.append(point) // this ensures that our trackPoints array & the polyline points have the same number of elements
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
self.init(coordinates: coordinates, count: coordinates.count)
self.trackPoints = trackPoints
}
}
}
class FlightOverlayRenderer: MKPolylineRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let polyline = polyline as? FlightOverlay, let trackPoints = polyline.trackPoints, polyline.pointCount > 1 else {
super.draw(mapRect, zoomScale: zoomScale, in: context)
return
}
context.setLineWidth(lineWidth)
var previousPoint = point(for: polyline.points()[0])
for index in 1..<polyline.pointCount {
let trackPoint = trackPoints[index]
let acceleration: Double
if let accelerationValue = trackPoint.extensions?[.acceleration].text, let accel = Double(accelerationValue) {
acceleration = accel
} else {
acceleration = 0
}
let color = UIColor(red: acceleration * 10, green: 0.5, blue: 0.6, alpha: 1).cgColor
context.setStrokeColor(color)
context.beginPath()
context.move(to: previousPoint)
let point = point(for: polyline.points()[index])
context.addLine(to: point)
context.closePath()
context.drawPath(using: .stroke)
previousPoint = point
}
}
}
Even if I comment out all the stuff which attempts to set colours according to a GPXTrackPoint & just draw a start-to-finish line in bright red, nothing appears. When I directly call super, it works fine with the same coordinates.
Any help much appreciated!
Well, maybe it's a rookie error, but it never occurred to me that I'd need to control the lineWidth as well. Turns out it needs to be adjusted according to the zoomScale - I was drawing the line exactly where it needed to be but it was too thin to be seen. For my case, I do -
let lineWidth = self.lineWidth * 20 / pow(zoomScale, 0.6)
context.setLineWidth(lineWidth)
which gives me a visually pleasing line thickness for all zoom levels.

Drawing rounders corners on 315° arc

I am drawing an arc that is 270° and has rounded coners on both ends. This is working fine, however now I would like to change my arch to be 315° (-45°), but then my corner calculation won't work.
I have tried to calculate this different ways but can't seem to find the formula for making a general function to add rounded corners to my arc when the start and ends are not vertical och horizontal.
This is my playground code:
import UIKit
import PlaygroundSupport
class ArcView: UIView {
private var strokeWidth: CGFloat {
return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
}
private let cornerRadius: CGFloat = 10
override open func draw(_ rect: CGRect) {
super.draw(rect)
backgroundColor = UIColor.white
drawNormalCircle()
}
func drawNormalCircle() {
let strokeWidth = CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0
var endAngle: CGFloat = 270.0
let bezierPath = UIBezierPath(arcCenter: self.center, radius: outerRadius, startAngle: 0, endAngle: endAngle * .pi / 180, clockwise: true)
var point = bezierPath.currentPoint
point.y += cornerRadius
let arc = UIBezierPath(arcCenter: point, radius: cornerRadius, startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
arc.apply(CGAffineTransform(rotationAngle: (360 - endAngle) * .pi / 180))
var firstCenter = bezierPath.currentPoint
firstCenter.y += cornerRadius
bezierPath.addArc(withCenter: firstCenter, radius: cornerRadius , startAngle: 270 * .pi / 180 , endAngle: 0, clockwise: true)
bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x, y: strokeWidth - cornerRadius))
var secondCenter = bezierPath.currentPoint
secondCenter.x -= cornerRadius
bezierPath.addArc(withCenter: secondCenter, radius: cornerRadius , startAngle: 0, endAngle: 90 * .pi / 180, clockwise: true)
bezierPath.addArc(withCenter: self.center, radius: innerRadius, startAngle: 270 * .pi / 180, endAngle: 0, clockwise: false)
var thirdCenter = bezierPath.currentPoint
thirdCenter.x += cornerRadius
bezierPath.addArc(withCenter: thirdCenter, radius: cornerRadius , startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x + strokeWidth - (cornerRadius * 2), y: bezierPath.currentPoint.y))
var fourthCenter = bezierPath.currentPoint
fourthCenter.y += cornerRadius
bezierPath.addArc(withCenter: fourthCenter, radius: cornerRadius , startAngle: 270 * .pi / 180, endAngle: 0, clockwise: true)
bezierPath.close()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = bezierPath.cgPath
backgroundLayer.strokeColor = UIColor.red.cgColor
backgroundLayer.lineWidth = 2
backgroundLayer.fillColor = UIColor.lightGray.cgColor
self.layer.addSublayer(backgroundLayer)
}
}
let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = arcView
The problem for me is how to calculate the arc center for the corners when the corner is not a given X - CornerRadius, or Y + corner Radius, which it is in perfectly horizontal or vertical cases. How can I have rounded corners when the arc is 315°.
Preface: usually when my answer to a question is "do something completely different," I aim to fix the original problem as is, and then also suggest the better approach in addition. However, that's unfeasible for this one, because the complexity of this code, if it were to be expanded in the same style, grows so immensely that it just wasn't worth it.
The problem here is fundamentally that of code organization. Many expressions are repeated by copy-pasting. Extracting them to a variable would not only give a central place for editing, but it also gives a name to the expression, immensely improving readability.
This code will be long. But that's okay, because it's going to be simple. Having a bunch of simple stuff almost always beats a small amount of complex stuff. You might be able to write some crazy trig code that lays out your bezier curve perfectly, but chances are, you won't get it right the first time. Debugging will be hard. It will be totally foreign, and way harder to anyone who isn't you ...and that includes future you. Future you will struggle.
Before we start, here's a rough diagram to help you orient yourself with the rest of this post:
A visual development environment
Firstly, we need to establish a good way to visualize our results.
Playground can quickly reload previews, so that's a plus. But debugging with closed paths is hard, because it's hard to tell the individual sections of a bezier path apart, and often times for convex shapes, the path will close over itself in a way that obscures the part of the path you're working on. So the first part of the code I would work on, is an abstraction layer for UIBezierPath
To remedy this, I'm going to develop the shape by stroking each section in a different colour. Unfortunately, you can't stroke a subsection of a UIBezierPath separately from the rest, so to achieve this, our shape will need to be composed of multiple UIBezierPaths, stroking each one as we go along. But this could be slow in performance-sensitive contexts, so ideally we only want to be doing this during development. I want to be able to pick between one of two different ways to do the same thing. Protocols are perfect for that, so let's start there.
I'll start with BezierPathBuilder. All it does is allow me append BezierPathRenderable parts to it (which I'll get to later), and build a final path, which I can hand off to my CALayer or whatever.
protocol BezierPathBuilder: AnyObject {
func append(_: BezierPathRenderable)
func build() -> UIBezierPath
}
The main implementation of this protocol is really simple, it just wraps a UIBezierPath. When the renderable is told to render itself, it'll simply operate on the path we give it, without needing to allocate any intermediate paths.
class BezierPathBuilderImpl: BezierPathBuilder {
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
renderable.render(into: self, self.path)
}
func build() -> UIBezierPath {
return path
}
}
The debug implementation is a bit more interesting. When appending a renderable, we don't let it draw itself directly into our main path. Instead, we make a new temporary path for it to use, where it'll draw itself. We then have the opportunity to stroke that path (with a different colour every time). Once we've done that, we can append that temporary path to the main path, and resume.
class DebugBezierPathBuilder: BezierPathBuilder {
var rainbowIterator = ([
.red, .orange, .yellow, .green, .cyan, .blue, .magenta, .purple ] as Array<UIColor>).makeIterator()
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
let newPathSegment = UIBezierPath()
renderable.render(into: self, newPathSegment)
// This will crash if you use too many colours, but it suffices for now.
rainbowIterator.next()!.setStroke()
newPathSegment.lineWidth = 20
newPathSegment.stroke()
path.append(newPathSegment)
}
func build() -> UIBezierPath {
return path
}
}
Objectifying our geometry
In your code, there's no separation between geometry calculations and drawing. As a result, you can't easily define one component in reference to another, because you don't have a way of "fishing out" that last arc you drew in the UIBezierPath, or whatever. So let's remedy that.
First of all, I'll define a protocol, BezierPathRenderable, which our program will use to define what it means for an entity to be renderable to a BezierPath.
protocol BezierPathRenderable {
func render(into builder: BezierPathBuilder, _ path: UIBezierPath)
}
This design isn't my favourite, but it's the best I could come up with. The two parameters here allow the conforming type to either draw itself directly into the path, or call append on the builder. The latter is useful for aggregate shapes that are composed out of simpler constituents (sound familiar?)
Wish oriented development
My favourite process for writing code involves scaffolding out a whole bunch of stuff early on, writing whatever parts of the code are interesting to me, and just adding stub implementations left-and-right. Every step of the way, I'm essentially answering the question "what API do I wish I had right now?", and then I stub it, and pretend that it exists.
Stubbing the main shape
I'll start with the main structure. We want an object that models a sector of an annulus with rounded corners. This object would have to do two things:
Store all values that parameterized shape
Define a way to render the shape, by conforming to BezierPathRenderable
So let's start with that:
struct RoundedAnnulusSector: BezierPathRenderable {
let center: CGPoint
var innerRadius: CGFloat
var outerRadius: CGFloat
var startAngle: Angle
var endAngle: Angle
var cornerRadius: CGFloat
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
/// ???
}
Preparing some rendering code
Let's write some scaffolding code to draw this using our new rendering system. For now I'll be using our debugging strokes to render, so I'll comment out the CAShapeLayer stuff:
import UIKit
import PlaygroundSupport
class ArcView: UIView {
private var strokeWidth: CGFloat {
return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
}
override open func draw(_ rect: CGRect) {
super.draw(rect)
self.backgroundColor = UIColor.white
let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0
let shape = RoundedAnnulusSector(
center: self.center,
innerRadius: innerRadius - 50,
outerRadius: outerRadius - 50,
startAngle: (45 * .pi) / 180,
endAngle: (315 * .pi) / 180,
cornerRadius: 25
)
let builder = DebugBezierPathBuilder()
builder.append(shape)
let path = builder.build()
let backgroundLayer = CAShapeLayer()
// backgroundLayer.path = path.cgPath
// backgroundLayer.strokeColor = UIColor.red.cgColor
// backgroundLayer.lineWidth = 2
// backgroundLayer.fillColor = UIColor.lightGray.cgColor
self.layer.addSublayer(backgroundLayer)
}
}
let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = arcView
This obviously does nothing, because we haven't implemented RoundedAnnulusSector.render(into:_:).
Stubbing the corners
We can notice that the entirety of this shape's drawing hinges upon the details of its 4 corners. If our shape has four corners, why don't we just say that?
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner { ??? }
private var corner2: RoundedAnnulusSectorCorner { ??? }
private var corner3: RoundedAnnulusSectorCorner { ??? }
private var corner4: RoundedAnnulusSectorCorner { ??? }
}
In writing that, I wished that a structure existed called RoundedAnnulusSectorCorner, which would do two things:
Store all values that parameterized shape
Define a way to render the shape, by conforming to BezierPathRenderable
Notice they're the same two roles that RoundedAnnulusSector fulfills. These things are intentionally simple, and meant to be composable.
For now we can just stub RoundedAnnulusSectorCorner
struct RoundedAnnulusSectorCorner {}
...and fill in our computed properties to return default instances. Next we'll want to define the inner and outer arcs of our shape.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner2: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner3: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner4: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var outerArc: Arc { ??? }
private var innerArc: Arc { ??? }
}
Implementing Arc
Again, Arc is just another shape, that'll fulfill the same two roles as the others. From our familiarity with UIBezierPath's arc APIs, we'll know that the arcs would need a center, radius, start/end angle, and an indicator of whether to draw them clockwise or counter-clockwise. So we can fill that out:
struct Arc: BezierPathRenderable {
let center: CGPoint
let radius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let clockwise: Bool
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
path.addArc(withCenter: center, radius: radius,
startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
}
}
First approximation of innerArc/outerArc
Now we need to determine the arguments to initialize our arcs with. We'll start without rounded corners, so we'll just use our startAngle/endAngle directly, with our innerRadius/outerRadius.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var outerArc: Arc {
return Arc(
center: self.center,
radius: self.outerRadius,
startAngle: self.startAngle,
endAngle: self.endAngle,
clockwise: true
)
}
private var innerArc: Arc {
return Arc(
center: self.center,
radius: self.innerRadius,
startAngle: self.endAngle,
endAngle: self.startAngle,
clockwise: false
)
}
}
Initial rendering
With these two parts complete, we can start drawing to see how it looks so far, by doing an initial implementation of RoundedAnnulusSector.render(into:_:)
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
let components: [BezierPathRenderable] = [
self.outerArc,
self.innerArc,
]
builder.append(contentsOf: components)
}
}
extension BezierPathBuilder {
func append<S: Sequence>(contentsOf renderables: S) where S.Element == BezierPathRenderable {
for renderable in renderables {
self.append(renderable)
}
}
}
As we progress, we can add more BezierPathRenderable components to this list. I saw this coming, so I made that BezierPathBuilder for handling sequences, so we can just feed it an array and have it automatically append all the elements within.
Stubbing startAngleEdge, endAngleEdge
This shape needs two straight lines. The first will connect corner 4 with corner 1 (which will be a radial line outwards from the center along the startAngle), and the second will connect corner 2 with corner 3 (which will be a radial line outwards from the center along the endAngle. Let's put those in:
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
let components: [BezierPathRenderable] = [
self.outerArc,
self.endAngleEdge,
self.innerArc,
self.startAngleEdge,
]
builder.append(contentsOf: components)
}
// ...
private var endAngleEdge: Line {
return Line()
}
private var startAngleEdge: Line {
return Line()
}
}
Implementing Line
We can just stub out Line, but we know a line just connects two points. It's so simple we may as well just finish it:
struct Line: BezierPathRenderable {
let start: CGPoint
let end: CGPoint
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
path.move(to: self.start)
path.addLine(to: self.end)
}
}
Implementation of startAngleEdge/endAngleEdge
Now we need to figure out what the start/end points will be for our two lines. It would be really convenient if our RoundedAnnulusSectorCorner had startPoint: CGPoint and endPoint: CGPoint properties.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var endAngleEdge: Line {
return Line(
start: self.corner2.endPoint,
end: self.corner3.startPoint)
}
private var startAngleEdge: Line {
return Line(
start: self.corner4.endPoint,
end: self.corner1.startPoint)
}
}
First approximation of startAngleEdge/endAngleEdge
Let's fulfill our wish
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
var startPoint: CGPoint { return .zero }
var endPoint: CGPoint { return .zero }
}
Because these are all implemented as CGPoint.zero, none of our edges would draw.
Second approximation of startAngleEdge/endAngleEdge
So let's implement some better approximation of startPoint/endPoint. Suppose our point had a rawCornerPoint: CGPoint. This would be a point that would be the location of the corner, if there was no rounding (i.e. rounding radius = 0). In the no-rounding world, our startPoint/endPoint would both be rawCornerPoint. Let's stub it and use it:
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
var rawCornerPoint: CGPoint { return .zero }
var startPoint: CGPoint { return self.rawCornerPoint }
var endPoint: CGPoint { return self.rawCornerPoint }
}
Implementing rawCornerPoint
Now, we'll need to derive it's real value. rawCornerPoint depends on two things:
the center of the parent shape,
the position of the corner, relative to the center of the parent shape. This itself relies on:
the distance to the parent shape's center
the angle relative to the parent shape's center
Each of these things is a parameter of our parent shape, so these properties will actually be stored (and initialized by the parent shape). We can then use them to compute the offset, and add that offset to the parentCenter.
struct RoundedAnnulusSectorCorner {
let parentCenter: CGPoint
let distanceToParentCenter: CGFloat
let angleToParentCenter: CGFloat
var rawCornerPoint: CGPoint {
let inset = CGPoint(
radius: self.distanceToParentCenter,
angle: self.angleToParentCenter
)
return self.parentCenter + inset
}
// ...
}
Clearly, an initializer for CGPoint that initializes it from polar coordinates would be just wonderful.
Also, writing .applying(CGAffineTransform(translationX: deltaX, y: deltaY) is annoying, it would be nice to just have a + operator.
Implementing some CGPoint utilities
Let's fulfill more of our wishes:
// Follows UIBezierPath convention on angles.
// 0 is "right" at 3 o'clock, and angle increase clockwise.
extension CGPoint {
init(radius: CGFloat, angle: CGFloat) {
self.init(x: radius * cos(angle), y: radius * sin(angle))
}
static func + (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x + r.x, y: l.y + r.y)
}
static func - (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x - r.x, y: l.y - r.y)
}
}
Populating the corners' fields
Now that our corners actually have stored properties, we can go back to our RoundedAnnulusSector and fill them in.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.outerRadius,
angleToParentCenter: self.startAngle
)
}
private var corner2: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.outerRadius,
angleToParentCenter: self.endAngle
)
}
private var corner3: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.innerRadius,
angleToParentCenter: self.endAngle
)
}
private var corner4: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.innerRadius,
angleToParentCenter: self.startAngle
)
}
// ...
}
Second rendering
Our main shape's render list already contains our lines, but now that we've implemented an approximation of them, we can actually test them. If I've explained things correctly so far, at this point there should be a closed-looking shape, with sharp corners. Success! (I hope)
Rounding things off
Makes it sound like we're almost done, but no way haha, this is where the good stuff starts.
Firstly, we should add all the stuff that comes with roundedness. We know our rounded corners are going to be arcs, and luckily, we've already implemented those!
Arcs need a start and end angle, so we'll need those too. We can stub them with 0 and 2π, so that we don't have to worry about the orientation of our rounded corners' arcs. For now, they'll just be full circles.
struct RoundedAnnulusSectorCorner {
// ...
let radius: CGFloat
var arc: Arc {
return Arc(center: rawCornerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}
/// The angle at which this corner's arc starts.
var startAngle: CGFloat { return 0 }
/// The angle at which this corner's arc ends.
var endAngle: CGFloat { return 2 * .pi }
}
Third rendering
Now that our corners have arcs, we can add those arcs to our render list for drawing:
struct RoundedAnnulusSector: BezierPathRenderable {
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let cornerRadius: CGFloat
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
let components: [BezierPathRenderable] = [
self.corner1.arc,
self.outerArc,
self.corner2.arc,
self.endAngleEdge,
self.corner3.arc,
self.innerArc,
self.corner4.arc,
self.startAngleEdge,
]
builder.append(contentsOf: components)
}
}
And behold, circles!
But uh'oh, they're centred around the rawCornerPoint. I suppose that shouldn't be surprising, because that's literally how we defined our Arc.
Insetting the center
But to get this right, we need to inset the center of the arc. Let's call it the center. The center needs to be inset, so that it's closer towards the "inside" of RoundedAnnulusSector, so that after adding the corner radius, the stroke lines up with the rest of the shape.
This insetting consists of two components:
1. The center needs to be rotated, relative to the parentCenter by an angle (call it rotationalInsetAngle), which makes it lay on the inner or outer arc, so that its arc extends rotationally to reach the radius
Here's a photo for reference:
The blue circle is our corner's current circle, centred on rawCornerPoint.
The small arrow marked 1 is the rotational component of the inset.
The pink circle is our corner's circle, after the rotational inset.
The small arrow marked 2 is the radial translation component of the inset
The green circle is our desired corner circle, centred on center.
The dashed arrow marked offset is the offset between rawCornerPoint and center, obtained as a composition of the rotional and radial translation components.
struct RoundedAnnulusSectorCorner {
// ...
/// The center of this rounded corner's arc
///
/// ...after insetting from the `rawCornerPoint`, so that this rounded corner's arc
/// aligns perfectly with the curves adjacent to it.
var center: CGPoint {
return self.rawCornerPoint
.rotated(around: self.parentCenter, by: self.rotationalInsetAngle)
.translated(towards: self.edgeAngle, by: self.radialInsetDistance)
}
}
We have a long wish list here: rotationalInsetAngle, radialInsetDistance, CGPoint.rotated(around:by:), CGPoint.translated(towards:, by:).
More CGPoint utilities
Luckily, these are pretty easy to implement, now that we have the polar initializer.
extension CGPoint {
func translated(towards angle: CGFloat, by r: CGFloat) -> CGPoint {
return self + CGPoint(radius: r, angle: angle)
}
func rotated(around pivot: CGPoint, by angle: CGFloat) -> CGPoint {
return (self - pivot).applying(CGAffineTransform(rotationAngle: angle)) + pivot
}
}
Getting stuck with rotationalInsetAngle, radialInsetDistance
This is where the shit hits the fan. We know that we have to inset our corner by translating it over by radius. But in which direction?
The inner two corners (#3 and #4) need to translate radially away from the parent, whereas the outer two corners (#1 and #3) need to translate radially inwards towards the parent.
Similarly, our rotional inset also need to vary. For the two corners on the starting edge (#1 and #4), we need to inset clockwise from the startEdge, whereas the two corners on the ending edge (#2 and #3) need to inset by counter-clockwise from the endEdge.
But our data model so far only tells our corners where they are in terms of an angle and a distance. It doesn't specify which side they need to stay on relative the defined distanceFromCenter and angleToParentCenter.
This will need some pretty big refactoring.
Implementing RadialPosition and RotationalPosition
Lets implement a type called RadialPosition. It'll capture not just a radial position (i.e. a distance form a central point), but also which side of that distance to "stay on". A struct containing a radialDistance: CGFloat and isInsideOfRadialDistance: Bool would do, but I know I've frequently made lots of bugs stemming from improperly handling conditions. Instead, I'll use a two case enum, where the inside/outside distinction is more explicit, and harder to miss. Because enum associated values are cumbersome to access, I'll add a helper computed property, distanceFromCenter, to hide away that annoying switch statement.
struct RoundedAnnulusSectorCorner {
// ...
enum RadialPosition {
case outside(ofRadius: CGFloat)
case inside(ofRadius: CGFloat)
var distanceFromCenter: CGFloat {
switch self {
case .outside(ofRadius: let d), .inside(ofRadius: let d): return d
}
}
}
// ...
}
Next, I'll do a similar thing for RotationalPosition:
struct RoundedAnnulusSectorCorner {
// ...
enum RotationalPosition {
case cw(of: CGFloat)
case ccw(of: CGFloat)
var edgeAngle: CGFloat {
switch self {
case .cw(of: let angle), .ccw(of: let angle): return angle
}
}
}
// ...
}
Now I'll have to remove the existing distanceToParentCenter: CGFloat and angleToParentCenter: CGFloat properties and replace them with these new models. We need to migrate their call sites to radialPosition.distanceFromCenter and RotationalPosition. edgeAngle. This is the final set of stored properties for RoundedAnnulusSectorCorner:
struct RoundedAnnulusSectorCorner {
let parentCenter: CGPoint
let radius: CGFloat
let radialPosition: RadialPosition
let rotationalPosition: RotationalPosition
// ...
/// The location of the corner, if this rounded wasn't rounded.
private var rawCornerPoint: CGPoint {
let inset = CGPoint(
radius: self.radialPosition.distanceFromCenter,
angle: self.rotationalPosition.edgeAngle
)
return self.parentCenter + inset
}
// ...
}
And we'll have to update our corner definitions to provide this new data. These are the final set of definitions for the corners.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
private var corner2: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner3: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner4: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
// ...
}
Fourth rendering
Running this code again, we see that now the circles are still centred on their rawCornerPoint, but this is good. It means our refactoring hasn't broken our already-working features. If we had unit tests all along, this is where they'd be useful.
To be continued
in my next answer, because I just got a StackOverflow error that I double many people have had before:
That's my solution.
It's just simple trigonometry
/// Create a path made with 6 small subpaths
///
/// - Parameters:
/// - startAngle: the start angle of the path in cartesian plane angles system
/// - endAngle: the end angle of the path in cartesian plane angles system
/// - outerRadius: the radius of the outer circle in % relative to the size of the view that holds it
/// - innerRadius: the radius of the inner circle in % relative to the size of the view that holds it
/// - cornerRadius: the corner radius of the edges
///
/// - Returns: the path itself
func createPath(from startAngle: Double, to endAngle: Double,
outerRadius:CGFloat, innerRadius:CGFloat,
cornerRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
let maxDim = min(view.frame.width, view.frame.height)
let oRadius: CGFloat = maxDim/2 * outerRadius
let iRadius: CGFloat = maxDim/2 * innerRadius
let center = CGPoint.init(x: view.frame.width/2, y: view.frame.height/2)
let startAngle = deg2rad(360.0 - startAngle)
let endAngle = deg2rad(360.0 - endAngle)
// Outer Finish Center point
let ofcX = center.x + (oRadius - cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
let ofcY = center.y + (oRadius - cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))
// Inner Finish Center point
let ifcX = center.x + (iRadius + cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
let ifcY = center.y + (iRadius + cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))
// Inner Starting Center point
let iscX = center.x + (iRadius + cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
let iscY = center.y + (iRadius + cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))
// Outer Starting Center point
let oscX = center.x + (oRadius - cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
let oscY = center.y + (oRadius - cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))
// Outer arch
path.addArc(withCenter: center, radius: oRadius,
startAngle: startAngle, endAngle: endAngle,
clockwise: true)
// Rounded outer finish
path.addArc(withCenter: CGPoint(x: ofcX, y: ofcY), radius: cornerRadius,
startAngle: endAngle, endAngle:endAngle + deg2rad(90),
clockwise: true)
// Rounded inner finish
path.addArc(withCenter: CGPoint(x: ifcX, y: ifcY), radius: cornerRadius,
startAngle: endAngle + deg2rad(90), endAngle: endAngle + deg2rad(180),
clockwise: true)
// Inner arch
path.addArc(withCenter: center, radius: iRadius,
startAngle: endAngle, endAngle: startAngle,
clockwise: false)
// Rounded inner start
path.addArc(withCenter: CGPoint(x: iscX, y: iscY), radius: cornerRadius,
startAngle: startAngle + deg2rad(180), endAngle: startAngle + deg2rad(270),
clockwise: true)
// Rounded outer start
path.addArc(withCenter: CGPoint(x: oscX, y: oscY), radius: cornerRadius,
startAngle: startAngle + deg2rad(270), endAngle: startAngle,
clockwise: true)
return path
}
func deg2rad(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
Usage:
#IBOutlet weak var mainView: UIView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let borderLayer = CAShapeLayer()
borderLayer.path = createPath(from: 30, to: 120, outerRadius: 0.9, innerRadius: 0.3, cornerRadius: 5).cgPath
borderLayer.strokeColor = UIColor.orange.cgColor
borderLayer.fillColor = UIColor.orange.cgColor
borderLayer.lineWidth = 0.0
mainView.layer.addSublayer(borderLayer)
}
Continued from the first part of my answer
Another try at rotationalInsetAngle, radialInsetDistance
Now that are corners are aware of what side they are relative to their distance and angle, we can implement our rotationalInsetAngle, radialInsetDistance.
radialInsetDistance is easy. We just move closer or further from the middle, by self.radius, depending on whether we're on the inside or outside.
struct RoundedAnnulusSectorCorner {
// ...
/// The distance towards/away from the disk's center
/// where this corner's center is going to be
internal var radialInsetDistance: CGFloat {
switch self.radialPosition {
case .inside(_): return -self.radius // negative: towards center
case .outside(_): return +self.radius // positive: away from center
}
}
}
The rotationalInsetAngle is a bit trickier, you'll need to bust out a notepad and do your best to recall high school trig.
struct RoundedAnnulusSectorCorner {
// ...
/// The angular inset (in radians) from the disk's edge
/// where this corner's center is going to be
internal var rotationalInsetAngle: CGFloat {
let angle = ???
switch self.rotationalPosition {
case .ccw(_): return -angle // negative: ccw from the edge
case .cw(_): return +angle // postiive: cw from the edge
}
}
}
We know we need to rotate by some angle, called it angle, whose magnitude is always the same, but whose sign depending on whether our corner is before the start or after before the end edge.
We know that after our translation, our corner circle's stroke will overlap the parent shape's edges/arcs. That distance is the self.radius, and it forms the "opposite" side a the right triangle. The hypotenuse is the radial we're rotating around, with a length of self.radialPosition.distanceFromCenter. Given that we have an opposite (o) and a hypotenuse (h), the correct trig function for the job is sin. angle = sin(o / h). In context:
struct RoundedAnnulusSectorCorner {
// ...
/// The angular inset (in radians) from the disk's edge
/// where this corner's center is going to be
internal var rotationalInsetAngle: CGFloat {
let angle = sin(self.radius / self.radialPosition.distanceFromCenter)
switch self.rotationalPosition {
case .ccw(_): return -angle // negative: ccw from the edge
case .cw(_): return +angle // postiive: cw from the edge
}
}
}
Ditching rawCornerPoint
Hippity hoppity, our center computed property should now world properly, because our newly implemented rotationalInsetAngle and radialInsetDistance.
We can update our corners' arcs to use it:
struct RoundedAnnulusSectorCorner {
// ...
var arc: Arc {
return Arc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}
// ...
}
Fifth rendering
If all has gone well, you should see the corner's circle are now at the right place (centred on centers, not rawCornerPoints).
Decircularifying
Up until now, we've rendered the rounded corners as full circles. That was useful for getting it going, but now we can fix it. Let's properly implement the startAngle/endAngle computed properties to derive the appropriate start and end angles for each rounded corner.
These are easy. Every rounded corner starts going with the edge angle, perpendicular to it, against it, or perpendicular the other way. The arc then continues for a quarter turn, so we can just obtain the endAngle by adding a quarter turn (2π) to startAngle.
struct RoundedAnnulusSectorCorner {
// ...
/// The angle at which this corner's arc starts.
var startAngle: CGFloat {
switch (radialPosition, rotationalPosition) {
case let ( .inside(_), .cw(of: edgeAngle)): return edgeAngle + (3 * .pi/2)
case let ( .inside(_), .ccw(of: edgeAngle)): return edgeAngle + (0 * .pi/2)
case let (.outside(_), .ccw(of: edgeAngle)): return edgeAngle + (1 * .pi/2)
case let (.outside(_), .cw(of: edgeAngle)): return edgeAngle + (2 * .pi/2)
}
}
/// The angle at which this corner's arc ends.
var endAngle: CGFloat {
return self.startAngle + .pi/2 // A quarter turn clockwise from the start
}
}
Sixth rendering
We're almost there! Now our circles are no more. We have rounded corners, centred on the correctly inset center points, arcing in the right arcs.
The only remaining issue, is that our startAngleEdge, endAngleEdge, innerArc, and outerArc don't terminate where the rounded corners terminate.
Fixing the edges
We can now replace our definition of startPoint/endPoint, which until now been computed as being rawCornerPoint.
Computing these is as easy translating our point, towards the start/end angle, by a distance of radius. Oh look, we already made a tool for that!
struct RoundedAnnulusSectorCorner {
// ...
/// The point at which this corner's arc starts.
var startPoint: CGPoint {
return self.center.translated(towards: startAngle, by: radius)
}
/// The point at which this corner's arc ends.
var endPoint: CGPoint {
return self.center.translated(towards: endAngle, by: radius)
}
// ...
}
Seventh rendering
Now our edges are in the right place!
Fixing the arcs
Fixing the arcs is easy. We know each corner's rotationalInsetAngle, we just need to add it to our start/end angles, so that they start/end later/earlier as necessary:
struct RoundedAnnulusSectorCorner {
// ...
private var outerArc: Arc {
return Arc(
center: self.center,
radius: self.outerRadius,
startAngle: self.startAngle + self.corner1.rotationalInsetAngle,
endAngle: self.endAngle + self.corner2.rotationalInsetAngle,
clockwise: true
)
}
private var innerArc: Arc {
return Arc(
center: self.center,
radius: self.innerRadius,
startAngle: self.endAngle + self.corner3.rotationalInsetAngle,
endAngle: self.startAngle + self.corner4.rotationalInsetAngle,
clockwise: false
)
}
// ...
}
Eighth rendering
It's done! Here's the final final gist, though I do strongly encourage following along and learning about the process.

Using UIViewPropertyAnimator to drive animation along curved path?

I have an interruptible transition that is driven by UIViewPropertyAnimator and I would like to animate a view along a non linear path to its destination.
Doing some research I've found that the way to do this is by using:
CAKeyframeAnimation. However, I'm having issue coordinating the animations also I'm having issue with the CAKeyframeAnimation fill mode.
It appears to be reverse even though I've set it to forwards.
Below is the bezier path:
let bezierPath = self.generateCurve(from: CGPoint(x: self.itemStartFrame.midX, y: self.itemStartFrame.midY), to: CGPoint(x: self.itemEndFrame.midX, y: self.itemEndFrame.midY), bendFactor: 1, thickness: 1)
func generateCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {
let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
let normalNormalized: CGPoint = {
let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
guard normalSize > 0.0 else { return .zero }
return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
}()
let path = UIBezierPath()
path.move(to: from)
let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)
path.addQuadCurve(to: to, controlPoint: closeControlPoint)
path.addQuadCurve(to: from, controlPoint: farControlPoint)
path.close()
return path
}
Path visualization:
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
containerView.layer.addSublayer(shapeLayer)
Here's the CAKeyframeAnimation that resides inside of the UIViewPropertyAnimator
let curvedPathAnimation = CAKeyframeAnimation(keyPath: "position")
curvedPathAnimation.path = bezierPath.cgPath
curvedPathAnimation.fillMode = kCAFillModeForwards
curvedPathAnimation.calculationMode = kCAAnimationPaced
curvedPathAnimation.duration = animationDuration
self.attatchmentView.layer.add(curvedPathAnimation, forKey: nil)
So far this animation along the correct path, however the animation reverse back to it's starting position and is not in sync with the property animator.
Any suggestions?

How to keep a 30˚ distance between two lines anchored at a point

I am trying to create two lines that are anchored at a certain point (sprite) and rotate to form a 30 degree angle between them. Below is an image what I want to achieve.
This is what I've done so far:
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class GameScene: SKScene, SKPhysicsContactDelegate {
var anchorSprite = SKSpriteNode(imageNamed: "anchorSprite")
var armLeft = SKSpriteNode(imageNamed: "lineSprite")
var armRight = SKSpriteNode(imageNamed: "lineSprite")
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx: 0, dy: -1.8)
self.physicsWorld.contactDelegate = self
var tealBg = SKSpriteNode(imageNamed: "tealBg")
tealBg.position = CGPoint(x: frame.midX, y: frame.midY)
tealBg.zPosition = 10
addChild(tealBg)
anchorSprite.position = CGPoint(x: frame.midX, y: frame.midY + frame.midY/2)
anchorSprite.zPosition = 20
anchorSprite.physicsBody = SKPhysicsBody(rectangleOf: anchorSprite.frame.size)
anchorSprite.physicsBody?.categoryBitMask = pinCategory
anchorSprite.physicsBody?.isDynamic = false
addChild(anchorSprite)
armRight.anchorPoint = CGPoint(x: 0.5, y: 1)
armRight.position = anchorSprite.position
armRight.zPosition = 20
armRight.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
armRight.zRotation = CGFloat(Double(15).degreesToRadians)//CGFloat(Double.pi/6)
armRight.physicsBody!.isDynamic = true
addChild(armRight)
armLeft.anchorPoint = CGPoint(x: 0.5, y: 1)
armLeft.position = anchorSprite.position
armLeft.zPosition = 20
armLeft.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
armLeft.zRotation = CGFloat(Double(-15).degreesToRadians)//CGFloat(-Double.pi/6)
armLeft.physicsBody!.isDynamic = true
addChild(armLeft)
//Pin joints
var pinAndRightArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armRight.frame.maxY))
self.physicsWorld.add(pinAndRightArmJoint)
var pinAndLeftArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armLeft.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armLeft.frame.maxY))
self.physicsWorld.add(pinAndLeftArmJoint)
}
Below is an image from running the above code (they are close together).
How can I make sure the lines are always 30˚ apart and maintain 30˚ apart even when rotated?
To keep your two lines separated by exactly 30°, you can use an SKPhysicsJointFixed, which is just what it sounds like: it pins two physicsBodies together in a fixed position. Since you already have them positioned the way you want, just add this code where you have the other SKPhysicsJoints to hold them that way:
let fixArms = SKPhysicsJointFixed.joint(withBodyA: armLeft.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint.zero)
self.physicsWorld.add(fixArms)
Result:
If you make the line nodes children of the anchor sprite (instead of the scene), rotating the anchor sprite node will rotate all the lines along with it without doing anything special with physics. You just need to mind the anchor points so that they align properly (i.e. line's anchor at its extremity rather than center)

Get viewport coordinates from mapView? regionDidChanged swift

The following function is called when the user moved the map.
It is possible to get the viewport? Viewport is latitude and longitude of north/east and the lat. and lon. of south/west.
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool){
print("The \(mapView.centerCoordinate)")
print("the maprect \(mapView.visibleMapRect.origin)")
print("the maprect \(mapView.visibleMapRect.size)")
//I find just this attributes for mapView
}
I don't know how get the viewports out of my data. I can't find anything in the www.
The destination is that I log the coordinates with google analytics and another tool evaluate the datas.
Has somebody an idea? Thanks a lot
MapKit
let northEast = mapView.convertPoint(CGPoint(x: mapView.bounds.width, y: 0), toCoordinateFromView: mapView)
let southWest = mapView.convertPoint(CGPoint(x: 0, y: mapView.bounds.height), toCoordinateFromView: mapView)
googleMaps
mapView has a property "projection" where you can use "coordinateForPoint" to convert a point from the mapView to a coordinate. so the following could work:
swift 3 for google maps
let northEast = mapView.projection.coordinate(for: CGPoint(x: mapView.layer.frame.width, y: 0))
let southWest = mapView.projection.coordinate(for: CGPoint(x: 0, y: mapView.layer.frame.height))
swift 2
let northEast = mapView.projection.coordinateForPoint(CGPoint(x: mapWidth, y: 0 ))
let southWest = mapView.projection.coordinateForPoint(CGPoint(x: 0, y: mapHeight))
you just need to enter your mapView width and height
Based on fel1xw answer
Swift 3 MapKit
let northEast = mapView.convert(CGPoint(x: mapView.bounds.width, y: 0), toCoordinateFrom: mapView)
let southWest = mapView.convert(CGPoint(x: 0, y: mapView.bounds.height), toCoordinateFrom: mapView)