Custom Views for the Capacitor Plugin
Register custom native views to use within Scenes.
Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship’s targeting and orchestration capabilities.
Requirements
To use Custom Views in Capacitor, you must extend the native Airship modules using the AirshipPluginExtender. See Extending Airship for setup instructions.
Registering Custom Views
Custom Views must be registered on each native platform separately:
iOS
See the Apple Custom Views documentation for detailed implementation instructions.
Custom Views should be registered after takeOff in your native iOS code:
import AirshipKit
@objc(AirshipExtender)
class AirshipExtender: NSObject, AirshipPluginExtenderDelegate {
func onAirshipReady() {
// Register custom views
AirshipCustomViewManager.shared.register(name: "my-custom-view") { args in
// Return your SwiftUI view
MyCustomView(args: args)
}
}
}Android
See the Android Custom Views documentation for detailed implementation instructions.
Custom Views should be registered during the onAirshipReady callback in your native Android code:
import com.urbanairship.AirshipCustomViewManager
class AirshipExtender : AirshipPluginExtender {
override fun onAirshipReady(context: Context) {
// Register custom views
AirshipCustomViewManager.register("my-custom-view") { context, args ->
// Return your Android View
MyCustomView(context, args)
}
}
}Using Custom Views
Once registered, Custom Views can be added to Scenes in the Airship dashboard:
- Create or edit a Scene
- Add the Custom View content element to a screen
- Enter the view name (e.g.,
my-custom-view) that matches the name you registered in your native code - Optionally add key-value pairs to pass custom properties to the view
The native view will be displayed within the Scene with the properties you configured.
Example: Embedding Capacitor Web Views
A powerful use case for Custom Views in Capacitor is embedding your Capacitor web app (or a subset of it) directly within a Scene. This allows you to display specific routes or pages from your web app inside an Airship Scene.
iOS Implementation
import Foundation
import AirshipFrameworkProxy
import Capacitor
import AirshipCore
import SwiftUI
import UIKit
@objc(AirshipPluginExtender)
public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol {
@MainActor
public static func onAirshipReady() {
AirshipCustomViewManager.shared.register(name: "capacitor") { args in
CapView(
startPath: args.properties?.object?["path"]?.string
)
.edgesIgnoringSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct CapView: UIViewControllerRepresentable {
let startPath: String?
func makeUIViewController(context: Context) -> CustomCapViewController {
let controller = CustomCapViewController()
controller.startPath = startPath
return controller
}
func updateUIViewController(_ uiViewController: CustomCapViewController, context: Context) {
// Handle updates (if necessary)
}
class CustomCapViewController: CAPBridgeViewController {
var startPath: String? = nil
override func instanceDescriptor() -> InstanceDescriptor {
let descriptor = super.instanceDescriptor()
descriptor.appStartPath = startPath
return descriptor
}
}
}Android Implementation
First, create a layout file at res/layout/custom_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.getcapacitor.CapacitorWebView
android:id="@+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</androidx.core.widget.NestedScrollView>Then, register the custom view in your AirshipExtender.kt:
package com.example.plugin
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnAttach
import androidx.fragment.app.Fragment
import com.getcapacitor.Bridge
import com.getcapacitor.CapConfig
import com.getcapacitor.Logger
import com.getcapacitor.PluginLoadException
import com.getcapacitor.PluginManager
import com.urbanairship.UAirship
import com.urbanairship.android.framework.proxy.AirshipPluginExtender
import com.urbanairship.android.layout.AirshipCustomViewArguments
import com.urbanairship.android.layout.AirshipCustomViewHandler
import com.urbanairship.android.layout.AirshipCustomViewManager
import com.urbanairship.json.optionalField
@Keep
class AirshipExtender: AirshipPluginExtender {
override fun onAirshipReady(context: Context, airship: UAirship) {
AirshipCustomViewManager.register("capacitor", CustomCapViewHandler())
}
}
class CustomCapViewHandler: AirshipCustomViewHandler {
override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View {
return CapView(context).also {
it.startPath = args.properties.optionalField<String>("path")
}
}
}
class CapView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
var startPath: String? = null
init {
id = View.generateViewId()
val fm = (context as AppCompatActivity).supportFragmentManager
fm.findFragmentById(id)?.let {
fm.beginTransaction()
.remove(it)
.commit()
}
doOnAttach {
findViewById<CapView>(id)?.let {
fm.beginTransaction()
.setReorderingAllowed(true)
.add(id, CapFragment.newInstance(startPath))
.commitNowAllowingStateLoss()
}
}
}
}
class CapFragment: Fragment() {
private var bridge: Bridge? = null
private val path: String? by lazy { arguments?.getString(ARG_PATH) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.custom_view, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
load(savedInstanceState)
}
override fun onStart() {
super.onStart()
bridge?.onStart()
}
override fun onResume() {
super.onResume()
bridge?.app?.fireStatusChange(true)
bridge?.onResume()
}
override fun onPause() {
super.onPause()
bridge?.app?.fireStatusChange(false)
bridge?.onPause()
}
override fun onStop() {
super.onStop()
bridge?.onStop()
}
override fun onDestroy() {
super.onDestroy()
bridge?.onDestroy()
bridge?.onDetachedFromWindow()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
bridge?.onConfigurationChanged(newConfig)
}
private fun load(savedInstanceState: Bundle?) {
if (bridge == null) {
bridge = Bridge.Builder(this)
.apply {
try {
val loader = PluginManager(requireActivity().assets)
addPlugins(loader.loadPluginClasses())
} catch (ex: PluginLoadException) {
Logger.error("Error loading plugins.", ex)
}
}
.setConfig(
CapConfig.Builder(context)
.setStartPath(this.path)
.create()
)
.setInstanceState(savedInstanceState)
.create()
}
}
companion object {
const val ARG_PATH: String = "path"
@JvmStatic
@JvmOverloads
fun newInstance(path: String? = null): CapFragment = CapFragment().apply {
arguments = Bundle().apply {
path?.let { putString(ARG_PATH, it) }
}
}
}
}Usage in Scenes
When creating a Scene in the Airship dashboard:
- Add a Custom View content element
- Set the view name to
capacitor - Optionally add a
pathproperty to specify which route in your Capacitor app to display (e.g.,/products,/settings)
Sizing limitation: The custom view must use hard-coded sizing (percentages or pixels). Auto-sizing is not currently supported for embedded Capacitor views.
This implementation creates a fully functional Capacitor web view within the Scene, complete with all your Capacitor plugins and routing capabilities. The optional path property allows you to display specific routes or pages from your web app.
Categories