web-dev-qa-db-fra.com

Bulle de légende MKAnnotation personnalisée avec bouton

Je développe une application, où l'utilisateur est localisé par le GPS, puis on lui demande s'il se trouve à un endroit spécifique. Pour le confirmer, une bulle de rappel lui est immédiatement présentée et lui demande s'il se trouve à un endroit spécifique. 

Comme il y a beaucoup de questions similaires, j'ai pu créer une bulle de texte personnalisée: Custom callout bubble

Mon problème: le bouton n'est pas "cliquable" .__ Mon hypothèse: parce que cette légende personnalisée est plus haute que la bulle de légende standard, j'ai dû la placer dans un "cadre" négatif, il est donc impossible de cliquer sur le bouton. Voici ma méthode didSelectAnnotationView 

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
    if(![view.annotation isKindOfClass:[MKUserLocation class]]) {
        CalloutView *calloutView = (CalloutView *)[[[NSBundle mainBundle] loadNibNamed:@"callOutView" owner:self options:nil] objectAtIndex:0];
        CGRect calloutViewFrame = calloutView.frame;
        calloutViewFrame.Origin = CGPointMake(-calloutViewFrame.size.width/2 + 15, -calloutViewFrame.size.height);
        calloutView.frame = calloutViewFrame;
        [calloutView.calloutLabel setText:[(MyLocation*)[view annotation] title]];
        [calloutView.btnYes addTarget:self
                               action:@selector(checkin)
                     forControlEvents:UIControlEventTouchUpInside];
        calloutView.userInteractionEnabled = YES;
        view.userInteractionEnabled = YES;
        [view addSubview:calloutView];
    }

}

CalloutView est juste une classe simple avec 2 propriétés (étiquette qui indique le nom du lieu et du bouton) et avec xib.

Je fais cette bulle de légende personnalisée depuis quelques jours. J'ai essayé d'utiliser "solutions asynchrones" solution mais je n'ai pas pu ajouter d'autre type de bouton que de bouton de divulgation.

Ma tentative suivante consistait à trouver quelque chose qui était plus facile que des solutions asynchrones et à le modifier à mon usage. C'est comme ça que j'ai trouvé la légende personnalisée de tochi

Tochi's custom callout

Sur la base de son travail, j'ai pu personnaliser sa bulle et le bouton d'informations sur mon bouton personnalisé. Mon problème est cependant resté le même. Afin de placer ma vue de légende personnalisée au-dessus de l'épingle, je devais lui donner un cadre négatif, mon bouton était donc "cliquable" uniquement dans les 5 derniers pixels. Il semble que je doive peut-être creuser plus avant dans la bulle de légende par défaut de l'ios, la sous-classer et en changer le cadre. Mais je suis vraiment sans espoir maintenant.

Si vous pouviez me montrer le bon chemin ou me donner des conseils, je serais heureux.

24
Yanchi

