Steve W
Steve W

Reputation: 1128

What methods are called after DidSelectAnnotationView/DidDeSelectAnnotationView in MapView in Xamarin iOS?

I'm working on a Xamarin iOS project and have a view with a MapView that contains modified MKAnnotations and implements IMKMapViewDelegate.

The view shows a map with a few annotations on the map. Tapping an annotation displays a callout bubble with extra info.

The view also has an overlay view at the bottom of the screen which can be swiped up to display detailed info. The problem is that when an annotation is selected or deselected, the overlay view moves half way up the screen. The annotation selection event calls the methods DidSelectAnnotationView/DidDeselectAnnotationView but the movement of the overlay is triggered after those calls and I cannot hit any break points in any of my code when this event occurs.

So the question is, which iOS methods get called after DidSelectAnnotationView and DidDeselectAnnotationView which could be causing my issue? The code below reverts the overlay back to its original position but with an obvious UI glitch.

       [Export("mapView:didSelectAnnotationView:")]
        public void DidSelectAnnotationView(MKMapView mapView, MKAnnotationView annotationView)
        {
            DelayedOverlayReset();
        }

        [Export("mapView:didDeselectAnnotationView:")]
        public void DidDeselectAnnotationView(MKMapView mapView, MKAnnotationView annotationView)
        {
            DelayedOverlayReset();
        }

        private async Task DelayedOverlayReset()
        {
            await Task.Run(async () =>
            {
                Thread.Sleep(150);
            });

            _overlayView?.SetUpOverlayMinPosition();
        }

I can share more code but its work related so I need to anonymise it. Happy to provide further detail or answer questions as necessary.

EDIT overlay view:

namespace Touch.Views.Detail
{
    [Register("DetailsOverlayView")]
    public sealed class DetailsOverlayView : UIView
    {
        private const float OverlayDragBarWidth = 56.0f;
        private const float OverlayDragBarTopPadding = 20.0f;
        private const float OverlayDragBarHeight = 6.0f;
        private const float OverlayDragBarBottomPadding = 18.0f;
        private const float CornerRadius = 18.0f;
        private const float ScoreImageWidth = 60.0f;
        private const float IconImageWidth = 16.0f;
        private const float ShadowOpacity = 0.15f;
        private const float ShadowRadius = 10.0f;
        private const float ShadowOffsetWidth = 0f;
        private const float ShadowOffsetHeight = 10.0f;
        private const float ViewAnchorMidPoint = 0.5f;
        private const float ImageDurationToLabelGap = 30;
        private const float AddressIconWidthAndPadding = 18.0f;

        private const double StartingOverlayAnimationSpeed = 0.3;
        private const double AnimationSpeedThreshold = 1.3;

        private const string DistanceImageName = "icon_map";
        private const string DurationImageName = "icon_clock";

        private static readonly TextStyle Heading3SemiBold = TextStyle.Heading3.SemiBold.BrandSecondary;
        private static readonly TextStyle BodyRegularCaption = TextStyle.Body.Regular.TextCaption;
        private readonly DetailOverlayDisplayData? _displayData;
        private readonly double _maxPosition;
        private readonly double _minPosition;

        private readonly UIView? _parentView;
        private readonly ElementLinksViewModel? _ActionViewModel;

        private NSLayoutConstraint? _dragBarBottomPadding;
        private UIView? _overlayDragBarView;
        private UIView? _overlayHeaderAndContentView;
        private UIPanGestureRecognizer? _panGestureRecognizer;
        private UIScrollView? _scrollView;
        private UIStackView? _stackView;
        private double _startingPosition;

        public NSLayoutConstraint? OverlayHeight;

