Custom Views for the Apple SDK

Register custom SwiftUI views with the AirshipCustomViewManager to use them in Scenes. Scene control: iOS SDK 20.0+

To use Custom Views, you must first register the view’s name with the AirshipCustomViewManager. The name is referenced when adding the Custom View to a Scene.

The view manager will call through to the view builders registered for that view’s name and provide the properties, name, and some layout hints as arguments.

All Custom Views should be registered after takeOff.

Registering Custom Views

struct CustomViewProperties: Decodable {
    var text: String
}

AirshipCustomViewManager.shared.register(name: "custom_view") { args in
    let viewProperties: CustomViewProperties = if let decoded = try? args.properties?.decode() {
        decoded
    } else {
        CustomViewProperties(text: "fallback")
    }

    Text(viewProperties.text)
}

Example custom view

The following example shows a Custom View that renders an embedded map when called to render a Custom View named map.

In our example, we have properties that defines a single place field, which is the address of the location that the map should render.

Custom Map View

First, define the view and its properties:

import SwiftUI
import MapKit
import CoreLocation

struct CustomMapView: View {
    struct Args: Decodable {
        let place: String
    }

    let args: Args
    @State private var region: MKCoordinateRegion?
    @State private var pinCoordinate: CLLocationCoordinate2D?

    var body: some View {
        if let region = region, let pinCoordinate = pinCoordinate {
            Map(coordinateRegion: .constant(region), annotationItems: [MapPin(coordinate: pinCoordinate)]) { pin in
                MapMarker(coordinate: pin.coordinate, tint: Color.red)
            }
        } else {
            Text("Loading map...")
                .onAppear {
                    geocodePlace()
                }
        }
    }

    private func geocodePlace() {
        let geocoder = CLGeocoder()
        geocoder.geocodeAddressString(args.place) { placemarks, error in
            if let placemark = placemarks?.first, let location = placemark.location {
                let coordinate = location.coordinate
                self.region = MKCoordinateRegion(
                    center: coordinate,
                    span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
                )
                self.pinCoordinate = coordinate
            } else {
                print("Failed to geocode place: \(error?.localizedDescription ?? "Unknown error")")
            }
        }
    }
}

struct MapPin: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
}

struct CustomMapView_Previews: PreviewProvider {
    static var previews: some View {
        CustomMapView(args: CustomMapView.Args(place: "Eiffel Tower"))
    }
}

Then register the view after takeOff:

AirshipCustomViewManager.shared.register(name: "map", builder: { args in
    if let args: CustomMapView.Args = try? args.properties?.decode() {
        CustomMapView(args: args)
    }
})

Scene Control iOS SDK 20.0+

Custom views control their parent scene through the AirshipSceneController, which is automatically injected as an @EnvironmentObject.

Accessing SceneController

Declare the scene controller as an @EnvironmentObject property in your custom view.

Accessing SceneController

struct CustomMapView: View {
    @EnvironmentObject var sceneController: AirshipSceneController
    let args: Args

    var body: some View {
        // Your custom view content
    }
}

Dismissing scenes

Call dismiss() to close the scene, or set cancelFutureDisplays to prevent it from displaying again.

Dismissing scenes

struct CustomMapView: View {
    @EnvironmentObject var sceneController: AirshipSceneController

    var body: some View {
        VStack {
            // Map display code...

            Button("Close Map") {
                sceneController.dismiss()
            }

            Button("Got It") {
                sceneController.dismiss(cancelFutureDisplays: true)
            }
        }
    }
}

Pager navigation

Navigate between pages using the pager controller’s canGoBack and canGoNext properties.

Pager navigation

struct CustomMapView: View {
    @EnvironmentObject var sceneController: AirshipSceneController

    var body: some View {
        HStack {
            if sceneController.pager.canGoBack {
                Button("Back") {
                    sceneController.pager.navigate(request: .back)
                }
            }

            if sceneController.pager.canGoNext {
                Button("Next Location") {
                    sceneController.pager.navigate(request: .next)
                }
            }
        }
    }
}

Sizing management

Use args.sizeInfo to determine appropriate sizing for your custom view.

Sizing with SizeInfo

AirshipCustomViewManager.shared.register(name: "map") { args in
    if let args: CustomMapView.Args = try? args.properties?.decode() {
        CustomMapView(args: args)
            .frame(
                width: args.sizeInfo.isAutoWidth ? 350 : nil,
                height: args.sizeInfo.isAutoHeight ? 500 : nil
            )
    }
}

Map custom view with scene control

Embedding Airship Views

Airship views like Preference Center can be embedded as custom views.

Embedding Preference Center

// Full preference center with navigation bar
AirshipCustomViewManager.shared.register(name: "preference_center") { args in
    let id = args.properties?.object?["id"]?.string ?? "default"

    return PreferenceCenterView(preferenceCenterID: id)
        .frame(
            maxWidth: args.sizeInfo.isAutoWidth ? nil : .infinity,
            maxHeight: args.sizeInfo.isAutoHeight ? nil : .infinity
        )
}

// Content only (no navigation bar)
AirshipCustomViewManager.shared.register(name: "preference_center_content") { args in
    let id = args.properties?.object?["id"]?.string ?? "default"

    return PreferenceCenterContent(preferenceCenterID: id)
        .frame(
            maxWidth: args.sizeInfo.isAutoWidth ? nil : .infinity,
            maxHeight: args.sizeInfo.isAutoHeight ? nil : .infinity
        )
}

Embedded preference center