Custom font for MKAnnotationView Callout
Asked Answered
V

2

3

Fortunately, the standard callout view for an MKAnnotationView meets our needs - title, subtitle, leftCalloutAccessoryView, and rightCalloutAccessoryView.

Unfortunately, we use custom fonts in our app, and would like to extend those custom fonts to these callouts.

MKAnnotationView provides no standard way to accomplish this.

How can I use a custom font in an MKAnnotation's callout view?

Vincents answered 8/5, 2015 at 7:19 Comment(0)
V
1

If all you need is a custom font, you need to subclass MKAnnotationView, but you don't have to recreate all the behavior that you get for free with a standard MKAnnotationView. It's actually pretty easy.

  1. Subclass MKAnnotationView
  2. Override -layoutSubviews
  3. When an MKAnnotationView is selected, the callout is added as a subview. Therefore, we can recursively loop through our subclass' subviews and find the UILabel we wish to modify.
  4. That's it!

The only drawback with this method is that you can see the callout adjust it's size if your font is smaller or larger than the standard system font it was expecting. It'd be great if all the adjustments were made before being presented to the user.

// elsewhere, in a category on UIView.
// thanks to this answer: https://mcmap.net/q/236621/-how-can-i-loop-through-all-subviews-of-a-uiview-and-their-subviews-and-their-subviews
//
typedef void(^ViewBlock)(UIView *view, BOOL *stop);

@interface UIView (Helpers)
- (void)loopViewHierarchy:(ViewBlock)block;
@end

@implementation UIView (Helpers)

- (void)loopViewHierarchy:(ViewBlock)block {
    BOOL stop = false;

    if (block) {
        block(self, &stop);
    }

    if (!stop) {
        for (UIView* subview in self.subviews) {
            [subview loopViewHierarchy:block];
        }
    }
}

@end

// then, in your MKAnnotationView subclass
//
@implementation CustomFontAnnotationView
- (void)layoutSubviews
{
    // MKAnnotationViews only have subviews if they've been selected.
    // short-circuit if there's nothing to loop over
    if (!self.selected) {
        return;
    }

    [self loopViewHierarchy:^(UIView *view, BOOL *stop) {
        if ([view isKindOfClass:[UILabel class]]) {
            *stop = true;
            ((UILabel *)view).font = {custom_font_name};
        }
    }];
}
@end

I inspected the view tree by first creating my MKAnnotationView subclass and setting a breakpoint in my overridden -layoutSubviews. In the debugger, I then issued po [self recursiveDescription]. Make sure to turn the breakpoint off when your map first loads, because as mentioned up above, MKAnnotationViews don't have any subviews until their selected. Before you make a selection, enable the breakpoint, tap your pin, break, and print out the view tree. You'll see a UILabel at the very bottom of the tree.

Vincents answered 8/5, 2015 at 7:19 Comment(2)
This works, however, I noticed that the the annotation view's frame size is based on the old font and then gets resized (with animation) to the new size based on the new font. Is there any way to get it to be appropriately sized right away?Remiss
@JoshGafni did you ever find a solution for this?Stoneware
S
8

Since I needed the Swift version - here it is. Also, you have to call setNeedsLayout() on didAddSubview() because otherwise when you deselect and reselect the annotation layoutSubviews() is not called and the callout has its old font.

// elsewhere, in a category on UIView.
// thanks to this answer: https://mcmap.net/q/236621/-how-can-i-loop-through-all-subviews-of-a-uiview-and-their-subviews-and-their-subviews

typealias ViewBlock = (_ view: UIView) -> Bool

extension UIView {
  func loopViewHierarchy(block: ViewBlock?) {

    if block?(self) ?? true {
      for subview in subviews {
        subview.loopViewHierarchy(block: block)
      }
    }
  }
}

// then, in your MKAnnotationView subclass

class CustomFontAnnotationView: MKAnnotationView {

  override func didAddSubview(_ subview: UIView) {
    if isSelected {
      setNeedsLayout()
    }
  }

  override func layoutSubviews() {

    // MKAnnotationViews only have subviews if they've been selected.
    // short-circuit if there's nothing to loop over

    if !isSelected {
      return
    }

    loopViewHierarchy { (view: UIView) -> Bool in
      if let label = view as? UILabel {
        label.font = labelFont
        return false
      }
      return true
    }
  }
}
Spunky answered 1/12, 2015 at 13:25 Comment(2)
I updated my answer to support Swift 3 (removed var parameters) and updated the syntax a little.Spunky
Thanks a lot brother was needing this a lot :), I had to customize the whole annotationView, achieved through your logicAlberta
V
1

If all you need is a custom font, you need to subclass MKAnnotationView, but you don't have to recreate all the behavior that you get for free with a standard MKAnnotationView. It's actually pretty easy.

  1. Subclass MKAnnotationView
  2. Override -layoutSubviews
  3. When an MKAnnotationView is selected, the callout is added as a subview. Therefore, we can recursively loop through our subclass' subviews and find the UILabel we wish to modify.
  4. That's it!

The only drawback with this method is that you can see the callout adjust it's size if your font is smaller or larger than the standard system font it was expecting. It'd be great if all the adjustments were made before being presented to the user.

// elsewhere, in a category on UIView.
// thanks to this answer: https://mcmap.net/q/236621/-how-can-i-loop-through-all-subviews-of-a-uiview-and-their-subviews-and-their-subviews
//
typedef void(^ViewBlock)(UIView *view, BOOL *stop);

@interface UIView (Helpers)
- (void)loopViewHierarchy:(ViewBlock)block;
@end

@implementation UIView (Helpers)

- (void)loopViewHierarchy:(ViewBlock)block {
    BOOL stop = false;

    if (block) {
        block(self, &stop);
    }

    if (!stop) {
        for (UIView* subview in self.subviews) {
            [subview loopViewHierarchy:block];
        }
    }
}

@end

// then, in your MKAnnotationView subclass
//
@implementation CustomFontAnnotationView
- (void)layoutSubviews
{
    // MKAnnotationViews only have subviews if they've been selected.
    // short-circuit if there's nothing to loop over
    if (!self.selected) {
        return;
    }

    [self loopViewHierarchy:^(UIView *view, BOOL *stop) {
        if ([view isKindOfClass:[UILabel class]]) {
            *stop = true;
            ((UILabel *)view).font = {custom_font_name};
        }
    }];
}
@end

I inspected the view tree by first creating my MKAnnotationView subclass and setting a breakpoint in my overridden -layoutSubviews. In the debugger, I then issued po [self recursiveDescription]. Make sure to turn the breakpoint off when your map first loads, because as mentioned up above, MKAnnotationViews don't have any subviews until their selected. Before you make a selection, enable the breakpoint, tap your pin, break, and print out the view tree. You'll see a UILabel at the very bottom of the tree.

Vincents answered 8/5, 2015 at 7:19 Comment(2)
This works, however, I noticed that the the annotation view's frame size is based on the old font and then gets resized (with animation) to the new size based on the new font. Is there any way to get it to be appropriately sized right away?Remiss
@JoshGafni did you ever find a solution for this?Stoneware

© 2022 - 2024 — McMap. All rights reserved.