Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

UI customization with SwiftUI

The iOS tutorial gets you set up with a “batteries included” UI and sane defaults, but this doesn’t work for every use case. This page walks you through the ways to customize the SwiftUI frontend to your liking.

Customizing the map

Ferrostar includes a map view built with the MapLibre SwiftUI DSL. This is designed to be fairly configurable, so if the existing customizations don’t work for you, we’d love to hear why via an issue on GitHub!

In the case that you want complete control though, the provided wrappers around map view are not that complex.

TODO: Docs on how to build your own navigation views + describe the current overlay layers.

The demo app is designed to be instructive in showing many available options, so be sure to look at that to build intuition.

Style

You can pass a style URL to any of the navigation map view constructors. You can vary this dynamically as your app theme changes (ex: in dark mode).

Camera

The camera supports two-way manipulation via SwiftUI bindings. TODO: more documentation

Adding map layers

You can add your own overlays too! The makeMapContent closure argument of the various map and navigation views enables you to add more layers. See the demo app for an example, where we add a little dot showing the raw location in addition to the puck, which snaps to the route line.

Replacing the route polyline overlay

Ferrostar includes a default route polyline overlay that is pre-styled. You can replace it using the navigationMapViewRouteOverlay() modifier. See RouteStyleLayer for the default polyline for reference.

            .navigationMapViewRouteOverlay { state in
                if let routeGeometry = state?.routeGeometry {
                    RouteStyleLayer(
                        polyline: MLNPolylineFeature(coordinates: routeGeometry.map(\.clLocationCoordinate2D)),
                        identifier: "route-polyline",
                        style: MyCustomRouteStyle()
                    )
                }
            }

Customizing the Map View's Content Inset

The content inset is used on the Ferrostar NavigationMapView (and MapLibre's SwiftUI DSL MapView) to control the padding around the center of the map. This is useful for moving the user's puck to the bottom of the screen and when landscape, to the trailing half.

Ferrostar includes the basic raw content inset modifier as well as some advanced landscape and portrait modifiers that take the view's actual height and apply a percentage. See NavigationMapViewContentInsetMode.

The content inset can be customized using the navigationMapViewContentInset(...) modifiers.

The content inset can accessed using the @Environment(\.navigationMapViewContentInsetConfiguration) environment property.

Customizing the Navigation Views

The batteries included UI has a lot of widgets, like zoom buttons and a status bar showing your ETA. We've also provided several ways to modify the appearance of these views.

Distance formatting

By default, banners and other UI elements involving distance will be formatted using an MKDistanceFormatter.

This should “just work” for most cases as it is aware of the device’s locale. However, you can customize the formatting by passing in any arbitrary Formatter. This can be your own specially configured MKDistanceFormatter or a custom subclass which formats things to your liking.

String formatters can be customized by providing a custom FormatterCollection using navigationFormatterCollection(_:) View modifier.

The formatter collection can be accessed using the @Environment(\.navigationFormatterCollection) environment property.

The InstructionsView is shipped with Ferrostar is the default banner view. It uses the public domain directions iconography from Mapbox in a standard layout.

This view is an excellent example of composability, and is comprised of several subviews. The units of the InstructionsView are controlled using the formatter settings you passed to the NavigationMapView (if you’re using it). If you’re not using the NavigationMapView, you can pass a formatter directly.

The OffRouteBannerView replaces the instructions view when the user is off route. This avoids providing incorrect instructions and should let the user know how to resolve the issue until a re-route is successful and the InstructionsView reappears.

The TripProgressView included in Ferrostar includes a basic layout showing the progress of the trip, formatted with the specified or default FormatterCollection.

The CurrentRoadNameView is a route colored label that specifies the name of the road the puck is currently on. It's styled to match the default Ferrostar route style.

All of these views can be replaced using their view modifier extensions.

    DynamicallyOrientingNavigationView(...)
        .navigationViewInstructionView { navigationState, isExpanded, sizeWhenNotExpanded in
            MyCustomTopCenterView(navigationState, isExpanded, sizeWhenNotExpanded)
        }
        .navigationViewOffRouteView { navigationState, size in
            MyCustomOffRouteView(navigationState, size)
        }
        .navigationViewProgressView { navigationState, onTapExit in
            MyCustomProgressView(navigationState, onTapExit)
        }
        .navigationViewCurrentRoadNameView { navigationState in
            MyCustomCurrentRoadNameView(navigationState)
        }

If you want to disable road names completely, you can return EmptyView() as shown above.

    DynamicallyOrientingNavigationView(...)
        .navigationCurrentRoadView { _ in
    		EmptyView()
    	}

These views can be accessed using the @Environment(\.navigationViewComponentsConfiguration) environment value. However, it's also likely if you were building a custom navigation view that you'd just build it with your own custom views.

Speed Limit Views

[!IMPORTANT] This is opt-in only.

If your route provider includes speed limits, you can use the navigationSpeedLimit view modifier extension.

    DynamicallyOrientingNavigationView(...)
        .navigationSpeedLimit(
            speedLimit: speedLimitMeasurement,
            speedLimitStyle: .viennaConvention // Vienna convention = most of the world; you can use .mutcdStyle for the US style
        )

The speed limit and speed limit style can be accessed using @Environment(\.speedLimitConfiguration)

Adding Views to the Inner Grid

The Inner Grid is a series of rectangle views that fill the area on the navigation view between the instructions bar and the progress view (or the right of the screen when landscape). This view allows easily adding widgets like buttons, labels, etc. Certain areas are already in use for included buttons and others are blocked like the center of the view.

    DynamicallyOrientingNavigationView(...)
        .navigationViewInnerGrid(
            topCenter: {
                MyCustomTopCenterView()
            },
            // ... Customize any or all of the others (topTrailing, midLeading, bottomLeading, bottomTrailing)
        )

The views can be accessed using @Environment(\.navigationInnerGridConfiguration). However, if you're building a custom NavigationView, you'd probably just use the InnerGridView directly.