I'm trying to draw an arrow on a MKPolyline, which is working well by overriding the draw method, however the result is a blurred line. Any clues on why this might be?
It also does not show the arrows when zoomed out (zoomScale < 0.05), and also increases the line width as you zoom out,
Code below:
class ArrowPolylineRenderer: MKPolylineRenderer {
override func applyStrokeProperties(to context: CGContext, atZoomScale zoomScale: MKZoomScale) {
super.applyStrokeProperties(to: context, atZoomScale: zoomScale)
UIGraphicsPushContext(context)
if let ctx = UIGraphicsGetCurrentContext() {
ctx.setLineWidth(self.lineWidth)
}
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
if self.polyline.pointCount < 2 { return }
let mapPoints = self.polyline.points()
let startPoint = mapPoints[0]
let endPoint = mapPoints[1]
let p = CGMutablePath()
if zoomScale >= 0.05 {
let midPoint = self.point(for: MKMapPoint(x: (startPoint.x + endPoint.x)/2, y: (startPoint.y + endPoint.y)/2))
let originX = endPoint.x - startPoint.x
let originY = endPoint.y - startPoint.y
let bearingR = atan2f(Float(originY), Float(originX))
let p1 = CGPoint(x: midPoint.x - 60 / (zoomScale + 0.5), y: midPoint.y + 80 / (zoomScale + 0.5))
let p2 = CGPoint(x: midPoint.x + 60 / (zoomScale + 0.5), y: midPoint.y)
let p3 = CGPoint(x: midPoint.x - 60 / (zoomScale + 0.5), y: midPoint.y - 80 / (zoomScale + 0.5))
let points = [p1, p2, p3]
let t1 = CGAffineTransform(translationX: midPoint.x, y: midPoint.y)
let t2 = t1.rotated(by: CGFloat(bearingR))
let t3 = t2.translatedBy(x: -midPoint.x, y: -midPoint.y)
p.addLines(between: points, transform: t3)
}
p.addLines(between: [self.point(for: startPoint), self.point(for: endPoint)])
p.closeSubpath()
context.setStrokeColor(UIColor.yellow.cgColor)
context.setLineWidth(5/zoomScale)
context.addPath(p)
context.strokePath()
}
}
Related
I used this tutorial to create a hexagon shape:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-draw-polygons-and-stars
My goal is to try to round the corners of my hexagon shape. I know I have to use the path.addCurve somehow, but I cannot figure out where I need to do that. I am only getting weird results. Has anyone got an idea?
struct Polygon: Shape {
let corners: Int
let smoothness: CGFloat
func path(in rect: CGRect) -> Path {
guard corners >= 2 else { return Path() }
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
var currentAngle = -CGFloat.pi / 2
let angleAdjustment = .pi * 2 / CGFloat(corners * 2)
let innerX = center.x * smoothness
let innerY = center.y * smoothness
var path = Path()
path.move(to: CGPoint(x: center.x * cos(currentAngle), y: center.y * sin(currentAngle)))
var bottomEdge: CGFloat = 0
for corner in 0 ..< corners * 2 {
let sinAngle = sin(currentAngle)
let cosAngle = cos(currentAngle)
let bottom: CGFloat
if corner.isMultiple(of: 2) {
bottom = center.y * sinAngle
path.addLine(to: CGPoint(x: center.x * cosAngle, y: bottom))
} else {
bottom = innerY * sinAngle
path.addLine(to: CGPoint(x: innerX * cosAngle, y: bottom))
}
if bottom > bottomEdge {
bottomEdge = bottom
}
currentAngle += angleAdjustment
}
let unusedSpace = (rect.height / 2 - bottomEdge) / 2
let transform = CGAffineTransform(translationX: center.x, y: center.y + unusedSpace)
return path.applying(transform)
}
}
struct Hexagon: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Polygon(corners: 3, smoothness: 1)
.fill(.clear)
.frame(width: 76, height: 76)
}
}
Haven't found a fix but this library does what I want:
https://github.com/heestand-xyz/PolyKit
I create this circle view like this:
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
let width = fmin(self.frame.size.width, self.frame.size.height)
let offset_x = abs(width - self.frame.size.width)/2
let offset_y = abs(width - self.frame.size.height)/2
let padding = CGFloat(0.5)
let radius_size = (width/2) - (padding*2)
let circle_width = radius_size/4
context.setStrokeColor(UIColor.black.cgColor)
// Draw a circle
for i in 0 ..< 4
{
let offset = CGFloat(i) * circle_width
context.strokeEllipse(in:
CGRect(
x: padding + offset + offset_x,
y: padding + offset + offset_y,
width: (radius_size - offset)*2,
height: (radius_size - offset)*2))
}
context.strokePath()
}
}
How can I create a line to the circle center, if I have an array, of angles for the most top circle? And how can I do the same, for the middle circle?
For example, I have an array, with the given angles in degrees: [87.0, 112.0, 150.0]
Here's a function drawLine that draws a line from a center point at an angle with a specific radius. To change which circle the line reaches, just change the radius:
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
let width = fmin(self.frame.size.width, self.frame.size.height)
let offset_x = abs(width - self.frame.size.width)/2
let offset_y = abs(width - self.frame.size.height)/2
let padding = CGFloat(0.5)
let radius_size = (width/2) - (padding*2)
let circle_width = radius_size/4
context.setStrokeColor(UIColor.black.cgColor)
// Draw a circle
for i in 0 ..< 4
{
let offset = CGFloat(i) * circle_width
context.strokeEllipse(in:
CGRect(
x: padding + offset + offset_x,
y: padding + offset + offset_y,
width: (radius_size - offset)*2,
height: (radius_size - offset)*2))
}
let angles: [CGFloat] = [87.0, 112.0, 150]
let angles2: [CGFloat] = [210.0, 250.0, 330.0]
let center = CGPoint(x: width/2 + offset_x, y: width/2 + offset_y)
for angle in angles {
drawLine(context: context, center: center, radius: radius_size, angle: angle)
}
for angle in angles2 {
drawLine(context: context, center: center, radius: radius_size * 3 / 4, angle: angle)
}
context.strokePath()
}
}
func drawLine(context: CGContext, center: CGPoint, radius: CGFloat, angle: CGFloat) {
context.move(to: center)
context.addLine(to: CGPoint(x: center.x + radius * cos(angle * .pi / 180), y: center.y - radius * sin(angle * .pi / 180)))
}
I have the following code, to draw a circle, with segments inside of it :
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
let width = fmin(self.frame.size.width, self.frame.size.height)
let offset_x = abs(width - self.frame.size.width)/2
let offset_y = abs(width - self.frame.size.height)/2
let padding = CGFloat(0.5)
let radius_size = (width/2) - (padding*2)
let circle_width = radius_size/4
context.setStrokeColor(UIColor.black.cgColor)
// Draw a circle
for i in 0 ..< 4
{
let offset = CGFloat(i) * circle_width
context.strokeEllipse(in:
CGRect(
x: padding + offset + offset_x,
y: padding + offset + offset_y,
width: (radius_size - offset)*2,
height: (radius_size - offset)*2))
}
let angles: [CGFloat] = [87.0, 112.0, 150]
let angles2: [CGFloat] = [210.0, 250.0, 330.0]
let center = CGPoint(x: width/2 + offset_x, y: width/2 + offset_y)
for angle in angles {
drawLine(context: context, center: center, radius: radius_size, angle: angle)
}
for angle in angles2 {
drawLine(context: context, center: center, radius: radius_size * 3 / 4, angle: angle)
}
context.strokePath()
}
}
func drawLine(context: CGContext, center: CGPoint, radius: CGFloat, angle: CGFloat) {
context.move(to: center)
context.addLine(to: CGPoint(x: center.x + radius * cos(angle * .pi / 180), y: center.y - radius * sin(angle * .pi / 180)))
}
I want to be able, to create a centered between the segments UILabels, with rotation to the right angle, I made an example in photo editor:
Thanks in advance.
I found the answer:
func drawCurvedText(angle: CGFloat, text: String) {
drawCurvedString(
on: textView.layer,
text: NSAttributedString(
string: text,
attributes: [
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)
]),
angle: -angle,
radius: 130)
}
func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) {
var radAngle = angle.radians
let textSize = text.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let perimeter: CGFloat = 2 * .pi * radius
let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi
var textRotation: CGFloat = 0
var textDirection: CGFloat = 0
if angle > CGFloat(10).radians, angle < CGFloat(170).radians {
// bottom string
textRotation = 0.5 * .pi
textDirection = -2 * .pi
radAngle += textAngle / 2
} else {
// top string
textRotation = 1.5 * .pi
textDirection = 2 * .pi
radAngle -= textAngle / 2
}
for c in 0..<text.length {
let letter = text.attributedSubstring(from: NSRange(c..<c+1))
let charSize = letter.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let letterAngle = (charSize.width / perimeter) * textDirection
let x = radius * cos(radAngle + (letterAngle / 2))
let y = radius * sin(radAngle + (letterAngle / 2))
let singleChar = drawText(
on: layer,
text: letter,
frame: CGRect(
x: (layer.frame.size.width / 2) - (charSize.width / 2) + x,
y: (layer.frame.size.height / 2) - (charSize.height / 2) + y,
width: charSize.width,
height: charSize.height))
layer.addSublayer(singleChar)
singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation))
radAngle += letterAngle
}
}
func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = text
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
I have a problem while animating the CAGradientLayer angle.
Angle in CAGradientLayer is represented through start and end point properties.
I want to animate the gradient in a circular fashion.
When I set it inside an animationGroup it doesn't work. No animation is happening.
When I am changing the properties in
DispatchQueue.main.asyncAfter(deadline: now() + 1.0) {
// change properties here
}
it works. But a very fast animation is happening. Which is not good enough.
On the internet the only thing there is is locations and color changes, but no angle change.
Below you can find a Playground project to play with
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
// Gradient layer specification
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.withAlphaComponent(0.5).cgColor, UIColor.orange.withAlphaComponent(0.5).cgColor, UIColor.orange.cgColor]
gradientLayer.locations = [0, 0.27, 1]
gradientLayer.frame = view.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
return gradientLayer
}()
func animationMapFunction(points: [(CGPoint, CGPoint)], keyPath: String) -> [CABasicAnimation] {
return points.enumerated().map { (arg) -> CABasicAnimation in
let (offset, element) = arg
let gradientStartPointAnimation = CABasicAnimation(keyPath: keyPath)
gradientStartPointAnimation.fromValue = element.0
gradientStartPointAnimation.toValue = element.1
gradientStartPointAnimation.beginTime = CACurrentMediaTime() + Double(offset)
return gradientStartPointAnimation
}
}
lazy var gradientAnimation: CAAnimation = {
let startPointAnimationPoints = [(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0)),
(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0))]
let endPointAnimatiomPoints = [(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0)),
(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0))]
let startPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "startPoint")
let endPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "endPoint")
let animationGroup = CAAnimationGroup()
animationGroup.duration = 5.0
animationGroup.repeatCount = Float.infinity
animationGroup.animations = startPointAnimations + endPointAnimations
return animationGroup
}()
override func loadView() {
let view = UIView(frame: UIScreen.main.bounds)
self.view = view
view.layer.addSublayer(gradientLayer)
}
func animate() {
view.layer.removeAllAnimations()
gradientLayer.add(gradientAnimation, forKey: nil)
}
}
// Present the view controller in the Live View window
let vc = MyViewController()
PlaygroundPage.current.liveView = vc
vc.animate()
So what I did was a timer, that triggers start and end points change.
I rotate from 0 to 360 degrees, while incrementing the angle with a given constant (in my case 6)
I created a function mapping: angle → (startPoint, endPoint)
func animate() {
stopAnimation()
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { timer in
self.angle += 6.0
self.angle.formTruncatingRemainder(dividingBy: 360)
CATransaction.begin()
// disable implicit animation
CATransaction.setDisableActions(true)
let pos = points(from: self.angle)
self.gradientLayer.startPoint = pos.0
self.gradientLayer.endPoint = pos.1
CATransaction.commit()
})
}
func stopAnimation() {
timer?.invalidate()
}
And here are the utility functions
extension CGPoint {
var inverse: CGPoint {
return CGPoint(x: 1 - x, y: 1 - y)
}
}
fileprivate func points(from angle: Double) -> (CGPoint, CGPoint) {
let start: CGPoint
switch angle {
case let x where 0 <= x && x < 90:
start = CGPoint(x: x / 180, y: 0.5 - x / 180)
case let x where 90 <= x && x < 180:
start = CGPoint(x: x / 180, y: x / 180 - 0.5)
case let x where 180 <= x && x < 270:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (x - 180) / 180)
case let x where 270 <= x && x < 360:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (360 - x) / 180)
default:
start = CGPoint.zero
}
return (start, start.inverse)
}
Okay so I want to rotate CGPoint(A) 50 degrees around CGPoint(B) is there a good way to do that?
CGPoint(A) = CGPoint(x: 50, y: 100)
CGPoint(B) = CGPoint(x: 50, y: 0)
Here's what I want to do:
This is really a maths question. In Swift, you want something like:
func rotatePoint(target: CGPoint, aroundOrigin origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
let dx = target.x - origin.x
let dy = target.y - origin.y
let radius = sqrt(dx * dx + dy * dy)
let azimuth = atan2(dy, dx) // in radians
let newAzimuth = azimuth + byDegrees * CGFloat(M_PI / 180.0) // convert it to radians
let x = origin.x + radius * cos(newAzimuth)
let y = origin.y + radius * sin(newAzimuth)
return CGPoint(x: x, y: y)
}
There are lots of ways to simplify this, and it's a perfect case for an extension to CGPoint, but I've left it verbose for clarity.
public extension CGFloat {
///Returns radians if given degrees
var radians: CGFloat{return self * .pi / 180}
}
public extension CGPoint {
///Rotates point by given degrees
func rotate(origin: CGPoint? = CGPoint(x: 0.5, y: 0.5), _ byDegrees: CGFloat) -> CGPoint {
guard let origin = origin else {return self}
let rotationSin = sin(byDegrees.radians)
let rotationCos = cos(byDegrees.radians)
let x = (self.x * rotationCos - self.y * rotationSin) + origin.x
let y = (self.x * rotationSin + self.y * rotationCos) + origin.y
return CGPoint(x: x, y: y)
}
}
Usage
var myPoint = CGPoint(x: 40, y: 50).rotate(45)
var myPoint = CGPoint(x: 40, y: 50).rotate(origin: CGPoint(x: 0, y: 0), 45)