How to custom the image of MKAnnotation pin - swift

I am trying to change the image that is inside the MKAnnotation without removing the rounded shape.
Here I create a custom class of MKAnnotation:
class MapPin: NSObject, MKAnnotation {
let title: String?
let locationName: String
let coordinate: CLLocationCoordinate2D
init(title: String, locationName: String, coordinate: CLLocationCoordinate2D) {
self.title = title
self.locationName = locationName
self.coordinate = coordinate
}
}
Here I create a MapPin and I add it to the mapView
func setPinUsingMKAnnotation() {
let pin1 = MapPin(title: "Here", locationName: "Device Location", coordinate: CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661))
let coordinateRegion = MKCoordinateRegion(center: pin1.coordinate, latitudinalMeters: 800, longitudinalMeters: 800)
mapView.setRegion(coordinateRegion, animated: true)
mapView.addAnnotations([pin1])
}
The first image is what I created, the second image is what I would like it to be.
I even implemented MKMapViewDelegate:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var annotationView = MKAnnotationView()
annotationView.image = #imageLiteral(resourceName: "heart")
return annotationView
}
This is the result:
The rounded shape disappears.
I saw many tutorials about how to custom a pin, but they only explained how to put an image instead of the pin (like the hearth image above). I would like to know how to change the image (and color) of the pin and keep the rounded shape (see the blue pin image above).
Any hints? Thanks

