I'm trying to change all the images of my MKMapView's markers. Here is the code:
for annotation:MKAnnotation in self.annotations as [MKAnnotation] {
println(annotation.title) // Returns the correct title
var updatedImage: UIImage! = self.bikeStationsManager.getMarker(annotation.title!, isABikeStation: displayingBikes) // Method to get the new image
println(updatedImage.size.height) // Returns a value > 0
self.viewForAnnotation(annotation).image = updatedImage // Fatal error: unexpectedly found nil while unwrapping an Optional value
}
Here is the weird thing: I've 83 markers and when I'm using this code it's crashing every time but not always for the same annotation and neither the annotation or the image returned seem the problem according to println(). So I really don't know what is wrong in my code, does anyone have an idea?
I also tried to update only the first annotation's image for example, it's working.
viewForAnnotation returns an optional, according to the documentation:
Return Value
The annotation view or nil if the view has not yet been
created. This method may also return nil if the annotation is not in
the visible map region and therefore does not have an associated
annotation view.
It returns even nil if the annotation is not in the visible area of your map so iterating over a loop of all annotations is not the right approach to change the annotation images.
You have to implement
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {}
of the MKMapViewDelegate protocol and create the annotation view there.
Related
I want to have the user location being shown as a custom image (not the blue beacon). I also want that whenever the user location changes its coordinates, this custom image is rotated accordingly to represent the user course. In order to achieve that, I implemented the code below. It is being called with annotation type userLocation when the app is started, so that the user location custom image is being shown fine. The problem is that when I simulate a change in user location coordinates, the delegate mapView(_:viewFor:) method is never called again with an annotation type userLocation, so the image is never rotated. (Note: the UIImage rotateImage(by:) is a custom method).
Is there some means to call mapView(_:viewFor:) so that I can update the image annotation for user location when user location changes? Thanks for any help.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isEqual(mapView.userLocation) {
// Annotation image for user location
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: nil)
annotationView.image = UIImage(named: "UserLocationImage.png")?.rotateImage(by: currentCourse)
return annotationView
}
}
Use CLLocationManagerDelegate
https://developer.apple.com/documentation/corelocation/cllocationmanagerdelegate
Implement this protocol in your code, and put your mapView function into its locationManager(_:didUpdateLocations:) method.
I am trying to draw a polyline on a map in Swift 2. It all works well, but I get a compiler warning for this code:
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
if overlay is MKPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = UIColor.redColor()
polylineRenderer.lineWidth = 5
return polylineRenderer
}
return nil
}
This will give me a warning says that 'Result and parameters in mapView (rendererForOverlay) have different optionality than expected by protocol MKMapViewDelegate'
Now, this will compile fine, but it bugs me that the compiler warning is showing.
If I change the first line to
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
by removing the !, the warning will go away but I get an error that the return cannot be nil and the code will not compile anymore.
This is also a follow up to this thread where the same problem was stated but no satisfying answer is available:
Swift 2 MKMapViewDelegate rendererForOverlay optionality
Can anyone shed any light on the correct way to use this function now in Swift 2?
Thanks.
Going by what autocomplete suggests the prototype looks like this:
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer
And apparently there's nothing you can do about it, except for returning return MKPolylineRenderer() where normally you would return nil.
To me it looks like an implementation bug, because here's what the documentation says about the returned object:
The renderer to use when presenting the specified overlay on the map. If you return nil, no content is drawn for the specified overlay object.
I suggest you create a case for it in Apple's bug report
Don't return nil. This is only called for overlays you create, so instead of checking if overlay is MKPolyline, check which of your overlays it is. If you have only one, return the specified polyline renderer without checking which one it is.
I currently have a UITableView that holds a list of events. Each event has a button where the user can press it to get access to a larger view of a photo associated with the event. I have the UIButton connected and set up but I'm having a hard time setting the image in the new window. Here is my function:
func largeMap(sender: UIButton!) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let manager = NSFileManager.defaultManager()
self.performSegueWithIdentifier("largeMapView", sender: self)
println(sender.tag.description)
println(documentsPath+"/Flight"+(sender.tag.description)+"/Map.png")
if let image = UIImage(data: manager.contentsAtPath(documentsPath+"/Flight"+(sender.tag.description)+"/Map.png")!) {
largeMapView.image = image
}
}
When I run the app, it crashes at largeMapView.image = image and gives me "fatal error: unexpectedly found nil while unwrapping an Optional value". I've checked and the path for the image is correct and it is accessing the image but it's not able to set it to the UIImageView. I have the UIImageView outlet set at the top of the class. Why is this happening? I've set UIImageView's in this manner before but it just won't work now.
Basically the problem is that you are using exclamation marks. Exclamation marks are never safe! They can be a "bad smell", a danger signal - and in your code, that's just what they are.
(1)
I have the UIImageView outlet set at the top of the class.
But you do not show how it is set. You do not show what largeMapView is or how it is declared. The declaration might look like this:
#IBOutlet var largeMapView : UIImageView!
If so, that is bad - that is an implicitly unwrapped optional, which means it might be nil. This could be the source of your problem. By using a conditionally unwrapped optional you are covering up the problem - until you try to set into nil, and then you crash. If it is never being set properly, that's the reason for the crash.
(2)
Along the same same lines, the rest of your code, too, should not force-unwrap Optionals (using ! postfix operator) unless you know for certain that they can never be nil. Rewrite like this so that every Optional is unwrapped conditionally and safely:
if let d = manager.contentsAtPath(documentsPath+"/Flight"+(sender.tag.description)+"/Map.png") {
if let image = UIImage(data: d) {
largeMapView.image = image
}
}
This way, if contentsAtPath fails or UIImage(data:) fails, nothing will happen - you won't set your image - but at least you won't crash. And you can use stepping in the debugger to figure out where you are failing. - But even this will not save you if the problem is that largeMapView is a nil implicitly unwrapped Optional.
As the question states, I'm having trouble setting the view frame for my custom camera overlay using the Swift language. I keep getting an error that states "value of optional type "CGRect" not unwrapped" that I don't quite understand. The problem line is this one:
cameraOverlay.frame = camera.cameraOverlayView?.frame
Xcode attempts to auto-correct the issue by adding a bang (!) at the end of frame, but that does not work either, and creates another error.
Here is my entire nib initialization code for my custom camera overlay:
#IBOutlet var cameraOverlay: UIView!
override func viewDidLoad()
{
super.viewDidLoad()
var camera = UIImagePickerController()
camera.delegate = self
camera.allowsEditing = false
camera.sourceType = UIImagePickerControllerSourceType.Camera
camera.showsCameraControls = false
NSBundle.mainBundle().loadNibNamed("CameraOverlay", owner: self, options: nil)
cameraOverlay.frame = camera.cameraOverlayView?.frame
camera.cameraOverlayView = cameraOverlay
cameraOverlay = nil
self.presentViewController(camera, animated: false, completion: nil)
}
Any help is appreciated. Thanks!
EDIT
Make sure your nib view does not have a white background, or else the camera will not appear. Set the view background color to clearColor, and make sure opaque is unchecked to be safe. I had this problem for a little while until I realized what was happening.
Use this
cameraOverlay.frame = camera.cameraOverlayView!.frame
Actually you are using ? optional chaining which returns frame as wrapped in optional.! is used for optional unwrapping
or you can also do
//it will not crash but you should handle nil case using unwrap by `!` as shown above using `if` condition
cameraOverlay.frame = (camera.cameraOverlayView?.frame)!
I have several MKAnnotations (and their corresponding views) in my map, and it sometimes gets really crowded. Now, the annotations in my app come in two flavors: some are bound to stay where they are, while others will move as time goes on. I'd prefer to have the more stable ones visually in the background and the moving ones to always pass in front of them.
One would think, perhaps, that the annotations most recently added to the map would end up to the front (or alternatively at the very back, at least) but this just doesn't seem to be the rule. As far as I can tell, I create and add ALL the non-moving annotations first, and then add some newly-instantiated moving annotations, but many of them (although not all!) end up drawn under the perpetually stock-still ones.
Interestingly, when time goes by, and yet new moving annotations are created, they tend to gravitate more to the top than the first ones - even if all moving annotation objects were created only after the nonmoving parts were already added to the map.
Does anyone know a trick to alter this strange natural order of the annotation views on the map? I tried to search the Map Kit API, but it doesn't seem to speak of such a thing.
Ok, so for solution use method from MKMapViewDelegate
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
In this method you should rearrange AnnotationView after it was added to mapKit View.
So, code may looks like this:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView * annView in views) {
TopBottomAnnotation * ann = (TopBottomAnnotation *) [annView annotation];
if ([ann top]) {
[[annView superview] bringSubviewToFront:annView];
} else {
[[annView superview] sendSubviewToBack:annView];
}
}
}
This works for me.
Under iOS 11 the implementation of displayPriority broke all the solutions which use bringSubviewToFront or zPosition.
If you override the annotation view's CALayer, you can wrestle control of zPosition back from the OS.
class AnnotationView: MKAnnotationView {
/// Override the layer factory for this class to return a custom CALayer class
override class var layerClass: AnyClass {
return ZPositionableLayer.self
}
/// convenience accessor for setting zPosition
var stickyZPosition: CGFloat {
get {
return (self.layer as! ZPositionableLayer).stickyZPosition
}
set {
(self.layer as! ZPositionableLayer).stickyZPosition = newValue
}
}
/// force the pin to the front of the z-ordering in the map view
func bringViewToFront() {
superview?.bringSubviewToFront(toFront: self)
stickyZPosition = CGFloat(1)
}
/// force the pin to the back of the z-ordering in the map view
func setViewToDefaultZOrder() {
stickyZPosition = CGFloat(0)
}
}
/// iOS 11 automagically manages the CALayer zPosition, which breaks manual z-ordering.
/// This subclass just throws away any values which the OS sets for zPosition, and provides
/// a specialized accessor for setting the zPosition
private class ZPositionableLayer: CALayer {
/// no-op accessor for setting the zPosition
override var zPosition: CGFloat {
get {
return super.zPosition
}
set {
// do nothing
}
}
/// specialized accessor for setting the zPosition
var stickyZPosition: CGFloat {
get {
return super.zPosition
}
set {
super.zPosition = newValue
}
}
}
Try to setup annotation view layer's zPosition (annotationView.layer.zPosition) in:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views;
Swift 3:
I get pin locations from API and I was having similar issues, the pins that had to be on top weren't. I was able to solve it like this.
var count = 0 // just so we don't get the same index in bottom pins
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
for view in views {
view.layer.zPosition = CGFloat(count)
}
count += 1
if count > 500 {
count = 250 // just so we don't end up with 999999999999+ as a value for count, plus I have at least 30 pins that show at the same time and need to have lower Z-Index values than the top pins.
}
}
Hope this helps
In the delegate function, you can select the pin to force it on top:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?` {
...
if my annotation is the special one {
annotationView.isSelected = true
}
...
}
I'm finding that this reordering the annotation views causes the callout that pops up when one is clicked to no longer be on top of all the annotations. I've even tried refining it so that instead of bringSubviewToFront and sendSubviewToBack, I use insertSubview:aboveSubview and insertSubview:belowSubview: where the second argument is the first annotationView in the list. This would seem to cause much less front to back scattering, but the call outs still pop up under some annotations.
I really needed to do this, and none of the (current) answers seemed to provide a reliable implementation. They sort of worked, but panning the map, selecting annotations, or zooming in could mess up the order again.
The final, well behaved solution wasn't so trivial, so I'll just outline the steps I took here. The annotation ordering that MKMapView uses doesn't respect the added order, or even the order of an overriden annotations property. So...
Steps
• Create a CADisplayLink
• Every frame, reorder annotations using both the layer zPosition, and the view's ordering in the superview's subviews array.
• If the view is selected, promote it to the front in your ordering scheme
• Tapping on annotations still respects internal MKMapView ordering, despite the already made changes. To counter this, add an MKMapViewDelegate
• In the delegate object's mapView:didSelect: method, check if the selected annotation is what you'd like it to be
• You can figure out the correct/prioritised annotation by running hit tests on the annotations yourself, with your own ordering taken into account
• If the selected annotation is correct, great. If not, manually select the correct annotation using selectAnnotation:animated:
And there you have it. The above method seems to work well, and the performance hit from running this each frame isn't too bad. You could also look at switching to MapBox, which I believe supports annotation ordering, but this isn't always an option for various reasons.