How to reposition compass of MKMapView?
Asked Answered
P

5

4

I want to move the MKMapView compass. I wanted to get a reference for it by something like this:

let compassView = mapView.subviews.filter {$0 is NSClassFromString("MKCompassView")}

However the compiler complains " Use of undeclared type 'NSClassFromString' ". How can I fix this code?

Primrose answered 19/1, 2018 at 14:33 Comment(1)
This other question stackoverflow.com/questions/38288269 is actually older (and actually had a correctish answer) but was closed in favor of this newer question (which only had totally wrong answers) - !Backwards
W
7

iOS 11

you should use MKCompassButton, doc explaining the new stuff: WWDC 2017 new MapKit presentation.

let compassButton = MKCompassButton(mapView:mapView)
compassButton.frame.origin = CGPoint(x: 20, y: 20)
compassButton.compassVisibility = .visible
view.addSubview(compassButton)

iOS < 11

You might try to use String(describing:), something like:

if let compassButton = (mapView.subviews.filter { String(describing:$0).contains("MKCompassView") }.first) {
   print(compassButton)
}
Wriest answered 19/1, 2018 at 15:1 Comment(5)
i tried but it didn't work. I guess apple just doesnt want you touching the compass for some reason :/Primrose
NSClassFromString is not deprecated. Not according to Apple's documentation nor in the header files for both Swift and Objective-C.Bernhardt
the old part of this answer is now completely wrong and has not worked for years. the new part of the answer is almost completely wrong as it misses three things you have to doBackwards
@Backwards it's not technically wrong, the answer was made in 2018 for iOS 11 or less, which was the current target at that time.Wriest
hi @Wriest it's definitely a difficult issue on SO. i personally just instantly erase (or edit away) all my own answers, as, they become legacy. Obviously each writer on SO such as yourself can and should do what they want. This is particularly true when dealing with the major categories on the site, UIKit and android and so on; systems like UIKit legacy things every six months. THey're not computer science questions, they're topical . I feel that eventually Prosus has to introduce a concept of "legacied answer" or "historic answer". Otherwise the site is simply becoming useless.Backwards
P
2

For iOS 11 and above, use MKCompassButton.

let compass = MKCompassButton(mapView: mapView)
Parentage answered 19/1, 2018 at 22:17 Comment(1)
Whilst correct there's unfortnately more to knowBackwards
B
1

The current answer as of about iOS 15 / 2024

The old "move the MKCompassView" method does not work

Apple have changed the view structure. If you try moving the frame of an MKCompassView you'll see it does nothing. Don't waste your time. (And just FWIW in theory you're using a hidden class so the app could be denied by Apple.)

You simply do this

///MapView with rose in different position
class BetterMKMapView: MKMapView {
    
    lazy var comp: MKCompassButton = {
        let v = MKCompassButton(mapView: self)
        v.isUserInteractionEnabled = true
        addSubview(v)
        return v
    }()
    
    override func layoutSubviews() {
        super.layoutSubviews()
        showsCompass = false
        comp.frame = CGRect(
            origin: CGPoint(x: 666, y: 666),
            size: comp.bounds.size)
    }
}

Everything works absolutely identically to the standard compass, in every way. Appearance, disappearance, tapping, animations, etc.

Note that "showsCompass" is (surprise) badly named, it should be named something like includeDefaultCompass. "showsCompass" has NO effect on any compasses you include.

Note that compassVisibility = .adaptive is the default, but include it if you wish.

Note that during experimentation, isUserInteractionEnabled sometimes came out as false. I could never quite make this reproducible but I left in isUserInteractionEnabled = true as a safety measure. (If you can ever repro the issue, give a yell.)

FYI, presently (2024) the default compass is inset 64 from right and 49 from top, if you want to match some of those values. Hence ...

If you want it bottom-right, copy-paste

/// Bottom-right rose; match (current) Apple feel
class DesignishMKMapView: MKMapView {
    
    lazy var comp: MKCompassButton = {
        let v = MKCompassButton(mapView: self)
        v.isUserInteractionEnabled = true
        addSubview(v)
        return v
    }()
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // FYI, for 2024, apple design is insets 49 top 64 right
        showsCompass = false
        comp.frame = CGRect(
            origin: CGPoint(x: bounds.width - 64 - comp.bounds.size.width,
                            y: bounds.height - 49 - comp.bounds.size.height),
            size: comp.bounds.size)
    }
}

If you truly want to literally move the supplied compass, perhaps experimentally

As of writing it's easy,

///Hah-hah Apple
class WeirdMKMapView: MKMapView {
    override func layoutSubviews() {
        super.layoutSubviews()
        if let compass = first(ofType: "MKCompassView") {
            compass.superview!.frame = CGRect(
                origin: CGPoint(x: 200, y: 200),
                size: compass.superview!.bounds.size)
        }
    }
}

but this can and will change any time Apple happens to be fooling with it.

Just FTR re the experimental one, if you don't have the utility function here it is

extension UIView {
    var subviewsRecursive: [UIView] {
        return subviews + subviews.flatMap { $0.subviewsRecursive }
    }
    ///Find by type by string.
    func first(ofType: String) -> UIView? {
        return subviewsRecursive.first(where:
                         {String(describing: $0).contains(ofType)})
    }
}
Backwards answered 10/3, 2024 at 18:2 Comment(0)
A
0

This is my solution for repositioning the compass view by subclassing MKMapView.
The code is Swift 5.0 tested on iOS10 and above.
Note: When you test this on iOS10 devices you have to rotate the map in order to make compass visible.

import MapKit
class MapView: MKMapView {
    override func layoutSubviews() {
        super.layoutSubviews()
        if #available(iOS 10.0, *) {
            self.showsCompass = true //*** - You have to set this true here, it does not work if you set it on storyboards or in a View Controller - ***
            if let compassButton = (self.subviews.filter { String(describing:$0).contains("MKCompassView") }.first) {
                compassButton.frame = CGRect(x: 20, y: 40, width: 36, height: 36)
            }
        } else {
            let compassButton = MKCompassButton(mapView:self)
            compassButton.frame.origin = CGPoint(x: 20, y: 40)
            compassButton.compassVisibility = .visible
            self.addSubview(compassButton)
        }
    }
}
Acidic answered 12/7, 2019 at 15:6 Comment(2)
@Backwards why so? Can you celebrate?Acidic
At the time of Jul 12, 2019, this worked for me lol welcome to the past.Acidic
P
0

I'm Targeting iOS12+. I wanted to reposition the default compass that is supplied with the MKTrackingButton but found out I can't. This is what I ended up with.

  // https://mcmap.net/q/1634319/-change-compass-position-in-map
  // Hide the default compass that comes with MkTrackingButton
  vcTrainMapView.showsCompass = false
  
  // Create the Compass
  gpsCompassButton = MKCompassButton(mapView:vcTrainMapView)
  gpsCompassButton.compassVisibility = .visible
  
  // Add Compass to MapView
  vcTrainMapView.addSubview(gpsCompassButton)
  
  // Add Constrains
  gpsCompassButton.translatesAutoresizingMaskIntoConstraints = false
  gpsCompassButton.topAnchor.constraint(equalTo: gpsEnabledIcon.bottomAnchor, constant: 10).isActive = true
  gpsCompassButton.trailingAnchor.constraint(equalTo: vcTrainMapView.trailingAnchor, constant: -5).isActive = true

enter image description here

Poplin answered 9/5, 2024 at 6:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.