If you want that rounded border, you can render it yourself, or easier, subclass MKMarkerAnnotationView rather than MKAnnotationView:
class CustomAnnotationView: MKMarkerAnnotationView {
override var annotation: MKAnnotation? {
didSet { configure(for: annotation) }
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
glyphImage = ...
markerTintColor = ...
configure(for: annotation)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(for annotation: MKAnnotation?) {
displayPriority = .required
// if doing clustering, also add
// clusteringIdentifier = ...
}
}
That way, not only do you get the circular border, but you get all of the marker annotation view behaviors (shows the title of the annotation view below the marker, if you select on the marker annotation view, it becomes larger, etc.). There’s a lot of marker annotation view behaviors that you probably don’t want to have to write from scratch if you don’t have to. By subclassing MKMarkerAnnotationView instead of the vanilla MKAnnotationView, you get all those behaviors for free.
For example, you could:
class CustomAnnotationView: MKMarkerAnnotationView {
static let glyphImage: UIImage = {
let rect = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))
return UIGraphicsImageRenderer(bounds: rect).image { _ in
let radius: CGFloat = 11
let offset: CGFloat = 7
let insetY: CGFloat = 5
let center = CGPoint(x: rect.midX, y: rect.maxY - radius - insetY)
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi, clockwise: true)
path.addQuadCurve(to: CGPoint(x: rect.midX, y: rect.minY + insetY), controlPoint: CGPoint(x: rect.midX - radius, y: center.y - offset))
path.addQuadCurve(to: CGPoint(x: rect.midX + radius, y: center.y), controlPoint: CGPoint(x: rect.midX + radius, y: center.y - offset))
path.close()
UIColor.white.setFill()
path.fill()
}
}()
override var annotation: MKAnnotation? {
didSet { configure(for: annotation) }
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
glyphImage = Self.glyphImage
markerTintColor = #colorLiteral(red: 0.005868499167, green: 0.5166643262, blue: 0.9889912009, alpha: 1)
configure(for: annotation)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(for annotation: MKAnnotation?) {
displayPriority = .required
// if doing clustering, also add
// clusteringIdentifier = ...
}
}
That yields:
Obviously, when you set glyphImage, set it to whatever image you want. The old SF Symbols doesn't have that “drop” image (though iOS 14 has drop.fill). But supply whatever 40 × 40 pt image view you want. I'm rendering it myself, but you can use whatever appropriately sized image from your asset catalog (or from the system symbols) that you want.
As an aside, since iOS 11, you wouldn't generally wouldn't implement mapView(_:viewFor:) at all, unless absolutely necessary (which it isn't in this case). For example, you can get rid of your viewFor method and just register your custom annotation view in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
mapView.register(CustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
...
}

Related

missing argument for parameter in coding error in trying to subclass a uibezierPath

I want my swift code to display a uibezierPath button. The code uses override func draw to draw the button. The code is getting a compile error. Its telling me I am missing a parameter in let customButton = FunkyButton(coder: <#NSCoder#>) you can see the error in NSCODER. I dont know what to put for nscoder. What do you think I should put?
import UIKit
class ViewController: UIViewController {
var box = UIImageView()
override open var shouldAutorotate: Bool {
return false
}
// Specify the orientation.
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeRight
}
let customButton = FunkyButton(coder: <#NSCoder#>)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(box)
// box.frame = CGRect(x: view.frame.width * 0.2, y: view.frame.height * 0.2, width: view.frame.width * 0.2, height: view.frame.height * 0.2)
box.backgroundColor = .systemTeal
customButton!.backgroundColor = .systemPink
self.view.addSubview(customButton!)
customButton?.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
print("hit")
}
}
class FunkyButton: UIButton {
var shapeLayer = CAShapeLayer()
let aPath = UIBezierPath()
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x: rect.width * 0.2, y: rect.height * 0.8))
aPath.addLine(to: CGPoint(x: rect.width * 0.4, y: rect.height * 0.2))
//design path in layer
shapeLayer.path = aPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.path = aPath.cgPath
// draw is called multiple times so you need to remove the old layer before adding the new one
shapeLayer.removeFromSuperlayer()
layer.addSublayer(shapeLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden == true || self.alpha < 0.1 || self.isUserInteractionEnabled == false {
return nil
}
if aPath.contains(point) {
return self
}
return nil
}
}
When instantiating FunkyButton, don’t manually call the coder rendition. Just call
let button = FunkyButton()
Or add it in IB and hook up an outlet to
#IBOutlet weak var button: FunkyButton!
In FunkyButton, you shouldn't update shape layer path inside draw(_:) method. During initialization, just add the shape layer to the layer hierarchy, and whenever you update the shape layer’s path, it will be rendered for you. No draw(_:) is needed/desired:
#IBDesignable
class FunkyButton: UIButton {
private let shapeLayer = CAShapeLayer()
private var path = UIBezierPath()
// called if button is instantiated programmatically (or as a designable)
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
// called if button is instantiated via IB
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
// called when the button’s frame is set
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard path.contains(point) else {
return nil
}
return super.hitTest(point, with: event)
}
}
private extension FunkyButton {
func configure() {
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1
layer.addSublayer(shapeLayer)
}
func updatePath() {
path = UIBezierPath()
path.move(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.8))
path.addLine(to: CGPoint(x: bounds.width * 0.4, y: bounds.height * 0.2))
path.addLine(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.2))
path.close()
shapeLayer.path = path.cgPath
}
}
If you really want to draw your path in draw(_:), that is an acceptable pattern, too, but you wouldn't use CAShapeLayer at all, and just manually stroke() the UIBezierPath in draw(_:). (If you implement this draw(_:) method, though, do not use the rect parameter of this method, but rather always refer back to the view’s bounds.)
Bottom line, either use draw(_:) (triggered by calling setNeedsDisplay) or use CAShapeLayer (and just update its path), but don't do both.
A few unrelated observations related to my code snippet:
You do not need to check for !isHidden or isUserInteractionEnabled in hitTest, as this method won't be called if the button is hidden or has user interaction disabled. As the documentation says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
I have also removed the alpha check in hitTest, as that is non-standard behavior. It is not a big deal, but this is the sort of thing that bites you later on (e.g. change button base class and now it behaves differently).
You might as well make it #IBDesignable so that you can see it in Interface Builder (IB). There is no harm if you're only using it programmatically, but why not make it capable of being rendered in IB, too?
I have moved the configuration of the path into layoutSubviews. Anything based upon the bounds of the view should be responsive to changes in the layout. Sure, in your example, you are manually setting the frame, but this is an unnecessary limitation to place on this button class. You might use auto-layout in the future, and using layoutSubviews ensures that it will continue to function as intended. Plus, this way, the path will be updated if the size of the button changes.
There's no point in checking for contains if the path is a line. So, I've added a third point so that I can test whether the hit point falls within the path.

Change MKMarkerAnnotationView size

