MKMapAnnotationView - how to support dark mode in Swift - ios

I have Apple's mapView and custom markers displayed in it. Each marker has 2 images in Assets for the dark and light mode appropriately. The problem is that markers do not react to the dark/light mode theme changes. What I've tried:
reload input views of mapView inside method traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
resetting image in the marker view class inside method traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
SO, how can I update markers when the theme changes?

I've just found the same thing, so I've modified my MKAnnotationView-derived class to handle it:
class MapPinView: MKAnnotationView {
// MKAnnotationView doesn't seem to automatically use the correct image for dark mode, so we have to ensure it does
var combinedImage: UIImage? {
didSet {
updateImage()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if let imageName = selected ? "pushPinSelected" : "pushPin",
let image = UIImage(named: imageName) {
self.combinedImage = image
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
updateImage()
}
}
private func updateImage() {
image = combinedImage?.imageAsset?.image(with: traitCollection) ?? combinedImage
if let image = self.image {
centerOffset = CGPoint(x: 0.0, y: -image.size.height / 2.0)
}
}
}
Note that I also initialise the combinedImage, when creating the MapPinView:
extension MapView: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: Constants.annotationId, for: annotation) as! MapPinView
annotationView.combinedImage = UIImage(named: "pushPin")
return annotationView
}
...

Related

How to assign light and dark mode to Google maps recenter button?

I'm using Google Maps SDK to embed maps on my application. I have also enabled maps recenter icon using:
extension DashboardVC: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
if !change{
googleMapView.settings.myLocationButton = true
ColorLocationButton()
}
change = false
}
func didTapMyLocationButton(for mapView: GMSMapView) -> Bool {
googleMapView.settings.myLocationButton = false
change = true
return false
}
func mapView(_ mapView: GMSMapView, didTapMyLocation location: CLLocationCoordinate2D) {
googleMapView.settings.myLocationButton = false
}
}
How can I add light and dark mode to this button rendering from Google Maps itself?
First Add a View and Add the Image inside a View
Then Make The Outlets on Your Controllers
#IBOutlet weak var recenterView: UIView!
#IBOutlet weak var hamburgerImage: UIImageView!
if Your View On the Google Maps View Must add these Line of Code in Your viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
googleMapView.settings.myLocationButton = false
self.googleMapView.bringSubviewToFront(self.recenterView)
recenterView.isUserInteractionEnabled = true
let tapOnRecenter = UITapGestureRecognizer(target: self, action: #selector(recenterTheMap(gesture:)))
tapOnRecenter.delegate = self
recenterView.addGestureRecognizer(tapOnRecenter)
// Setup The Recenter View For Corner Radius and Shadow
recenterView.clipsToBounds = true
recenterView.layer.cornerRadius = recenterView.frame.size.width / 2
recenterView.layer.shadowColor = UIColor(ciColor: .gray).cgColor
recenterView.layer.shadowRadius = 12
}
your Recenter Button Code
#objc func recenterTheMap(gesture: UITapGestureRecognizer){
googleMapView.camera = GMSCameraPosition.camera(withTarget: marker.position, zoom: 15)
}
Do not Forget the Add Gestures Delegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Detect Light or DarkMode Through This Delegate Method
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
if traitCollection.userInterfaceStyle == .light {
recenterView.tintColor = .white
}
else {
recenterView.tintColor = .black
}
}
} else {
// Fallback on earlier versions
}
}
Here is Your Code For Current Location
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location: CLLocation = locations.last!
}

Custom View Xib for MKAnnotationView button tap