        public DetailsOverlayView(DetailOverlayDisplayData displayData, ElementLinksViewModel action,
            UIView parentView)
        {
            _parentView = parentView;
            _displayData = displayData;
            _ActionViewModel = action;

            TranslatesAutoresizingMaskIntoConstraints = false;
            BackgroundColor = UIColor.White;
            ClipsToBounds = false;
            Layer.CornerRadius = CornerRadius;
            Hidden = true;

            Layer.ShadowOpacity = ShadowOpacity;
            Layer.ShadowOffset = new CGSize(ShadowOffsetWidth, -ShadowOffsetHeight);
            Layer.ShadowRadius = ShadowRadius;

            SetUp();

            var headerView = new DetailHeaderView();
            headerView.SetUp(displayData, _scrollView!, _stackView!, parentView);

            CreateContentPanel();
            SetUpOverlayConstraints();

            _minPosition = Math.Round(parentView.Frame.Height - GetHeaderHeight(displayData, parentView.Bounds.Width));
            _maxPosition = 0;
        }

        public void SetUpOverlayStartingPosition()
        {
            if (_parentView == null)
            {
                return;
            }

            Center = new CGPoint(Center.X, _parentView.Frame.Height);
            Layer.AnchorPoint = new CGPoint(ViewAnchorMidPoint, 0f);
            Hidden = false;

            SetOverlayPosition(StartingOverlayAnimationSpeed, _minPosition, CornerRadius, true);

            _overlayDragBarView!.Hidden = false;
        }

        public void SetUpOverlayMinPosition()
        {    
            SetOverlayPosition(0.01, _minPosition, CornerRadius, true);

            _overlayDragBarView!.Hidden = false;
        }

        private void SetUp()
        {
            _overlayDragBarView = new UIView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                BackgroundColor = UIColor.LightGray,
                ClipsToBounds = true
            };
            _overlayDragBarView.RoundCorners(ViewCornerStyle.All, OverlayDragBarHeight / 2);

            _overlayHeaderAndContentView = new UIView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                ClipsToBounds = true
            };

            AddSubview(_overlayDragBarView);
            AddSubview(_overlayHeaderAndContentView);

            _panGestureRecognizer = new UIPanGestureRecognizer(PanOverlayView);
            _panGestureRecognizer.CancelsTouchesInView = true;
            AddGestureRecognizer(_panGestureRecognizer);

            var (outerScrollView, _, innerStackView) =
                _overlayHeaderAndContentView!.CreateScrollViewWithFooterAndStackView(UIColor.White);
            _stackView = innerStackView;
            _stackView.LayoutMarginsRelativeArrangement = true;