Il existe plusieurs approches pour personnaliser les légendes:

  1. La solution la plus simple consiste à utiliser les accessoires de légende gauche et gauche existants et à placer votre bouton dans l’un de ceux-ci. Par exemple:

    - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
        static NSString *identifier = @"MyAnnotationView";
    
        if ([annotation isKindOfClass:[MKUserLocation class]]) {
            return nil;
        }
    
        MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (view) {
            view.annotation = annotation;
        } else {
            view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            view.canShowCallout = true;
            view.animatesDrop = true;
            view.rightCalloutAccessoryView = [self yesButton];
        }
    
        return view;
    }
    
    - (UIButton *)yesButton {
        UIImage *image = [self yesButtonImage];
    
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.frame = CGRectMake(0, 0, image.size.width, image.size.height); // don't use auto layout
        [button setImage:image forState:UIControlStateNormal];
        [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
    
        return button;
    }
    
    - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
        NSLog(@"%s", __PRETTY_FUNCTION__);
    }
    

    Cela donne:

     enter image description here

  2. Si vous n'aimez vraiment pas le bouton de droite, là où les accessoires vont généralement, vous pouvez le désactiver, et iOS 9 offre la possibilité de spécifier la detailCalloutAccessoryView, qui remplace le sous-titre de la légende par la vue souhaitée:

    - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
        static NSString *identifier = @"MyAnnotationView";
    
        if ([annotation isKindOfClass:[MKUserLocation class]]) {
            return nil;
        }
    
        MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (view) {
            view.annotation = annotation;
        } else {
            view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            view.canShowCallout = true;
            view.animatesDrop = true;
        }
        view.detailCalloutAccessoryView = [self detailViewForAnnotation:annotation];
    
        return view;
    }
    
    - (UIView *)detailViewForAnnotation:(PlacemarkAnnotation *)annotation {
        UIView *view = [[UIView alloc] init];
        view.translatesAutoresizingMaskIntoConstraints = false;
    
        UILabel *label = [[UILabel alloc] init];
        label.text = annotation.placemark.name;
        label.font = [UIFont systemFontOfSize:20];
        label.translatesAutoresizingMaskIntoConstraints = false;
        label.numberOfLines = 0;
        [view addSubview:label];
    
        UIButton *button = [self yesButton];
        [view addSubview:button];
    
        NSDictionary *views = NSDictionaryOfVariableBindings(label, button);
    
        [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|" options:0 metrics:nil views:views]];
        [view addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
        [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]-[button]|" options:0 metrics:nil views:views]];
    
        return view;
    }
    
    - (UIButton *)yesButton {
        UIImage *image = [self yesButtonImage];
    
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.translatesAutoresizingMaskIntoConstraints = false; // use auto layout in this case
        [button setImage:image forState:UIControlStateNormal];
        [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
    
        return button;
    }
    

    Cela donne:

     enter image description here

  3. Si vous souhaitez vraiment développer vous-même une légende personnalisée, le Guide de programmation des emplacements et des cartes / décrit les étapes à suivre:

    Dans une application iOS, il est recommandé d’utiliser la méthode déléguée mapView:annotationView:calloutAccessoryControlTapped: pour répondre lorsque les utilisateurs appuient sur le contrôle d’une vue de légende (tant que le contrôle est un descendant de UIControl). Lors de l’implémentation de cette méthode, vous pouvez découvrir l’identité de la vue annotation de la vue de légende afin de savoir quelle annotation l’a tapé. Dans une application Mac, le contrôleur de vue de la légende peut implémenter une méthode d'action qui répond lorsqu'un utilisateur clique sur le contrôle dans une vue de légende.

    Lorsque vous utilisez une vue personnalisée au lieu d'une légende standard, vous devez effectuer un travail supplémentaire pour vous assurer que votre légende s'affiche et se cache correctement lorsque les utilisateurs interagissent avec celle-ci. Les étapes ci-dessous décrivent le processus de création d'une légende personnalisée contenant un bouton:

    • Concevez une sous-classe NSView ou UIView qui représente la légende personnalisée. Il est probable que la sous-classe doit implémenter la méthode drawRect: pour dessiner votre contenu personnalisé.

    • Créez un contrôleur de vue qui initialise la vue de légende et effectue l'action liée au bouton.

    • Dans la vue des annotations, implémentez hitTest: pour répondre aux occurrences situées en dehors des limites de la vue des annotations mais à l’intérieur des limites de la vue des légendes, comme indiqué dans le Listing 6-7.

    • Dans la vue d'annotation, implémentez setSelected:animated: pour ajouter votre vue de légende en tant que sous-vue de la vue d'annotation lorsque l'utilisateur la clique ou la tapote. 

    • Si la vue de légende est déjà visible lorsque l'utilisateur la sélectionne, la méthode setSelected: doit supprimer la sous-vue de légende de la vue des annotations (voir Listing 6-8).

    • Dans la méthode initWithAnnotation: de la vue des annotations, définissez la propriété canShowCallout sur NO afin d'empêcher la carte d'afficher la légende standard lorsque l'utilisateur sélectionne l'annotation.

    Tandis qu’il se trouve dans Swift, https://github.com/robertmryan/CustomMapViewAnnotationCalloutSwift illustre un exemple de la manière dont vous pouvez effectuer cette personnalisation complète de la légende (par exemple, changer la forme de la bulle, modifier la couleur de l’arrière-plan, etc.).

  4. Ce point précédent décrit un scénario assez compliqué (c’est-à-dire que vous devez écrire votre propre code pour détecter les taps en dehors de la vue afin de le supprimer). Si vous prenez en charge iOS 9, vous pouvez simplement utiliser un contrôleur de vue popover, par exemple:

    - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
        static NSString *identifier = @"MyAnnotationView";
    
        if ([annotation isKindOfClass:[MKUserLocation class]]) {
            return nil;
        }
    
        MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (view) {
            view.annotation = annotation;
        } else {
            view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            view.canShowCallout = false;  // note, we're not going to use the system callout
            view.animatesDrop = true;
        }
    
        return view;
    }
    
    - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
        PopoverController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"AnnotationPopover"];
        controller.modalPresentationStyle = UIModalPresentationPopover;
    
        controller.popoverPresentationController.sourceView = view;
    
        // adjust sourceRect so it's centered over the annotation
    
        CGRect sourceRect = CGRectZero;
        sourceRect.Origin.x += [mapView convertCoordinate:view.annotation.coordinate toPointToView:mapView].x - view.frame.Origin.x;
        sourceRect.size.height = view.frame.size.height;
        controller.popoverPresentationController.sourceRect = sourceRect;
    
        controller.annotation = view.annotation;
    
        [self presentViewController:controller animated:TRUE completion:nil];
    
        [mapView deselectAnnotation:view.annotation animated:true];  // deselect the annotation so that when we dismiss the popover, the annotation won't still be selected
    }
    
