inverted circle map fill in swift 4 - swift4

I am trying to draw a circle on map, when user select option 1 the circle is filled with blue color, when user select option 2, the whole map will be filled with blue while the circle area is colorless. how is that possible?
`func addRadiusOverlay(forGeotification geotification: Geotification) {
mapView?.addOverlay(MKCircle(center: geotification.coordinate, radius: 300))
}`
`func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKCircle {
let circleRenderer = MKCircleRenderer(overlay: overlay)
circleRenderer.lineWidth = 5.0
circleRenderer.strokeColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1.0)
circleRenderer.fillColor = circleRenderer.strokeColor!.withAlphaComponent(0.1)
return circleRenderer
}
return MKOverlayRenderer(overlay: overlay)
}`

In case of Option-2, to draw a circle with filled outside and transparent hole, use MKPolygon.polygonWithPoints:count:interiorPolygons: with interiorPolygons parameter to be the circle MKPolygon, like so:
MKPolygon(coordinates: WORLD_COORDINATES, count: WORLD_COORDINATES.count, interiorPolygons: circlePolygon)
Use following method to generate polygons
func setupRadiusOverlay(forGeotification geotification: Geotification) {
let c = makeCircleCoordinates(geotification.coordinate, radius: RADIUS)
self.option1polygon = MKPolygon(coordinates: c, count: c.count, interiorPolygons: nil)
self.option2polygon = MKPolygon(coordinates: WORLD_COORDINATES, count: WORLD_COORDINATES.count, interiorPolygons: option1polygon)
}
Use following method to add a polygon
func addRadiusOverlay(isOption2Selected: Bool) {
guard let mapView = mapView else { return }
let overlay = isOption2Selected ? self.option2polygon : self.option1polygon
if mapView.overlays.index(where: { $0 === overlay }) == nil {
mapView.removeOverlays(mapView.overlays.filter{ $0 is MKPolygon })
mapView.addOverlay(overlay)
}
}
Change delegate method mapView(_:rendererFor:)
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
guard overlay is MKPolygon else {
return MKOverlayRenderer(overlay: overlay)
}
let color = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1.0)
let renderer = MKPolygonRenderer(overlay: overlay)
renderer.lineWidth = 5.0
renderer.strokeColor = color
renderer.fillColor = color.withAlphaComponent(0.1)
return renderer
}
following would be world coordinates
let WORLD_COORDINATES = [
CLLocationCoordinate2D(latitude: 90, longitude: 0),
CLLocationCoordinate2D(latitude: 90, longitude: 180),
CLLocationCoordinate2D(latitude:-90, longitude: 180),
CLLocationCoordinate2D(latitude:-90, longitude: 0),
CLLocationCoordinate2D(latitude:-90, longitude:-180),
CLLocationCoordinate2D(latitude: 90, longitude:-180)
]
And following helper method, courtesy of my old answer
func makeCircleCoordinates(_ coordinate: CLLocationCoordinate2D, radius: Double, tolerance: Double = 3.0) -> [CLLocationCoordinate2D] {
let latRadian = coordinate.latitude * .pi / 180
let lngRadian = coordinate.longitude * .pi / 180
let distance = (radius / 1000) / 6371 // kms
return stride(from: 0.0, to: 360.0, by: tolerance).map {
let bearing = $0 * .pi / 180
let lat2 = asin(sin(latRadian) * cos(distance) + cos(latRadian) * sin(distance) * cos(bearing))
var lon2 = lngRadian + atan2(sin(bearing) * sin(distance) * cos(latRadian),cos(distance) - sin(latRadian) * sin(lat2))
lon2 = fmod(lon2 + 3 * .pi, 2 * .pi) - .pi // normalise to -180..+180º
return CLLocationCoordinate2D(latitude: lat2 * (180.0 / .pi), longitude: lon2 * (180.0 / .pi))
}
}
option-2 selection yields
option-1 should do inverse :)

Related

Giving a CAShapeLayer a 3d effect