How to change MKMarkerAnnotationView size?
I tried to set annotationView.bounds.size = CGSize(width: 50, height: 50) but it does not look like the size has changed. I also tried to print out the size of the view and looks like it is defaulted to 28,28
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard annotation is MKPointAnnotation else { return nil }
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: Constant.Indentifier.mapPoint)
annotationView.canShowCallout = true
annotationView.animatesWhenAdded = true
annotationView.glyphImage = UIImage(systemName: "house.fill")
annotationView.glyphTintColor = .systemBlue
annotationView.markerTintColor = .white
print(annotationView.bounds.size) // defaulted to 28,28
annotationView.bounds.size = CGSize(width: 50, height: 50) // Does not change bubble size
return annotationView
}
See glyphImage documentation, which talks about the size of the glyph:
The glyph image is displayed when the marker is in the normal state. Create glyph images as template images so that the glyph tint color can be applied to it. Normally, you set the size of this image to 20 by 20 points on iOS and 40 by 40 points on tvOS. However, if you do not provide a separate selected image in the selectedGlyphImage property, make the size of this image 40 by 40 points on iOS and 60 by 40 points on tvOS instead. MapKit scales images that are larger or smaller than those sizes.
Bottom line, the MKMarkerAnnotationView has a fixed sizes for its two states, selected and unselected.
If you want a bigger annotation view, you’ll want to write your own MKAnnotationView. E.g., simply creating a large house image is relatively easy:
class HouseAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
let configuration = UIImage.SymbolConfiguration(pointSize: 50)
image = UIImage(systemName: "house.fill", withConfiguration: configuration)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
By the way, I’d suggest registering this annotation view class, like below, and then removing the mapView(_:viewFor:) method entirely.
mapView.register(HouseAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
Now, the above annotation view only renders a large “house” image. If you want it in a bubble, like MKMarkerAnnotationView does, you’ll have to draw that yourself:
class HouseAnnotationView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
configureImage()
configureView()
configureAnnotationView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension HouseAnnotationView {
func configureImage() {
let radius: CGFloat = 25
let center = CGPoint(x: radius, y: radius)
let rect = CGRect(origin: .zero, size: CGSize(width: 50, height: 60))
let angle: CGFloat = .pi / 16
let image = UIGraphicsImageRenderer(bounds: rect).image { _ in
UIColor.white.setFill()
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: .pi / 2 - angle, endAngle: .pi / 2 + angle, clockwise: false)
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.close()
path.fill()
let configuration = UIImage.SymbolConfiguration(pointSize: 24)
let house = UIImage(systemName: "house.fill", withConfiguration: configuration)!
.withTintColor(.blue)
house.draw(at: CGPoint(x: center.x - house.size.width / 2, y: center.y - house.size.height / 2))
}
self.image = image
centerOffset = CGPoint(x: 0, y: -image.size.height / 2) // i.e. bottom center of image is where the point is
}
func configureView() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowRadius = 5
layer.shadowOffset = CGSize(width: 3, height: 3)
layer.shadowOpacity = 0.5
}
func configureAnnotationView() {
canShowCallout = true
}
}
That yields:
But even that doesn’t reproduce all of the MKMarkerAnnotationView behaviors. So it all comes down to how much of the MKMarkerAnnotationView behaviors/appearance you need and whether having a larger annotation view is worth all of that effort.

Is there any way to move MKCircle overlay with MKUserLocation without flicking or blinking?

Currently, I am using this code to draw the circle.
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? MKCircle {
let circleRenderer = MKCircleRenderer(overlay: overlay)
circleRenderer.fillColor = UIColor.black.withAlphaComponent(0.19)
circleRenderer.lineWidth = 1
return circleRenderer
}
return MKOverlayRenderer(overlay: overlay)
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
let circle = MKCircle(center: userLocation.coordinate, radius: self.regionRadius)
print("\(userLocation.coordinate)")
if (CLLocationManager.authorizationStatus() == .denied || CLLocationManager.authorizationStatus() == .notDetermined) {
mapView.removeOverlays(mapView.overlays)
} else {
mapView.removeOverlays(mapView.overlays)
mapView.addOverlay(circle)
}
}
Current output:
It is working fine but the circle is blinking and flickering. I need a smooth movement of the circle. I am aware that it's an iOS 13 issue.
There are two options:
I’ve found that, in general, the flickering effect is diminished if you add the new overlay before removing the old one.
You might consider making the circle an custom annotation view rather than an overlay. That way, you can just adjust the coordinate without adding/removing.
By putting the circle in the annotation, itself, it’s seamless, both with user tracking on:
I turned off user tracking half way through, so you could see both patterns.
class CirclePointerAnnotationView: MKAnnotationView {
let circleShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.lightGray.withAlphaComponent(0.25).cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()
let pinShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "woman")
imageView.clipsToBounds = true
return imageView
}()
var pinHeight: CGFloat = 100
var pinRadius: CGFloat = 30
var annotationViewSize = CGSize(width: 300, height: 300)
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
layer.addSublayer(circleShapeLayer)
layer.addSublayer(pinShapeLayer)
addSubview(imageView)
bounds.size = annotationViewSize
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
let radius = min(bounds.width, bounds.height) / 2
let center = CGPoint(x: bounds.midX, y: bounds.midY)
circleShapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
let angle = asin(pinRadius / (pinHeight - pinRadius))
let pinCenter = CGPoint(x: center.x, y: center.y - (pinHeight - pinRadius))
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: pinCenter, radius: pinRadius, startAngle: .pi - angle, endAngle: angle, clockwise: true)
path.close()
pinShapeLayer.path = path.cgPath
let imageViewDimension = pinRadius * 2 - 15
imageView.bounds.size = CGSize(width: imageViewDimension, height: imageViewDimension)
imageView.center = pinCenter
imageView.layer.cornerRadius = imageViewDimension / 2
}
}

