iOS Swift Mapkit annotations order - swift

I need to keep an order for the annotations. I use custom annotationViews.
Each annotations are related to a point of interest, from 1 to n, so I need to open POI 1 when the map open the first time and then, when I click next or previous, I need to open POI 2 etc..
I keep an index to show in the detail callout the Point 1. I know that annotations are a Set, not an array.
Thanks
tour.poi?.forEach {
let coord = CLLocationCoordinate2DMake(CLLocationDegrees($0.lat), CLLocationDegrees($0.long))
let poiAnnotation = PoiAnnotation(coord: coord, poi: $0, index: index)
map.addAnnotation(poiAnnotation)
index = index + 1
}
My ugly solution :
let annotation = map.annotations.first { $0.subtitle == String(poiIndex) }
UIView.animate(withDuration: 0.2) {
self.map.setCenter(annotation!.coordinate, animated: false)
}completion: { (_) in
self.map.selectAnnotation(annotation!, animated: true)
}
I've added some animation to always center the pin before opening the callout.

You said:
I know that annotations are a Set, not an array.
That is incorrect.
The map view annotations property is an array, not a set. To quote the docs:
annotations
The complete list of annotations associated with the receiver.
var annotations: [MKAnnotation] { get }
Note the type of annotations: [MKAnnotation]. That's an array.
You should be able to keep an instance variable with the index of the annotation you have displayed most recently, and increment it when you display the map again.
If you're opening and closing a map view over and over with the same set of annotations you might want to keep your array of annotations rather than rebuilding it each time. For that matter you might want to hide the map view rather than closing it, and then simply show it again. Map views use a lot of network data to display.

Related

How to force the order of UIKit pop up menu button items?

I have a couple of UIKit pop-up menu buttons with identical menu items on the same screen in a Swift app. The buttons are built by calling a function that uses an array of strings to create the list of menu items.
The problem is that depending on the button's vertical position on the screen, the menu items may appear in the order specified by the function, or reversed. If the button is in the upper half of the screen, the menu items are listed in the correct order. If the button is in the lower half of the screen the menu items are listed in reverse order.
I would prefer the menu items to appear in the same order regardless of the button's position on the screen. I could check the button location and have the menu creation function reverse the order, but that seems kind of clunky. I am hoping there's a cleaner way to override this behaviour.
The code and array used to create the button menus:
let buttonMenuItems = ["Spring","Summer","Autumn","Winter"]
func createAttributeMenu(menuNumber: Int)->UIMenu {
var menuActions: [UIAction] = []
for attribute in buttonMenuItems {
let item = UIAction(title: attribute) { action in
self.updateMenu(menuID: menuNumber, selected: attribute)
}
menuActions.append(item)
}
return UIMenu(title: "", children: menuActions)
}
The result is this:
Versions I'm using now in testing: Xcode 14.1, iOS 16.1, but I have seen this behaviour on earlier versions as well. (back to iOS 14.x)
Starting with iOS 16, there is a .preferredMenuElementOrder property that can be set on the button:
case automatic
A constant that allows the system to choose an ordering strategy according to the current context.
case priority
A constant that displays menu elements according to their priority.
case fixed
A constant that displays menu elements in a fixed order.
Best I can tell (as with many Apple definitions), there is no difference between .automatic and .priority.
From the .priority docs page:
Discussion
This ordering strategy displays the first menu element in the UIMenu closest to the location of the user interaction.
So, we get "reversed" order based on the position of the menu relative to the button.
To keep your defined order:
buttonNearTop.menu = createAttributeMenu(menuNumber: 1)
buttonNearBottom.menu = createAttributeMenu(menuNumber: 2)
if #available(iOS 16.0, *) {
buttonNearBottom.preferredMenuElementOrder = .fixed
buttonNearTop.preferredMenuElementOrder = .fixed
} else {
// out of luck... you get Apple's "priority" ordering
}

Why would MKMapView not add new annotations from Firestore data?

I am using an MKMapView wrapped in UIVIewRepresentable under SwiftUI to display various locations on the map. The data is fetched from a Google Firestore collection and annotations added to the map view when the app loads - no problem there. Locations are also loaded into a lazy grid for display - also no problems.
The issue arises if a new location is added to the Firestore set no corresponding annotation(s) will be added to the map view. They will be aded to the lazy grid however.
Locations in Firestore can be deleted and the annotations on the map will be removed, data can be edited (like text values for display) and these will update on the map.
I'm not sure where to go from here to solve the issue ...
the updateUIView code > 'siteData' is a simple array of site structs and populated from Firestore.
func updateUIView(_ uiView: MKMapView, context: Context) {
if siteData.count != uiView.annotations.count {
uiView.removeAnnotations(uiView.annotations)
for points in siteData {
let annotation = SiteAnnotation(title: points.name, subtitle: points.shortDescription, site: points, coordinate: points.locationCoordinate)
uiView.addAnnotation(annotation)
}
}
uiView.showsUserLocation = true
}
}
What am I missing in this?
Came back to this this morning and realized that the uiView.annotations.count is including the pin for the user's location marked on the map. So when if siteData.count != uiView.annotations.count {, the siteData.count and uiView.annotations.count are actually equal.
Guess it's the little things that can really trip us up.

How to write if and else statement more elegantly in Swift