I am playing around with countdown timer in swift. What I want to give the countdown circle a 3D shadow of effect.
the code below will operate the countdown time perfectly but i guess its more of the visuals i am trying to edit with it. is there any way to make the countdown circle larger and while giving it a 3d effect. If you run the code you will see it is just a 2d type of fill. I have been playing around with overlapping circles with a different color and alpha like a dark color to try and make it look more 3d, but Its definitely not the most efficient because it involves drawing multiple circles at once. Is there a way to get a similar 3d effect like the image below without having to redraw multiple overlapping circles. the code below is for the basic 2d flat version of the counter.
//for countdown timer: ----------------
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
var timeLeft: TimeInterval = 10
var endTime: Date?
var timeLabel = UILabel()
var timer = Timer()
// here you create your basic animation object to animate the strokeEnd
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.white.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
view.layer.addSublayer(timeLeftShapeLayer)
}
func addTimeLabel() {
timeLabel = UILabel(frame: CGRect(x: view.frame.midX-50 ,y: view.frame.midY/3, width: 100, height: 50))
timeLabel.textAlignment = .center
timeLabel.text = timeLeft.time
view.addSubview(timeLabel)
}
#objc func updateTime() {
if timeLeft > 0 {
timeLeft = endTime?.timeIntervalSinceNow ?? 0
timeLabel.text = timeLeft.time
} else {
timeLabel.text = "00:00"
timer.invalidate()
}
}
//-------------timer finish------------(extension for timer at bottom of file------
Extensions:
//extensions for timer
extension TimeInterval {
var time: String {
return String(format:"%02d:%02d", Int(self/60), Int(ceil(truncatingRemainder(dividingBy: 60))) )
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * .pi / 180
}
}
ViewDidload:
//for countdown timer: -------------
view.backgroundColor = UIColor(white: 0.94, alpha: 1.0)
drawBgShape()
drawTimeLeftShape()
addTimeLabel()
// here you define the fromValue, toValue and duration of your animation
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = timeLeft
// add the animation to your timeLeftShapeLayer
timeLeftShapeLayer.add(strokeIt, forKey: nil)
// define the future end time by adding the timeLeft to now Date()
endTime = Date().addingTimeInterval(timeLeft)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
To obtain 3D effects, you usually work with color gradients. In your use case, you would work with a radial CAGradientLayer. You have to mask this layer to see only the area you want to be visible. The path to be filled consists of the area of the outer and the inner circle.
This fill path can be created as follows:
let path = UIBezierPath(arcCenter: centerPoint, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
let inner = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path.append(inner.reversing())
For the gradients, you can use the locations parameter to specify an array of NSNumber objects that define the location of the gradient stops. The values must be in the range [0,1]. The corresponding associated colors of type CGColor are set in colors property.
In a simple case you could define something like:
gradient.locations = [0, //0
NSNumber(value: innerRadius / outerRadius), //1
NSNumber(value: middle / outerRadius), //2
1] //3
let colors = [color, //0
color, //1
color.lighter(), //2
color, //3
]
gradient.colors = colors.map { $0.cgColor }
However, the desired 3D appearance will be visible only after applying a mask with the corresponding path, see the right part of the figure:
Animation
It is easy to see that you can use one CAGradientLayer for the background and one for the foreground. The question then naturally arises, how can we animate the fill process with the foreground gradient?
This can be achieved by placing the foreground gradient over the background gradient and using a CAShapeLayer as a mask for the foreground gradient. In doing so, the animation is done similarly to the example in your question using the strokeEnd property. Since it is a mask, the foreground gradient becomes visible gradually.
Gradients
Gradients can contain several areas. 3D effects are usually achieved by combining slightly lighter or darker gradations of similar colors. For the demo example, I used this nice, minimally modified answer to get lighter or darker color variants.
Demo
Using the above points, this may look like the following:
The colors, distances and the gradients depend of course strongly on the requirements, this serves only as an example, how one could make such a thing. For the foreground gradient, two similar but different colors were chosen for the shadow area (inner circular area) and the outer circular area, which is in the light.
Self-Contained Complete Example
CircleProgressView.swift
import UIKit
class CircleProgressView: UIView {
private let backgroundGradient = CAGradientLayer()
private let foregroundGradient = CAGradientLayer()
private let timeLeftShapeLayer = CAShapeLayer()
private let backgroundMask = CAShapeLayer()
private let thickness: CGFloat
private let innerBackgroundColor: UIColor
private let outerBackgroundColor: UIColor
private let innerForegroundColor: UIColor
private let outerForegroundColor: UIColor
init(_ thickness: CGFloat,
_ innerBackgroundColor: UIColor,
_ outerBackgroundColor: UIColor,
_ innerForegroundColor: UIColor,
_ outerForegroundColor: UIColor) {
self.thickness = thickness
self.innerBackgroundColor = innerBackgroundColor
self.outerBackgroundColor = outerBackgroundColor
self.innerForegroundColor = innerForegroundColor
self.outerForegroundColor = outerForegroundColor
super.init(frame: .zero)
backgroundGradient.type = .radial
layer.addSublayer(backgroundGradient)
foregroundGradient.type = .radial
layer.addSublayer(foregroundGradient)
timeLeftShapeLayer.strokeColor = UIColor.white.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = thickness
layer.addSublayer(timeLeftShapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func circle(_ gradient: CAGradientLayer,
_ path: UIBezierPath,
_ outerRadius: CGFloat,
_ innerColor: UIColor,
_ outerColor: UIColor) {
let innerRadius = outerRadius - thickness
let middle = outerRadius - thickness / 2
let slice: CGFloat = thickness / 16
gradient.frame = bounds
gradient.locations = [0, //0
NSNumber(value: (innerRadius) / outerRadius), //1
NSNumber(value: (middle - slice) / outerRadius), //2
NSNumber(value: (middle) / outerRadius), //3
NSNumber(value: (middle + slice) / outerRadius), //4
1] //5
let colors = [innerColor, //0
innerColor, //1
innerColor.darker(), //2
outerColor, //3
outerColor.lighter(), //4
outerColor //5
]
gradient.colors = colors.map { $0.cgColor }
gradient.bounds = path.bounds
gradient.startPoint = CGPoint(x: 0.5, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
}
override func layoutSubviews() {
super.layoutSubviews()
let outerRadius: CGFloat = min(bounds.width, bounds.height) / 2.0
let centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let path = UIBezierPath(arcCenter: centerPoint, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
let inner = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path.append(inner.reversing())
circle(backgroundGradient, path, outerRadius, innerBackgroundColor, outerBackgroundColor)
backgroundMask.frame = bounds
backgroundMask.path = path.cgPath
backgroundMask.lineWidth = 0
backgroundGradient.mask = backgroundMask
circle(foregroundGradient, path, outerRadius, innerForegroundColor, outerForegroundColor)
let middlePath = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
middlePath.lineWidth = thickness
timeLeftShapeLayer.path = middlePath.cgPath
foregroundGradient.mask = timeLeftShapeLayer
timeLeftShapeLayer.strokeEnd = 0
}
func startAnimation() {
timeLeftShapeLayer.removeAllAnimations()
timeLeftShapeLayer.strokeEnd = 1
DispatchQueue.main.async {
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = 5
self.timeLeftShapeLayer.add(strokeIt, forKey: nil)
}
}
}
UIColor+Brightness.swift
Only for the sake of completeness, please note that the original can be found at https://stackoverflow.com/a/31466450.
import UIKit
extension UIColor {
func lighter(amount : CGFloat = 0.15) -> UIColor {
return hueColorWithBrightness(amount: 1 + amount)
}
func darker(amount : CGFloat = 0.15) -> UIColor {
return hueColorWithBrightness(amount: 1 - amount)
}
private func hueColorWithBrightness(amount: CGFloat) -> UIColor {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
return UIColor( hue: hue,
saturation: saturation,
brightness: brightness * amount,
alpha: alpha )
} else {
return self
}
}
}
ViewController.swift
The call is rather unsurprising and should look something like this:
import UIKit
class ViewController: UIViewController {
private var circleProgressView: CircleProgressView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0xBB / 0xFF, green: 0xBB / 0xFF, blue: 0xBB / 0xFF, alpha: 1)
let innerBackgroundColor = UIColor(red: 0x65 / 0xFF, green: 0x79 / 0xFF, blue: 0x85 / 0xFF, alpha: 1)
let outerBackgroundColor = innerBackgroundColor
let innerForegroundColor = UIColor(red: 0xCF / 0xFF, green: 0xC9 / 0xFF, blue: 0x22 / 0xFF, alpha: 1)
let outerForegroundColor = UIColor(red: 0xF3 / 0xFF, green: 0xCA / 0xFF, blue: 0x46 / 0xFF, alpha: 1)
let progressView = CircleProgressView(24, innerBackgroundColor, outerBackgroundColor, innerForegroundColor, outerForegroundColor)
circleProgressView = progressView
progressView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressView)
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setTitle("Start", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(onStart), for: .touchUpInside)
let margin: CGFloat = 24
NSLayoutConstraint.activate([
progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: margin),
progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
button.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: margin),
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -margin),
])
}
#objc func onStart() {
circleProgressView?.startAnimation()
}
}

