using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using CoreLocation; using Foundation; using MapKit; using ObjCRuntime; using UIKit; using Xamarin.Forms.Platform.iOS; using RectangleF = CoreGraphics.CGRect; namespace Xamarin.Forms.Maps.iOS { internal class MapDelegate : MKMapViewDelegate { // keep references alive, removing this will cause a segfault readonly List List = new List(); Map _map; UIView _lastTouchedView; bool _disposed; internal MapDelegate(Map map) { _map = map; } public override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation) { MKPinAnnotationView mapPin = null; // https://bugzilla.xamarin.com/show_bug.cgi?id=26416 var userLocationAnnotation = Runtime.GetNSObject(annotation.Handle) as MKUserLocation; if (userLocationAnnotation != null) return null; const string defaultPinId = "defaultPin"; mapPin = (MKPinAnnotationView)mapView.DequeueReusableAnnotation(defaultPinId); if (mapPin == null) { mapPin = new MKPinAnnotationView(annotation, defaultPinId); mapPin.CanShowCallout = true; } mapPin.Annotation = annotation; AttachGestureToPin(mapPin, annotation); return mapPin; } void AttachGestureToPin(MKPinAnnotationView mapPin, IMKAnnotation annotation) { UIGestureRecognizer[] recognizers = mapPin.GestureRecognizers; if (recognizers != null) { foreach (UIGestureRecognizer r in recognizers) { mapPin.RemoveGestureRecognizer(r); } } Action action = g => OnClick(annotation, g); var recognizer = new UITapGestureRecognizer(action) { ShouldReceiveTouch = (gestureRecognizer, touch) => { _lastTouchedView = touch.View; return true; } }; List.Add(action); List.Add(recognizer); mapPin.AddGestureRecognizer(recognizer); } void OnClick(object annotationObject, UITapGestureRecognizer recognizer) { // https://bugzilla.xamarin.com/show_bug.cgi?id=26416 NSObject annotation = Runtime.GetNSObject(((IMKAnnotation)annotationObject).Handle); if (annotation == null) return; // lookup pin Pin targetPin = null; for (var i = 0; i < _map.Pins.Count; i++) { Pin pin = _map.Pins[i]; object target = pin.Id; if (target != annotation) continue; targetPin = pin; break; } // pin not found. Must have been activated outside of forms if (targetPin == null) return; // if the tap happened on the annotation view itself, skip because this is what happens when the callout is showing // when the callout is already visible the tap comes in on a different view if (_lastTouchedView is MKAnnotationView) return; targetPin.SendTap(); } protected override void Dispose(bool disposing) { if (_disposed) { return; } _disposed = true; if (disposing) { _map = null; _lastTouchedView = null; } base.Dispose(disposing); } } public class MapRenderer : ViewRenderer { CLLocationManager _locationManager; bool _shouldUpdateRegion; bool _disposed; const string MoveMessageName = "MapMoveToRegion"; public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) { return Control.GetSizeRequest(widthConstraint, heightConstraint); } // iOS 10 has some issues with releasing memory from map views; each one we create allocates // a bunch of memory we can never get back. Until that's fixed, we'll just reuse MKMapViews // as much as possible to prevent creating new ones and losing more memory // For the time being, we don't want ViewRenderer handling disposal of the MKMapView // if we're on iOS 10; during Dispose we'll be putting the MKMapView in a pool instead protected override bool ManageNativeControlLifetime => !FormsMaps.IsiOs10OrNewer; protected override void Dispose(bool disposing) { if (_disposed) { return; } _disposed = true; if (disposing) { if (Element != null) { var mapModel = (Map)Element; MessagingCenter.Unsubscribe(this, MoveMessageName); ((ObservableCollection)mapModel.Pins).CollectionChanged -= OnCollectionChanged; } var mkMapView = (MKMapView)Control; mkMapView.RegionChanged -= MkMapViewOnRegionChanged; mkMapView.GetViewForAnnotation = null; mkMapView.Delegate.Dispose(); mkMapView.Delegate = null; mkMapView.RemoveFromSuperview(); if (FormsMaps.IsiOs10OrNewer) { // This renderer is done with the MKMapView; we can put it in the pool // for other rendererers to use in the future MapPool.Add(mkMapView); } // For iOS versions < 10, the MKMapView will be disposed in ViewRenderer's Dispose method if (_locationManager != null) { _locationManager.Dispose(); _locationManager = null; } } base.Dispose(disposing); } protected override void OnElementChanged(ElementChangedEventArgs e) { base.OnElementChanged(e); if (e.OldElement != null) { var mapModel = (Map)e.OldElement; MessagingCenter.Unsubscribe(this, MoveMessageName); ((ObservableCollection)mapModel.Pins).CollectionChanged -= OnCollectionChanged; } if (e.NewElement != null) { var mapModel = (Map)e.NewElement; if (Control == null) { MKMapView mapView = null; if (FormsMaps.IsiOs10OrNewer) { // See if we've got an MKMapView available in the pool; if so, use it mapView = MapPool.Get(); } if (mapView == null) { // If this is iOS 9 or lower, or if there weren't any MKMapViews in the pool, // create a new one mapView = new MKMapView(RectangleF.Empty); } SetNativeControl(mapView); var mkMapView = (MKMapView)Control; var mapDelegate = new MapDelegate(mapModel); mkMapView.GetViewForAnnotation = mapDelegate.GetViewForAnnotation; mkMapView.RegionChanged += MkMapViewOnRegionChanged; } MessagingCenter.Subscribe(this, MoveMessageName, (s, a) => MoveToRegion(a), mapModel); if (mapModel.LastMoveToRegion != null) MoveToRegion(mapModel.LastMoveToRegion, false); UpdateMapType(); UpdateIsShowingUser(); UpdateHasScrollEnabled(); UpdateHasZoomEnabled(); ((ObservableCollection)mapModel.Pins).CollectionChanged += OnCollectionChanged; OnCollectionChanged(((Map)Element).Pins, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == Map.MapTypeProperty.PropertyName) UpdateMapType(); else if (e.PropertyName == Map.IsShowingUserProperty.PropertyName) UpdateIsShowingUser(); else if (e.PropertyName == Map.HasScrollEnabledProperty.PropertyName) UpdateHasScrollEnabled(); else if (e.PropertyName == Map.HasZoomEnabledProperty.PropertyName) UpdateHasZoomEnabled(); else if (e.PropertyName == VisualElement.HeightProperty.PropertyName && ((Map)Element).LastMoveToRegion != null) _shouldUpdateRegion = true; } public override void LayoutSubviews() { base.LayoutSubviews(); if (_shouldUpdateRegion) { MoveToRegion(((Map)Element).LastMoveToRegion, false); _shouldUpdateRegion = false; } } void AddPins(IList pins) { foreach (Pin pin in pins) { var annotation = new MKPointAnnotation { Title = pin.Label, Subtitle = pin.Address ?? "" }; pin.Id = annotation; annotation.SetCoordinate(new CLLocationCoordinate2D(pin.Position.Latitude, pin.Position.Longitude)); ((MKMapView)Control).AddAnnotation(annotation); } } void MkMapViewOnRegionChanged(object sender, MKMapViewChangeEventArgs mkMapViewChangeEventArgs) { if (Element == null) return; var mapModel = (Map)Element; var mkMapView = (MKMapView)Control; mapModel.VisibleRegion = new MapSpan(new Position(mkMapView.Region.Center.Latitude, mkMapView.Region.Center.Longitude), mkMapView.Region.Span.LatitudeDelta, mkMapView.Region.Span.LongitudeDelta); } void MoveToRegion(MapSpan mapSpan, bool animated = true) { Position center = mapSpan.Center; var mapRegion = new MKCoordinateRegion(new CLLocationCoordinate2D(center.Latitude, center.Longitude), new MKCoordinateSpan(mapSpan.LatitudeDegrees, mapSpan.LongitudeDegrees)); ((MKMapView)Control).SetRegion(mapRegion, animated); } void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) { switch (notifyCollectionChangedEventArgs.Action) { case NotifyCollectionChangedAction.Add: AddPins(notifyCollectionChangedEventArgs.NewItems); break; case NotifyCollectionChangedAction.Remove: RemovePins(notifyCollectionChangedEventArgs.OldItems); break; case NotifyCollectionChangedAction.Replace: RemovePins(notifyCollectionChangedEventArgs.OldItems); AddPins(notifyCollectionChangedEventArgs.NewItems); break; case NotifyCollectionChangedAction.Reset: var mapView = (MKMapView)Control; mapView.RemoveAnnotations(mapView.Annotations); AddPins((IList)(Element as Map).Pins); break; case NotifyCollectionChangedAction.Move: //do nothing break; } } void RemovePins(IList pins) { foreach (object pin in pins) ((MKMapView)Control).RemoveAnnotation((IMKAnnotation)((Pin)pin).Id); } void UpdateHasScrollEnabled() { ((MKMapView)Control).ScrollEnabled = ((Map)Element).HasScrollEnabled; } void UpdateHasZoomEnabled() { ((MKMapView)Control).ZoomEnabled = ((Map)Element).HasZoomEnabled; } void UpdateIsShowingUser() { if (FormsMaps.IsiOs8OrNewer && ((Map)Element).IsShowingUser) { _locationManager = new CLLocationManager(); _locationManager.RequestWhenInUseAuthorization(); } ((MKMapView)Control).ShowsUserLocation = ((Map)Element).IsShowingUser; } void UpdateMapType() { switch (((Map)Element).MapType) { case MapType.Street: ((MKMapView)Control).MapType = MKMapType.Standard; break; case MapType.Satellite: ((MKMapView)Control).MapType = MKMapType.Satellite; break; case MapType.Hybrid: ((MKMapView)Control).MapType = MKMapType.Hybrid; break; } } } }