            _scrollView = outerScrollView;
            _scrollView.UserInteractionEnabled = false;
            _scrollView.Bounces = true;
            _scrollView.SetContentOffset(CGPoint.Empty, true);
            _scrollView.Scrolled += ScrollViewOnScrolled;
        }

        private void CreateContentPanel()
        {
            var view = new UIView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                BackgroundColor = UIColor.Clear,
                ClipsToBounds = true
            };

            var contentStackView = new UIStackView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                Axis = UILayoutConstraintAxis.Vertical,
                LayoutMarginsRelativeArrangement = true,
                Spacing = StyleGuideDimens.PaddingSmall,
                DirectionalLayoutMargins =
                    new NSDirectionalEdgeInsets(0, StyleGuideDimens.PaddingSmall, StyleGuideDimens.PaddingSmall,
                        StyleGuideDimens.PaddingSmall)
            };

            _stackView!.AddArrangedSubview(view);
            view.AddSubview(contentStackView);

            NSLayoutConstraint.ActivateConstraints(new[]
            {
                view.LeadingAnchor.ConstraintEqualTo(view.Superview.LeadingAnchor),
                view.TrailingAnchor.ConstraintEqualTo(view.Superview.TrailingAnchor),

                contentStackView.TopAnchor.ConstraintEqualTo(view.TopAnchor),
                contentStackView.BottomAnchor.ConstraintEqualTo(view.BottomAnchor),
                contentStackView.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor),
                contentStackView.TrailingAnchor.ConstraintEqualTo(view.TrailingAnchor)
            });

            var milesTimePanel = SetUpDistanceAndDurationLayout();

            var ActionView = ElementLinksView.Create();
            _ActionViewModel!.Alignment = Alignment.Center;
            ActionView.SetUp(_ActionViewModel);

            var scoreSummaryView = new ScoreSummaryView();
            scoreSummaryView.SetUp(_displayData!.ScoreSummary);

            contentStackView.AddArrangedSubview(milesTimePanel);
            contentStackView.AddArrangedSubview(ActionView);
            contentStackView.AddArrangedSubview(scoreSummaryView);

            contentStackView.SetCustomSpacing(StyleGuideDimens.PaddingMedium, milesTimePanel);

            milesTimePanel.LayoutIfNeeded();
            view.LayoutIfNeeded();
            _stackView.LayoutIfNeeded();
        }

        private void SetUpOverlayConstraints()
        {
            NSLayoutConstraint.ActivateConstraints(new[]
            {
                _overlayDragBarView!.CenterXAnchor.ConstraintEqualTo(CenterXAnchor),
                _overlayDragBarView.TopAnchor.ConstraintEqualTo(TopAnchor, OverlayDragBarTopPadding),
                _overlayDragBarView.WidthAnchor.ConstraintEqualTo(OverlayDragBarWidth),
                _overlayDragBarView.HeightAnchor.ConstraintEqualTo(OverlayDragBarHeight),
                _dragBarBottomPadding = _overlayHeaderAndContentView!.TopAnchor.ConstraintEqualTo(
                    _overlayDragBarView!.BottomAnchor, OverlayDragBarBottomPadding),

                _overlayHeaderAndContentView.BottomAnchor.ConstraintEqualTo(BottomAnchor),
                _overlayHeaderAndContentView.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
                _overlayHeaderAndContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor)
            });

            _overlayDragBarView.LayoutIfNeeded();
            _overlayHeaderAndContentView.LayoutIfNeeded();
        }

        private void PanOverlayView(UIPanGestureRecognizer gesture)
        {
            if (_parentView == null || OverlayHeight == null)
            {
                return;
            }

            if (gesture.State == UIGestureRecognizerState.Began)
            {
                _startingPosition = Frame.Y;
            }

            var scrollOffset = _scrollView!.ContentOffset.Y;
            var velocity = gesture.VelocityInView(_parentView);
            var y = _parentView.Frame.GetMinY();

            var newPos = _startingPosition + gesture.TranslationInView(_parentView).Y;

            if (newPos >= _minPosition)
            {
                SetOverlayPosition(0, _minPosition);
            }
            else if (newPos <= _maxPosition)
            {
                SetOverlayPosition(0, _maxPosition, 0, true, 0);
            }
            else
            {
                //Scroll the scrollView content to the top
                _scrollView!.SetContentOffset(CGPoint.Empty, true);

                if (scrollOffset <= 0)
                {
                    Frame = new CGRect(0, newPos, Frame.Width, Frame.Height);
                }
            }

            if (gesture.State == UIGestureRecognizerState.Ended)
            {
                var duration = velocity.Y < 0 ? (y - _maxPosition) / -velocity.Y : _minPosition - (y / velocity.Y);
                duration = duration > AnimationSpeedThreshold ? 1 : duration;

                if (velocity.Y > 0)
                {
                    if (scrollOffset <= 0)
                    {
                        //Scroll the scrollView content to the top
                        _scrollView!.SetContentOffset(CGPoint.Empty, true);

                        _scrollView!.UserInteractionEnabled = false;
                        SetOverlayPosition(duration, _minPosition);
                    }
                }
                else
                {
                    _scrollView!.UserInteractionEnabled = true;
                    SetOverlayPosition(duration, _maxPosition, 0, true, 0);
                }
            }
        }

        private void SetOverlayPosition(double animationDuration, double position, float cornerRadius = CornerRadius,
            bool dragBarVisibility = false, float dragBarBottomPadding = 18.0f)
        {
            Animate(animationDuration, () =>
            {
                Frame = new CGRect(0, position, Frame.Width, Frame.Height);
                _overlayDragBarView!.Hidden = dragBarVisibility;
                _dragBarBottomPadding!.Constant = dragBarBottomPadding;
            });

            AnimateCornerRadius(cornerRadius);
        }

        private void ScrollViewOnScrolled(object sender, EventArgs e)
        {
            var scrollOffset = _scrollView!.ContentOffset.Y;

            if (scrollOffset >= -1 && _panGestureRecognizer != null)
            {
                _scrollView.UserInteractionEnabled = false;
            }
        }

        #region Helper Methods

        private void AnimateCornerRadius(float cornerRadius) =>
            Animate(0.1, () => { Layer.CornerRadius = cornerRadius; });

        private UIView SetUpDistanceAndDurationLayout()
        {
            var containerView = new UIView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                ClipsToBounds = false
            };

            if (_displayData == null)
            {
                return containerView;
            }

            var imageDistance = new UIImageView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                Image = UIImage.FromBundle(DistanceImageName),
                ContentMode = UIViewContentMode.ScaleAspectFit
            };

            var imageDuration = new UIImageView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                Image = UIImage.FromBundle(DurationImageName),
                ContentMode = UIViewContentMode.ScaleAspectFit
            };

            var distanceDurationWrapperView = new UIView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                ClipsToBounds = true
            };

            var stackView = new UIStackView
            {
                TranslatesAutoresizingMaskIntoConstraints = false,
                Axis = UILayoutConstraintAxis.Horizontal,
                Alignment = UIStackViewAlignment.Fill,
                Spacing = 0
            };
            containerView.AddSubview(stackView);

            stackView.TopAnchor.ConstraintEqualTo(containerView.TopAnchor).Active = true;
            stackView.BottomAnchor.ConstraintEqualTo(containerView.BottomAnchor).Active = true;
            stackView.LeadingAnchor.ConstraintEqualTo(containerView.LeadingAnchor).Active = true;
            stackView.TrailingAnchor.ConstraintEqualTo(containerView.TrailingAnchor, 0).Active = true;

            var distanceLabel = _displayData.TotalMiles.CreateLabel(BodyRegularCaption, ElementMargins.None(),
                UITextAlignment.Left);
            var durationLabel = _displayData.TotalJourneyTime.CreateLabel(BodyRegularCaption, ElementMargins.None(),
                UITextAlignment.Left);

            stackView.AddArrangedSubview(distanceDurationWrapperView);
            distanceDurationWrapperView.AddSubview(imageDistance);
            distanceDurationWrapperView.AddSubview(distanceLabel);
            distanceDurationWrapperView.AddSubview(imageDuration);
            distanceDurationWrapperView.AddSubview(durationLabel);

            imageDistance.WidthAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
            imageDistance.HeightAnchor.ConstraintEqualTo(IconImageWidth).Active = true;

            imageDuration.WidthAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
            imageDuration.HeightAnchor.ConstraintEqualTo(IconImageWidth).Active = true;

            imageDistance.CenterYAnchor.ConstraintEqualTo(distanceDurationWrapperView.CenterYAnchor).Active = true;
            imageDistance.LeadingAnchor.ConstraintEqualTo(distanceDurationWrapperView.LeadingAnchor).Active = true;

            distanceLabel.TopAnchor.ConstraintEqualTo(distanceDurationWrapperView.TopAnchor).Active = true;
            distanceLabel.BottomAnchor.ConstraintEqualTo(distanceDurationWrapperView.BottomAnchor, 0).Active = true;
            distanceLabel.LeadingAnchor.ConstraintEqualTo(imageDistance.TrailingAnchor,
                StyleGuideDimens.PaddingExtraSmall).Active = true;

            imageDuration.CenterYAnchor.ConstraintEqualTo(distanceDurationWrapperView.CenterYAnchor).Active = true;
            imageDuration.LeadingAnchor.ConstraintEqualTo(distanceLabel.TrailingAnchor, ImageDurationToLabelGap)
                .Active = true;

            durationLabel.TopAnchor.ConstraintEqualTo(distanceDurationWrapperView.TopAnchor).Active = true;
            durationLabel.BottomAnchor.ConstraintEqualTo(distanceDurationWrapperView.BottomAnchor, 0).Active = true;
            durationLabel.LeadingAnchor.ConstraintEqualTo(imageDuration.TrailingAnchor,
                StyleGuideDimens.PaddingExtraSmall).Active = true;
            durationLabel.TrailingAnchor.ConstraintEqualTo(distanceDurationWrapperView.TrailingAnchor).Active = true;

            distanceDurationWrapperView.LayoutIfNeeded();
            containerView.LayoutIfNeeded();
            stackView.LayoutIfNeeded();

            return containerView;
        }

        public static nfloat GetHeaderHeight(JourneyDetailOverlayDisplayData displayData, nfloat availableWidth)
        {
            //Left padding, left score panel padding, score panel width, right padding
            var totalAvailableWidth = availableWidth - (StyleGuideDimens.PaddingSmall +
                StyleGuideDimens.PaddingSmall + ScoreImageWidth + StyleGuideDimens.PaddingSmall);

            var headerString = new NSString(displayData.JourneyDate);
            var distance = new NSString(displayData.TotalMiles);
            var duration = new NSString(displayData.TotalJourneyTime);

            var sizeOfHeader = headerString.GetCalculatedSizeOfString(FontStyle.SemiBold.GetFont(Heading3SemiBold.Size),
                totalAvailableWidth);

            var (sizeOfStartAddress, sizeOfEndAddress) = GetAddressPanelHeights(displayData, availableWidth);

            var sizeOfDistance = distance.GetCalculatedSizeOfString(FontStyle.Regular.GetFont(BodyRegularCaption.Size),
                totalAvailableWidth);

            var sizeOfDuration = duration.GetCalculatedSizeOfString(FontStyle.Regular.GetFont(BodyRegularCaption.Size),
                totalAvailableWidth);

            var distanceHeight = sizeOfDistance.Height > IconImageWidth
                ? sizeOfDistance.Height
                : IconImageWidth;

            var durationHeight = sizeOfDuration.Height > IconImageWidth
                ? sizeOfDuration.Height
                : IconImageWidth;

            var distanceDurationPanelHeight = distanceHeight > durationHeight ? distanceHeight : durationHeight;

            return OverlayDragBarTopPadding
                   + OverlayDragBarHeight
                   + OverlayDragBarBottomPadding
                   + sizeOfHeader.Height
                   + StyleGuideDimens.PaddingSmall
                   + sizeOfStartAddress.Height
                   + StyleGuideDimens.PaddingExtraSmall
                   + sizeOfEndAddress.Height
                   + StyleGuideDimens.PaddingMedium
                   + distanceDurationPanelHeight
                   + StyleGuideDimens.PaddingSmall;
        }

        public static (CGSize startAddressHeight, CGSize endAddressHeight) GetAddressPanelHeights(
            JourneyDetailOverlayDisplayData displayData, nfloat availableWidth)
        {
            //Left padding, Address icon width and padding, left score panel padding, score panel width, right padding
            var totalAvailableWidth = availableWidth - (StyleGuideDimens.PaddingSmall + AddressIconWidthAndPadding +
                StyleGuideDimens.PaddingSmall + ScoreImageWidth + StyleGuideDimens.PaddingSmall);

            var startAddress = new NSString(displayData.JourneyStartAddress);
            var endAddress = new NSString(displayData.JourneyEndAddress);

            var sizeOfStartAddress = startAddress.GetCalculatedSizeOfString(
                FontStyle.Regular.GetFont(BodyRegularCaption.Size),
                totalAvailableWidth);

            var sizeOfEndAddress = endAddress.GetCalculatedSizeOfString(
                FontStyle.Regular.GetFont(BodyRegularCaption.Size),
                totalAvailableWidth);

            return (sizeOfStartAddress, sizeOfEndAddress);
        }

        #endregion
    }
}

Upvotes: 0

Views: 107

Answers (0)

Related Questions