Subviews are not moving with parent UIView

I'm trying to make an app based on image editing and I'm founding some problems coding a resizing by corners dragging crop area.
I have croppingView.swift where the crop rectangle is drawn when launched:
override func viewDidAppear(_ animated: Bool) {
let imagePoint = CGPoint(x: contentView.center.x, y: contentView.center.y)
let imageSize = CGSize(width: cropImageView.image!.size.width, height: cropImageView.image!.size.height)
let widthScale = cropImageView.frame.width / imageSize.width
let heightScale = cropImageView.frame.height / imageSize.height
let scale = min(widthScale, heightScale)
let width = imageSize.width * scale
let height = imageSize.height * scale
let size = CGSize(width: width, height: height)
let originViewFrame = CGPoint(x: imagePoint.x - width/2.0, y: imagePoint.y - height/2.0)
let cropSize = CGSize(width: width * 0.7, height: height * 0.7)
let cropViewFrame = CGRect(origin: originViewFrame, size: cropSize)
cropView = ShapeView(frame: cropViewFrame)
cropView.backgroundColor = UIColor(red: 224.0/255.0, green: 224.0/255.0, blue: 224.0/255.0, alpha: 0.3)
cropView.layer.borderColor = UIColor(red: 97.0/255.0, green: 97.0/255.0, blue: 97.0/255.0, alpha: 1.0).cgColor
cropView.layer.borderWidth = 1.0
cropView.center = imagePoint
self.view.addSubview(cropView)
}
The ShapeView.swift to draw the cropping area is as follows:
class ShapeView: UIView {
var previousLocation = CGPoint.zero
var topLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var topRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
override func didMoveToSuperview() {
superview?.addSubview(topLeft)
superview?.addSubview(topRight)
superview?.addSubview(bottomLeft)
superview?.addSubview(bottomRight)
var pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handleMove))
self.addGestureRecognizer(pan)
self.updateDragHandles()
}
func updateDragHandles() {
topLeft.center = self.transformedTopLeft()
topRight.center = self.transformedTopRight()
bottomLeft.center = self.transformedBottomLeft()
bottomRight.center = self.transformedBottomRight()
}
//Gesture Methods
#IBAction func handleMove(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self.superview!)
var center = self.center
center.x += translation.x
center.y += translation.y
self.center = center
gesture.setTranslation(CGPoint.zero, in: self.superview!)
updateDragHandles()
}
#IBAction func handlePan(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
switch gesture.view! {
case topLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 1))
}
self.bounds.size.width -= translation.x
self.bounds.size.height -= translation.y
case topRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0, y: 1))
}
self.bounds.size.width += translation.x
self.bounds.size.height -= translation.y
case bottomLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 0))
}
self.bounds.size.width -= translation.x
self.bounds.size.height += translation.y
case bottomRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint.zero)
}
self.bounds.size.width += translation.x
self.bounds.size.height += translation.y
default:()
}
gesture.setTranslation(CGPoint.zero, in: self)
updateDragHandles()
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
}
}
As you can see I'm adding one circle in each corner of the rectangle. These circles are used to resize the cropping area by dragging. Here the key is the updateDragHandles()function which updates the positions of all other circle keeping them on the corners.
So when launching the cropping area appears like:
And when user moves for example the bottom-right corner by dragging everything is working fine:
Now the problem is when I click on the portrait button to change cropping area orientation. With the next code into croppingView.swift the grey rectangle correctly changes the size but the circles remain on the their position:
#IBAction func portButton(_ sender: Any) {
portButton.tintColor = UIColor.systemBlue
landButton.tintColor = UIColor.systemGray
isPortait = 1
let inWidth = cropView.frame.size.width
let inHeight = cropView.frame.size.height
if inWidth > inHeight {
let scaleW = cropView.frame.size.height / cropView.frame.size.width
let scaleH = cropView.frame.size.width / cropView.frame.size.height
let fixPoint = CGPoint(x: 0.5, y: 0.5)
cropView.setAnchorPoint(anchorPoint: fixPoint)
cropView.transform = cropView.transform.scaledBy(x: scaleW, y: scaleH)
let vc = ShapeView()
vc.updateDragHandles()
}
}
It's like updateDragHandles is not working from this ViewController...
Probably it's a basic mistake but I can't find out the solution.
Any suggestion?
Thank you in advance!
DragHandle.swift
let diameter:CGFloat = 30
import UIKit
class DragHandle: UIView {
var fillColor = UIColor.darkGray
var strokeColor = UIColor.lightGray
var strokeWidth: CGFloat = 1.0
required init(coder aDecoder: NSCoder) {
fatalError("Use init(fillColor:, strokeColor:)")
}
init(fillColor: UIColor, strokeColor: UIColor, strokeWidth width: CGFloat = 1.0) {
super.init(frame: CGRect(x: 0, y: 0, width: diameter, height: diameter))
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = width
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
super.draw(rect)
let handlePath = UIBezierPath(ovalIn: rect.insetBy(dx: 10 + strokeWidth, dy: 10 + strokeWidth))
fillColor.setFill()
handlePath.fill()
strokeColor.setStroke()
handlePath.lineWidth = strokeWidth
handlePath.stroke()
}
}
UIViewExtensions.swift
import Foundation
import UIKit
extension UIView {
func offsetPointToParentCoordinates(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + self.center.x, y: point.y + self.center.y)
}
func pointInViewCenterTerms(point:CGPoint) -> CGPoint {
return CGPoint(x: point.x - self.center.x, y: point.y - self.center.y)
}
func pointInTransformedView(point: CGPoint) -> CGPoint {
let offsetItem = self.pointInViewCenterTerms(point: point)
let updatedItem = offsetItem.applying(self.transform)
let finalItem = self.offsetPointToParentCoordinates(point: updatedItem)
return finalItem
}
func originalFrame() -> CGRect {
let currentTransform = self.transform
self.transform = .identity
let originalFrame = self.frame
self.transform = currentTransform
return originalFrame
}
func transformedTopLeft() -> CGPoint {
let frame = self.originalFrame()
let point = frame.origin
return self.pointInTransformedView(point: point)
}
func transformedTopRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
return self.pointInTransformedView(point: point)
}
func transformedBottomRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func transformedBottomLeft() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
self.layer.position = position
self.layer.anchorPoint = anchorPoint
}
}

