How do you properly display custom pins in SwiftUI / Mapkit? - swift

I am in the process of building a SwiftUI app that relies on MapKit.
But I am encountering an issue with the rendering of the custom pin marker.
Whenever I add the pin, this is rendered from the center of the image so it does not properly align with the current location.
I have tried adding offsets, changing the origin, but the custom pin image basically goes out of bounds.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: nil)
let size = CGSize(width: 35, height: 40)
UIGraphicsBeginImageContext(size)
UIImage(named: "customPin")?.draw(in: CGRect(origin: CGPoint(x: 0, y: 20), size: size))
//let context = UIGraphicsGetCurrentContext()!
//context.move(to: CGPoint(x:0, y: -200))
// This code didn't affect the rendering
view.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
view.canShowCallout = true
return view
}
Basically what I think is happening is that the offsets that I am applying are moving the image but the bounds of the CGRect remain in the same spot. I do not know how to offset both.

In Apple’s docs: Annotating a Map with Custom Data they provide the following sample code:
private func setupSanFranciscoAnnotationView(for annotation: SanFranciscoAnnotation, on mapView: MKMapView) -> MKAnnotationView {
let reuseIdentifier = NSStringFromClass(SanFranciscoAnnotation.self)
let flagAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier, for: annotation)
flagAnnotationView.canShowCallout = true
// Provide the annotation view's image.
let image = #imageLiteral(resourceName: "flag")
flagAnnotationView.image = image
// Provide the left image icon for the annotation.
flagAnnotationView.leftCalloutAccessoryView = UIImageView(image: #imageLiteral(resourceName: "sf_icon"))
// Offset the flag annotation so that the flag pole rests on the map coordinate.
let offset = CGPoint(x: image.size.width / 2, y: -(image.size.height / 2) )
flagAnnotationView.centerOffset = offset
return flagAnnotationView
}
They’re using the centerOffset to reposition the image/annotation. Have you tried this? They have shifted the image up and also to the right as they want to align a corner to the coordinate, usually I just want center-bottom aligned so supply zero for the x offset.

Related

Using Swift and CAShapeLayer() with masking, how can I avoid inverting the mask when masked regions intersect?

This question was challenging to word, but explaining the situation further should help.
Using the code below, I'm essentially masking a circle on the screen wherever I tap to reveal what's underneath the black UIView. When I tap, I record the CGPoint in an array to keep track of the tapped locations. For every subsequent tap I make, I remove the black UIView and recreate each tapped point from the array of CGPoints I'm tracking in order to create a new mask that includes all the previous points.
The result is something like this:
I'm sure you can already spot what I'm asking about... How can I avoid the mask inverting wherever the circles intersect? Thanks for your help!
Here's my code for reference:
class MiniGameShadOViewController: UIViewController {
//MARK: - DECLARATIONS
var revealRadius : CGFloat = 50
var tappedAreas : [CGPoint] = []
//Objects
#IBOutlet var shadedRegion: UIView!
//Gesture Recognizers
#IBOutlet var tapToReveal: UITapGestureRecognizer!
//MARK: - VIEW STATES
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - USER INTERACTIONS
#IBAction func regionTapped(_ sender: UITapGestureRecognizer) {
let tappedPoint = sender.location(in: view)
tappedAreas.append(tappedPoint) //Hold a list of all previously tapped points
//Clean up old overlays before adding the new one
for subview in shadedRegion.subviews {
if subview.accessibilityIdentifier != "Number" {subview.removeFromSuperview()}
}
//shadedRegion.layer.mask?.removeFromSuperlayer()
createOverlay()
}
//MARK: - FUNCTIONS
func createOverlay(){
//Create the shroud that covers the orbs on the screen
let overlayView = UIView(frame: shadedRegion.bounds)
overlayView.alpha = 1
overlayView.backgroundColor = UIColor.black
overlayView.isUserInteractionEnabled = false
shadedRegion.addSubview(overlayView)
let path = CGMutablePath()
//Create the box that represents the inverse/negative area relative to the circles
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
//For each point tapped so far, create a circle there
for point in tappedAreas {
path.addArc(center: point, radius: revealRadius, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
path.closeSubpath() //This is required to prevent all circles from being joined together with lines
}
//Fill each of my circles
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = .evenOdd
//Cut out the circles inside that box
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
}
}
You asked:
how can I avoid inverting the mask when masked regions intersect?
In short, do not use the .evenOdd fill rule.
You have specified a fillRule of .evenOdd. That results in intersections of paths to invert. Here is a red colored view with a mask consisting of a path with two overlapping circular arcs with the .evenOdd rule:
If you use .nonZero (which, coincidentally, is the default fill rule for shape layers), they will not invert each other:
E.g.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var maskLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
var points: [CGPoint] = [] // this isn't strictly necessary, but just in case you want an array of the points that were tapped
var path = UIBezierPath()
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.mask = maskLayer
}
#IBAction func handleTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
points.append(point)
path.move(to: point)
path.addArc(withCenter: point, radius: 40, startAngle: 0, endAngle: .pi * 2, clockwise: true)
maskLayer.path = path.cgPath
}
}
Resulting in:

How to custom the image of MKAnnotation pin

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

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.

How to enable MapKit floor level?