79
Rob

J'ai fini par adopter une approche différente. J'ai essayé les autres, mais ils semblaient gonflés et je ne voulais pas ajouter d'autres classes ni me fier à MKMapViewDelegate pour gérer l'interaction.

Au lieu de cela, je remplace setSelected: animé de ma sous-classe MKAnnotationView. L'astuce consiste à élargir les limites de l'annotationView après l'avoir sélectionnée pour englober complètement la vue d'appel, puis à revenir à la normale après sa désélection. Cela permettra à vos appels personnalisés d'accepter les touches et les interactions en dehors des limites d'origine de MKAnnotationView. 

Voici un exemple de code réduit pour que tout le monde puisse commencer: 

#define kAnnotationCalloutBoxTag    787801
#define kAnnotationCalloutArrowTag  787802
#define kAnnotationTempImageViewTag 787803

-(void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    if (selected == self.selected)
    {
        NSLog(@"annotation already selected, abort!");
        return;
    }
    if (selected)
    {
        self.image = nil; //hide default image otherwise it takes the shape of the entire bounds

        UIView* calloutBox = [self newCustomCallout];
        float imgW = [self unselectedSize].width;
        float imgH = [self unselectedSize].height;
        float arrowW = 20;
        float arrowH = 12;

        //Annotation frames wrap a center coordinate, in this instance we want the call out box to fall under the
        //central coordinate, so we need to adjust the height to be double what the callout box height would be
        //making the height *2, this is to make sure the callout view is inside of it.
        self.bounds = CGRectMake(0, 0, calloutBox.frame.size.width, calloutBox.frame.size.height*2 + arrowH*2 + imgH);
        CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);

        UIView* imgView = [[UIImageView alloc] initWithImage:icon];
        [imgView setFrame:CGRectMake(center.x - imgW/2, center.y-imgH/2, imgW, imgH)];
        imgView.tag = kAnnotationTempImageViewTag;
        [self addSubview:imgView];

        UIView* triangle = [self newTriangleViewWithFrame:CGRectMake(center.x-arrowW/2, center.y+imgH/2, arrowW, arrowH)];
        triangle.tag = kAnnotationCalloutArrowTag;
        [self addSubview:triangle];

        [calloutBox setFrame:CGRectMake(0, center.y+imgH/2+arrowH, calloutBox.width, calloutBox.height)];
        calloutBox.tag = kAnnotationCalloutBoxTag;
        [self addSubview:calloutBox];
    }
    else
    {
        //return things back to normal
        UIView* v = [self viewWithTag:kAnnotationCalloutBoxTag];
        [v removeFromSuperview];
        v = [self viewWithTag:kAnnotationCalloutArrowTag];
        [v removeFromSuperview];
        v = [self viewWithTag:kAnnotationTempImageViewTag];
        [v removeFromSuperview];

        self.image = icon;
        self.bounds = CGRectMake(0, 0, [self unselectedSize].width, [self unselectedSize].height);
    }
    [super setSelected:selected animated:animated];
}

-(CGSize)unselectedSize
{
    return CGSizeMake(20,20);
}

-(UIView*)newCustomCallout
{
    //create your own custom call out view
    UIView* v = [[UIView alloc] initWithFrame:CGRectMake(0,0,250,250)];
    v.backgroundColor = [UIColor greenColor];
    return v;
}

-(UIView*)newTriangleWithFrame:(CGRect)frame
{
    //create your own triangle
    UIImageView* v = [[UIImageView alloc] initWithFrame:frame];
    [v setImage:[UIImage imageNamed:@"trianglePointedUp.png"]];
    return v;
}
1
psy
(void)mapView:(MKMapView *)mapViewIn didSelectAnnotationView:(MKAnnotationView *)view {
    if(![view.annotation isKindOfClass:[MKUserLocation class]])
    {
        CustomeCalloutViewController *calloutView = [[CustomeCalloutViewController alloc]initWithNibName:@"CustomeCalloutViewController" bundle:nil];
        [calloutView setPopinTransitionStyle:BKTPopinTransitionStyleSlide];
        [calloutView setPopinTransitionDirection:BKTPopinTransitionDirectionTop];
        [self presentPopinController:calloutView animated:YES completion:^{
            NSLog(@"Popin presented !");
        }];
        [mapView deselectAnnotation:view.annotation animated:true];
    }
}
0
Abo3atef