How to draw custom shapes on MKMapView

I need to draw custom shapes like Arc, Semi-circle? I tried the below code but it's not rendering anything on the MKMapView.
Is this the right way to draw custom shapes?
class ViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
mapView.delegate = self
let center = CLLocationCoordinate2D(latitude: 15.463157486154865, longitude: 73.78846049308775)
let radius = CLLocationCoordinate2D(latitude: 15.495608080208948, longitude: 73.83418584279791)
addCircle(center: center, radius: radius)
}
private func createArcPath() -> UIBezierPath {
let center = CLLocationCoordinate2D(latitude: 15.463157486154865, longitude: 73.78846049308775)
// converting the coordinates to CGPoint with respect to MKMapView.
let centerPoint = mapView.convert(center, toPointTo: self.mapView)
// Creating bezierPath of arc.
let path = UIBezierPath(arcCenter: centerPoint, radius: 6080.205481929489, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 2, clockwise: true)
return path
}
}
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKCircle {
let arcRenderer = MKOverlayPathRenderer()
arcRenderer.path = createArcPath().cgPath
arcRenderer.strokeColor = UIColor.red
arcRenderer.fillColor = UIColor.red
arcRenderer.lineWidth = 10
arcRenderer.alpha = 1
arcRenderer.lineCap = .round
return arcRenderer
}
return MKOverlayRenderer()
}
}
extension ViewController {
private func addCircle(center ccoordinate: CLLocationCoordinate2D, radius rcoordinate: CLLocationCoordinate2D) {
let centerLocation = CLLocation.init(latitude: ccoordinate.latitude, longitude: ccoordinate.longitude)
let radiusLocation = CLLocation.init(latitude: rcoordinate.latitude, longitude: rcoordinate.longitude)
let radius = centerLocation.distance(from: radiusLocation)
let circle = MKCircle(center: ccoordinate, radius: radius)
mapView.addOverlay(circle)
}
}
Use this to add custom shape in MAPVIEW
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
{
if overlay is MKCircle
{
print("overlay latitude: "+String(overlay.coordinate.latitude))
print("overlay longitude: "+String(overlay.coordinate.longitude))
let circleOverlay = overlay as! MKCircle
if(circleOverlay.accessibilityPath != nil)
{
let arcRenderer = MKOverlayPathRenderer()
arcRenderer.path = circleOverlay.accessibilityPath?.cgPath
arcRenderer.strokeColor = UIColor.red
arcRenderer.lineWidth = 10
arcRenderer.alpha = 0.3
return arcRenderer
}
let circle = MKCircleRenderer(overlay: overlay)
circle.strokeColor = UIColor.black
circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1)
circle.lineWidth = 1
circle.alpha = 0.3
return circle
}
}
After some RND I came across a library curvyRoute & used it to draw arch on MKMapView.
// Adds arch overlay to the mapView
private func addArcOverlays() {
let pointA = CLLocationCoordinate2D(latitude: 15.463157486154865, longitude: 73.78846049308775)
let pointB = CLLocationCoordinate2D(latitude: 15.495608080208948, longitude: 73.83418584279791)
mapView.addOverlay(LineOverlay(origin: pointA, destination: pointB))
let style = LineOverlayStyle(strokeColor: .red, lineWidth: 4, alpha: 1)
let arc = ArcOverlay(origin: pointA, destination: pointB, style: style)
arc.radiusMultiplier = 0.5
mapView.addOverlay(arc)
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let lineOverlay as LineOverlay:
let linerender = MapLineOverlayRenderer(lineOverlay)
setVisibleMapRect(linerender.overlay, animated: true)
return linerender
case let polyline as MKPolyline:
let renderer = MKPolylineRenderer(overlay: polyline)
renderer.strokeColor = UIColor.yellow.withAlphaComponent(0.5)
renderer.lineWidth = 4
return renderer
default:
return MKOverlayRenderer()
}
}