I'm making a custom MKAnnotationView and i have a button inside the xib, inwhich i want to get when is touchedup inside.
Although there are questions with the same issue, none of them is working with my code.
I have the following.
class CustomCalloutView: MKAnnotationView {
var didTapGoButton: (() -> Void)?
// MARK: - Detect taps on callout
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds;
var isInside: Bool = rect.contains(point);
if(!isInside)
{
for view in self.subviews
{
isInside = view.frame.contains(point);
if isInside
{
break;
}
}
}
return isInside;
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if (hitView != nil)
{
self.superview?.bringSubview(toFront: self)
}
return hitView
}
#IBAction func viewEventDetails(_ sender: Any) {
didTapGoButton?()
}
func configureCell(events: Events) {
eventName.text = events.eventName
}
}
ViewController
class EventAnnotation : MKPointAnnotation {
var myEvent:Events?
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else { return }
if view.annotation!.isKind(of: MKUserLocation.self){
return
}
if let eventAnnotation = view.annotation as? EventAnnotation {
let theEvent = eventAnnotation.myEvent
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height-7, width: 315, height: 298)
customView.configureCell(events: theEvent!)
customView.didTapGoButton = { [weak mapView ] in
print(theEvent?.eventName ?? "noname")
}
view.addSubview(customView)
}
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)
{
for childView:AnyObject in view.subviews{
childView.removeFromSuperview();
}
}
So when i tap the button nothing is being printed !
Any idea where is my mistake in the code?
I solved it , the idea is to position the customView with in the boundary of the pinAnnotationView image as any click out side of it will be considered a didSelect method call , so play with frame until click detected , it will work either in custom view or viewController
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: calloutViewFrame.size.height, width: 315, height: 298)
and here is a demo to illustrate the situation
customPinAnnotationButton

Custom Callout View Button to Map

I am working on a MapView where on click of any custom annotation pin, I am showing custom callout view (load from xib file).
From this Custom Callout I have an UIButton, I already can detect click on this button but I want to access on the Map like the : view?.rightCalloutAccessoryView in the basic callout.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView, {
if view.annotation!.isKind(of: MKUserLocation.self){
return
}
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height+10, width: 315, height: 170)
view.addSubview(customView)
let region = MKCoordinateRegion(center: pinToZoomOn!.coordinate, span: span)
mapView.setRegion(region, animated: true)
}
The route is correctly calculated from the classic callout but I can't know how to access my map from the button of my custom callout.
My CustomCalloutViewClass :
import UIKit
import MapKit
class CustomCalloutView: MKAnnotationView {
#IBOutlet weak var goButton: UIButton!
#IBAction func goButton(_ sender: AnyObject) {
print("Button clicked sucessfully")
}
// MARK: - Detect taps on callout
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView != nil {
superview?.bringSubview(toFront: self)
}
return hitView
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = rect.contains(point)
if !isInside {
for view in subviews {
isInside = view.frame.contains(point)
if isInside {
break
}
}
}
return isInside
}
}
If someone have an idea it will be helpfull I'm stuck on this issue.
Thank you in advance.
Option 1: Capture a MKMapView instance in the closure passed to a CustomCalloutView
Add the closure which will be called on the button action. The closure will capture the MKMapView instance and you will be able to us is inside.
class CustomCalloutView: MKAnnotationView {
var didTapGoButton: (() -> Void)?
#IBAction func goButton(_ sender: AnyObject) {
didTapGoButton?()
}
}
Option 2: Add a weak reference to MKMapView as a property of the callout
This is not a clean solution but it may be an option under some circumstances. You only have to create a weak property storing a reference to MKMapView instance in CustomCalloutView
class CustomCalloutView: MKAnnotationView {
weak var mapView: MKMapView?
}
Configuration
This is how you can wire up the CustomCalloutView for both solutions. Remember to use swift capture list to capture a weak reference to a MKMapView instance. Without it you may create a strong reference cycle.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// ...
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
// Option 1
customView.didTapGoButton = { [weak mapView ] in
print(mapView?.annotations.count)
}
// Option 2
customView.mapView = mapView
// ...
}
Thank you so much for your help !
I used the first option to capture the MKMapView instance.
If someone is stuck on the same issue you can use the first option and add this in your didSelect MapView function:
let selectedLoc = view.annotation
print("Annotation '\(String(describing: selectedLoc?.title!))' has been selected")
let location = view.annotation as! YourCustomClassType
let launchOptions = [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving]
customView.didTapGoButton = { location.mapItem().openInMaps(launchOptions: launchOptions) }
Thank's
var didTapGoButton: (() -> Void)? this types of closures are very helpful for navigation from the view to viewControllers through the button actions in the call layoutviews in the mkmapview.

