MKAnnotationViews and AutoLayout

Having just developed a plist-based design framework that works in Interface Builder, I’m more interested than ever in using xibs and storyboards to design my views.

I also like using AutoLayout. There are significant advantages when it comes to things like labels that make it so much easier to deal with than manually setting frames everywhere in code. Things like Dynamic Type, accessibility, and localisation all become easier and there’s less room for error.

There are some things that do become more complicated with AutoLayout (mostly transforms) but there are well established workarounds for most of these.

I was recently designing a new app that involved using MapKit and I wanted to use AutoLayout to design a subclsss of MKAnnotationView but this isn’t entirely straightforward.

When a view is first dequeued, its frame is set directly by the map. This means that any MKAnnotationView subclass cannot switch off `translatesAutoresizingMaskIntoConstraints`, which is required to get AutoLayout working correctly.

The subview can’t have AutoLayout resetting its frame when it’s finished it’s calculation passes. If this property is set to false, AutoLayout will move the view up to the top left corner of the map view (as it sets the subclass frame without any further position information, i.e. constraints to position the annotation view in the map view itself) . Manually dragging the map triggers a reset of the annotation view’s position, but this is still an unacceptably awful UI experience.

What the MKAnnotationView subclass needs is an additional container view: one who can catch the setting of the frame by AutoLayout and discard the position information.

The Xib file already has a UIView attached, which is the `contentView`. We then need to add an additional `containerView`, which is pinned to the `contentView`. I am then free to use AutoLayout to layout all of my other internal views. This `containerView` is what allows the whole thing to be resized to fit the contents.

Image of the view hierarchy for the Xib file

During the setup phase, we load the Nib and attach the contentView. We then directly set the `MKAnnotationView`’s bounds (not frame, so no position information is affected). Then we centre the containerView to the `MKAnnotationView’s` center (i.e. its grandparent view, not its direct parent) so that it gets its position information from the AnnotationView.

Finally, when `layoutSubviews` or `prepareForReuse` are called on the annotation view, the `MKAnnotationView` gets its size information from the `containerView` and adjusts accordingly, again ignoring the position information (which comes from the `MKMapView` it belongs to).

import MapKit
class ExampleAnnotationView: MKAnnotationView {

	@IBOutlet weak var containerView : UIView?
	@IBOutlet weak var contentView : UIView?
	// ... Other outlets
	
	override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
		super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
		self.setup()
	}
	
	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		self.setup()
	}

	
	func setup() {
		
		guard let nibView = loadViewFromNib() else {
			return
		}
		
		contentView = nibView

		bounds = nibView.frame
		addSubview(nibView)
		
		contentView?.translatesAutoresizingMaskIntoConstraints = false
		containerView?.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0).isActive = true
		containerView?.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0).isActive = true
		
	}
	
	func loadViewFromNib() -> UIView! {
		let bundle = Bundle(for: type(of: self))
		let name =  "ExampleAnnotationView"
		let nib = UINib(nibName: name, bundle: bundle)
		let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
		return view
	}
	
	override func layoutSubviews() {
		super.layoutSubviews()
		bounds = containerView!.bounds
	}
	override func prepareForReuse() {
		super.layoutSubviews()
		bounds = containerView!.bounds
		
	}

}

AutoLayout passes can be expensive if there are a lot of them which, on a map view with many annotations, there might be. Performance testing on real devices will be crucial.

Still, on modern devices this shouldn’t be so much of an issue and AutoLayout makes it easier to design views and uses a lot less code than manually setting frames. There are also advantages to be had in terms of getting a lot of work done for free by AutoLayout when dynamically adding new views or animating existing ones (e.g. using Stack Views to hide or show content).

The example project is available on GitHub.