Reload values for draw circle

Hi I'm creating a semicircle that updates your line widh depending on the values I give you that are always varying. I want to know how to update the values to determine the end angle and that these are always updated. Thanks
class Circle: UIView {
//These 2 var (total dictionary and Secondary dictionary) have the number of the keys and the values ​​are always varying because they are obtained from json data
var dictionaryTotal: [String:String] = ["a":"153.23", "b":"162.45", "c":"143.65", "d":"140.78", "e":"150.23"]
var dictionarySecundario: [String:String] = ["w":"153.23", "l":"162.45", "v":"143.65"]
var lineWidth: CGFloat = 25
// circles radius
var half = CGFloat.pi
var quarter = CGFloat(3 * CGFloat.pi / 2)
func drawCircleCenteredAtVariavel(center:CGPoint, withRadius radius:CGFloat) -> UIBezierPath{
let circlePath = UIBezierPath(arcCenter: centerOfCircleView, radius: halfOfViewsSize, startAngle: half, endAngle: CGFloat(((CGFloat(self.dictionarySecundario.count) * CGFloat.pi) / CGFloat(self. dictionaryTotal.count)) + CGFloat.pi), clockwise: true)
circlePath.lineWidth = lineWidth
UIColor(red: 0, green: 0.88, blue: 0.70, alpha: 1).set()
return circlePath
}
override func draw(_ rect: CGRect) {
drawCircleCenteredAtVariavel(center: centerOfCircleView, withRadius: halfOfViewsSize).stroke()
}
}
This is the picture of semicircle
It's not clear from your question what you're trying to accomplish, so this isn't really answer, but it's more than I wanted to type in a comment.
The first thing you should do is separate responsibilities in so the Circle class above should only be responsible for drawing the semicircle for a particular configuration.
For example, a semicircular progress view based on your code above:
class CircleProgressView: UIView {
var lineWidth: CGFloat = 25
var barColor = UIColor(red: 0, green: 0.88, blue: 0.70, alpha: 1)
var progress: Float = 0
{
didSet {
progress = max(min(progress, 1.0), 0.0)
setNeedsDisplay()
}
}
func semicirclePath(at center: CGPoint, radius: CGFloat, percentage: Float) -> UIBezierPath {
let endAngle = CGFloat.pi + (CGFloat(percentage) * CGFloat.pi)
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat.pi, endAngle: endAngle, clockwise: true)
circlePath.lineWidth = lineWidth
return circlePath
}
override func draw(_ rect: CGRect) {
barColor.set()
let center = CGPoint.init(x: bounds.midX, y: bounds.midY)
let radius = max(bounds.width, bounds.height)/2 - lineWidth/2
semicirclePath(at: center, radius: radius, percentage: progress).stroke()
}
}
Then, outside of this class, you would have other code that would determine what progress is based on the JSON values you're receiving.
Assuming that your drawCircleCenteredAtVariavel works, you should redraw whenever the lineWidth value is changed. Try this
var lineWidth: CGFloat = 25 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
drawCircleCenteredAtVariavel(center: centerOfCircleView, withRadius: halfOfViewsSize).stroke()
}