regiondidchange called several times on app load swift

I have a map that when i pan away or type in a location and navigate to it that I want a simple print function to run. I have this accomplished, however, when i first load the app it calls that print function several times until it is finished zooming in. Is there a way to NOT count the initial on app load region change?
answer finally found herehttp://ask.ttwait.com/que/5556977
private var mapChangedFromUserInteraction = false
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
let view = self.mapView.subviews[0]
// Look through gesture recognizers to determine whether this region change is from user interaction
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if( recognizer.state == UIGestureRecognizerState.Began || recognizer.state == UIGestureRecognizerState.Ended ) {
return true
}
}
}
return false
}
func mapView(mapView: MKMapView, regionWillChangeAnimated animated:
Bool) {
mapChangedFromUserInteraction =
mapViewRegionDidChangeFromUserInteraction()
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated:
Bool) {
if (mapChangedFromUserInteraction) {
// user changed map region
}
}
Here's a Swift 4.2 answer that's more concise.
private var mapChangedFromUserInteraction = false
public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if (mapChangedFromUserInteraction) {
// do something
}
}
private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
return map.subviews.first?.gestureRecognizers?
.contains(where: {
$0.state == .began || $0.state == .ended
}) == true
}
public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
if !mapChangedFromUserInteraction { // I only wanted to check until this was true
mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
}
}

Custom CalloutView with Swift 2.0 Best Approach?

I want to create a custom CalloutView from xib. I created a xib and custom AnnotationView class. I want to use my MapViewController segue which name is showDetail segue because this segue is a Push segue. When the user taps the button in my calloutView it should perform my showDetail segue.
I searched all documents, tutorials, guides and questions but I could
not find a any solution with Swift. There are no custom libraries also. I couldn't find any solution it as been 3 days.
My CalloutView.xib file View is my CustomAnnotationView which name is CalloutAnnotationView.swift and File's Owner is MapViewController it allows me to use MapViewController segues.
There is my CalloutAnnotationView.swift
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if (hitView != nil) {
self.superview?.bringSubviewToFront(self)
}
return hitView
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = CGRectContainsPoint(rect, point)
if (!isInside) {
for view in self.subviews {
isInside = CGRectContainsPoint(view.frame, point)
if (isInside) {
}
}
}
return isInside
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let calloutViewController = MapViewController(nibName: "CalloutView", bundle: nil)
if selected {
calloutViewController.view.clipsToBounds = true
calloutViewController.view.layer.cornerRadius = 10
calloutViewController.view.center = CGPointMake(self.bounds.size.width * 0.5, -calloutViewController.view.bounds.size.height * 0.5)
self.addSubview(calloutViewController.view)
} else {
// Dismiss View
calloutViewController.view.removeFromSuperview()
}
}
My MapViewController.swift codes;
// MARK: - MapKit Delegate Methods
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? Veterinary { // Checking annotations is not User Location.
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(Constants.MapViewAnnotationViewReuseIdentifier) as? CalloutAnnotationView
if pinView == nil { // If it is nil it created new Pin View
pinView = CalloutAnnotationView(annotation: annotation, reuseIdentifier: Constants.MapViewAnnotationViewReuseIdentifier)
pinView?.canShowCallout = false
} else { pinView?.annotation = annotation } // If it is NOT nil it uses old annotations.
// Checking pin colors from Veterinary Class PinColor Method
pinView?.image = annotation.pinColors()
//pinView?.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
return pinView
}
return nil
}
These codes are working fine to load my xib file. I created IBAction from a button and It performs segues from MapViewControllerto my DetailViewController with showDetail segue. But problem is It doesn't remove my customCalloutView from superview so I can't dismiss callout views.
How can I solve this problem or What is the best way to create custom calloutView for use my MapViewController push segues ?
Thank you very much.
In your MKMapView delegate see if this gets called when segueing:
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if let annotation = view.annotation as? Veterinary {
mapView.removeAnnotation(annotation)
}
}
I hope this helps you with the problem
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if view.isKindOfClass(AnnotationView)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}

Resources