I can't get an arc to fill in Swift 4

I have a class that makes a UIView of a circle. I when I add the circle to my view, it shows the red outline, but the interior is clear instead of black. I want it to have a black fill.
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(1.0);
UIColor.red.set()
let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let radius = (frame.size.width - 10)/2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.strokePath()
context.closePath()
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}
}
}
This is more easily solved with a UIBezierPath.
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: bounds)
path.lineWidth = 1
UIColor.red.setStroke()
UIColor.black.setFill()
path.fill()
path.stroke()
}
If you look to the documentation of strokePath():
The current path is cleared as a side effect of calling this function.
Basically, after stroking your path, your paths is reset and there is nothing to fill.
A workaround could be to save context.path first and then use context.addPath(path). However, I usually prefer to build a separate CGMutablePath first and then add it to the context.

How to draw a line between two views in Swift 3?

I need help with drawing a simple line between two Outline Views in Swift 3 (Xcode 8).
My situation:
Main ViewController
|--- Main View
|--- Outline View
|--- Outline View
So I Need help to get the coordinates of both Outline Views and draw a line with them (the line itself is not that difficult, more to get the coordinates). The goal is to draw a line (programmatically) that connects both Outline Views (f.ex. from one edge to the other, or from the top, ...).
I already tried following:
class Line: NSView{
var origin = CGPoint()
var destination = CGPoint()
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
}
init(fromPoint: CGPoint, toPoint: CGPoint){
self.origin = fromPoint
self.destination = toPoint
super.init(frame: CGRect(origin: fromPoint, size: CGSize(width: destination.x - origin.x, height: destination.y - origin.y)))
}
override func draw(_ dirtyRect: NSRect){
let myPath = NSBezierPath()
myPath.move(to: CGPoint(x: origin.x, y: origin.y))
myPath.line(to: CGPoint(x: destination.x - origin.x, y: destination.y - origin.y))
myPath.stroke()
}
}
class ViewController: NSViewController{
override func viewDidLoad(){
super.viewDidLoad()
let line = Line(fromPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(1)), toPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(2)))
view.addSubview(line)
}
}
But that didn't do anything.
I would appreciate your help!
Thank you
I now solved my problem (more or less) as following:
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
class ViewController: NSViewController{
override function viewDidLoad(){
super.viewDidLoad()
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
}
}
I need to know how to do this on runtime. Do I always have to create a new view in order to draw a path?
A full example:
//
// ViewController.swift
// DrawConnectViews
//
// Created by T M on 17.06.17.
// Copyright © 2017 TM. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let subview1 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 200.0, height: 200.0)))
let subview2 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 360.0, y: 360.0), size: CGSize(width: 200.0, height: 200.0)))
// create a subview programatically:
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
subview1.setColor(color: NSColor.red)
subview2.setColor(color: NSColor.blue)
self.view.addSubview(subview1)
self.view.addSubview(subview2)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
class CustomViewWithColor: NSView{
var color = NSColor()
func setColor(color: NSColor){
self.color = color
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath(rect: self.bounds)
self.color.setFill()
path.fill()
}
}
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
That produces following:
Output of program
A lot of people will think that this is overkill, but what I do is add a subview whose background is the line color, whose height is constrained to the desired line thickness, and whose leading and trailing edges are constrained to the superview's leading and trailing edges. This ensures that the border will always adjust with the superview's size, which is why adding a border as a layer or customizing the view's draw(in:) to draw the border as a path don't work as well.