I'm having a bit of a brain fart in Swift and I know this code could be written better. Basically what it is, I have two images and I check if a value is over 3 to show an image and hide the other.
currently I have it like this
let greaterThanThree = value > 3
image1.isHidden = greaterThanThree
image2.isHidden = !greaterThanThree
But I feel like there is a more elegant way to write this.
I'd write it like this:
image1.isHidden = value > 3
image2.isHidden = !image1.isHidden
Anything shorter than that is just code golfing.
There seems to be a rule here that exactly one of these two views should be visible at all times. If so, I'd create, as part of my view controller's viewDidLoad, an instance of this struct:
struct AlternateViews {
let views : [UIView]
init(_ v1:UIView, _ v2:UIView) {
views = [v1,v2]
}
func hide(first:Bool) {
views[0].isHidden = first
views[1].isHidden = !first
}
}
let alternateViews = AlternateViews(image1, image2)
Okay, that's a lot of work to set up initially, but the result is that later you can just say
self.alternateViews.hide(first: value > 3)
The struct is acting as a tiny state machine, making sure that your view controller's views remain in a coherent state. This technique of moving the rules for state into utility structs attached to your view controller is recommended in a WWDC 2016 video and I've been making a lot of use of it ever since.
If you have more pairs of alternating views, just make and maintain more instances of the struct.
(If the rule that I've assumed is not quite the real rule, make a struct that does express the real rule.)
You can do this:
(image1.isHidden, image2.isHidden) = (value > 3) ? (true, false) : (false, true)
Basically if the value is greater than 3, the first image will be hidden and the second one won't. Otherwise, the second image will be hidden and the first one will not.

Use a text field to set the title of an MKAnnotation in Swift

having registered a long press at a point on the map, I'd like a text field to pop up, so I can set the input as the title of a pin dropped at that point. How would I go about doing this?
Heres my code which currently registers a longpress and drops a pin, that all works fine! I'm not sure how to bring up a text field and get the users input though
func DropPin(gestureRecognizer:UIGestureRecognizer) {
if gestureRecognizer.state == .Began {
var point:CGPoint = gestureRecognizer.locationInView(self.Map)
var pinLoc: CLLocationCoordinate2D = self.Map.convertPoint(point, toCoordinateFromView: self.Map)
let x = CustomAnnotation(coordinate: pinLoc, title: "Pin", subtitle: "Pin", imageName: "TouchPin")
self.Map.addAnnotation(x)
}
}
There are several ways to do so, but you can choose one of the two for example.
In your view, you add a text field where the user set the title before you drop the pin. But that might not be intuitive or look bad (if you need to have a full screen map for example).
So, what I would suggest is to fire an UIAlertController with a text field inside. Here is an example of how you could do it.
Either you :
Create the annotation
Add it to the map
An alert appear
The user enter a name (keep it somewhere)
You retrieve the last annotation you've added
You set it's title with the name entered previously
Or :
You detect your long press
An alert appear
The user enter a name (keep it somewhere)
Create your annotation with the name entered previously
Add it to the map
Those are just 2-3 examples. You might think of something else :)

How to prevent overlapping pushpins at the same geolocation with Bing Maps?

If you have 2 pushpins on 'London' at the same geolocation, is there anything in the API to move them apart so they are both visible?
I can only find documentation on their old map points API which had PreventIconCollisions, this is what I want but can't see any reference to this in the new API.
I am using the JavaScript API.
So if I understand correctly, you have similar information on the same location, it this correct?
In order to display both information, you will have two options:
Merge information in the textbox using an appropriate way to present the information inside this ui element (using your own tabbed infobox for example)
Decluster the point manually when you're at a certain level of zoom
There is no default property to set this and it would really messy to do this on many pushpins, but in the main idea, you would have to: detect viewchangeend event, if you're at a certain level of zoom (or higher zoom level) then you're declustering them (I call it decluter nearby pushpins).
// Bind pushpin mouseover.
Microsoft.Maps.Events.addHandler(pin, 'mouseover', function (e) {
var currentPin = e.target;
currentPin.setOptions({ visible: false });
var currentLocation = currentPin.getLocation().clone();
var currentPoint = bmGlobals.geo.map.tryLocationToPixel(currentLocation);
if (currentPin.associatedCluster.length == 2) {
// Display the first pushpin
var pinA = createPin(currentPin.associatedCluster[0]);
var locA = bmGlobals.geo.map.tryPixelToLocation(new Microsoft.Maps.Point(currentPoint.x - pinA.getWidth(), currentPoint.y));
pinA.setLocation(locA);
bmGlobals.geo.layerClusteredPin.push(pinA);
// Display the second pushpin
var pinB = createPin(currentPin.associatedCluster[1]);
var locB = bmGlobals.geo.map.tryPixelToLocation(new Microsoft.Maps.Point(currentPoint.x + pinB.getWidth(), currentPoint.y));
pinB.setLocation(locB);
bmGlobals.geo.layerClusteredPin.push(pinB);
}
});
I will try to write a bing maps module about this, but in the fact, you'll have to get your clustered pushpins (or your own pushpin that has two associated data object) and then you will have to set their position based on the rendering on the client side.
I know this question is really old, but if someone is looking for something similar (clustering the pins) here is a good start: http://rtsinani.github.io/PinClusterer/