I am trying to show floor level into my apple app. I know in apple map there are some selected place like airport or shopping mall where this floor level can be seen. I need to achieve exactly that. Just need to show the floor level where this is available. As you can see in the picture, in the right hand side of the image there are 5F,4F,3F,2F etc. I have searched the net but left with no clue yet.
You need to use MKOverlay. You would add each floor as an overlay to your MKMapView and show whatever floor the user selects, hide the others.
Here is a sample for making an overlay:
import MapKit
class MapOverlay: NSObject, MKOverlay {
var coordinate: CLLocationCoordinate2D
var boundingMapRect: MKMapRect
override init() {
let location = CLLocationCoordinate2D(latitude: 75.3307, longitude: -152.1929) // change these for the position of your overlay
let mapSize = MKMapSize(width: 240000000, height: 200000000) // change these numbers for the width and height of your image
boundingMapRect = MKMapRect(origin: MKMapPoint(location), size: mapSize)
coordinate = location
super.init()
}
}
class MapOverlayRenderer: MKOverlayRenderer {
let overlayImage: UIImage
init(overlay: MKOverlay, image: UIImage) {
self.overlayImage = image
super.init(overlay: overlay)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let imageReference = overlayImage.cgImage else { return }
let rect = self.rect(for: overlay.boundingMapRect)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0.0, y: -rect.size.height)
context.draw(imageReference, in: rect)
}
}
Then add it to your map:
let mapOverlay = MapOverlay()
mapView.addOverlay(mapOverlay)
And don't forget the delegate:
mapView.delegate = self
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
return MapOverlayRenderer(overlay: overlay, image: UIImage(named: "overlayImage")!)
}
}

Swift - How to create a view with a shape cropped in it

I'm trying to achieve the result shown in the image using swift 1.2 and xcode 6.
Basically I want to create a view with a shape cut in it to be able to see the the view below to make a tutorial for my app.
I know how to create a circular shape but i don't know how to cut it out in a view.
I need a complete example on how to do it please.
Thanks in advance
Even though there is an answer, i'd like to share my way:
// Let's say that you have an outlet to the image view called imageView
// Create the white view
let whiteView = UIView(frame: imageView.bounds)
let maskLayer = CAShapeLayer() //create the mask layer
// Set the radius to 1/3 of the screen width
let radius : CGFloat = imageView.bounds.width/3
// Create a path with the rectangle in it.
let path = UIBezierPath(rect: imageView.bounds)
// Put a circle path in the middle
path.addArcWithCenter(imageView.center, radius: radius, startAngle: 0.0, endAngle: CGFloat(2*M_PI), clockwise: true)
// Give the mask layer the path you just draw
maskLayer.path = path.CGPath
// Fill rule set to exclude intersected paths
maskLayer.fillRule = kCAFillRuleEvenOdd
// By now the mask is a rectangle with a circle cut out of it. Set the mask to the view and clip.
whiteView.layer.mask = maskLayer
whiteView.clipsToBounds = true
whiteView.alpha = 0.8
whiteView.backgroundColor = UIColor.whiteColor()
//If you are in a VC add to the VC's view (over the image)
view.addSubview(whiteView)
// Annnnnd you're done.
//assume you create a UIImageView and content image before execute this code
let sampleMask = UIView()
sampleMask.frame = self.view.frame
sampleMask.backgroundColor = UIColor.black.withAlphaComponent(0.6)
//assume you work in UIViewcontroller
self.view.addSubview(sampleMask)
let maskLayer = CALayer()
maskLayer.frame = sampleMask.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 150
circleLayer.frame = CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height)
let finalPath = UIBezierPath(roundedRect: CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(ovalIn: CGRect(x:sampleMask.center.x - 150, y:sampleMask.center.y - 150, width: 300, height: 300))
finalPath.append(circlePath.reversing())
circleLayer.path = finalPath.cgPath
circleLayer.borderColor = UIColor.white.withAlphaComponent(1).cgColor
circleLayer.borderWidth = 1
maskLayer.addSublayer(circleLayer)
sampleMask.layer.mask = maskLayer
Here is sample code for how you can make a circle Mask for a UIView:
let sampleView = UIView(frame: UIScreen.mainScreen().bounds)
let maskLayer = CALayer()
maskLayer.frame = sampleView.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 100
circleLayer.frame = CGRectMake(sampleView.center.x - 100, sampleView.center.y - 100, 200, 200)
let circlePath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 200, 200))
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.blackColor().CGColor
maskLayer.addSublayer(circleLayer)
sampleView.layer.mask = maskLayer
Here is what I made in the playground:
The easiest way to do this would be to create a png image with partly transparent white around the outside and a clear circle in the middle. Then stack 2 image views on top of each other, with the masking image on top, and set its "opaque" flag to false.
You could also do this by creating a CAShapeLayer and set it up to use a translucent white color, then install a shape that is the square with the hole cut out of it shape. You'd install that shape layer on top of your image view's layer.
The most general-purpose way to do that would be to create a custom subclass of UIImageView and have the init method of your subclass create and install the shape layer. I just created a gist yesterday that illustrated creating a custom subclass of UIImageView. Here is the link: ImageViewWithGradient gist
That gist creates a gradient layer. It would be a simple matter to adapt it to create a shape layer instead, and if you modified the layoutSubviews method you could make it adapt the view and path if the image view gets resized.
EDIT:
Ok, I took the extra step of creating a playground that creates a cropping image view. You can find that at ImageViewWithMask on github
The resulting image for my playground looks like this:
class MakeTransparentHoleOnOverlayView: UIView {
#IBOutlet weak var transparentHoleView: UIView!
// MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
if self.transparentHoleView != nil {
// Ensures to use the current background color to set the filling color
self.backgroundColor?.setFill()
UIRectFill(rect)
let layer = CAShapeLayer()
let path = CGMutablePath()
// Make hole in view's overlay
// NOTE: Here, instead of using the transparentHoleView UIView we could use a specific CFRect location instead...
path.addRect(transparentHoleView.frame)
path.addRect(bounds)
layer.path = path
layer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = layer
}
}
override func layoutSubviews () {
super.layoutSubviews()
}
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}