Search code examples
iosswiftautolayoutmapboxmapbox-ios

Anchoring custom views to existing view in Mapbox NavigationViewController in Swift


I'm using the Mapbox framework on iOS in Swift to create a custom NavigationViewController. I need help getting a reference to the FloatingStackView on the NavigationView.

I've added an additional button (see top left of screenshot) and I'd like to anchor it to the FloatingStackView on the right so that it moves up and down as the top banner changes (similar to the FloatingStackView behavior). The trouble is I can't seem to access the FloatingStackView. From what I've seen in the Mapbox source code the FloatingStackView is a subview of a NavigationView which is the view of a RouteMapViewController which is the mapViewController of a NavigationViewController. Trouble is I can't access the mapViewController as it is inaccessible due to internal protection level.

Here's an example of what I'd like to do:

import UIKit
import MapboxNavigation

class DemoNavigationViewController: NavigationViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let mapVC = mapViewController
        // Do any additional setup after loading the view.
    }

}

Here's the error on the let mapVC line:

'mapViewController' is inaccessible due to 'internal' protection level

My end goal would be to be able to access the FloatingStackView via something like this:

let navigationView = mapViewController.view as? NavigationView
let floatingStackView = navigationView.floatingStackView

If I can get a reference to that floatingStackView I think I can handle the layout.

How can I get around the is inaccessible due to 'internal' protection level error or find another way to access the floating stack view?

screenshot

From NavigationView class source code

NavigationViewClass


Solution

  • You will not be able to reference it directly if it is marked internal. There is however a hacky way to get to any view and use it for AutoLayout (assuming the view uses autoLayout and has anchors you can use).

    As each view has a subViews array you can iterate through the subviews to find the one you want. All subViews are visible, even the ones that come from private instances. The trick is identifying the one you need.

    If the Type definition is accessible, you can do something like this

    let navView = viewController.subViews.first{$0 is NavigationView}!
    let stackView = navView.subViews.first{$0 is UIStackView}!
    floatingButton.topAnchor.constraint(equalTo: stackView.topAnchor).isActive  true
    

    If you don't have a Type you can match against you'll have to do something even more hacky, such as matching against the description: .first{$0.description conatins "stack"}. Or even by trial and error by indexing the subView array until you get the result you want, and then hoping the order of views is always the same!

    Whether you'd want to risk this in production code is only a decision you can make as it subject to breaking if the library changes it's UI design. Depending on the app's user base I might risk matching against type, but probably not the other ways!

    Note I've force-unwrapped the views above for brevity - you'd want to be a bit more cautious in production code with that too :-)