Can't draw a polyline via MapKit in Swift

I'm trying to draw a line between two annotations on my map. The code is working, but i don't see it on map. Here is my code:
func longPressGesture()
{
let lpg = UILongPressGestureRecognizer(target: self, action: "longPressAction:")
lpg.minimumPressDuration = 1;
Map.addGestureRecognizer(lpg)
}
func longPressAction(myRecognizer : UILongPressGestureRecognizer)
{
let currPoint = myRecognizer.locationInView(Map)
let point = Map.convertPoint(currPoint, toCoordinateFromView: Map)
points.append(currPoint);
if(points.count>1)
{
let startPoint = Map.convertPoint(points[points.count-2], toCoordinateFromView: Map)
let endPoint = Map.convertPoint(currPoint, toCoordinateFromView: Map)
var lineCoords = [startPoint,endPoint]
var line = MKPolyline(coordinates: &lineCoords, count: 2)
Map.addOverlay(line)
}
let myAnnotation = MKPointAnnotation();
myAnnotation.coordinate = point
myAnnotation.title = "Test"
myAnnotation.subtitle = "Test subtitle"
Map.addAnnotation(myAnnotation);
}
mapView method(not sure if it is used, i'm new to swift and i didn't write this):
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
print(overlay)
if overlay is MKCircle {
let circle = MKCircleRenderer(overlay: overlay);
circle.strokeColor = UIColor.redColor();
circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1);
circle.lineWidth = 1;
return circle;
}else if overlay is MKTileOverlay {
let carte_Renderer = MKTileOverlayRenderer(overlay: overlay)
carte_Renderer.alpha = 1
return carte_Renderer
}else if overlay is MKPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay);
polylineRenderer.strokeColor = UIColor.blueColor();
polylineRenderer.lineWidth = 5;
return polylineRenderer;
}else {
return MKPolylineRenderer();
}
}
Ensure you've got an rendererForOverlay method, which defines how your MKPolyline objects will look.
You'll need to include theMKMapViewDelegate as well for this to work.
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer!
{
if overlay is MKPolyline
{
let route: MKPolyline = overlay as MKPolyline
let routeRenderer = MKPolylineRenderer(polyline:route)
routeRenderer.lineWidth = 3.0
routeRenderer.strokeColor = UIColor(red: 45.0/255.0, green: 200.0/255.0, blue: 0.0/255.0, alpha: 1);
return routeRenderer
}
return nil
}