# Apple Integrate the Apple Airship SDK into your mobile applications for iOS, tvOS, and visionOS. # Deep Links > Configure deep link handling for Airship messaging. Deep linking allows Airship messaging to open your app to specific resources or screens. When a user interacts with a message (notification, in-app message, etc.), the deep link can navigate them directly to the relevant content in your app. ## Listening for deep links The SDK provides a way to listen for deep links so you can handle them in your app. This handler receives all deep links except for Message Center and Preference Center display requests, which are handled automatically by their respective features. > **Note:** For Message Center and Preference Center display requests, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#handling-display-requests) and [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/#handling-display-requests). #### Swift Set the deep link listener [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/): ```swift // Set deep link callback Airship.onDeepLink = { url in // Handle deep link asynchronously await someNavigationTask(url) } ``` #### Objective-C Conform to `UADeepLinkDelegate` and set the delegate [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/): ```objc @interface AppDelegate() @end @implementation AppDelegate // ... in didFinishLaunchingWithOptions, after takeOff ... UAirship.deepLinkDelegate = self; - (void)receivedDeepLink:(NSURL *_Nonnull)deepLink completionHandler:(void (^_Nonnull)(void))completionHandler { // Handle deep link completionHandler(); } @end ``` # Actions > Airship Actions provide a convenient way to automatically perform tasks by name in response to push notifications, Message Center App Page interactions, and JavaScript. An action describes a function, which takes an optional argument and performs a predefined task, producing an optional result. Actions may restrict or vary the work they perform depending on the arguments they receive, which may include type introspection and runtime context. The Airship SDK includes built-in actions for common tasks, and you can create custom actions to extend functionality. In iOS, actions are sent as part of the notification payload as top-level key values, where the key is the action name and the value is the action's argument (any valid JSON value). For a complete list of available built-in actions, see the [Actions User Guide](https://www.airship.com/docs/guides/messaging/messages/actions/). ## Action Situations Actions are triggered with extra context in the form of a Situation. The different situations allows actions to determine if they should run or not, and possibly do different behavior depending on the situation. | Description | iOS | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| | Action was invoked manually. | [manualInvocation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/manualinvocation) | | Action was invoked from a launched push notification. | [launchedFromPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/launchedfrompush) | | Action was invoked from a received push notification in the foreground. | [foregroundPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/foregroundpush) | | Action was invoked from a received push notification in the background. | [backgroundPush](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/backgroundpush) | | Action was invoked from JavaScript or a URL. | [webViewInvocation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/webviewinvocation) | | Action was invoked from a foreground interactive notification button. | [foregroundInteractiveButton](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/foregroundinteractivebutton) | | Action was invoked from a background interactive notification button. | [backgroundInteractiveButton](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/backgroundinteractivebutton) | | Action was invoked from automation. | [automation](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/actionsituation/automation) | ## Action Registry The action registry is the central place to register actions by name. Each entry in the registry contains an action, the names that the action is registered under, a predicate that allows filtering when an action should run, and allows specifying alternative actions for different situations. #### Swift ```swift Airship.actionRegistry.registerEntry( names: ["action_name", "action_alias"], ) { return ActionEntry(action: action) } ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. #### Swift ```swift let entry = Airship.actionRegistry.entry(name: "action_name") ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. #### Swift ```swift // Predicate that only allows the action to run if it was launched from a push let predicate: @Sendable (ActionArguments) async -> Bool = { args in return args.situation == .launchedFromPush } // Update the predicate Airship.actionRegistry.updateEntry(name: "action_name", predicate: predicate) ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. ## Triggering Actions In addition to triggering an action from a message, they can be programmatically triggered as well. #### Swift ```swift let result = await ActionRunner.run( actionName: "action_name", arguments: ActionArguments( string: "some value", situation: .manualInvocation ) ) // Run an action directly let result = await ActionRunner.run( action: action, arguments: ActionArguments( string: "some value", situation: .manualInvocation ) ) ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. ## Custom Actions The action framework supports any custom actions. Create an action by extending the `Action` protocol on iOS. iOS also allows actions to be defined using blocks. After `takeoff`, register the action. The action can be triggered the same way as built-in actions. #### Swift ```swift let customAction = BlockAction { args in print("Action is performing with args: \(args)") return nil } Airship.actionRegistry.registerEntry(names: ["custom_action"]) { return ActionEntry(action: customAction) } ``` #### Objective-C > **Note:** Actions are not supported in Objective-C. # Feature Flags > {{< glossary_definition "feature_flag" >}} ## Accessing flags The Airship SDK will refresh feature flags when the app is brought to the foreground. If a feature flag is accessed before the foreground refresh completes, or after the foreground refresh has failed, feature flags will be refreshed during flag access. Feature flags will only be updated once per session and will persist for the duration of each session. Once [defined in the dashboard](https://www.airship.com/docs/guides/experimentation/feature-flags/#create-feature-flags), a feature flag can be accessed by its name in the SDK after `takeOff`. The SDK provides asynchronous access to feature flags using an async method, which are intended to be called from a Task or a function that supports concurrency. For more information, see [Concurrency guide](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). ```swift // Get the FeatureFlag let flag: FeatureFlag = try? await Airship.featureFlagManager.flag(name: "YOUR_FLAG_NAME") // Check if the app is eligible or not if (flag?.isEligible == true) { // Do something with the flag } else { // Disable feature or use default behavior } ``` > **Note:** This feature is not supported in Objective-C. ## Tracking interaction To generate the [Feature Flag Interaction Event](https://www.airship.com/docs/developer/rest-api/connect/schemas/events/#feature-flag-interaction), you must manually call `trackInteraction` with the feature flag. Analytics must be enabled. See: [Data Collection: Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). ```swift Airship.featureFlagManager.trackInteraction(flag: featureFlag) ``` ## Error handling If a feature flag allows evaluation with stale data, the SDK evaluates the flag if a definition for the flag is found. Otherwise, feature flag evaluation depends on updated local state. If the SDK cannot evaluate a flag because data cannot be fetched, the SDK returns or raises an error. The app can either treat the error as the flag being ineligible or retry at a later time. ```swift do { let flag = try await Airship.featureFlagManager.flag(name: "YOUR_FLAG_NAME") if (flag.isEligible == true) { // Do something with the flag } } catch { // Do something with the error } ``` # Live Activities > Integrate Live Activities into your iOS app to display real-time updates on the Lock Screen and Dynamic Island. {{< badge "axp" >}} For the push API method, see the [iOS Live Activities](https://www.airship.com/docs/guides/messaging/features/ios-live-activities/) messaging guide. See also the [iOS Live Activities](https://www.airship.com/docs/guides/features/messaging/live-activities-updates/) feature guide. ## App Setup To support Live Activities, you must call `restoreLiveActivityTracking` *once* after `takeOff` with all the Live Activity types that you might track with Airship. This allows Airship to resume tracking any previously tracked activities across app inits and to automatically track the `pushToStartToken` that allows starting activities through a push notification. #### Swift ```swift Airship.takeOff(config, launchOptions: launchOptions) Airship.channel.restoreLiveActivityTracking { restorer in await restorer.restore(forType: Activity.self) await restorer.restore(forType: Activity.self) } ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. After the `restore` call above, Airship will track the `pushToStartTokens` for the activity's attribute types. You can then start a Live Activity through a push notification. Starting a Live Activity does not automatically track it. Instead, the app will be woken up and you must call through to Airship with the activity instance and the name. ### Watching for Live Activities There is no entry point into the app when it is started for a Live Activity being created. Instead, you need to query Live Activities on init and when a `pushToStartToken` update is received to track them through Airship. Airship provides an extension `Activity.airshipWatchActivities(activityBlock:)` that can be used to do this for you. In this example, we assume the `gameID` on our `SportsActivityAttributes` will be used to send updates through Airship after it is created: #### Swift ```swift Airship.channel.restoreLiveActivityTracking { restorer in await restorer.restore(forType: Activity.self) } Activity.airshipWatchActivities { activity in Airship.channel.trackLiveActivity(activity, name: activity.attributes.gameID) } ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Starting Live Activities To start a Live Activity from the app, make sure to set the `pushType` to `.token`. After it is started, immediately track it with `Airship.channel.trackLiveActivity(_:name:)`. #### Swift ```swift let activity = try Activity.request( attributes: attributes, content: content, pushType: .token ) Airship.channel.trackLiveActivity( activity, name: attributes.gameID ) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Updating Live Activities To update a Live Activity, use the standard ActivityKit APIs. First find the Activity instance then call `update` on it: #### Swift ```swift guard let activity = Activity.activities.first(where: { $0.id == "sports-game-123" }) else { // not found return } activity.update(contentUpdate) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. ## Ending Live Activities To end a Live Activity, use the standard ActivityKit APIs. First find the Activity instance then call `end` on it with a dismissal policy: #### Swift ```swift guard let activity = Activity.activities.first(where: { $0.id == "sports-game-123" }) else { // not found return } activity.end(contentUpdate, dismissalPolicy: .default) ``` #### Objective-C > **Note:** Live Activities are not supported in Objective-C. Use Swift for Live Activity implementation. # Analytics > Track user engagement and app performance with Airship analytics, including custom events, screen tracking, and associated identifiers. Analytics allows you to track user engagement and app performance through custom events, screen tracking, and associated identifiers. For information about controlling what data Airship collects, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). > **Note:** Analytics events are batched and uploaded asynchronously in the background to minimize battery impact. The database size is fixed, so events are safely stored even when offline. Events may not upload immediately and may wait until the next app initialization if the app is closed before the upload completes. ## Custom Events Track user activities and key conversions with [Custom Events](https://www.airship.com/docs/reference/glossary/#custom_event). They require enabling analytics for your app. #### Swift ```swift var event = CustomEvent(name: "event_name", value: 123.12) try event.setProperties( [ "my_custom_property": "some custom value", "is_neat": true, "any_json": [ "foo": "bar" ] ] ) event.track() ``` #### Objective-C ```objc UACustomEvent *event = [[UACustomEvent alloc] initWithName:@"event_name" value:123.12]; [event setProperties: @{ @"my_custom_property": @"some custom value", @"is_neat": @YES, @"any_json": @{ @"foo": @"bar" } } error:&error]; [event track]; ``` ### Templates Custom Event Templates are a wrapper for Custom Events and are available for iOS, [Android](https://www.airship.com/docs/developer/sdk-integration/android/analytics/#templates), and [Web](https://www.airship.com/docs/developer/sdk-integration/web/analytics-and-reporting/#templates). See also [CustomEvent](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/customevent) in the iOS SDK library. #### Account Use this template to create Custom Events for account-related events. The template is written with account registration as the example. #### Swift Track a registered account event: ```swift let acctEvent = CustomEvent(accountTemplate: .registered) acctEvent.track() ``` With optional properties: ```swift var acctEvent = CustomEvent( accountTemplate: .registered, properties: CustomEvent.AccountProperties( category: "Premium", isLTV: true ) ) acctEvent.eventValue = 9.99 acctEvent.transactionID = "12345" acctEvent.track() ``` #### Media Use this template to create Custom Events for media-related events, including consuming, browsing, starring, and sharing content. #### Swift Track a consumed content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .consumed) mediaEvent.track() ``` With an optional value: ```swift var mediaEvent = CustomEvent( mediaTemplate: .consumed, properties: CustomEvent.MediaProperties(isLTV: true) ) mediaEvent.eventValue = 1.99 mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .consumed, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true, isLTV: true ) ) mediaEvent.eventValue = 2.99 mediaEvent.track() ``` #### Swift Track a starred content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .starred) mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .starred, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.eventValue = 2.99 mediaEvent.track() ``` #### Swift Track a browsed content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .browsed) mediaEvent.track() ``` With optional properties: ```swift let mediaEvent = CustomEvent( mediaTemplate: .browsed, properties: CustomEvent.MediaProperties( id: "12322", category: "entertainment", type: "video", eventDescription: "Browsed latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.track() ``` #### Swift Track a shared content event: ```swift let mediaEvent = CustomEvent(mediaTemplate: .shared) mediaEvent.track() ``` With a source and medium: ```swift let mediaEvent = CustomEvent( mediaTemplate: .shared(source: "facebook", medium: "social") ) mediaEvent.track() ``` With optional properties: ```swift var mediaEvent = CustomEvent( mediaTemplate: .shared(source: "facebook", medium: "social"), properties: CustomEvent.MediaProperties( id: "1234", category: "entertainment", type: "video", eventDescription: "Watching latest entertainment news.", author: "UA Enterprises", isFeature: true ) ) mediaEvent.track() ``` #### Retail Use this template to create Custom Events for retail-related events, including browsing a product, adding an item to a cart, purchasing an item, starring a product, and sharing a product. #### Swift Track a purchased event: ```swift let retailEvent = CustomEvent(retailTemplate: .purchased) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .purchased, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", isLTV: true, brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a browsed event: ```swift let retailEvent = CustomEvent(retailTemplate: .browsed) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .browsed, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track an added-to-cart event: ```swift let retailEvent = CustomEvent(retailTemplate: .addedToCart) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .addedToCart, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a starred product event: ```swift let retailEvent = CustomEvent(retailTemplate: .starred) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .starred, properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.eventValue = 99.99 retailEvent.transactionID = "13579" retailEvent.track() ``` #### Swift Track a shared product event: ```swift let retailEvent = CustomEvent(retailTemplate: .shared()) retailEvent.track() ``` With a source and medium: ```swift let retailEvent = CustomEvent( retailTemplate: .shared(source: "facebook", medium: "social") ) retailEvent.track() ``` With optional properties: ```swift var retailEvent = CustomEvent( retailTemplate: .shared(source: "facebook", medium: "social"), properties: CustomEvent.RetailProperties( id: "1234", category: "mens shoe", eventDescription: "Low top", brand: "SpecialBrand", isNewItem: true ) ) retailEvent.transactionID = "13579" retailEvent.track() ``` ## Associated Identifiers Associated identifiers (also called custom identifiers) associate an external identifier with a [Channel ID](https://www.airship.com/docs/reference/glossary/#channel_id). They are visible in [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds). We recommend adding any IDs that you may want to be visible in your event stream. You can assign up to 20 associated identifiers to a device. Unlike other identifiers (e.g., tags), you cannot use associated identifiers to target your users. #### Swift ```swift let identifiers = Airship.analytics.currentAssociatedDeviceIdentifiers() identifiers.set(identifier: "value", key:"key") Airship.analytics.associateDeviceIdentifiers(identifiers) ``` #### Objective-C ```objc UAAssociatedIdentifiers *identifiers = [UAirship.analytics currentAssociatedDeviceIdentifiers]; [identifiers setIdentifier:@"value" forKey:@"key"]; [UAirship.analytics associateDeviceIdentifiers:identifiers]; ``` ## Screen Tracking The Airship SDK gives you the ability to track which screens a user views within the application, how long a user stayed on each screen, and also includes the user's previous screen. These events then come through [Real-Time Data Streaming](https://www.airship.com/docs/reference/glossary/#rtds), allowing you to see the path a user took through the application, or trigger actions based on a user visiting a particular area of the application. #### Swift ```swift Airship.analytics.trackScreen("MainScreen") ``` #### Objective-C ```objc [UAirship.analytics trackScreen:@"MainScreen"]; ``` # Apple SDK Changelog > The latest updates to the Airship iOS SDK. See the [SDK Support Policy](https://www.airship.com/docs/reference/sdk-support-policy/) for version coverage and maintenance windows. ## 20.7.0 April 30, 2026 Minor release that adds support for Native Message Center and fixes Firebase coexistence for reliable push handling. ### Changes - Added support for rendering Native Content in Message Center - Fixed Firebase coexistence to ensure reliable push handling ## 20.6.3 April 24, 2026 Patch release with Scenes reliability fixes and a VoiceOver accessibility fix for paged scenes. ### Changes - Fixed form submit ordering so the form status is marked submitted before the onSubmit callback runs - Fixed pager summary events to include the correct layout context on dismiss and reflect pager completion - Fixed VoiceOver so dismiss and navigation buttons remain reachable on paged scenes ## 19.11.8 April 21, 2026 Patch release that fixes Xcode 26.4 build issues with whole module optimization. ### Changes - Fixed Xcode 26.4 build issues with whole module optimization. ## 20.6.2 April 2, 2026 Patch release that improves border rendering in Scenes. ### Changes - Support per-corner border for shapes drawn in Scenes ## 19.11.7 March 31, 2026 Patch release that improves pager navigation reliability in Scenes. ### Changes - Improved pager navigation reliability by fixing a race condition that could cause desync when swiping rapidly during scroll. ## 20.6.0 March 21, 2026 Minor release that adds Objective-C wrappers for the Message Center native bridge to support .NET bindings, adds subscript and superscript text support in Scenes, and improves Message Center reliability. ### Changes - Added `UAMessageCenterNativeBridge` and `UAMessageCenterNativeBridgeDelegate` to support .NET MAUI bindings for Message Center web view deep link handling. - Added subscript and superscript text support in Scenes. - Fixed Message Center mark-as-read not updating the list UI. - Improved Message Center content type handling. - Improved Message Center behavior when a message is unavailable or deleted. ## 20.5.0 March 12, 2026 Minor release that improves video playback and improves pager navigation reliability in Scenes. ### Changes - Improved pager navigation reliability by fixing a race condition that could cause desync when swiping rapidly during scroll. - Improved NPS score selector tap targets to remain consistent when toggling between selected and unselected states. - Improved video playback lifecycle handling to prevent potential memory leaks on dismiss. - Improved In-App Automation schedule parsing to retry failed schedules when remote data is updated. - Removed remaining Objective-C from AirshipBasement. - Replaced Objective-C based swizzling with a more reliable Swift implementation. - Fixed video aspect ratio to avoid cropping content when using center-inside display mode. ## 20.4.0 February 25, 2026 Minor release that adds support for Native Message Center. Native content type requires displaying the message content in a `MessageCenterMessageView`. Apps that do not use Airship's message views (e.g. using a WebView directly) should filter out messages where `message.contentType` is not `.html`. ### Changes - Added support for Native Message Center. - Removed gzip encoding using internal UACompression class and `libz` dependency from AirshipBasement. - Added `ExpressibleBy` protocol conformances to `AirshipJSON` allowing initialization with literals. - Added `UAEmbeddedViewControllerFactory` to the `AirshipObjectiveC` module for embedding `AirshipEmbeddedView` in Objective-C applications. - Improved accessibility for single choice and multiple choice questions in Scenes. ## 20.3.2 February 24, 2026 Patch release that fixes Message Center unread indicator behavior, improves spinner fallback on older iOS versions, and resolves visionOS availability checks. ### Changes - Fixed Message Center unread indicator rendering so the unread indicator is only shown for unread messages. - Fixed spinner behavior on iOS versions earlier than 18 by adding a rotating fallback icon. - Fixed visionOS compilation and availability handling for newer iOS and visionOS APIs. ## 20.3.1 February 19, 2026 Patch release that fixes an Xcode 26.4 beta compilation issue and improves Scene stability. ### Changes - Fixed an Xcode 26.4 beta compilation issue. - Improved Scene stability by adding guards for non-finite layout values used in SwiftUI frame, offset, and position calculations. - Added additional Scene layout safety checks for container, pager, story indicator, video controls, and wrapping layout views. - Improved pager timer progression by safely handling zero and invalid page delays. ## 19.11.6 February 18, 2026 Patch release that fixes an Xcode 26.4 beta compilation issue and improves Scene stability. ### Changes - Fixed an Xcode 26.4 beta compilation issue. - Improved Scene stability by adding guards for non-finite layout values used in SwiftUI frame, offset, and position calculations. - Added additional Scene layout safety checks for container, pager, story indicator, video controls, and wrapping layout views. - Improved pager timer progression by safely handling zero and invalid page delays. ## 20.3.0 January 30, 2026 Minor release that adds Objective-C wrapper for deep link processing and fixes a Message Center migration issue when upgrading from 17.x or older to 20.x. ### Changes - Added `UAirship.processDeepLink(_:completionHandler:)` Objective-C wrapper for programmatic deep link handling. - Fixed a Message Center migration issue when upgrading from 17.x or older to 20.x ## 20.2.0 January 30, 2026 Minor release that adds Objective-C wrappers for .NET bindings and improves bundle resource lookup for SPM and Tuist projects. ### Changes - Added Objective-C wrappers for permissions manager and push notification status APIs to support .NET bindings. - Improved bundle resource lookup to better support Swift Package Manager and Tuist generated projects. - Fixed page tabbing, radio buttons, and checkbox accessibility in Scenes. - Improved banner IAA message accessibility. ## 20.1.1 January 16, 2026 Patch release that improves VoiceOver focus control and sizing for the progress bar indicator in Story views. ### Changes - Added support for sizing inactive segments in Story view progress indicators. - Improved VoiceOver focus handling for Message Center Web content. ## 20.1.0 January 10, 2026 Minor release that includes several fixes and improvements for Scenes, In-App Automations, and Message Center. ### Changes - Added support for Story pause/resume and back/next controls. - Added Scenes content support in Message Center. - Added support for additional text styles in Scenes. - In-App Automations and Scenes that were not available during app launch can now be triggered by events that happened in the previous 30 seconds. - Fixed pinned container background image scaling when the keyboard is visible in Scenes. - Fixed banner presentation and layout in Scenes. - Fixed progress icon rendering in Scenes. - Fixed container layout alignment in Scenes. ## 20.0.3 December 18, 2025 Patch release that fixes keyboard safe area with Scenes. ### Changes - Fixed safe area handling during keyboard presentation in Scenes. ## 19.11.5 December 18, 2025 Patch release that fixes keyboard safe area with Scenes. ### Changes - Fixed safe area handling during keyboard presentation in Scenes. ## 20.0.2 November 25, 2025 Patch release that fixes an issue with delayed video playback in Scenes when initially loading or paging and addresses a direct open attribution race condition which could cause direct open events to be missed in some edge cases. ### Changes - Fixed an issue where the video ready callback was not assigned before observers were set up, causing the pager to miss the ready signal and advance before they loaded completely. - Fixed a potential race condition that could result in missed direct open attributions by ensuring notification response handling completes synchronously before the app becomes active. ## 19.11.4 November 25, 2025 Patch release that fixes an issue with delayed video playback in Scenes when initially loading or paging. Applications that use videos in Scenes must update to resolve the playback delays. ### Changes - Fixed an issue where the video ready callback was not assigned before observers were set up, causing the pager to miss the ready signal and advance before the loaded completely. ## 19.11.3 November 19, 2025 Patch release that further addresses the direct open attribution race condition introduced in 19.0.0. While 19.11.0 attempted to fix this issue by introducing a synchronous completion handler method, the implementation still executed work asynchronously, which could cause direct open events to be missed in some edge cases. ### Changes - Fixed a potential race condition that could result in missed direct open attributions by ensuring notification response handling completes synchronously before the app becomes active. ## 19.11.2 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.1 November 14, 2025 Patch release that fixes YouTube video playback in In-App Automation and Scenes. Applications that use YouTube videos in Scenes and non-html In-App Automations (IAA) must update to resolve playback errors. ### Changes - Fixed looping behavior in video views within Scenes. - Fixed Message Center icon display when icons are enabled. - Fixed pager indicator accessibility to prevent duplicate VoiceOver announcements. - Added dismiss action to banner in-app messages for improved VoiceOver accessibility. - Fixed YouTube video embedding to comply with YouTube API Client identification requirements. ## 20.0.0 October 9, 2025 Major SDK release with several breaking changes. See the [Migration Guide](https://github.com/urbanairship/ios-library/blob/main/Documentation/Migration/migration-guide-19-20.md) for more info. ### Changes - Xcode 26+ is now required. - Updated minimum deployment target to iOS 16+. - Refactored Message Center and Preference Center UI to provide clearer separation between navigation and content views. See the migration guide for API changes. - Introduced modern, block-based and async APIs as alternatives to common delegate protocols (`PushNotificationDelegate`, `DeepLinkDelegate`, etc.). The delegate pattern is still supported but will be deprecated in a future release. - Refactored core Airship components to use protocols instead of concrete classes, improving testability and modularity. See the migration guide for protocol renames and class-to-protocol conversions. - Added support for split view in the Message Center, improving the layout on larger devices. - Updated the Preference Center with a refreshed design and fixed UI issues on tvOS and visionOS. - Fixed Package.swift to remove macOS as a supported platform. - CustomViews within a Scene can now programmatically control their parent Scene, enabling more dynamic and interactive custom content. - Accessibility updates for Scenes. - New AirshipDebug package that exposes insights and debugging capabilities into the Airship SDK for development builds, providing enhanced visibility into SDK behavior and performance. - Removed automatic collection of connection_type and carrier device properties ## 19.11.1 October 7, 2025 Patch release addressing the longstanding Swift concurrency crash (GH-434) and improving the internal rate-limiting system for better stability and efficiency. ### Changes - Refactored WorkRateLimiter to improve efficiency and reliability, reduce memory overhead, and eliminate unnecessary temporary allocations. - Added stronger safeguards to WorkRateLimiter prevent rare edge-case crashes in rate-limiting logic. ## 19.11.0 October 1, 2025 This is an important update for apps using manual push notification integration (automaticSetup = false). We are addressing a lifecycle issue caused by Apple's async notification delegate being called on a background thread, unlike the main-thread-guaranteed completionHandler version. To align with the correct lifecycle, we are deprecating our async handler and introducing a new completionHandler method. Using the async version can cause direct open counts to be lower than expected. ### Changes - Added a new synchronous `AppIntegration.userNotificationCenter(_:didReceive:withCompletionHandler:)` method. Apps must use this and the corresponding synchronous delegate method to ensure notification responses are handled before the app becomes active. - The async `AppIntegration.userNotificationCenter(_:didReceive:)` method is now deprecated. - Landing pages no longer display for push notifications received when the app is in the foreground. ## 19.10.0 September 22, 2025 Minor release that adds a new flag to work around the critical crash (GH-434) affecting Swift 5 apps on Xcode 16.1+. The problematic feature is now disabled by default. ### Changes - Added `isDynamicBackgroundWaitTimeEnabled` flag. This defaults to `false` to avoid the crash. It is strongly recommended to keep this `false` for Swift 5 apps. Swift 6 apps can safely set this to `true` to restore previous behaviors. ## 19.9.2 September 16, 2025 Patch release that resolves a crash eminating from the Thomas video player, fixes a bug that causes Scenes to sometimes display after being stopped, and fixes some UI bugs exposed by iOS 26. ### Changes - Fixed refreshing out of date In-App Automations and Scenes before displaying. - Fixed KVO in ThomasVideoPlayer to use modern patterns and properly release observers. - Fixed Message Center title bar theming in iOS 26. - Improved tab bar UI in iOS 26. ## 19.9.0 September 4, 2025 Minor release that adds a new flag to HTML In-App message content to force full screen on all devices. ### Changes - Added `forceFullScreen` to HTML In-App message content ## 19.8.3 August 25, 2025 A patch release that includes a targeted fix for the ongoing Swift interoperability crashes outlined in GH-434. ### Changes - Updated the concurrency pattern in the background task scheduler, replacing a `for-await` loop with a `TaskGroup`. This change targets a suspected instability in Swift's concurrency runtime and is expected to mitigate the crashes seen in mixed Swift 5/6 environments. - Fixed a Scene issue where labels marked as H3 were being treated as H1. - Improved accessibility of embedded Scenes by announcing screen changes when an embedded view is displayed. ## 19.8.2 August 19, 2025 A patch release with bug fixes for video in scenes and Swift interoperability crashes. Users that have upgraded to SDK 19.6.0+ and display Youtube or Vimeo videos in Scenes or In-app Messages or are experiencing crashes like those outlined in GH-434 are encouraged to update. ### Changes - Fixed bug preventing proper rendering of youtube and vimeo videos in Scenes and In-app Messages. - Fixed Swift 5-6 interop issues causing crashes in Workers.calculateBackgroundWaitTime(maxTime:) outlined in github issue 434. ## 19.8.1 August 7, 2025 A patch release with improved Xcode 26 support, a fix for custom font scaling in scenes, and internal improvements to image loading. ### Changes - Fixed issues affecting custom font scaling. - Improved image loading support. - Fixed a compilation error caused by SwiftUICore import exposed by Xcode 26 beta 4. ## 19.8.0 July 24, 2025 A minor release with improvements to Scenes and a new `dismiss` command for the JS interface. ### Changes - Added support in Scenes for linking form inputs to a label for better accessibility. - Added container item alignment to Scenes to change the natural alignment within a container. - Added a new `dismiss` command to the JavaScript interface for parity with Android. The new `UAirship.dismiss()` method behaves the same as `UAirship.cancel()`. ## 19.7.0 July 18, 2025 A minor release that simplifies takeOff by deprecating methods with launchOptions, adds flexibility for initialization, and includes several bug fixes. ### Changes - Deprecated `Airship.takeOff` methods that include launchOptions. The takeOff method still needs to be called before `application(_:didFinishLaunchingWithOptions:)` finishes to ensure proper notification delegate is set up. - Updated `Airship.takeOff` to allow it to be called from `MainApp.init` before the application delegate is set, even with automatic setup enabled. - Fixed a stack overflow exception when using Scenes in the iOS 26 beta. - Added a potential workaround for reported crashes within `AirshipWorkManager` and `AirshipChannel`. - Fixed a race condition in Scene asset file operations and improved file management. ## 18.14.4 July 15, 2025 Patch release backported to ensure in-app views are still in the window hierarchy before updating constraints. ### Changes - Fixed crash caused by constraints updating when in-app view isn't in the view hierarchy. ## 19.6.1 June 24, 2025 Patch release with bug fixes for memory management, survey interactions, and accessibility improvements. ### Changes - Fixed a memory issue in `AirshipWorkManager` where temporary arrays were being created unnecessarily when calculating background wait times. - Fixed an issue where NPS survey score selection required double-tapping by properly restoring both the score value and index when loading from form state. - Fixed a potential crash when updating constraints for banner views that have been removed from the view hierarchy. - Improved VoiceOver accessibility by ensuring toggles, checkboxes, and radio inputs remain accessible even without explicit accessibility descriptions. - Added accessibility header traits to section titles in Message Center, Preference Center, and other UI components for better VoiceOver navigation. ## 19.6.0 June 17, 2025 A minor update with enhancements to Scenes and Message Center functionality and a bug fix for Automation. This version is required for Scene branching and phone number collection. ### Changes Automation: - Fixed version trigger predicate matching to properly evaluate app version conditions. Message Center: - Added support to automatically pick up UIKit navigation controller styling. Scenes: - Fixed layout issues with modal frames, specifically related to margins and borders. - Fixed several issues related to Scene branching. - Added support for custom corner radii on borders. - Added support for more flexible survey toggles. ## 19.5.0 May 23, 2025 Minor release focused on performance improvements for Scenes. ### Improvements - Improved load times for Scenes by prefetching assets concurrently. ## 19.4.0 May 15, 2025 Minor release that adds support for using Feature Flags as an audience condition for other Feature Flags and Vimeo videos in Scenes. ### Changes - Added support for using Feature Flags as an audience condition for other Feature Flags. - Added support for Vimeo videos in Scenes. ## 19.3.2 May 8, 2025 Patch release that fixes Message Center listing not refreshing on push received. This issue was introduced in 19.0.0. Apps using Message Center should update. ### Changes - Fixed Message Center behavior on push received. ## 19.3.1 April 28, 2025 Patch release that fixes an issue in a branching scene where a button required two presses to navigate to the next page instead of one. Apps planning on using the upcoming branching feature should update. ### Changes - Fixed Scene button navigation with branching. ## 19.3.0 April 24, 2025 Minor release adding branching and SMS support for Scenes. ### Changes - Added support for branching in Scenes. - Added support for phone number collection and registration in Scenes. - Added `Airship.inAppAutomation.statusUpdates` to track rule update statuses for In-App Automation, Scenes, and Surveys. - Added `Airship.featureFlagManager.statusUpdates` to monitor rule update statuses. - Added support for setting JSON attributes for Channels and Contacts. - Added missing bindings for Obj-C. - Improved accessibility for Banner In-App messages and automations. - Added `TagActionMutation` stream to emit tag updates from `AddTagsAction` and `RemoveTagsAction`. ## 19.2.1 April 17, 2025 Resolved a regression introduced in 19.2.0 where channel audience updates and In-App experiences were unintentionally blocked when the Contact privacy manager flag was disabled. ### Changes - Fixed Channel operations and IAX being blocked when Contacts are disabled. ## 19.2.0 April 3, 2025 Minor release with Custom Views functionality allowing native SwiftUI views to be displayed in Scenes. ### Changes - Added Custom Views functionality allowing native SwiftUI views to be displayed in Scenes. ## 19.1.2 March 31, 2025 Patch release with bug fix for swipe gestures in Scenes. ### Changes - Fixed regression that caused horizontal swipe gestures to be disabled on some devices. ## 19.1.1 March 25, 2025 Patch release with bug fixes and minor improvements. ### Changes - Fixed a bug that allowed channel registration updates to proceed in certain cases when all features were disabled via the Privacy Manager. - Fixed a potential bug involving unecessary comparison checks in the layout system. ## 19.1.0 February 20, 2025 Minor release that adds support for email registration in Scenes, fixes bugs, and improves Airship configuration, Scene keyboard avoidance, and logging. ### Changes - Updated the keyboard avoidance for Scenes to use standard window insets - Added `resolveInProduction()` method on `AirshipConfig` to expose how Airship resolves the `inProduction` flag during takeOff - Added support for email registration in Scenes - Fixed regression with log level check that was introduced in 19.0.0 - Fixed voice over with NPS score for Surveys - Added logger to `UANotificationServiceExtension`. The logger can be configured by overriding the `airshipConfig` property. - Fixed Carthage build failures caused by UIKit Sample project ## 19.0.3 February 4, 2025 Patch release to fix a crash caused by combine subjects being updated from multiple queues. ### Changes - Fixed a crash caused by combine subjects being updated from multiple queues ## 19.0.2 February 3, 2025 Patch release to fix a crash caused by banner size changes during dismissal. ### Changes - Fixed crash caused by banner size changes during dismissal. ## 18.14.3 January 31, 2025 Patch release backported to fix landing page dismiss button default color on iOS and crash caused by banner size changes during dismissal. ### Changes - Updated landing page dismiss button default color on iOS to black. - Fixed crash caused by banner size changes during dismissal. ## 19.0.1 January 30, 2025 Patch release that fixes a crash when the device toggles airplane mode. Apps using 19.0.0 should update. ### Changes - Fixed crash in `WorkConditionsMonitor` when the device toggles airplane mode. - Added `@MainActor` to `RegistrationDelegate` protocol methods. - Updated default dismiss button color from white to black for landing pages to match Android. - Removed top padding on modal and full screen IAAs when using header_media_body and header_body_media without anything above the media. ## 19.0.0 January 16, 2025 Major SDK release with several breaking changes. see the [Migration Guide](https://github.com/urbanairship/ios-library/tree/main/Documentation/Migration/migration-guide-18-19.md) for more info. ### Changes - Xcode 16.2+ is now required. - Updated min versions to iOS 15+ & tvOS 18+. - Migrated all modules to Swift 6. - Objective-C support has been moved into AirshipObjectiveC framework freeing the SDK to expose Swift only APIs. - Updated several APIs to use structs instead of classes. - AppIntegration and PushNotificationDelegate expose async methods instead of completion handlers. - Airship.takeOff can now throw instead of silently failing for better error handling. - New CustomEvent template APIs. - Remove unused NotificationContent extension. - Fixed Scene animation when the device screen orientation changes with auto-height modals. - Added support for wrapping score views in Scenes. - Added support for Preference Center and Feature Flags to tvOS. - Added support for Feature Flag experimentation. ## 18.14.2 January 9, 2025 Patch release to fix extra spacing in a Banner In-App Automations if its missing the heading or body. ### Changes - Fix Banner In-App Automation extra spacing. ## 18.14.1 December 20, 2024 Patch release to fix Banner In-App Automations if the image is taller than the text. ### Changes - Fix Banner In-App Automation sizing issue. ## 18.14.0 December 19, 2024 Minor release that fixes issues with Banner In-App Automations, reduces power usage with In-App Automations & Scenes, and updates how Feature Flags are resolved. ### Changes - Added `resultCache` to `FeatureFlagManager`. This cache is managed by the app and can be optionally used when resolving a flag as a fallback if the flag fails to resolve or if the flag rule set does not exist. - FeatureFlag resolution will now resolve a rule set even if the listing is out of date. - Fixed issue with In-App Automation banners constraints, causing the banner to sometimes steal focus from the underlying app screen or not fully display. - Fixed issue with Surveys that require multi choice or single choice questions not blocking submission. - Reduced the CPU overhead with In-App Automations & Scene execution to reduce overall power usage. ## 18.13.0 December 5, 2024 Minor release that improves a11y support, updated Preference Center UI, and fixes several minor and improvements in Scenes and in-app message banners. ### Changes - Added support for email collection in Scenes - Updated Preference Center UI to use standard padding, titles, and colors to improve the look and feel across different platforms. - Added support to mark a label as a heading in Scenes. - Added support for auto-height modals in Scenes. - Fixed banner duration not dismissing the banner. - Fixed dismissal issues for banners with a height less than 100pts. - Fixed padding issue in bottom-placed in-app banners. ## 18.12.2 November 27, 2024 Patch release that resolves a minor memory-related bug and adds more useful logging around Feature Flag evaluation. ### Changes - Fixed minor memory-related bug that could result in a rare crash. - Improved logging around Feature Flag evaluation. ## 18.12.1 November 6, 2024 Patch release that resolves an issue with Firebase integrations in React Native and Flutter and an issue with opt-in checks when `requestAuthorizationToUseNotifications` is set to false. ### Changes - Fixed issues caused by swizzling conflicts with some Firebase framework integrations. - Fixed opt-in check permissions querying when `requestAuthorizationToUseNotifications` is set to false. ## 18.12.0 November 1, 2024 Minor release with several enhancements to Scenes. ### Changes - Added box shadow support for modal Scenes - Added a new implementation of the Scene pager to lazily load pages on iOS 17+, reducing the overall memory while a Scene is displaying - Added new Scene layout to make anything clickable within a Scene - Added additional logging to deep link handling to make it obvious how the deep link is being processed - Updated border handling on Scenes. Borders are no longer overlaid to avoid issues with borders that are not fully opaque and button borders being overdrawn when tapped - Improved accessibility of scene story indicator. Indicator has been updated to make it obvious which page is active by reducing the height of the inactive pages. Previously this was conveyed only through color - Fixed center_crop scaling in a Scene when a dimension is `auto` but the image is unable to fully fit in the container - Fixed IAA banners drag to dismiss gesture when the gestures starts within a button ## 18.11.1 October 15, 2024 Patch release to avoid implicit unwrap when UINavigationBar appearance tintColor is unset. Applications that use the PreferenceCenter should update. ### Changes - Removes implicit unwrap of the UINavigationBar appearance tintColor. ## 18.11.0 October 12, 2024 Minor release with Message Center and Preference center theming bug fixes and improvements, and a bug fix for IAA videos. Applications that send IAA videos or theme the Message Center or Preference Center and should update. ### Changes - Improved Message Center theming with a focus on improving nagivation components. - Improved Preference Center theming with a focus on improving nagivation components. - Fixed an issue that prevented IAA videos from properly displaying. ## 18.10.0 October 3, 2024 Minor release with accessibility updates, Message Center theming improvements and several bug fixes. ### Changes - Fixed Message Center background color and back button theming. - Fixed tap events in Scenes being registered by their containers in some instances. - Improved accessibility support in Scenes, Message Center and Preference Center with paging actions, localized content descriptions and traits. - Added ability to theme Message Center with a custom style. - Updated webview backgrounds to be clear when displaying media. ## 18.9.2 September 23, 2024 Patch release to fix an issue with high energy usage for In-App Automations, Scenes, and Surveys that was introduced in 18.0.0. This issue is not very common but it can occur if the device is unable to connect to our backend to fetch an update to the In-App rules on the device after an SDK update or locale change. Application that are receiving high energy usage reports should update. ### Changes - Fixed high energy usage for In-App Automations, Scenes, and Surveys if remote-data fails to refresh. - Fixed requesting additional notification options if they change after the first prompt. ## 18.9.1 September 13, 2024 Patch release to fix Scene button not able to be tapped in some cases. #### Changes - Fix Scene buttons not able to be tapped if the last page of the scene contains a wide image background. ## 18.9.0 September 10, 2024 Minor release that introduces `fallback` parameter when requesting permission updates and the permission is denied. This release also contains a fix for a regression in 18.8.0 where Channel Registration would continuously update for channels that have upgraded from an earlier SDK versions. Applications using 18.8.0 should update. #### Changes - Added new method `Airship.permissionsManager.requestPermission(_:enableAirshipUsageOnGrant:fallback:)` and `Airship.push.enableUserPushNotifications(fallback:)` that allows you to specify a fallback behavior if the permission is already denied. - Fixed high CPU issues with embedded messages that define a percent based size. - Fixed Channel Registration bug that was introduced in 18.8. ## 18.8.0 September 6, 2024 Minor release with several enhancements to In-App Automation, Scenes, and Surveys. ### Changes - Added support to disable plain markdown (text markup) support in a Scene. - Added support to theme markdown links in a Scene. - Added execution window support to In-App Automation, Scenes, and Surveys. - Added `displayNotificationStatus` status to the `AirshipNotificationStatus` object to get the user notification permission status. - Added `Airship.permissionManager.statusUpdates(for:)` that returns an async stream of permission status updates. - Added `MessageCenter.shared.inbox.unreadCountUpdates` that returns an async stream of unread count updates. - Added `MessageCenter.shared.inbox.messageUpdates` that returns an async stream of message updates. - Updated handling of priority for In-App Automation, Scenes, and Surveys. Priority is now taken into consideration at each step of displaying a message instead of just sorting messages that are triggered at the same time. - Updated handling of long delays for In-App Automation, Scenes, and Surveys. Delays will now be preprocessed up to 30 seconds before it ends before the message is prepared. - Fixed Message Center theme loader when trying to theme the OOTB Message Center window. ## 18.7.2 August 10, 2024 Patch release that fixes in-app experience displays when resuming from a paused state. Apps that use in-app experiences are encouraged to update. ### Changes - Fixed Automation Engine updates when pause state changes. ## 18.7.1 August 1, 2024 Patch release that prevents In-App Automation, Scenes, and Surveys from being able to trigger off custom events or screen views when analytics is disabled. The actual event was not being tracked by Airship in these cases, just processed locally. ### Changes - Prevent screen view and custom events from being processed by automations when analytics is disabled. ## 18.7.0 July 30, 2024 Minor release that fixes some layout issues with images and videos in a Scene, accessibility improvements, and fixes a potential crash with JSON encoding/decoding due to using a JSONEncoder/JSONDecoder across threads. ### Changes - Fixed video & image scaling/cropping in scenes. - Removed reusing `JSONEncoder`/`JSONDecoder` across tasks. - Removed `@MainActor` requirement from `AirshipPush.authorizedNotificationSettings`. - Announce screen changes when banners In-App messages are displayed. - `MessageCenterController` is now optional when creating a `MessageCenterView`. ## 18.6.0 July 16, 2024 Minor release with some improvements to preference center, a fix for in-app message veritcal sizing, accessibility improvements and markdown support in scenes. ### Changes - Added warning message to preference center email entry field. - Updated preference center country_code. - Fixed bug preventing preference center channel management from fully opting-out registered channels. - Fixed padding bug preventing modal in-app messages from properly sizing to their content. - Added accessibility improvements. - Added markdown support to scenes. ## 18.5.0 July 1, 2024 Minor release that includes cert pinning and various fixes and improvements for Preference Center, In-app Messages and Embedded Content. ### Changes - Added ability to inject a custom certificate verification closure that applies to all API calls - Added width and height parameters to in-app dismiss button theming - Fixed bug that caused HTML in-app message backgrounds to default to clear instead of system background - Fixed extra payload parsing in in-app messages - Set default banner placement to bottom - Increased impression interval for embedded in-app views - Improved in-app banner view accessibility - Preference center contact channel listing is now refreshed on foreground and from background pushes ## 18.4.1 June 21, 2024 Patch release to fix a regression with In-App Automations, Scenes, and Surveys ignoring screen, version, and custom event triggers. Apps using those triggers that are on 18.4.0 should update. ### Changes - Fixed trigger regression for IAX introduced in 18.4.0. ## 18.4.0 June 14, 2024 Minor release that adds contact management support to the preference center, support for anonymous channels, per-message in-app message theming, message center customization and logging improvements. Apps that use the message center or stories should update to this version. ### Changes - Added support for anonymous channels - Added contact management support in preference centers - Added improved theme support and per message theming for in-app messages - Added public logging functions - Fixed bug in stories page indicator - Fixed message center list view background theming ## 18.3.1 May 27, 2024 Patch release with bug fix for message center customization. Apps that use the message center should update to this version. ### Changes - Fixed background color application in message center. ## 18.3.0 May 21, 2024 Minor release with updates to message center customization, a bug fix for story pager transition animation and a bug fix for in-app banner button rendering. ### Changes - Fixed in-app message banner button rendering. - Fixed story pager transition animation. - Added message center list and list container background color customization via new plist keys `messageListBackgroundColor`, `messageListBackgroundColorDark`, `messageListContainerBackgroundColor` and `messageListContainerBackgroundColorDark` ## 18.2.2 May 16, 2024 Patch release includes a fix for submission issues when building with XCFrameworks, a bug fix for emitting pager events from in-app pager views, and a bug fix for the in-app banner's default title and body alignment to match the dashboard preview. Apps using XCFrameworks should update. ### Changes - Fixed pager event emission from in-app pager views. - Fixed submission issue when building with XCFrameworks. - Fixed in-app banner title and body default alignment. [View Older Releases](https://github.com/urbanairship/ios-library/releases?q=created%3A%3C2024-05-15&expanded=true) # Apple SDK Resources > SDK modules, API references, and other resources for iOS, tvOS, and visionOS development. ## Platform Support {#platform-support} | Feature | iOS | tvOS | visionOS | |----------------------------------------|-----|-------|----------| | Push Notifications | ✅ | ✅ | ✅ | | Live Activities | ✅ | ❌ | ❌ | | In-App Experiences | ✅ | ✅ ¹ | ✅ | | Embedded Content | ✅ | ✅ | ✅ | | Message Center | ✅ | ❌ | ✅ | | Preference Center | ✅ | ✅ | ✅ | | Feature Flags | ✅ | ✅ | ✅ | | Analytics | ✅ | ✅ | ✅ | | Contacts | ✅ | ✅ | ✅ | | Tags, Attributes & Subscription Lists | ✅ | ✅ | ✅ | | Privacy Controls | ✅ | ✅ | ✅ | | SwiftUI Support | ✅ | ✅ | ✅ | ¹ **tvOS In-App Experiences:** Scenes, Banners, and non-HTML In-App Automations are supported. However, scheduled In-App Experiences will no longer display if the app's cache is wiped due to tvOS storage limitations. s ## API references - [AirshipCore](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore) - [AirshipMessageCenter](https://urbanairship.github.io/ios-library/v20/AirshipMessageCenter/documentation/airshipmessagecenter/) - [AirshipAutomation](https://urbanairship.github.io/ios-library/v20/AirshipAutomation/documentation/airshipautomation/) - [AirshipPreferenceCenter](https://urbanairship.github.io/ios-library/v20/AirshipPreferenceCenter/documentation/airshippreferencecenter/) - [AirshipFeatureFlags](https://urbanairship.github.io/ios-library/v20/AirshipFeatureFlags/documentation/airshipfeatureflags/) - [AirshipObjectiveC](https://urbanairship.github.io/ios-library/v20/AirshipObjectiveC/documentation/airshipobjectivec/) - [AirshipNotificationServiceExtensions](https://urbanairship.github.io/ios-library/v20/AirshipNotificationServiceExtension/documentation/airshipnotificationserviceextension/) ## Github Samples * [Sample Apps](https://github.com/urbanairship/apple-sample-apps) — includes tvOS ## Source * [Source](https://github.com/urbanairship/ios-library) ## License All Airship SDKs and frameworks are open sourced and licensed under Apache Software License 2.0. * [iOS license](https://github.com/urbanairship/ios-library/blob/main/LICENSE). ## SDK Installation Complete installation and configuration guides for the Airship SDK, including setup, advanced integration, logging, and locale configuration. # Install and Set Up the Apple SDK > Learn how to install the Airship SDK using SPM, CocoaPods, Carthage, or xcframeworks, and initialize the SDK in your iOS, tvOS, or visionOS applications. The Airship SDK is a modern, Swift 6-native SDK designed for Apple platforms. It provides type-safe, actor-isolated APIs with full Swift concurrency support that work seamlessly across iOS, tvOS, and visionOS — all from a single SDK. For a complete reference of feature support across iOS, tvOS, and visionOS, see [Platform Support](https://www.airship.com/docs/developer/sdk-integration/apple/resources/#platform-support). > **Tip:** If you use an AI coding assistant, you can connect it to Airship with Skills and an MCP server. See [Airship AI Tools](https://www.airship.com/docs/developer/ai-tools/ai-tools/). ## Requirements * Minimum iOS version: 16.0+ * Minimum tvOS version: 18.0+ * Minimum visionOS version: 1.0+ * Requires Xcode 26.0+ ## SDK installation The Airship SDK can be installed using SPM (Swift Package Manager), CocoaPods, Carthage, or xcframeworks. We recommend SPM for new projects. #### SPM 1. In your Xcode project, select your project in the Project Navigator. 2. Select your target, then go to the **Package Dependencies** tab. 3. Click the **+** button to add a package. 4. Enter the package URL: `https://github.com/urbanairship/ios-library` 5. Select the version rule (recommended: "Up to Next Major Version"). 6. Click **Add Package**. 7. Select the Airship package products you want to include in your app: **Available package products:** - `AirshipBasement` : Required by AirshipCore - `AirshipCore` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter` : Message center - `AirshipAutomation` : Automation and in-app messaging - `AirshipPreferenceCenter` : Preference Center - `AirshipFeatureFlags` : Feature Flags - `AirshipObjectiveC` : Objective-C Bindings - `AirshipDebug` : Debugging tools - `AirshipNotificationServiceExtension` : Service Extension framework (only for Notification Service Extension targets, not the app target)* 8. Click **Add Package**. 9. Import the modules in your code where needed. Import statements match the module names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` For more details, see Apple's guide on [adding package dependencies to your app](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). #### Cocoapods > **Note:** CocoaPods trunk is [moving to read-only mode in December 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/). Airship will continue to support CocoaPods as long as possible, but we recommend using SPM (Swift Package Manager) for new projects. Existing CocoaPods installations will continue to work after trunk becomes read-only. 1. Install CocoaPods if you haven't already: `$ gem install cocoapods` 2. Navigate to your project directory in Terminal. 3. Create a `Podfile` (if one doesn't exist): `$ pod init` 4. Open your `Podfile` and add the Airship pod. The `Airship` pod is modular and divided into subspecs: **Available subspecs:** - `Airship/Core` : Push messaging features including channels, tags, named user and default actions (required) - `Airship/MessageCenter` : Message center - `Airship/Automation` : Automation and in-app messaging - `Airship/PreferenceCenter` : Preference Center module - `Airship/FeatureFlags` : Feature Flags module - `Airship/ObjectiveC` : Objective-C bindings ```ruby target "" do pod 'Airship' end ``` **Or specify individual subspecs:** ```ruby target "" do pod 'Airship/Core' pod 'Airship/MessageCenter' pod 'Airship/Automation' pod 'Airship/FeatureFlags' end ``` **For tvOS projects, specify the platform:** ```ruby platform :tvos, '18.0' target "" do pod 'Airship' end ``` 5. Install the pods: `$ pod install` 6. **Important:** After running `pod install`, an Xcode workspace (`.xcworkspace`) file is generated. Always open the workspace file instead of the project file (`.xcodeproj`) when building your project. If you encounter issues, see the [CocoaPods troubleshooting guide](http://guides.cocoapods.org/using/troubleshooting.html). #### Carthage 1. Install Carthage if you haven't already. See the [Carthage installation guide](https://github.com/Carthage/Carthage#installing-carthage). 2. Verify `Enable Modules` and `Link Frameworks Automatically` are enabled in your project's Build Settings. 3. Follow Carthage's [adding frameworks to an application](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) instructions to add frameworks to your application. 4. Specify the Airship iOS SDK in your `Cartfile`: ```text github "urbanairship/ios-library" ``` 5. Build the frameworks: `$ carthage update` 6. Add the frameworks to your project. Airship is modular, so select only the frameworks you need: **Available frameworks:** - `AirshipBasement` : Required by AirshipCore - `AirshipCore` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter` : Message center - `AirshipAutomation` : Automation and in-app messaging - `AirshipPreferenceCenter` : Preference Center - `AirshipFeatureFlags` : Feature Flags - `AirshipObjectiveC` : Objective-C Bindings - `AirshipDebug` : Debugging tools - `AirshipNotificationServiceExtension` : Service Extension framework (only for Notification Service Extensions)* 7. Import the frameworks in your code. Import statements match the framework names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` #### xcframeworks 1. Download and decompress the latest version of the [iOS SDK](https://github.com/urbanairship/ios-library/releases). 2. Inside the folder you should see a collection of XCFrameworks. Airship is modular, so select only the XCFrameworks you need: **Available XCFrameworks:** - `AirshipBasement.xcframework` : Required by AirshipCore - `AirshipCore.xcframework` : Push messaging features including channels, tags, named user and default actions (required) - `AirshipMessageCenter.xcframework` : Message center - `AirshipAutomation.xcframework` : Automation and in-app messaging - `AirshipPreferenceCenter.xcframework` : Preference Center - `AirshipFeatureFlags.xcframework` : Feature Flags - `AirshipObjectiveC.xcframework` : Objective-C Bindings - `AirshipDebug.xcframework` : Debugging tools - `AirshipNotificationServiceExtension.xcframework` : Service Extension framework (only for Notification Service Extensions)* 3. Add XCFrameworks to your project: - Open your project in Xcode - Click on your project in the Project Navigator - Select your target - Make sure the General tab is selected - Scroll down to **Frameworks, Libraries, and Embedded Content** - Drag in desired XCFrameworks from the downloaded SDK. They are wired up automatically as dependencies of your target 4. Verify Build Settings: - `Enable Modules` should be set to `Yes` - `Link Frameworks Automatically` should be set to `Yes` ![Enable Modules build setting in Xcode](https://www.airship.com/docs/images/enable-modules_hu_f56ed603b2699427.webp) *Enable Modules build setting in Xcode* ![Link Frameworks Automatically build setting in Xcode](https://www.airship.com/docs/images/link-frameworks-automatically_hu_e08f4e14a1a2ca09.webp) *Link Frameworks Automatically build setting in Xcode* 5. Import the frameworks in your code. Import statements match the framework names: ```swift import AirshipCore import AirshipMessageCenter import AirshipAutomation ``` ## Initialize Airship The Airship SDK requires only a single entry point, known as *takeOff*. For UIKit apps, initialize during the application delegate's `application(_:didFinishLaunchingWithOptions:)` method. For SwiftUI apps, you can initialize in the App's `init()` method. Before calling `takeOff`, configure the following: - **Project Credentials**: Airship requires your project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret)to authenticate your application. To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. You need separate credentials for development and production environments. On iOS, this is necessary because Apple provides separate APNS (Apple Push Notification Service) environments: - **Development/Sandbox**: Used for testing and development builds - **Production**: Used for App Store and TestFlight builds The SDK automatically selects the correct credentials based on your build configuration. Configure both sets of credentials in your code, and use the `#if DEBUG` conditional to switch between environments. - **Cloud Site**: Airship config defaults to the US cloud site. If your application is set up for the EU site, set the site on the config options to `.eu`. ### Calling takeOff The following examples show how to configure and call `takeOff` programmatically. Alternatively, you can configure Airship using an `AirshipConfig.plist` file—see [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/#configuring-airship-via-plist-file) for details. #### Swift ```swift import SwiftUI import AirshipCore @main struct MyApp: App { init() { var config = AirshipConfig() // Set credentials config.productionAppKey = "YOUR PRODUCTION APP KEY" config.productionAppSecret = "YOUR PRODUCTION APP SECRET" config.developmentAppKey = "YOUR DEVELOPMENT APP KEY" config.developmentAppSecret = "YOUR DEVELOPMENT APP SECRET" // Set cloud site (.us or .eu) config.site = .us #if DEBUG config.inProduction = false config.isAirshipDebugEnabled = true #else config.inProduction = true #endif try! Airship.takeOff(config) } var body: some Scene { WindowGroup { ContentView() } } } ``` For **UIKit** apps, call `takeOff` in your `AppDelegate`'s `application(_:didFinishLaunchingWithOptions:)` method instead of the App's `init()`. #### Objective-C ```objective-c @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UAConfig *config = [UAConfig config]; // Set credentials config.productionAppKey = @"YOUR PRODUCTION APP KEY"; config.productionAppSecret = @"YOUR PRODUCTION APP SECRET"; config.developmentAppKey = @"YOUR DEVELOPMENT APP KEY"; config.developmentAppSecret = @"YOUR DEVELOPMENT APP SECRET"; // Set cloud site (UACloudSiteUS or UACloudSiteEU) config.site = UACloudSiteUS; #if DEBUG config.inProduction = NO; config.isAirshipDebugEnabled = YES; #else config.inProduction = YES; #endif NSError *airshipError; [UAirship takeOff:config error:&airshipError]; NSAssert(airshipError == nil, @"TakeOff failed %@", airshipError); return YES; } @end ``` The Airship SDK automatically integrates with your app by default, so you don't need to implement push-related `UIApplicationDelegate` or `UNUserNotificationCenterDelegate` methods. This works for most applications out of the box. For advanced use cases or to disable automatic integration, see the [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/##manual-integration) guide. ## Test the integration After completing the setup, verify your integration: 1. **Build and run your app** in Xcode 2. **Check the console logs** for Airship channel creation: - Look for a log message: `Channel ID: ` - The channel ID will be displayed in the console output - For more detailed logging, see [Logging](https://www.airship.com/docs/developer/sdk-integration/apple/installation/logging/) If you see the channel ID in the console logs and no errors, your integration is successful. You can now proceed with configuring [deep links](https://www.airship.com/docs/developer/sdk-integration/apple/deep-links/), [push notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/), and other Airship features. If you don't see a channel ID in the console logs or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/initialization/) for common problems and solutions. # Advanced Integration > Disable automatic integration, manually forward app delegate methods, and configure URL allowlists for the Airship SDK. ## Configuring Airship via plist file If no config is provided to `takeOff` programmatically, Airship will default to loading config from the `AirshipConfig.plist` file in your application's bundle. This can be useful for apps with multiple build variants, or for keeping credentials out of version control. Sample `AirshipConfig.plist` file: ```xml detectProvisioningMode developmentAppKey Your Development App Key developmentAppSecret Your Development App Secret productionAppKey Your Production App Key productionAppSecret Your Production App Secret ``` The keys used in the `AirshipConfig.plist` file match the field names in [AirshipConfig](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipconfig) . If your app uses Airship's EU cloud site, add the `site` key: ```xml site EU ``` If you don't see a Channel ID in the console logs or encounter errors during initialization, see [Troubleshooting Initialization](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/initialization/). ## Manual Integration By default, the Airship SDK automatically integrates with your app using *method swizzling*. This allows the SDK to intercept app delegate messages and forward them automatically, so you don't need to implement push-related `UIApplicationDelegate` or `UNUserNotificationCenterDelegate` protocol methods. For most applications, automatic integration works out of the box. However, if you have custom app delegate requirements or prefer explicit control over method forwarding, you can disable automatic integration and handle it manually. ### Disabling automatic integration Set [AirshipConfig.isAutomaticSetupEnabled](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipconfig/isautomaticsetupenabled) to `false` in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff): #### Swift ```swift var config = AirshipConfig() config.isAutomaticSetupEnabled = false try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; config.isAutomaticSetupEnabled = NO; [UAirship takeOff:config error:&airshipError]; ``` ### Forwarding app delegate methods When automatic integration is disabled, you must forward the appropriate app delegate methods to the Airship SDK. #### Swift `UIApplicationDelegate` methods: ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Set up the UNUserNotificationCenter delegate UNUserNotificationCenter.current().delegate = self return true } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { AppIntegration.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { AppIntegration.application(application, didFailToRegisterForRemoteNotificationsWithError: error) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { return await AppIntegration.application(application, didReceiveRemoteNotification: userInfo) } ``` `UNUserNotificationCenterDelegate` methods: ```swift func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping @Sendable () -> Void ) { // Call through to Airship. The completion handler version is called on the main actor. // It's important to use the `completionHandler` version and not the async one, or it // will be called on a background thread and Airship might not receive the event in time // to count the direct open. MainActor.assumeIsolated { AppIntegration.userNotificationCenter( center, didReceive: response, withCompletionHandler: completionHandler ) } } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { return await AppIntegration.userNotificationCenter(center, willPresent: notification) } ``` #### Objective-C `UIApplicationDelegate` methods: ```objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Set up the UNUserNotificationCenter delegate [UNUserNotificationCenter currentNotificationCenter].delegate = self; return YES; } - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [UAAppIntegration application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [UAAppIntegration application:application didFailToRegisterForRemoteNotificationsWithError:error]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [UAAppIntegration application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } ``` `UNUserNotificationCenterDelegate` methods: ```objc - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { [UAAppIntegration userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler]; } - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { [UAAppIntegration userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler]; } ``` ## URL allowlist The [URLAllowList](https://urbanairship.github.io/ios-library/v20/AirshipCore/documentation/airshipcore/airshipurlallowlist) controls which URLs the Airship SDK is able to act on. The SDK divides up usages of URLs into two different scopes: - `SCOPE_OPEN_URL`: Only URLs allowed for this scope can be opened from an action, displayed in landing page, or displayed in an HTML in-app message. Defaults to allowing all URLs if not specified in the config. - `SCOPE_JAVASCRIPT_INTERFACE`: These URLs are checked before the Airship JavaScript interface is injected into the webview. Defaults to any Airship originated URLs. Allowed URLs should be provided when configuring the Airship Config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). #### Swift ```swift var config = AirshipConfig() // Allow all URLs for both scopes config.urlAllowList = ["*"] // Or configure specific scopes: config.urlAllowListScopeOpenURL = ["https://example.com/*", "https://*.youtube.com/*"] config.urlAllowListScopeJavaScriptInterface = ["https://example.com/*"] try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Allow all URLs for both scopes config.urlAllowList = @[@"*"]; // Or configure specific scopes: config.urlAllowListScopeOpenURL = @[@"https://example.com/*", @"https://*.youtube.com/*"]; config.urlAllowListScopeJavaScriptInterface = @[@"https://example.com/*"]; [UAirship takeOff:config error:&airshipError]; ``` **Valid URL pattern syntax** ```text := '*' | '://'/ | '://' | ':/' | ':///' := := '*' | '*.' | := ``` # Logging > Configure log levels, privacy settings, and custom log handlers to control how the Airship SDK logs messages. The Airship SDK provides configurable log levels to help you debug issues without overwhelming the console. If you don't configure logging, the SDK uses **Info** for development builds and **Error** for production builds with **private** privacy level. ## Log levels The log level acts as a minimum threshold—only logs at that level and higher will be logged. Available log levels, ordered from most to least verbose: | Log Level | Prefix | Description | | :-------- | :----- | :---------- | | **Verbose** | `[Airship] [V]` | Highly detailed SDK status for deep debugging and troubleshooting | | **Debug** | `[Airship] [D]` | General SDK status with more detailed information than Info | | **Info** | `[Airship] [I]` | General SDK status and lifecycle events | | **Warning** | `[Airship] [W]` | API deprecations, invalid setup, and other recoverable issues | | **Error** | `[Airship] [E]` | Critical errors and exceptions that the SDK cannot gracefully handle | | **None** | — | Disables all logging | ## Log privacy levels Control the visibility of log contents using privacy levels. This is especially useful when debugging release builds without exposing sensitive information. - **private** (default): Uses `os.Logger` to log all messages at the `private` level. The content of most logs will be redacted and will not be visible in the Console app by default. Use this for production builds to protect sensitive data. - **public**: Sends all logs to `os.Logger` with a `public` privacy level, preventing their content from being redacted. Use this when you need to capture detailed logs from release builds for debugging. > **Note:** When using `public` privacy level, `verbose` and `debug` log messages are automatically elevated to `info` level because Console log doesn't support those levels directly. This ensures all detailed logs are visible when debugging production builds. ## Configuration You can set separate log levels and privacy levels for development and production builds in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). ### Common configuration Typical setup: more verbose logging for development, minimal logging for production: #### Swift ```swift var config = AirshipConfig() // Development: verbose logging for debugging config.developmentLogLevel = .verbose config.developmentLogPrivacyLevel = .public // Production: minimal logging to reduce noise config.productionLogLevel = .error config.productionLogPrivacyLevel = .private try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Development: verbose logging for debugging config.developmentLogLevel = UAAirshipLogLevelVerbose; // Production: minimal logging to reduce noise config.productionLogLevel = UAAirshipLogLevelError; // Privacy levels (set via AirshipConfig.plist) // developmentLogPrivacyLevel // public // productionLogPrivacyLevel // private [UAirship takeOff:config error:&airshipError]; ``` ### Debugging production issues When debugging issues in production builds, temporarily enable verbose logging to capture detailed SDK behavior: #### Swift ```swift var config = AirshipConfig() // Production debugging: enable verbose logs config.productionLogLevel = .verbose config.productionLogPrivacyLevel = .public try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // Production debugging: enable verbose logs config.productionLogLevel = UAAirshipLogLevelVerbose; // Set via AirshipConfig.plist: // productionLogPrivacyLevel // public [UAirship takeOff:config error:&airshipError]; ``` ## Custom log handler You can provide a custom log handler to intercept and handle all Airship log messages. This is useful when you need to integrate Airship logs with your own logging system or customize how logs are formatted or stored. When a custom log handler is set, the default Airship log handler is completely replaced. Log level filtering is performed before your handler is called, so your handler will only receive logs that meet the configured log level threshold. Implement the `AirshipLogHandler` protocol and set it on your Airship config: ```swift import os.log final class CustomLogHandler: AirshipLogHandler { private let logger = Logger(subsystem: "com.yourapp.airship", category: "Airship") func log( logLevel: AirshipLogLevel, message: String, fileID: String, line: UInt, function: String ) { // Forward to your logging system let osLogLevel: OSLogType switch logLevel { case .verbose, .debug: osLogLevel = .debug case .info: osLogLevel = .info case .warning: osLogLevel = .default case .error: osLogLevel = .error case .none: return } logger.log(level: osLogLevel, "\(message)") // Optionally: send to remote logging service // YourLoggingService.log(message, level: logLevel) } } var config = AirshipConfig() config.logHandler = CustomLogHandler() try! Airship.takeOff(config) ``` # Locale > Configure locale behavior and override the default locale that Airship uses. Airship uses the [Locale](https://www.airship.com/docs/reference/glossary/#locale) for various SDK operations. By default, the SDK automatically uses the device's locale settings, but you can configure it to use the user's preferred language or override it programmatically. ## Configuring locale behavior You can configure how Airship determines the locale by setting the `useUserPreferredLocale` option in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). By default, `useUserPreferredLocale` is `false`, and the SDK uses `Locale.autoupdatingCurrent`, which reflects the device's current locale settings. When set to `true`, the SDK uses the first language from the user's preferred languages list (`Locale.preferredLanguages[0]`), which is useful when you want the SDK to match the user's language preference rather than the device's current locale settings. #### Swift ```swift var config = AirshipConfig() // ... other config settings ... // Use preferred language instead of current locale config.useUserPreferredLocale = true try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; // ... other config settings ... // Use preferred language instead of current locale config.useUserPreferredLocale = YES; [UAirship takeOff:config error:&airshipError]; ``` ## Overriding the locale You can override the locale programmatically at runtime, which takes precedence over both the configured locale behavior and the device's locale settings. #### Swift ```swift Airship.localeManager.currentLocale = Locale(identifier:"de") ``` #### Objective-C ```objc UAirship.localeManager.currentLocale = [NSLocale localeWithLocaleIdentifier:@"de"]; ``` ## Clearing the locale override To remove a locale override and return to using the configured locale behavior: #### Swift ```swift Airship.localeManager.clearLocale() ``` #### Objective-C ```objc [UAirship.localeManager clearLocale]; ``` ## Getting the current locale To retrieve the locale that Airship is currently using: #### Swift ```swift let airshipLocale = Airship.localeManager.currentLocale ``` #### Objective-C ```objc NSLocale *airshipLocale = UAirship.localeManager.currentLocale; ``` ## Push Notifications Comprehensive guides for implementing push notifications, including setup, rich media support, interactive notifications, badge management, quiet time, and more. # Push Notifications > How to configure your application to receive and respond to notifications. Before setting up push notifications in your app, you need to configure APNs (Apple Push Notification service) in the Airship dashboard. See [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) for instructions on uploading your APNs certificate or token. ## Enable Capabilities Before enabling push notifications, you need to configure your app's capabilities in Xcode. ### Enable Push Notifications Capability 1. Open your project in Xcode. 2. Click on your project in the Project Navigator. 3. Select your main app target and then click the **Signing & Capabilities** tab. 4. If you do not see Push Notifications enabled, click **+ Capability** and add **Push Notifications**. ![Adding the Push Notifications capability in Xcode](https://www.airship.com/docs/images/ios-enable-push-notifications-capabilities_hu_2e1789fffb02612b.webp) *Adding the Push Notifications capability in Xcode* ### Enable Background Modes 1. Select your main app target and then click the **Signing & Capabilities** tab. 2. Click **+ Capability** and add **Background Modes**. ![Adding the Background Modes capability in Xcode](https://www.airship.com/docs/images/ios-enable-background-mode-capabilities_hu_f135d9fec0ba0d06.webp) *Adding the Background Modes capability in Xcode* 3. In the **Background Modes** section, select the **Remote notifications** checkbox. ![Enabling Remote notifications in Background Modes](https://www.airship.com/docs/images/ios-background-mode-remote-notifications_hu_7e38b08288fcd7b2.webp) *Enabling Remote notifications in Background Modes* ## Add Rich Media Support To support rich media attachments (images, animated GIFs, video) in push notifications, you need to create a Notification Service Extension. See [Notification Service Extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) for setup instructions. ## Enable User Notifications The Airship SDK distinguishes between *user notifications* (visible to users) and *silent push notifications* (background data delivery). User notifications require explicit permission from the user. By default, user notifications are disabled. Enable them when you want to show visible notifications to users. ### Basic Enablement The simplest way to enable user notifications is to set the `userPushNotificationsEnabled` property: #### Swift ```swift Airship.push.userPushNotificationsEnabled = true ``` #### Objective-C ```objc UAirship.push.userPushNotificationsEnabled = YES; ``` ### Checking Authorization State To check whether the user granted permission, use the async method which returns the system authorization state: #### Swift ```swift let authorized = await Airship.push.enableUserPushNotifications() if authorized { // User granted permission } else { // User denied permission } ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use the basic `userPushNotificationsEnabled` property instead. > **Note:** The return value represents the **system authorization state** (whether the user granted permission), not the state of `userPushNotificationsEnabled`, which will always be set to `true` after calling this method. ### Handling Denied Permissions If the user has already denied notification permissions, you can provide a fallback action to guide them to system settings or show a custom message: #### Swift ```swift // Navigate to system settings if permission is denied let authorized = await Airship.push.enableUserPushNotifications( fallback: .systemSettings ) // Or provide a custom callback let authorized = await Airship.push.enableUserPushNotifications( fallback: .callback { // Show custom UI explaining why notifications are important // and guide user to system settings } ) // Or no fallback let authorized = await Airship.push.enableUserPushNotifications( fallback: .none ) ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use the basic `userPushNotificationsEnabled` property instead. The `PromptPermissionFallback` options are: - `.none` - No fallback action - `.systemSettings` - Automatically navigate to system settings if permission is denied - `.callback` - Execute a custom callback to handle the denied state > **Tip:** To increase the likelihood that users will accept notification permissions, avoid prompting immediately on app launch. Instead, wait for a more appropriate moment, such as after the user completes an action or views relevant content. ## Configure Notification Options Before enabling user notifications, you can configure which notification types your app will request permission for. ### Standard Notification Options By default, the Airship SDK requests permission for alerts, badges, and sounds. You can customize these options: #### Swift ```swift Airship.push.notificationOptions = [.alert, .badge, .sound] ``` #### Objective-C ```objc UAirship.push.notificationOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert); ``` ### Provisional Authorization Provisional authorization allows you to send notifications without initially prompting the user. Notifications are delivered quietly to the Notification Center until the user explicitly chooses to keep them. #### Swift ```swift Airship.push.notificationOptions = [.alert, .badge, .sound, .provisional] ``` #### Objective-C ```objc UAirship.push.notificationOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionProvisional); ``` > **Note:** With provisional authorization, you can programmatically enable user notifications without showing a permission prompt. This is still required for any visible notification delivery. ## Foreground Presentation Options When your app is in the foreground, iOS silences notifications by default. Configure how notifications are displayed when the app is active: #### Swift ```swift Airship.push.defaultPresentationOptions = [.alert, .badge, .sound] ``` #### Objective-C ```objc UAirship.push.defaultPresentationOptions = (UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound); ``` ## Handle Notification Events The Airship SDK provides callbacks for when notifications are received or interacted with. These callbacks are optional—the SDK will handle notifications automatically if you don't set them. #### Swift ```swift // Handle when user taps a notification Airship.push.onReceivedNotificationResponse = { response in // Handle notification response } // Handle notification received while app is in foreground Airship.push.onReceivedForegroundNotification = { userInfo in // Handle foreground notification } // Handle background content-available notification Airship.push.onReceivedBackgroundNotification = { userInfo in // Handle background notification return .noData } // Customize presentation options per notification Airship.push.onExtendPresentationOptions = { options, notification in // Return presentation options for this specific notification return [.badge, .list, .banner, .sound] } ``` #### Objective-C ```objc // Handle when user taps a notification UAirship.push.onReceivedNotificationResponse = ^(UNNotificationResponse *response) { // Handle notification response }; // Handle notification received while app is in foreground UAirship.push.onReceivedForegroundNotification = ^(NSDictionary *userInfo) { // Handle foreground notification }; // Handle background content-available notification UAirship.push.onReceivedBackgroundNotification = ^UIBackgroundFetchResult(NSDictionary *userInfo) { // Handle background notification return UIBackgroundFetchResultNoData; }; // Customize presentation options per notification UAirship.push.onExtendPresentationOptions = ^UNNotificationPresentationOptions(UNNotificationPresentationOptions options, UNNotification *notification) { // Return presentation options for this specific notification return UNNotificationPresentationOptionList | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; }; ``` ## Silent Notifications Silent notifications are push messages that don't display a notification to the user. They're typically used to wake your app in the background to perform tasks or fetch content. To send a silent notification, set the `content_available` property to `true` in the [iOS override object](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). > **Important:** Thoroughly test your implementation to confirm that silent notifications don't generate any visible device notifications. > **Note:** Silent notifications (`content_available`) don't have guaranteed delivery. Factors affecting delivery include battery life, WiFi connectivity, and the number of silent pushes sent recently. These metrics are determined solely by iOS and APNs. > > Use silent notifications to supplement your app's regular behavior rather than for critical functionality. For example, use them to pre-fetch data ahead of time to reduce load times when the user launches the app. ## Next Steps - [Notification Service Extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) - Add rich media support to push notifications - [Interactive Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/interactive-notifications/) - Add action buttons to notifications - [Badge Management](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/badge-management/) - Set, reset, or enable auto-badge functionality - [Quiet Time](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/quiet-time/) - Suppress notifications during specific hours - [App Clips](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/app-clips/) - Configure push notifications for App Clips If push notifications aren't working as expected, see [Troubleshooting Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/push-notifications/) to check notification status and fix common issues. # Notification Service Extension > Create and configure a notification service extension to support rich media attachments like images, animated GIFs, and videos in push notifications. To support rich media attachments (images, animated GIFs, video) in push notifications, you need to create a [notification service extension](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications) (NSE). ## Create a Notification Service Extension Target 1. In Xcode, click **File** → **New** → **Target...**. 2. Select **Notification Service Extension**. 3. Click **Next** and configure your extension: - **Product Name**: Your extension name (e.g., `NotificationServiceExtension`) - **Bundle Identifier**: Typically your app's bundle ID with a suffix (e.g., `com.example.app.NotificationServiceExtension`) ![Creating a Notification Service Extension target in Xcode](https://www.airship.com/docs/images/create-notification-service-extension_hu_8bb42b1a35cc5e03.webp) *Creating a Notification Service Extension target in Xcode* 4. Verify that your app target's **Embed App Extensions** includes the newly created extension. ![Verifying the extension is embedded in the app target](https://www.airship.com/docs/images/embed-extension-in-app_hu_393854a222d22be6.webp) *Verifying the extension is embedded in the app target* ## Install Dependencies #### SPM 1. Select your service extension target in the Project Navigator. 2. Go to the **Package Dependencies** tab. 3. If you haven't already added the Airship package, click **+** and add: `https://github.com/urbanairship/ios-library` 4. Select the `AirshipNotificationServiceExtension` package product for your service extension target. > **Note:** The `AirshipNotificationServiceExtension` package should only be added to the Notification Service Extension target, not the main app target. 5. Import the module: ```swift import AirshipNotificationServiceExtension ``` #### CocoaPods Add to your `Podfile`: ```ruby target "" do pod 'AirshipServiceExtension' end ``` Install: `$ pod install` #### Carthage 1. Add `AirshipNotificationServiceExtension.framework` to your service extension target following [Carthage's instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). 2. Add to your `Cartfile`: ```text github "urbanairship/ios-library" ``` 3. Build: `$ carthage update` 4. Verify that **Enable Modules** and **Link Frameworks Automatically** are enabled in Build Settings. #### xcframeworks 1. Download the latest [iOS SDK release](https://github.com/urbanairship/ios-library/releases). 2. Add `AirshipNotificationServiceExtension.xcframework` to your **main app target**: - Select your main app target - Go to **General** → **Frameworks, Libraries, and Embedded Content** - Drag in `AirshipNotificationServiceExtension.xcframework` - Set **Embed** to **Embed & Sign** 3. Add `AirshipNotificationServiceExtension.xcframework` to your **service extension target**: - Select your service extension target - Go to **General** → **Frameworks, Libraries, and Embedded Content** - Add `AirshipNotificationServiceExtension.xcframework` - Set **Embed** to **Do Not Embed** 4. Configure the service extension's runpath: - Select your service extension target - Go to **Build Settings** → search for **Runpath Search Paths** (`LD_RUNPATH_SEARCH_PATHS`) - Add: `@executable_path/../../Frameworks` 5. Verify Build Settings for both targets: - **Enable Modules**: `Yes` - **Link Frameworks Automatically**: `Yes` > **Note:** The framework must be embedded in the main app target, not the extension. Extensions cannot contain nested frameworks, which will cause App Store rejection. The runpath setting allows the extension to find the framework in the main app's `Frameworks` directory. ## Implement the Service Extension Replace the default `NotificationService` implementation with Airship's base class: #### Swift (SPM/Carthage/xcframeworks) ```swift import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { } ``` #### Swift (CocoaPods) ```swift import AirshipServiceExtension class NotificationService: UANotificationServiceExtension { } ``` #### Objective-C (SPM/Carthage/xcframeworks) ```objc // NotificationService.h @import AirshipNotificationServiceExtension; @interface NotificationService : UANotificationServiceExtension @end // NotificationService.m @import Foundation; #import "NotificationService.h" @implementation NotificationService @end ``` #### Objective-C (CocoaPods) ```objc // NotificationService.h @import AirshipServiceExtension; @interface NotificationService : UANotificationServiceExtension @end // NotificationService.m @import Foundation; #import "NotificationService.h" @implementation NotificationService @end ``` That's it! The Airship base class handles downloading and attaching media from URLs in your push notifications. When you send a push notification with a media URL, the service extension will automatically download and attach the media before the notification is displayed. ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) If you experience problems, see [Troubleshooting Notification Service Extensions](https://www.airship.com/docs/developer/sdk-integration/apple/troubleshooting/notification-service-extensions/) to verify setup and debug issues. # In-App Messaging > Legacy In-App Messages are banner messages delivered through push notifications that appear in-app when the user opens the application. Legacy [in-app messages](https://www.airship.com/docs/guides/messaging/messages/content/app/in-app-messages/) are delivered through push messages and automatically converted to In-App Automation (IAA) banners for display. You can customize how these messages are converted using extender blocks on the legacy in-app message manager. > **Note:** This page covers **legacy In-App Messages** delivered via push notifications. For In-App Automation (IAA) and Scenes, which are separate features with different triggers and capabilities, see [In-App Experiences](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/getting-started/). For general In-App Automation styling options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Modify the schedule **Modify the schedule** ```swift InAppAutomation.shared.legacyInAppMessaging.scheduleExtender = { schedule in // Modify the schedule schedule.limit = 2 } ``` ## Modify the message **Modify the message** ```swift InAppAutomation.shared.legacyInAppMessaging.messageExtender = { message in /// Modify the message if case .banner(var bannerInfo) = message.displayContent { bannerInfo.borderRadius = 10.0 message.displayContent = .banner(bannerInfo) } } ``` # Landing Pages > Landing Pages are web pages triggered from a notification response that are automatically converted to HTML In-App Automation experiences. Landing Pages are web pages triggered as an action from a notification response. When a user taps a notification with a landing page action, the landing page is automatically converted to an HTML In-App Automation experience for display. For general In-App Automation styling options, including HTML customization, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Customize Landing Pages You can customize landing pages by registering a custom action that extends the HTML schedule before display: #### Swift ```swift Airship.actionRegistry.registerEntry( names: LandingPageAction.defaultNames ) { let action = LandingPageAction() { args, schedule in guard case .inAppMessage(var message) = schedule.data else { return } guard case .html(var htmlContent) = message.displayContent else { return } // Customize the HTML content htmlContent.forceFullscreen = true message.displayContent = .html(htmlContent) schedule.data = .inAppMessage(message) } return ActionEntry(action: action) } ``` #### Objective-C > **Note:** Custom action registration with closures is not available in Objective-C. Use Swift or subclass the action directly. # Badge Management > Manage the badge number that appears on your app icon, including manual control and automatic incrementing. The badge appears as a number on your app icon. You can set, reset, or enable auto-badge functionality. ## Get Badge Value The `badgeNumber` property returns the current badge number used by both the device and the Airship server. This property must be accessed on the main thread. #### Swift ```swift let currentBadge = await Airship.push.badgeNumber ``` #### Objective-C > **Note:** This async property is not available in Objective-C. Use `UIApplication.shared.applicationIconBadgeNumber` to read the current badge value. ## Set Badge Value Set the badge number to a specific value. This updates both the device badge and the value stored on Airship servers. #### Swift ```swift try await Airship.push.setBadgeNumber(20) ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Use `UIApplication.shared.applicationIconBadgeNumber` to set the badge value directly. ## Reset Badge Reset the badge to zero on both the device and Airship servers. #### Swift ```swift try await Airship.push.resetBadge() ``` #### Objective-C > **Note:** This async method is not available in Objective-C. Set `UIApplication.shared.applicationIconBadgeNumber = 0` to reset the badge directly. ## Auto-Badge Auto-badge automatically updates the badge number stored by Airship every time the app is started or foregrounded, instead of setting an exact value. #### Swift ```swift Airship.push.autobadgeEnabled = true ``` #### Objective-C ```objc UAirship.push.autobadgeEnabled = YES; ``` > **Important:** When using auto-badge, only modify the badge value through Airship methods to ensure the value stays in sync. # Interactive Notifications > Configure standard and custom interactive notification categories with action buttons for iOS push notifications. Interactive notifications allow users to take actions directly from the notification without opening your app. You can add buttons to notifications that perform specific actions when tapped. ## Standard Interactive Notifications Airship provides built-in interactive notification types with pre-configured action buttons. See [Built-In Interactive Notification Types](https://www.airship.com/docs/reference/messages/built-in-interactive-notifications/) for available types and button configurations. To use a standard interactive notification type, specify the category ID in your push payload. The notification will automatically display the appropriate action buttons. ## Custom Interactive Notification Categories You can define custom notification categories with specific actions tailored to your app's needs. > **Note:** Airship reserves category IDs prefixed with `ua_`. Any custom categories with that prefix will be ignored. ### Create a Custom Category #### Swift ```swift // Define an action for the category let categoryAction = UNNotificationAction( identifier: "category_action", title: "Action!", options: [.authenticationRequired, .foreground, .destructive] ) // Define the category let category = UNNotificationCategory( identifier: "custom_category", actions: [categoryAction], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "Sensitive Content Hidden", options: [] ) // Register the custom category Airship.push.customCategories = [category] ``` #### Objective-C ```objc // Define an action for the category UNNotificationAction *categoryAction = [UNNotificationAction actionWithIdentifier:@"category_action" title:@"Action!" options:(UNNotificationActionOptionForeground | UNNotificationActionOptionDestructive | UNNotificationActionOptionAuthenticationRequired)]; // Define the category UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"custom_category" actions:@[categoryAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone]; // Register the custom category UAirship.push.customCategories = [NSSet setWithArray:@[category]]; ``` ### Action Options When creating notification actions, you can specify the following options: - **`.foreground`** / `UNNotificationActionOptionForeground`: Opens the app when the action is tapped - **`.destructive`** / `UNNotificationActionOptionDestructive`: Displays the action button in red (use for destructive actions like "Delete") - **`.authenticationRequired`** / `UNNotificationActionOptionAuthenticationRequired`: Requires the device to be unlocked before the action can be performed ### Hidden Preview Placeholder The `hiddenPreviewsBodyPlaceholder` parameter specifies placeholder text that will be shown instead of the notification's body when the user has disabled notification previews for your app. This is useful for sensitive content that shouldn't be displayed in notification previews. ### Handle Action Responses When a user taps an action button, handle the response in your notification callback: #### Swift ```swift Airship.push.onReceivedNotificationResponse = { response in if response.actionIdentifier == "category_action" { // Handle the action } } ``` #### Objective-C ```objc UAirship.push.onReceivedNotificationResponse = ^(UNNotificationResponse *response) { if ([response.actionIdentifier isEqualToString:@"category_action"]) { // Handle the action } }; ``` ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) # App Clips > Set up App Clips with Airship to enable push notifications for lightweight app experiences. [App Clips](https://developer.apple.com/documentation/app_clips) are lightweight versions of your app that users can access quickly without installing the full app. They're designed for specific, focused tasks and can be launched via QR codes, NFC tags, links, or App Clip codes. App Clips support push notifications, specifically for transactional use cases. When a user downloads an App Clip, they're automatically opted-in to notifications for 8 hours. After this period, you can ask users to extend notification permissions for up to 1 week. ## Prerequisites > **Important:** An App Clip requires a separate application identifier in the Apple Developer Portal and a separate project in the Airship dashboard. You need to follow the same setup steps required for the main application in both the Apple Developer Portal and the [Airship dashboard](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration). ## Register an App Clip Identifier To create a push certificate for your App Clip, you need to register a new application identifier: 1. In the **Certificates, Identifiers & Profiles** section of your Apple Developer account, click **Identifiers**. 2. Click the **+** button to register a new identifier. 3. Select **App Clips** as the identifier type. ![Selecting App Clips as the identifier type](https://www.airship.com/docs/images/app-clip-identifier_hu_c7c5b6bc665a4d40.webp) *Selecting App Clips as the identifier type* 4. Specify the app ID of the parent app and the product name: ![Specifying the parent app ID and product name](https://www.airship.com/docs/images/app-clip-identifier2_hu_dd057b939ae88cc1.webp) *Specifying the parent app ID and product name* 5. Complete the registration process. ## Create an App Clip Target 1. In Xcode, click **File** → **New** → **Target...**. 2. Select **App Clip** in the **Application** section. ![Selecting the App Clip target type in Xcode](https://www.airship.com/docs/images/app-clip-target_hu_87e2d0a265a4c2c3.webp) *Selecting the App Clip target type in Xcode* 3. Configure your App Clip target and click **Finish**. 4. **Important**: Add the **Push Notifications** capability to your App Clip target: - Select your App Clip target - Go to **Signing & Capabilities** - Click **+ Capability** and add **Push Notifications** ## Configure Ephemeral Notifications By default, App Clips can receive notifications for 8 hours after installation. To enable ephemeral notifications, add the following to your App Clip's `Info.plist`: ![Ephemeral notification configuration in Info.plist](https://www.airship.com/docs/images/app-clip-plist_hu_ad54638ec83033df.webp) *Ephemeral notification configuration in Info.plist* ## Enable Extended Push Notification Permissions To send push notifications for an extended period (up to 1 week), enable extended push notification permissions: #### Swift ```swift Airship.push.extendedPushNotificationPermissionEnabled = true ``` #### Objective-C ```objc UAirship.push.extendedPushNotificationPermissionEnabled = YES; ``` ## Initialize Airship in Your App Clip Initialize the Airship SDK in your App Clip the same way you would in your main app. See the [Getting Started guide](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) for initialization instructions. ## Sending Notifications to App Clips When sending push notifications to App Clips, include the `target_content_id` in the iOS override object. This identifies the specific App Clip experience: ```json { "notification": { "ios": { "target_content_id": "https://example.com/restaurants/cafe_portland/order/1234", "alert": { "title": "Order Status", "subtitle": "Cafe Portland", "body": "Your order is ready!" } } } } ``` For more details on the iOS override object, see the [API reference](https://www.airship.com/docs/developer/rest-api/ua/schemas/platform-overrides/#iosoverrideobject). ## Related Documentation - [Getting Started with Push Notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/getting-started/) - [Installing the Airship SDK](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) - [iOS Channel Configuration](https://www.airship.com/docs/guides/getting-started/developers/configure-channels/#ios-channel-configuration) # Quiet Time > Configure quiet time to prevent notifications from being displayed during specific hours while still receiving them. Quiet time allows you to suppress notifications during specific hours. Notifications are still received but won't be displayed to the user during the quiet time window. ## Configure Quiet Time #### Swift ```swift // Set quiet time from 7:30pm to 7:30am Airship.push.setQuietTimeStartHour(19, startMinute: 30, endHour: 7, endMinute: 30) // Enable quiet time Airship.push.quietTimeEnabled = true ``` #### Objective-C ```objc // Set quiet time from 7:30pm to 7:30am [UAirship.push setQuietTimeStartHour:19 startMinute:30 endHour:7 endMinute:30]; // Enable quiet time UAirship.push.quietTimeEnabled = YES; ``` ## In-App Experiences Implement Scenes, In-App Automations, custom views, and embedded content to deliver engaging in-app experiences to your users. # In-App Experiences > Integrate Scenes & In-App Automations into your Apple app to display embedded content and create custom in-app experiences with minimal code. In-App Experiences use Airship's on-device automation framework to provide instant, personalized content that integrates natively with your app. This includes [Scenes](https://www.airship.com/docs/reference/glossary/#scene), which can be displayed as modal or fullscreen overlays or embedded directly within your app screens, and In-App Automations (IAA), which power banner, modal, and fullscreen in-app messages triggered by events. Scenes are fully customizable in the Airship dashboard and require minimal SDK integration. For advanced In-App Automation customization options, see [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/). ## Requirements To use In-App Experiences, you need: - The `AirshipAutomation` module installed (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/)) - Airship SDK initialized with `takeOff` (see [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/)) > **Note:** **In-App Experiences work out of the box**: Once you install the `AirshipAutomation` module and initialize Airship with `takeOff`, In-App Experiences will function automatically. The rest of this documentation covers optional customization and advanced features. ## Adding custom fonts Custom fonts added to your app bundle can be used in In-App Experiences. To add fonts to your app, follow Apple's guide on [Adding a Custom Font to Your App](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app). Once added, you'll need the font family name to use it in Airship. To find the family name: 1. Add the font files to your Xcode project 2. Use this code to print all available font family names: #### Swift ```swift for family in UIFont.familyNames.sorted() { print("Family: \(family)") for name in UIFont.fontNames(forFamilyName: family) { print(" - \(name)") } } ``` #### Objective-C ```objc for (NSString *familyName in [UIFont familyNames]) { NSLog(@"Family: %@", familyName); for (NSString *fontName in [UIFont fontNamesForFamilyName:familyName]) { NSLog(@" - %@", fontName); } } ``` After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). You can then select the stack when [setting In-App Experience defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ## Controlling display Control when and how In-App Experiences are displayed in your app. You can auto-pause displays on launch (useful for splash screens), manually pause all in-app displays, set intervals between displays, control when individual messages are ready to display, and specify which scene window displays messages in multi-scene apps. ### Auto-pausing on launch For apps with splash screens, you can configure Airship to automatically pause In-App Automation on launch. This prevents In-App Experiences from displaying during the splash screen. Once your app is ready, resume display by setting `isPaused` to `false`. Set the `autoPauseInAppAutomationOnLaunch` option in your Airship config when calling `takeOff`: #### Swift ```swift var config = AirshipConfig() // ... other config settings ... // Auto-pause on launch for splash screen config.autoPauseInAppAutomationOnLaunch = true try! Airship.takeOff(config) // Later, when splash screen is dismissed and app is ready: Airship.inAppAutomation.isPaused = false ``` #### Objective-C ```obj-c UAConfig *config = [UAConfig config]; // ... other config settings ... // Auto-pause on launch for splash screen config.autoPauseInAppAutomationOnLaunch = YES; [UAirship takeOff:config error:&airshipError]; // Later, when splash screen is dismissed and app is ready: UAirship.inAppAutomation.isPaused = NO; ``` ### Pausing display Pausing will still allow In-App Experiences to be triggered and queued up for execution, but they will not display. This is useful for preventing in-app experiences from displaying on screens where it would be detrimental to the user experience, such as splash screens, settings screens, or landing pages. #### Swift ```swift Airship.inAppAutomation.isPaused = true ``` #### Objective-C ```obj-c Airship.inAppAutomation.isPaused = YES ``` ### Display interval The display interval controls the amount of time to wait before the manager can display the next triggered In-App Experience. The default value is set to **0 seconds** and can be adjusted to any amount of time in seconds. #### Swift ```swift Airship.inAppAutomation.inAppMessaging.displayInterval = 30 ``` #### Objective-C ```obj-c UAirship.inAppAutomation.inAppMessaging.displayInterval = 30 ``` ### Controlling per-message display You can control when individual In-App Experiences are ready to display and listen for when they are displayed or finished. This is useful when you need to check app state before displaying content, such as: - Verifying the current screen or view controller is appropriate for the message - Checking custom data in the message's extras (custom keys) to determine if it should display - Ensuring certain app conditions are met before showing the message - Integrating with other in-app messaging products #### Swift Set a closure on to control the display. You have access to the message and schedule ID: ```swift Airship.inAppAutomation.inAppMessaging.onIsReadyToDisplay = { message, scheduleID in // Return false to prevent display return false } ``` `onIsReadyToDisplay` will be called whenever state in the app changes (screen, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```swift Airship.inAppAutomation.inAppMessaging.notifyDisplayConditionsChanged() ``` #### Objective-C Implement the `InAppMessageDisplayDelegate` to control the display. You have access to the message and schedule ID: **Implement the InAppMessageDisplayDelegate** ```obj-c - (BOOL)isMessageReadyToDisplay:(UAInAppMessage *)message scheduleID:(NSString *)scheduleID { // Return NO to prevent display return NO; } ``` **Set the delegate** ```obj-c UAirship.inAppAutomation.inAppMessaging.displayDelegate = self; ``` `isMessageReadyToDisplay` will be called whenever state in the app changes (screen, app state, message finished displaying, etc...), you can also trigger it manually with `notifyDisplayConditionsChanged`: ```obj-c [UAirship.inAppAutomation.inAppMessaging notifyDisplayConditionsChanged]; ``` ### Displaying in multiple scene apps By default, In-App Experiences are displayed in the last active window scene. The [InAppMessageSceneDelegate](https://urbanairship.github.io/ios-library/v20/AirshipAutomation/documentation/airshipautomation/inappmessagescenedelegate) allows you to override this behavior and control which UIWindowScene displays a given in-app message. Use this delegate when your app supports multiple scenes and you need to customize which scene displays the message. #### Swift **Implement the InAppMessageSceneDelegate** ```swift func sceneForMessage(_ message: InAppMessage) -> UIWindowScene? { // return a custom scene or nil to use default } ``` **Set the delegate** ```swift Airship.inAppAutomation.inAppMessaging.sceneDelegate = sceneDelegate ``` #### Objective-C **Implement the InAppMessageSceneDelegate** ```obj-c - (UIWindowScene *)sceneForMessage:(UAInAppMessage *)message { // return a custom scene or nil to use default return nil; } ``` **Set the delegate** ```obj-c UAirship.inAppAutomation.inAppMessaging.sceneDelegate = self; ``` ## Next steps - Learn how to [create Scenes in the Airship dashboard](https://www.airship.com/docs/guides/messaging/in-app-experiences/scenes/create/) - Present Scene content with [Embedded Content](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/embedded-content/) - Create reusable components with [Custom Views](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/custom-views/) - Customize [In-App Automation](https://www.airship.com/docs/developer/sdk-integration/apple/in-app-experiences/in-app-automation/) for IAA # Embedded Content > Integrate Embedded Content into your iOS app to display Scene content directly within your app's screens. For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see [Embedded Content](https://www.airship.com/docs/guides/features/messaging/scenes/embedded-content/). ## Adding an embedded view The `AirshipEmbeddedView` is a SwiftUI view that defines a place for an Airship Embedded Content to be displayed. When defining an `AirshipEmbeddedView`, specify the `embeddedId` for the content it should display. The value of the `embeddedId` must be the ID of an Embedded Content view style in your project. **Basic integration** ```swift // Show any "home_banner" Embedded Content AirshipEmbeddedView(embeddedID: "home_banner") ``` ## Placeholders If content is unavailable to display, the default behavior is to show an `EmptyView`. You can customize this by providing a placeholder. **Basic integration with placeholder** ```swift AirshipEmbeddedView(embeddedID: "home_banner") { Text("Placeholder!") } ``` ## Placing in a scroll view When placed directly in a `ScrollView`, or a child view within the `ScrollView` that is allowed to grow unbounded in the scrollable direction, you need to pass the maximum size of the embedded view to make percent-based sizing work correctly. The easiest way is to wrap the `ScrollView` in a `GeometryReader` and pass the size info to the embedded view. **GeometryReader example** ```swift struct ScrollViewExample: View { var body: some View { GeometryReader { geometryProxy in ScrollView(showsIndicators: false) { AirshipEmbeddedView( embeddedID: "home_banner", embeddedSize: AirshipEmbeddedSize( parentBounds: geometryProxy.size ) ) } } } } ``` When using a `GeometryReader`, it takes up as much space as allowed. To avoid this and instead measure the current size of the content, you can use the view extension `airshipMeasureView`. **airshipMeasureView example** ```swift struct ScrollViewExample: View { @State var state: CGSize? var body: some View { ScrollView(showsIndicators: false) { AirshipEmbeddedView( embeddedID: "home_banner", embeddedSize: AirshipEmbeddedSize( maxWidth: state?.width, maxHeight: state?.height ) ) } .airshipMeasureView(self.$state) } } ``` ## Styling You can set a custom style on the embedded view, which allows you to modify how the content is displayed or what pending content is displayed. In this example, the embedded view has a Dismiss Button above it: **Custom style** ```swift public struct CustomEmbeddedViewStyle: AirshipEmbeddedViewStyle { @ViewBuilder public func makeBody(configuration: AirshipEmbeddedViewStyleConfiguration) -> some View { if let view = configuration.views.first { VStack { Button("Dismiss") { view.dismiss() } view } } else { configuration.placeHolder } } } ``` **Setting the style** ```swift AirshipEmbeddedView(embeddedID: "home_banner") .setAirshipEmbeddedStyle(CustomEmbeddedViewStyle()) ``` ## Observing available embedded content Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An `AirshipEmbeddedView` will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an `AirshipEmbeddedObserver` to watch for updates. An `AirshipEmbeddedObserver` is an `ObservableObject` that you can use as a `StateObject` to automatically refresh the view when new Embedded Content is available. It allows for more dynamic handling of Embedded Content than just content or a placeholder. **Observable example** ```swift struct ObservableExample: View { @StateObject private var embeddedObserver: AirshipEmbeddedObserver = AirshipEmbeddedObserver(embeddedID: "home_banner") @State var tabIndex = 0 var body: some View { if (embeddedObserver.embeddedInfos.isEmpty) { Text("No banner available") } else { Text("Banner available") AirshipEmbeddedView(embeddedID: "home_banner") } } } ``` The `AirshipEmbeddedObserver` can be created to watch for one `embeddedID`, all embedded IDs, or use custom filtering for embedded IDs. The `embeddedInfos` is the FIFO order of embedded info, including the extras you can set through the Scene composer when creating the content. # Custom Views > Register custom SwiftUI views with the AirshipCustomViewManager to use them in Scenes. ![Custom View rendered in a Scene on iOS](https://www.airship.com/docs/images/custom-views-apple_hu_32d8fbb68fdc44c2.webp) *Custom View rendered in a Scene on iOS* 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](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/). #### Swift ```swift 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. #### Swift First, define the view and its properties: ```swift 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: ```swift AirshipCustomViewManager.shared.register(name: "map", builder: { args in if let args: CustomMapView.Args = try? args.properties?.decode() { CustomMapView(args: args) } }) ``` ## Scene Control 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. #### Swift ```swift 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. #### Swift ```swift 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. #### Swift ```swift 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. #### Swift ```swift 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 in a Scene on iOS](https://www.airship.com/docs/images/custom-views-map-scene-apple_hu_46a1471648ebe081.webp) *Map Custom View in a Scene on iOS* ## Embedding Airship Views Airship views like Preference Center can be embedded as custom views. #### Swift ```swift // 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 ) } ``` ![Preference Center Custom View in a Scene on iOS](https://www.airship.com/docs/images/custom-views-preference-center-scene-apple_hu_6e4d0728f598b316.webp) *Preference Center Custom View in a Scene on iOS* # In-App Automation > Integrate options for In-App Automation (IAA) customization. In-App Automation (IAA) powers banner, modal, and fullscreen in-app messages. ## Customization Various options are available for customizing the container view for In-App Automation content via the native SDKs. Scenes are fully customizable in the dashboard and cannot be customized via the SDK. ## Styles Plists can be used to modify any of the default message styles that the SDK provides. Each message type can be customized with a different plist: - **Banner**: `UAInAppMessageBannerStyle.plist` - **HTML**: `UAInAppMessageHTMLStyle.plist` - **FullScreen**: `UAInAppMessageFullScreenStyle.plist` - **Modal**: `UAInAppMessageModalStyle.plist` These plists support the following values: - **Banner** - `additionalPadding`: _Padding_. Adds padding around the view. - `headerStyle`: _Text Style_. Customizes the message's header. - `bodyStyle`: _Text Style_. Customizes the message's body. - `mediaStyle`: _Media Style_. Customizes the message's media. - `buttonStyle`: _Buttons Style_. Customizes the message's buttons. - `maxWidth`: _Points_. Max width. - `tapOpacity`: _Tap Opacity_. Customizes the message's opacity it's tapped and a tap action is present. - `shadowStyle`: _Shadow Style_. Customizes the message's shadow. - **FullScreen** - `headerStyle`: _Text Style_. Customizes the banner's header. - `bodyStyle`: _Text Style_. Customizes the banner's body. - `mediaStyle`: _Media Style_. Customizes the banner's media. - `buttonStyle`: _Buttons Style_. Customizes the banner's buttons. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - **Modal** - `additionalPadding`: _Padding_. Adds padding around the view. - `headerStyle`: _Text Style_. Customizes the banner's header. - `bodyStyle`: _Text Style_. Customizes the banner's body. - `mediaStyle`: _Media Style_. Customizes the banner's media. - `buttonStyle`: _Buttons Style_. Customizes the banner's buttons. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - `maxWidth`: _Points_. Max width. - `maxHeight`: _Points_. Max height. - `extendFullScreenLargeDevice`: _Boolean_. True to allow the option 'Display fullscreen on small screen device' to extend to large devices as well. - **HTML** - `additionalPadding`: _Padding_. Adds padding around the view. - `dismissIconResource`: String. Resource name for a custom dismiss icon. - `maxWidth`: _Points_. Max width. - `maxHeight`: _Points_. Max height. - `extendFullScreenLargeDevice`: _Boolean_. True to allow the option 'Display fullscreen on small screen device' to extend to large devices as well. - **Padding** - `top`: _Points_. Top padding. - `bottom`: _Points_. Bottom padding. - `leading`: _Points_. Leading padding. - `trailing`: _Points_. Trailing padding. - **Buttons Style** - `additionalPadding`: _Padding_. Adds padding around the button area. - `buttonHeight`: _Points_. Button height. - `stackedButtonSpacing`: _Points_. Button spacing in the stacked layout. - `separatedButtonSpacing`: _Points_. Button spacing in the separated layout. - `borderWidth`: _Points_. Button's border width. - `buttonTextStyle`: _Text Style_. Text style for each button. - **Text Style** - `additionalPadding`: _Padding_. Adds padding around the view. - `letterSpacing`: _Points_. Spacing between the letters. - `lineSpacing`: _Points_. Spacing between lines. - **Media Style** - `additionalPadding`: _Padding_. Adds padding around the view. - **Shadow Style** - `colorHex`: _Color_. Shadow color. - `radius`: _Points_. Shadow radius. - `xOffset`: _Points_. Shadow x-axis offset. - `yOffset`: _Points_. Shadow y-axis offset. ## Fonts You can use custom fonts in your in-app messages by adding them to your app bundle and configuring them in the Airship dashboard. ### Custom Fonts Fonts added to the app bundle are available for use with in-app messaging. To add fonts, please read the [The UIKit Custom Fonts Guide](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app). ![iOS Custom Font](https://www.airship.com/docs/images/ios/ios-custom-font_hu_abb515eea5dfec6b.webp) *iOS Custom Font* After adding fonts to your app, create a Font Stack in the Airship dashboard by following the steps in [Setting brand guidelines](https://www.airship.com/docs/guides/messaging/features/brand-guidelines/). Then you can select the stack when [setting in-app message defaults](https://www.airship.com/docs/guides/messaging/in-app-experiences/configuration/defaults/) and creating in-app messages. ### Dynamic fonts With HTML in-app messages Most In-App message styles support automatically scaling fonts through the use of Dynamic Type. However, automatically scaling fonts in HTML In-App messages requires you to use the following Apple system fonts when specifying the CSS font property: - `-apple-system-body` - `-apple-system-headline` - `-apple-system-subheadline` - `-apple-system-caption1` - `-apple-system-caption2` - `-apple-system-footnote` - `-apple-system-short-body` - `-apple-system-short-headline` - `-apple-system-short-subheadline` - `-apple-system-short-caption1` - `-apple-system-short-footnote` - `-apple-system-tall-body` For example, to have the HTML body default to the Apple system font body style: ```html body { font: -apple-system-body; // available on Apple devices only } ``` For more information about dynamic type, please see this [WWDC video](https://developer.apple.com/videos/play/wwdc2017/245/). ## Customizing HTML In-App Messages > **Note:** In order for the Airship JavaScript interface to be loaded into the webview, the URL must be specified in the URL Allowlist. See [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/#url-allowlist) for configuration details. HTML in-app messages provide a way to display custom content inside a native web view. These types of in-app messages display with a dismiss button built in, but can also be customized to provide their own buttons capable of dismissing the view. Dismissing a view requires calling the dismiss function on the UAirship JavaScript interface with a button resolution object passed in as a parameter. The button resolution object is a JSON object containing information about the interaction type and the button performing the dismissal. It should match the following format: ```javascript { "type" : "button_click", "button_info" : { "id" : "button identifier", "label" : {"text": "foo"} } } ``` The button resolution requires each of the key fields shown above. These include: - `type` — The type key with the value of resolution type `button_click` - `button_info` — The button info object containing required id and label fields - `id` — The button identifier - `label` — Label object containing the required text key - `text` — The text key with a string value representing the label text Providing a basic dismiss button in HTML: ```html ``` ## Custom adapters Providing an adapter allows defining the behavior of the custom type or overriding any of the default message types. The adapter will be created by the in-app messaging manager when a message's schedule is triggered. Once created, the adapter can define when the message is ready to display and the display behavior. After the message is displayed, the caller of the display method must be notified that the message is finished displaying by returning a `CustomDisplayResolution` when finished. This will allow for subsequent in-app messages to be displayed. **Example custom banner adapter** ```swift final class CustomBannerAdapter: CustomDisplayAdapter { private let message: InAppMessage private let assets: AirshipCachedAssetsProtocol init(message: InAppMessage, assets: any AirshipCachedAssetsProtocol) { self.message = message self.assets = assets } @MainActor var isReady: Bool { get { /// Called before display return true } } @MainActor func waitForReady() async { // If `isReady` is false this will be called to wait for the // adapter to be ready } @MainActor func display(scene: UIWindowScene) async -> CustomDisplayResolution { return await withCheckedContinuation { continuation in /// After displaying call the continuation with the result continuation.resume(returning: .userDismissed) } } } ``` **Register a factory block to return the adapter** ```swift /// Set the factory block after takeOff InAppAutomation.shared.inAppMessaging.setAdapterFactoryBlock(forType: .banner) { message, assets in return CustomBannerAdapter(message: message, assets: assets) } ``` ## Message Center Implement Message Center to provide an inbox for rich HTML-based messages, including display, theming, embedding, and advanced customization. # Message Center > Message Center provides an inbox for rich HTML-based messages that users can view at their convenience, with support for custom theming and display handling. ![Message Center inbox on iOS](https://www.airship.com/docs/images/message-center-apple.webp) *Message Center inbox on iOS* Message Center provides an inbox for rich, HTML-based messages. Learn more about Message Center in our [feature guide](https://www.airship.com/docs/guides/features/messaging/message-center/). ## Display the Message Center Display the Message Center with a single method call: #### Swift ```swift Airship.messageCenter.display() ``` #### Objective-C ```objc [UAirship.messageCenter display]; ``` This displays the Message Center as an overlay window, allowing users to view and manage their messages. When the user closes the Message Center, any changes (such as marking messages as read) are automatically synced with Airship. > **Note:** To embed the Message Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Message Center](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#handling-display-requests) to handle navigation to your embedded Message Center. ## Applying a Custom Theme You can customize the appearance of the Message Center by creating a `MessageCenterTheme` instance and setting its properties. The theme applies globally to all Message Centers displayed in your app. ### Setting the Theme Programmatically (Swift) #### Swift ```swift var theme = MessageCenterTheme() theme.cellTitleFont = .title theme.cellDateFont = .body theme.cellTitleColor = .primary theme.cellDateColor = .secondary theme.unreadIndicatorColor = .blue // Set the theme on the Message Center Airship.messageCenter.theme = theme ``` ### Setting the Theme from a Plist You can also customize the theme without writing code by creating a plist file. All keys in the plist correspond to properties on the `MessageCenterTheme` class. **Color Format:** - **Named colors**: Must correspond to a named color defined in a color asset within the main bundle - **Hexadecimal colors**: Use separate keys for light/dark mode (e.g., `cellTitleColor` and `cellTitleColorDark`) > **Note:** If your app is written in Objective-C, you must use the plist file to customize your theme, as `MessageCenterTheme` is a Swift struct. Save the plist as `MessageCenterTheme.plist` in your app bundle. #### Example Theme Plist **MessageCenterTheme.plist** ```xml refreshTintColor #333333 refreshTintColorDark #DDDDDD iconsEnabled placeholderIcon placeholderIcon cellTitleFont fontName ChalkboardSE-Regular fontSize 16 cellDateFont fontName ChalkboardSE-Regular fontSize 14 cellColor #DDDDDD cellColorDark #333333 cellTitleColor #000000 cellTitleColorDark #FFFFFF cellDateColor #222222 cellDateColorDark #CCCCCC cellSeparatorStyle none cellSeparatorColor #FFFFFF cellSeparatorColorDark #000000 cellTintColor #FF0000 cellTintColorDark #00FF00 unreadIndicatorColor #FF0000 unreadIndicatorColorDark #FF0000 selectAllButtonTitleColor #333333 selectAllButtonTitleColorDark #DDDDDD deleteButtonTitleColor #333333 deleteButtonTitleColorDark #DDDDDD markAsReadButtonTitleColor #333333 markAsReadButtonTitleColorDark #DDDDDD hideDeleteButton editButtonTitleColor #333333 editButtonTitleColorDark #DDDDDD cancelButtonTitleColor #333333 cancelButtonTitleColorDark #DDDDDD backButtonColor #333333 backButtonColorDark #DDDDDD navigationBarTitle Nav Bar Title ``` ## Working with Messages The Message Center provides methods to fetch, mark as read, and delete messages programmatically. ### Fetch Messages Retrieve messages from the inbox: #### Swift ```swift let messages = await Airship.messageCenter.inbox.messages ``` #### Objective-C ```objc [UAirship.messageCenter.inbox getMessagesWithCompletionHandler:^(NSArray *messages) { // Handle messages }]; ``` ### Listen for Message Updates Subscribe to message updates using Combine publishers: #### Swift ```swift Airship.messageCenter.inbox.messagePublisher .receive(on: RunLoop.main) .sink(receiveValue: { messages in // Update your UI with the new messages self.messages = messages }) .store(in: &self.subscriptions) ``` #### Objective-C ```objc // Not available in Objective-C. Use KVO or polling instead. ``` ### Listen for Unread Count Changes Subscribe to unread count updates: #### Swift ```swift Airship.messageCenter.inbox.unreadCountPublisher .receive(on: RunLoop.main) .sink { unreadCount in // Update badge or UI self.unreadCount = unreadCount } .store(in: &self.subscriptions) ``` #### Objective-C ```objc // Not available in Objective-C. Use KVO or polling instead. ``` ### Refresh Messages Manually refresh the message list from the server: #### Swift ```swift let refreshed = await Airship.messageCenter.inbox.refreshMessages() ``` #### Objective-C ```objc [UAirship.messageCenter.inbox refreshMessagesWithCompletionHandler:^(BOOL result) { // Handle result }]; ``` ### Mark Messages as Read Mark one or more messages as read: #### Swift ```swift await Airship.messageCenter.inbox.markRead(messageIDs: [messageID]) ``` #### Objective-C ```objc [UAirship.messageCenter.inbox markReadWithMessageIDs:@[messageID] completionHandler:^{ // Marked read }]; ``` ### Delete Messages Delete one or more messages: #### Swift ```swift await Airship.messageCenter.inbox.delete(messageIDs: [messageID]) ``` #### Objective-C ```objc [UAirship.messageCenter.inbox deleteWithMessageIDs:@[messageID] completionHandler:^{ // Deleted }]; ``` ## Filter Messages by Named User By default, Message Center displays all messages sent to the device's channel. If multiple users log into your app on the same device, they'll all see the same messages. To filter messages by named user, set up filtering in your custom Message Center implementation. See [Message Center Filtering](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/embedding/#message-center-filtering) in the Embedding guide. When creating Message Center messages, include a custom key with `named_user_id` as the key and the user's actual ID as the value: - **For the API**: Use the `extra` object in the [Message Center object](https://www.airship.com/docs/developer/rest-api/ua/schemas/push/#messageobject). - **In the dashboard**: See [Add custom keys](https://www.airship.com/docs/guides/messaging/messages/content/app/message-center/#add-custom-keys) in the Message Center content guide. ### Filtering Behavior With named user filtering enabled: - If you target `User A` in a message while they are logged in, the message appears in their inbox. - If you target `User B` in a message while they are logged in, the message appears in their inbox. - If you target `User A` or `User B` while the other is logged in, the message does not appear. - If you target `User A` or `User B` while neither is logged in, the message does not appear. # Embed the Message Center > Customize the Message Center appearance with SwiftUI view styles, create custom implementations with UIKit, and filter messages by named user. By default, Airship displays the Message Center as an overlay on top of your app. For tighter integration with your app's navigation flow, you can embed the Message Center views directly into your app. ## Prerequisites Message Center requires the `AirshipMessageCenter` module. See the [SDK installation guide](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) for setup instructions. ## Embedding the Message Center View The `MessageCenterView` (Swift) provides a complete Message Center with a built-in navigation stack. You can choose between different navigation styles for optimal display on iPhone and iPad. ### Using MessageCenterView with Navigation #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { var body: some View { // Default navigation style (adaptive) MessageCenterView() } } ``` #### Objective-C ```objc // Not supported. Use display callbacks to show custom UI (see below). ``` ### Navigation Styles `MessageCenterView` supports different navigation styles via the `navigationStyle` parameter: #### Swift ```swift // Auto navigation (default - adaptive based on device) MessageCenterView(navigationStyle: .auto) // Stack navigation (single column) MessageCenterView(navigationStyle: .stack) // Split navigation (master-detail for iPad) MessageCenterView(navigationStyle: .split) ``` - **`.auto`** (default): Automatically uses split view on iPad and stack view on iPhone - **`.stack`**: Single-column navigation for all devices - **`.split`**: Two-column master-detail layout for all devices ### Content View Without Navigation Stack For full control over the navigation, use `MessageCenterContent` (Swift only) which provides just the content without a built-in navigation stack. This requires a `MessageCenterController` to manage the message list state. #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { @StateObject private var controller = MessageCenterController() var body: some View { NavigationStack { MessageCenterContent(controller: controller) .navigationTitle("Messages") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Settings") { // Custom action } } } } } } ``` You can also provide a predicate to filter messages: #### Swift ```swift MessageCenterContent( controller: controller, predicate: CustomPredicate() ) ``` > **Note:** To apply a custom theme globally, see [Getting Started: Applying a Custom Theme](https://www.airship.com/docs/developer/sdk-integration/apple/message-center/getting-started/#applying-a-custom-theme). ### Using MessageCenterController with NavigationView (Legacy) For apps that need to support older iOS versions or integrate with `NavigationView`, you can use `MessageCenterController` to manually manage navigation state: #### Swift ```swift import SwiftUI import AirshipMessageCenter struct MyMessageCenterScreen: View { @StateObject private var messageCenterController = MessageCenterController() var body: some View { NavigationView { ZStack { MessageCenterContent(controller: self.messageCenterController) NavigationLink( destination: Group { if case .message(let messageID) = self.messageCenterController.path.last { MessageCenterMessageViewWithNavigation(messageID: messageID) { // Clear selection on close self.messageCenterController.path.removeAll() } } else { EmptyView() } }, isActive: Binding( get: { self.messageCenterController.path.last != nil }, set: { isActive in if !isActive { self.messageCenterController.path.removeAll() } } ) ) { EmptyView() } .hidden() } } } } ``` This pattern is also useful for UIKit integration where you need manual control over navigation state. ## Customizing View Styles (Swift Only) You can customize the appearance of the Message Center by creating custom styles for the view wrapper and message list. ### Custom Message Center View Style Create a custom style that implements `MessageCenterViewStyle` to control the navigation wrapper around the Message Center content: #### Swift ```swift struct CustomMessageCenterViewStyle: MessageCenterViewStyle { @ViewBuilder func makeBody(configuration: Configuration) -> some View { if #available(iOS 16.0, *) { NavigationStack { configuration.content .navigationBarTitleDisplayMode(.inline) .navigationTitle(Text("Custom Message Center")) .toolbarBackground(.mint, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(configuration.colorScheme) } } else { NavigationView { configuration.content .navigationTitle(Text("Custom Message Center")) .navigationBarTitleDisplayMode(.large) } .navigationViewStyle(.stack) } } } ``` Apply the custom style: #### Swift ```swift MessageCenterView() .messageCenterViewStyle(CustomMessageCenterViewStyle()) ``` ### Customize Item and Message Views Customize the Message Center item view and message view: #### Swift ```swift MessageCenterView() .messageCenterTheme(theme) .setMessageCenterItemViewStyle(messageCenterListItemViewStyle) .setMessageCenterMessageViewStyle(messageViewStyle) ``` ## Message Center Filtering Filter messages using a predicate. Only messages that match the predicate will be displayed. ### Filter by Named User Filter messages to show only those for the current named user: #### Swift ```swift class NamedUserPredicate: MessageCenterPredicate { func evaluate(message: MessageCenterMessage) -> Bool { guard let namedUserID = Airship.contact.namedUserID else { return false } // Check if message has matching named_user_id in extras if let extras = message.extras, let messageNamedUserID = extras["named_user_id"] as? String { return messageNamedUserID == namedUserID } return false } } Airship.messageCenter.predicate = NamedUserPredicate() ``` #### Objective-C ```objc // Not supported. Filtering requires Swift implementation. ``` ### Custom Filtering Create custom predicates for any filtering logic: #### Swift ```swift class CustomPredicate: MessageCenterPredicate { func evaluate(message: MessageCenterMessage) -> Bool { // Example: Only show messages with "cool" in the title return message.title.contains("cool") } } Airship.messageCenter.predicate = CustomPredicate() ``` #### Objective-C ```objc // Not supported. Filtering requires Swift implementation. ``` If you're embedding `MessageCenterView` directly, pass the predicate through the view modifier: #### Swift ```swift MessageCenterView( controller: MessageCenterController() ) .messageCenterPredicate(CustomPredicate()) ``` ## Custom Message Center Implementation For complete control over Message Center placement and navigation, create a custom implementation using the Message Center components. ### Key Components **MessageCenter** : The main entry point for fetching messages and handling callbacks. Access via `Airship.messageCenter`. **MessageCenterInboxProtocol** : Provides an interface for retrieving messages asynchronously and accessing the local message array. > **Note:** The message list uses CoreData. Message objects are ephemeral references refreshed with the list. Don't hold onto individual message instances indefinitely. **MessageCenterMessage** : Model object representing an individual message. Instances don't contain the message body—they point to authenticated URLs that should be displayed in a webview. **Display Callbacks** : Set `Airship.messageCenter.onDisplay` to handle when messages should be displayed, and `Airship.messageCenter.onDismissDisplay` to handle dismiss events. **NativeBridge** : For custom webview implementations, set a `NativeBridge` instance as the navigation delegate on your `WKWebView` to enable JavaScript bridge functionality. ### Handling Display Requests {#handling-display-requests} Set display callbacks after `takeOff` to handle Message Center display events: #### Swift ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Call takeOff try! Airship.takeOff(config, launchOptions: launchOptions) // Set Message Center display callback Airship.messageCenter.onDisplay = { messageID in // Navigate to your custom Message Center UI // messageID is optional - nil means show the full list // Return true to prevent default SDK display return true } // Set Message Center dismiss callback Airship.messageCenter.onDismissDisplay = { // Dismiss your custom Message Center UI } return true } ``` #### Objective-C ```objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Call takeOff [UAirship takeOff:config launchOptions:launchOptions error:nil]; // Set Message Center display callback UAirship.messageCenter.onDisplay = ^BOOL(NSString * _Nullable messageID) { // Navigate to your custom Message Center UI // messageID is optional - nil means show the full list // Return YES to prevent default SDK display return YES; }; // Set Message Center dismiss callback UAirship.messageCenter.onDismissDisplay = ^{ // Dismiss your custom Message Center UI }; return YES; } ``` ## Badge Updates Update the app badge to reflect the Message Center unread count: #### Swift ```swift Task { UIApplication.shared.applicationIconBadgeNumber = await Airship.messageCenter.inbox.unreadCount } ``` #### Objective-C ```objc [UAirship.messageCenter.inbox getUnreadCountWithCompletionHandler:^(NSInteger unreadCount) { dispatch_async(dispatch_get_main_queue(), ^{ UIApplication.sharedApplication.applicationIconBadgeNumber = unreadCount; }); }]; ``` > **Important:** If you use this method, don't include badge values in push notification payloads, as the Message Center will override them. ## Preference Center Implement Preference Centers to allow users to manage their subscription preferences, including display, theming, and embedding options. # Preference Center > Display Preference Centers using the Airship UI, which automatically handles user preferences and syncs with the Airship backend. ![Preference Center on iOS](https://www.airship.com/docs/images/preference-center-apple_hu_615c93ae107ba1cb.webp) *Preference Center on iOS* > **Important:** Airship Preference Centers are widgets that can be embedded in a page in an app or website. Please verify with your legal team that your full Preference Center page, including any web page for email Preference Centers, is compliant with local privacy regulations. The Preference Center allows users to opt in and out of subscription lists configured in the Airship Dashboard. The `AirshipPreferenceCenter` module provides a complete, ready-to-use UI that displays over your app. For more information about configuring Preference Centers, see the [Preference Center user guide](https://www.airship.com/docs/guides/messaging/features/preference-centers/). ## Displaying a Preference Center Display a Preference Center with a single method call. The Preference Center will appear in its own window over your app with the provided Airship UI. #### Swift ```swift Airship.preferenceCenter.display("my-first-pref-center") ``` #### Objective-C ```objc [UAirship.preferenceCenter display:@"my-first-pref-center"]; ``` This displays the Preference Center as an overlay window, allowing users to manage their subscription preferences. When the user closes the Preference Center, any changes are automatically synced with Airship. > **Note:** To embed the Preference Center directly in your app's navigation instead of displaying it as an overlay, see [Embedding the Preference Center](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/). You can also [intercept display requests](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/embedding/#handling-display-requests) to handle navigation to your embedded Preference Center. ## Applying a Custom Theme You can customize the appearance of the Preference Center by creating a `PreferenceCenterTheme` instance and setting its properties. The theme applies globally to all Preference Centers displayed in your app. ### Setting the Theme Programmatically (Swift) #### Swift ```swift // Customize your Theme var theme = PreferenceCenterTheme() theme.viewController = PreferenceCenterTheme.ViewController( navigationBar: PreferenceCenterTheme.NavigationBar( title: "My preference center", backgroundColor: .orange ) ) theme.preferenceCenter = PreferenceCenterTheme.PreferenceCenter( subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .subheadline, color: .yellow ), retryButtonBackgroundColor: .green, retryButtonLabelAppearance: PreferenceCenterTheme.TextAppearance( font: .title3, color: .black ) ) theme.contactSubscription = PreferenceCenterTheme.ContactSubscription( titleAppearance: PreferenceCenterTheme.TextAppearance( font: .title, color: .red ), subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .title2, color: .yellow ) ) theme.channelSubscription = PreferenceCenterTheme.ChannelSubscription( titleAppearance: PreferenceCenterTheme.TextAppearance( font: .title, color: .red ), subtitleAppearance: PreferenceCenterTheme.TextAppearance( font: .title2, color: .yellow ) ) // Set the Theme on the Preference Center Airship.preferenceCenter.theme = theme ``` ### Setting the Theme in SwiftUI In SwiftUI, you can apply a theme directly to the `PreferenceCenterView`: #### Swift ```swift PreferenceCenterView( preferenceCenterID: "preferenceCenter-ID" ) .preferenceCenterTheme(theme) ``` ### Setting the Theme from a Plist You can also customize the theme without writing code by creating a plist file. All keys in the plist correspond to properties on the `PreferenceCenterTheme` class. Colors are represented by strings, either a valid color hexadecimal (e.g., `#FF0000`) or a named color. Named color strings must correspond to a named color defined in a color asset within the main bundle. > **Note:** If your app is written in Objective-C, you must use the plist file to customize your theme, as `PreferenceCenterTheme` is a Swift struct. Save the plist as `AirshipPreferenceCenterTheme.plist` in your app bundle, then load it: #### Swift ```swift try Airship.preferenceCenter.setThemeFromPlist("AirshipPreferenceCenterTheme") ``` #### Objective-C ```objc NSError *error = nil; [UAirship.preferenceCenter setThemeFromPlist:@"AirshipPreferenceCenterTheme" error:&error]; if (error) { NSLog(@"Failed to set theme: %@", error); } ``` #### Example Theme Plist **AirshipPreferenceCenterTheme.plist** ```xml viewController navigationBar title Preference Center titleFont fontName Helvetica fontSize 15 titleColor #0000FF preferenceCenter subtitleAppearance commonSection titleAppearance color #de0000 font fontName Helvetica fontSize 32 subtitleAppearance color #da833b font fontName Helvetica fontSize 25 labeledSectionBreak titleAppearance channelSubscription titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 contactSubscription titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 contactSubscriptionGroup titleAppearance color #034710 font fontName Helvetica fontSize 20 subtitleAppearance color #8fe388 font fontName Helvetica fontSize 15 chip checkColor #3bd2d6 borderColor #0a0fc9 labelAppearance color #7c6bea font fontName Helvetica fontSize 15 alert titleAppearance color #0a0fc9 font fontName Helvetica fontSize 15 subtitleAppearance color #d1b4d4 buttonLabelAppearance color #78c8c0 font fontName Helvetica fontSize 25 buttonBackgroundColor #da833b ``` # Embed the Preference Center > Embed the Preference Center view directly in your app's navigation instead of displaying it as an overlay. By default, Airship displays the Preference Center as an overlay on top of your app. For tighter integration with your app's navigation flow, you can embed the Preference Center views directly into your app. ## Embedding the Preference Center View The `PreferenceCenterView` (Swift) or `UAPreferenceCenterViewControllerFactory` (Objective-C) provides a complete Preference Center with a built-in navigation stack. #### Swift ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { PreferenceCenterView(preferenceCenterID: "my_preference_center_id") } } ``` #### Objective-C ```objc @import AirshipCore; // Create a view controller UIViewController *preferenceCenterVC = [UAPreferenceCenterViewControllerFactory makeViewControllerWithPreferenceCenterID:@"my_preference_center_id"]; // Present or push the view controller [self.navigationController pushViewController:preferenceCenterVC animated:YES]; ``` Or embed it in a container view: ```objc @import AirshipCore; // Embed the preference center in a container view NSError *error = nil; UIView *containerView = [UAPreferenceCenterViewControllerFactory embedWithPreferenceCenterID:@"my_preference_center_id" preferenceCenterThemePlist:nil inParentViewController:self error:&error]; if (error) { NSLog(@"Failed to embed preference center: %@", error); } else { // Add the container view to your view hierarchy containerView.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:containerView]; [NSLayoutConstraint activateConstraints:@[ [containerView.topAnchor constraintEqualToAnchor:self.view.topAnchor], [containerView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [containerView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [containerView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] ]]; } ``` ### Content View Without Navigation Stack For more control over the navigation, use `PreferenceCenterContent` (Swift only) which provides just the content without a built-in navigation stack. ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { NavigationView { PreferenceCenterContent(preferenceCenterID: "my_preference_center_id") .navigationTitle("Preferences") } } } ``` > **Note:** To apply a custom theme globally, see [Getting Started: Applying a Custom Theme](https://www.airship.com/docs/developer/sdk-integration/apple/preference-center/getting-started/#applying-a-custom-theme). ## Customizing View Styles (Swift Only) For SwiftUI views, you can apply style overrides to customize individual components within the Preference Center. These view modifiers allow granular control over specific sections without affecting the global theme. ### Available Style Overrides - **`.channelSubscriptionStyle(_:)`** - Customize channel subscription views - **`.commonSectionViewStyle(_:)`** - Adjust common sections - **`.contactManagementSectionStyle(_:)`** - Modify contact management sections - **`.contactSubscriptionGroupStyle(_:)`** - Tailor contact subscription groups - **`.contactSubscriptionStyle(_:)`** - Customize individual contact subscriptions - **`.labeledSectionBreakStyle(_:)`** - Define labeled section breaks - **`.alertStyle(_:)`** - Adjust alert appearance ### Example ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { PreferenceCenterView(preferenceCenterID: "my_preference_center_id") .channelSubscriptionStyle(MyCustomChannelStyle()) .contactSubscriptionStyle(MyCustomContactStyle()) .alertStyle(MyCustomAlertStyle()) } } // Define custom styles by conforming to the respective protocols struct MyCustomChannelStyle: ChannelSubscriptionStyle { // Implement required style methods } struct MyCustomContactStyle: ContactSubscriptionStyle { // Implement required style methods } struct MyCustomAlertStyle: AlertStyle { // Implement required style methods } ``` For detailed information on creating custom styles and the available properties for each style protocol, see the [AirshipPreferenceCenter View extension documentation](https://urbanairship.github.io/ios-library/v20/AirshipPreferenceCenter/documentation/airshippreferencecenter/swiftuicore/view). ## Advanced: Custom Loading (Swift Only) For SwiftUI apps, you can customize the loading behavior and respond to phase changes using `PreferenceCenterContent`: ```swift import SwiftUI import AirshipPreferenceCenter struct MyPreferenceCenterScreen: View { var body: some View { NavigationView { PreferenceCenterContent( preferenceCenterID: "my_preference_center_id", onLoad: { preferenceCenterID in // Custom loading logic // Return a PreferenceCenterContentPhase return await loadPreferenceCenter(preferenceCenterID) }, onPhaseChange: { phase in switch phase { case .loading: print("Loading preference center...") case .error(let error): print("Failed to load: \(error)") case .loaded(let state): print("Loaded with state: \(state)") } } ) .navigationTitle("Preferences") } } func loadPreferenceCenter(_ id: String) async -> PreferenceCenterContentPhase { // Custom loading implementation // Return .loading, .error, or .loaded return .loading } } ``` The `PreferenceCenterViewPhase` enum represents the current state of the Preference Center: - `.loading` — The view is loading - `.error(Error)` — The view failed to load the config - `.loaded(PreferenceCenterState)` — The view is loaded with the state ## Handling Display Requests When embedding a Preference Center, you need to intercept Airship's display requests and navigate to your embedded view instead of showing the default overlay. #### Swift ```swift Airship.preferenceCenter.onDisplay = { preferenceCenterID in guard preferenceCenterID == "my_embedded_preference_center_id" else { // Not the embedded one, allow Airship to display it as an overlay return false } // Navigate to your embedded view // Example: Use your app's navigation system to show the screen NotificationCenter.default.post( name: .showEmbeddedPreferenceCenter, object: nil, userInfo: ["id": preferenceCenterID] ) // Return true to indicate you handled the display return true } ``` #### Objective-C ```objc @import AirshipCore; // In your app delegate or preference center manager @interface MyAppDelegate () @end @implementation MyAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Set the open delegate UAirship.preferenceCenter.openDelegate = self; return YES; } - (BOOL)openPreferenceCenter:(NSString *)preferenceCenterID { if (![preferenceCenterID isEqualToString:@"my_embedded_preference_center_id"]) { // Not the embedded one, allow Airship to display it as an overlay return NO; } // Navigate to your embedded view [[NSNotificationCenter defaultCenter] postNotificationName:@"ShowEmbeddedPreferenceCenter" object:nil userInfo:@{@"id": preferenceCenterID}]; // Return YES to indicate you handled the display return YES; } @end ``` By returning `true` (or `YES` in Objective-C), you tell Airship that you've handled the display request, preventing the default overlay from appearing. ## Audience Management Integrate audience management features into your app. This guide covers how to identify contacts, access channel IDs, and set tags, attributes, and subscription lists on channels and contacts. For information about using these features for segmentation and targeting, see the [Audience User Guide]({{< ref "/guides/audience/segmentation/segmentation.md" >}}). # Channels > Access and manage channel IDs, listen for channel creation, and configure the channel capture tool. Each device/app install will generate a unique identifier known as the Channel ID. Once a Channel ID is created, it will persist in the application until the app is reinstalled, or has its internal data is cleared. If `AirshipMessageCenter` is installed, the SDK will attempt to restore the Channel ID across app reinstalls. For information about finding Channel IDs, using the Channel Capture tool, and other methods to access Channel IDs, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). ## Accessing the Airship Channel ID Apps can access the Channel ID directly through the SDK. #### Swift ```swift let channelID = Airship.channel.identifier ``` #### Objective-C ```objc NSString *channelID = UAirship.channel.identifier; ``` The Channel ID is asynchronously created, so it may not be available right away on the first run. Changes to Channel data will automatically be batched and applied when the Channel is created, so there is no need to wait for the Channel to be available before modifying any data. Applications that need to access the Channel ID can use a listener to be notified when it is available. #### Swift Using `identifierUpdates` (AsyncStream): ```swift Task { for await channelID in Airship.channel.identifierUpdates { print("Channel ID: \(channelID)") } } ``` Using `NotificationCenter`: ```swift NotificationCenter.default.addObserver( self, selector: #selector(refreshView), name: AirshipNotifications.ChannelCreated.name, object: nil ) ``` #### Objective-C Using `NotificationCenter`: ```objc [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshView) name:UAirshipNotificationChannelCreated.name object:nil]; ``` ## Channel Capture tool The Channel Capture tool is a feature built into the SDK that helps users find their Channel ID. For detailed information about how it works and how to use it, see [Finding Channel IDs](https://www.airship.com/docs/guides/getting-started/developers/identifiers/). The Channel Capture tool can be disabled through the Airship Config options passed to `takeOff` during SDK initialization. For information about setting up the Airship SDK and configuring `AirshipConfig`, see [iOS SDK Setup](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/). #### Swift ```swift config.isChannelCaptureEnabled = false ``` #### Objective-C ```objc config.isChannelCaptureEnabled = NO; ``` ## Delaying channel creation Airship creates the channel if at least one feature is enabled in the Privacy Manager. To delay channel creation, use the Privacy Manager to disable all features during takeOff. For more information about Privacy Manager, see [Privacy Manager](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/privacy-manager/). # Contacts > Identify contacts, reset contacts, and get named user IDs. A Contact is any user in your project. Contacts are identified as either an Anonymous Contact or a Named User. Airship can set targeting data on these identifiers, which are also used to map devices and channels to a specific user. For detailed information about contacts and named users, see [Named users](https://www.airship.com/docs/guides/audience/named-users/). ## Managing the Contact's identifier (Named User ID) Identify can be called multiple times with the same Named User ID. The SDK will automatically deduplicate `identify` calls made with the same Named User ID. If the ID is changed from a previous value, the Contact will automatically be dissociated from the previous Named User ID. #### Swift ```swift Airship.contact.identify("some named user ID") ``` #### Objective-C ```objective-c [UAirship.contact identify:@"some named user ID"]; ``` If the user logs out of the device, you may want to reset the contact. This will clear any anonymous data and dissociate the contact from the Named User ID, if set. This should only be called when the user manually logs out of the app, otherwise you will not be able to target the Channel by its Contact data. #### Swift ```swift Airship.contact.reset() ``` #### Objective-C ```objc [UAirship.contact reset]; ``` You can get the Named User ID only if you set it through the SDK. #### Swift ```swift await Airship.contact.namedUserID ``` #### Objective-C ```objc [UAirship.contact getNamedUserIDWithCompletionHandler:^(NSString *namedUserID) { }]; ``` ### Email channel association When an email address is registered through the SDK, it will be registered for both transactional and commercial emails by default. To change this behavior, you can override the options to request [[Double Opt-In](https://www.airship.com/docs/reference/glossary/#double_opt_in)](https://www.airship.com/docs/developer/api-integrations/email/getting-started/#double-opt-in) for commercial messages. #### Swift ```swift let options = EmailRegistrationOptions.commercialOptions( transactionalOptedIn: transactionalDate, commercialOptedIn: commercialDate, properties: properties ) Airship.contact.registerEmail("your@example.com", options: options) ``` #### Objective-C ```objc UAEmailRegistrationOptions* options = [UAEmailRegistrationOptions commercialOptionsWithTransactionalOptedIn:transactionalDate commercialOptedIn:commercialDate properties:properties]; [UAirship.contact registerEmail:@"your@example.com" options:options]; ``` ### SMS channel association When an [MSISDN](https://www.airship.com/docs/reference/glossary/#msisdn) is registered through the SDK, Airship sends a message to that number, prompting them to opt in. For more information, see the SMS platform documentation: [Non-Mobile Double Opt-In](https://www.airship.com/docs/developer/api-integrations/sms/opt-in-out-handling/#non-mobile-double-opt-in). #### Swift ```swift let options = SMSRegistrationOptions.optIn(senderID: "senderId") Airship.contact.registerSMS("yourMsisdn", options: options) ``` #### Objective-C ```objc UASMSRegistrationOptions* options = [UASMSRegistrationOptions optInSenderID:@"senderId"]; [UAirship.contact registerSMS:"yourMsisdn" options:options]; ``` ### Open Channel association Open Channels support notifications to any medium that can accept a JSON payload, through either the Airship API or web dashboard. For more information about Open Channels, see the [Open Channels documentation](https://www.airship.com/docs/developer/api-integrations/open/getting-started/). #### Swift ```swift let options = OpenRegistrationOptions.optIn(platformName: "platformName", identifiers: identifiers) Airship.contact.registerOpen("address", options: options) ``` #### Objective-C ```objc UAOpenRegistrationOptions* options = [UAOpenRegistrationOptions optInSenderID:@"platformName"]; [UAirship.contact registerOpen:"address" options:options]; ``` # Tags > Set device tags, contact tags, and tag groups for audience segmentation. For information about tags, including how to use them for segmentation and targeting, see the [Tags user guide](https://www.airship.com/docs/guides/audience/tags/). ## Channel Tags Channel tags are tags managed on the Channel by the SDK. Device tags (tags without a group) can be modified or fetched from the Channel. #### Swift ```swift Airship.channel.editTags { editor in editor.set(["one", "two", "three"]) editor.add("a_tag") editor.remove("three") } // Accessing channel tags let tags = Airship.channel.tags ``` #### Objective-C ```objc [UAirship.channel editTags:^(UATagEditor *editor) { [editor setTags:@[@"one", @"two", @"three"]]; [editor addTag:@"a_tag"]; [editor removeTag:@"three"]; }]; // Accessing channel tags NSArray* tags = [[UAirship channel] tags]; ``` ## Channel Tag Groups Tag groups are tags scoped within a group. Tag groups can be modified from the SDK but cannot be fetched. Device tags (tags without a group) can be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Swift ```swift Airship.channel.editTagGroups { editor in editor.add(["silver-member", "gold-member"], group:"loyalty") editor.remove(["bronze-member", "club-member"], group:"loyalty") editor.set(["bingo"], group:"games") } ``` #### Objective-C ```objc [UAirship.channel editTagGroups:^(UATagGroupsEditor *editor) { [editor addTags:@[@"silver-member", @"gold-member"] group:@"loyalty"]; [editor removeTags:@[@"bronze-member", @"club-member"] group:@"loyalty"]; [editor setTags:@[@"bingo"] group:@"games"]; }]; ``` ## Contact Tag Groups Contact tag groups are tags scoped within a group at the Contact level. Tag groups can be modified from the SDK but cannot be fetched. If you need to be able to fetch tag groups, consider using subscription lists. #### Swift ```swift Airship.contact.editTagGroups { editor in editor.add(["silver-member", "gold-member"], group:"loyalty") editor.remove(["bronze-member", "club-member"], group:"loyalty") editor.set(["bingo"], group:"games") } ``` #### Objective-C ```objc [UAirship.contact editTagGroups:^(UATagGroupsEditor *editor) { [editor addTags:@[@"silver-member", @"gold-member"] group:@"loyalty"]; [editor removeTags:@[@"bronze-member", @"club-member"] group:@"loyalty"]; [editor setTags:@[@"bingo"] group:@"games"]; }]; ``` ## Verifying Tags To verify that tags have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the tags and tag groups associated with a channel or contact. # Attributes > Set channel and contact attributes as key-value pairs for personalization. For information about Attributes, including overview, use cases, and how to target Attributes, see [About Attributes](https://www.airship.com/docs/guides/audience/attributes/about/). ## Channel Attributes Channel attributes are attributes managed on the Channel by the SDK. #### Swift ```swift Airship.channel.editAttributes { editor in editor.set(string: "Bobby's Phone", attribute: "device_name") editor.set(number: 4.99, attribute: "average_rating") editor.remove("vip_status") } ``` #### Objective-C ```objc [UAirship.channel editAttributes:^(UAAttributesEditor * editor) { [editor setString:@"Bobby's Phone" attribute:@"device_name"]; [editor setNumber:@(4.99) attribute:@"average_rating"]; [editor removeAttribute:@"vip_status"]; }]; ``` ## Contact Attributes Contact attributes are attributes managed on the Contact by the SDK. #### Swift ```swift Airship.contact.editAttributes { editor in editor.set(string: "Bobby", attribute: "first_name") } ``` #### Objective-C ```objc [UAirship.contact editAttributes:^(UAAttributesEditor * editor) { [editor setString:@"Bobby" attribute:@"first_name"]; }]; ``` ## JSON Attributes JSON Attributes are data objects containing one or more string, number, date, or boolean key-value pairs. You can set and remove JSON Attributes on a Channel or a Contact. #### Swift ```swift Airship.contact.editAttributes { editor in try! editor.set( json: [ "key": .string("value"), "another_key": .string("another_value") ], attribute: "attribute_name", instanceID: "instance_id", expiration: Date.now ) } ``` ## Verifying Attributes To verify that attributes have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the attributes associated with a channel or contact. # Subscription Lists > Manage channel and contact subscription lists for topic-based messaging. For information about Subscription Lists, including overview, use cases, and how to create subscription lists, see [Subscription Lists](https://www.airship.com/docs/guides/audience/segmentation/audience-lists/subscription/). ## Channel Subscription Lists Channel subscriptions apply only to the single channel. #### Swift ```swift // Modifying channel subscription lists Airship.channel.editSubscriptionLists { editor in editor.subscribe("food") editor.unsubscribe("sports") } // Fetching channel subscription lists let channelSubscriptions = try await Airship.channel.fetchSubscriptionLists() ``` #### Objective-C ```objc // Modifying channel subscription lists UASubscriptionListEditor *channelEditor = [UAirship.channel editSubscriptionLists]; [channelEditor subscribe:@"food"]; [channelEditor unsubscribe:@"sports"]; [channelEditor apply]; // Fetching channel subscription lists [[UAChannel shared] fetchSubscriptionListsWithCompletionHandler:^(NSArray * _Nullable channelSubscriptionLists, NSError * _Nullable error) { // Use the channelSubscriptionLists }]; ``` ## Contact Subscription Lists Contact subscriptions are set at the user-level and require a Channel scope specifying the types that the subscription list applies to. #### Swift ```swift // Modifying contact subscription lists Airship.contact.editSubscriptionLists { editor in editor.subscribe("food", scope: .app) editor.unsubscribe("sports", scope: .sms) } // Fetching contact subscription lists let contactSubscriptions = try await Airship.contact.fetchSubscriptionLists() ``` #### Objective-C ```objc // Modifying contact subscription lists UAScopedSubscriptionListEditor *contactEditor = [[UAContact shared] editSubscriptionLists]; [contactEditor subscribe:"food" scope:"app"]; [contactEditor unsubscribe:"sports" scope:"sms"]; [contactEditor apply]; // Fetching contact subscription lists [UAirship.contact fetchSubscriptionListsWithCompletionHandler:^(NSDictionary * _Nullable, NSError * _Nullable) { // Use the subscriptionLists }]; ``` ## Verifying Subscription Lists To verify that subscription lists have been set correctly, look up the channel or contact in the [Contact Management](https://www.airship.com/docs/guides/audience/contact-management/) view. You can search by Channel ID or Named User ID to view the subscription lists associated with a channel or contact. ## Troubleshooting Common issues and solutions for Airship SDK setup, initialization, and integration. # Troubleshooting Initialization > Troubleshoot common initialization issues and apply solutions. When following steps in [Getting Started](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/) or [Advanced Integration](https://www.airship.com/docs/developer/sdk-integration/apple/installation/advanced-integration/), if you don't see a channel ID in the console logs or encounter errors during initialization, review the following common problems and solutions. ## takeOff errors The `takeOff` method throws an error in these cases: - `takeOff` has already been successfully called. - `takeOff` was called without an `AirshipConfig` instance and the SDK could not load or parse `AirshipConfig.plist`. - `takeOff` was called without an `AirshipConfig` instance and the parsed `AirshipConfig.plist` is invalid due to missing credentials. - `takeOff` was called with an `AirshipConfig` instance that has an invalid config due to missing credentials. ## takeOff called multiple times If `takeOff` throws because it has already been successfully called, verify the following: - `takeOff` is only called once per app launch - It's not called in both `application(_:didFinishLaunchingWithOptions:)` and your App's `init()` method - For SwiftUI apps, `takeOff` is called only in the App's `init()` method ## takeOff credentials The `takeOff` method only validates that credentials are present and formatted correctly. It does not verify that the credentials are valid against Airship servers. If the config is properly set up and Airship is only called once, no error will be thrown even if the credentials themselves are invalid. The credentials used by `takeOff` are your Airship project's [App Key](https://www.airship.com/docs/reference/glossary/#app_key) and [App Secret](https://www.airship.com/docs/reference/glossary/#app_secret). To find them, select the dropdown menu (▼) next to your project name, and then **Project details**. **Symptoms of missing or invalid credentials:** - No channel ID appears in the console logs - Warnings or errors in the logs after initialization - Channel is not created in the Airship dashboard If you are experiencing credential issues, do the following: 1. Compare your Airship project credentials with the values in your app, either in code or in `AirshipConfig.plist`. - Credentials must match the expected format and character set. - Credentials must not be empty strings or contain extra whitespace. 1. Ensure both `productionAppKey`/`productionAppSecret` and `developmentAppKey`/`developmentAppSecret` are set before calling `takeOff`. **Guidelines for credentials:** - Use development credentials for development builds and production credentials for release and TestFlight builds. - Configure both development and production credentials in your app, either in code or in `AirshipConfig.plist`. The SDK chooses which to use based on your build configuration. ## AirshipConfig.plist not found or invalid If `takeOff` fails because the SDK could not load or parse `AirshipConfig.plist`, or the plist is invalid, verify the following: - The file exists in your app bundle - The file is included in your target's Copy Bundle Resources build phase - All required keys are present in the plist file: `productionAppKey`, `productionAppSecret`, `developmentAppKey`, and `developmentAppSecret` - The plist file format is valid XML # Troubleshooting Push Notifications > Check push notification status and fix common issues. If [push notifications](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/) aren't working as expected, you can check the notification status to diagnose the issue. The SDK provides detailed information about their current state. ## Get Current Notification Status Read the current notification status from `Airship.push.notificationStatus` to inspect each field: #### Swift ```swift let status = await Airship.push.notificationStatus print("User notifications enabled: \(status.isUserNotificationsEnabled)") print("Notifications allowed: \(status.areNotificationsAllowed)") print("Privacy feature enabled: \(status.isPushPrivacyFeatureEnabled)") print("Push token registered: \(status.isPushTokenRegistered)") print("User opted in: \(status.isUserOptedIn)") print("Fully opted in: \(status.isOptedIn)") print("Display status: \(status.displayNotificationStatus)") ``` #### Objective-C > **Note:** This async property is not available in Objective-C. Use the `userPushNotificationsEnabled` property and check authorization status directly with `UNUserNotificationCenter`. ## Listen for Status Changes Use the following to monitor notification status changes in real time: #### Swift ```swift Task { for await status in await Airship.push.notificationStatusUpdates { print("Notification status changed:") print("User opted in: \(status.isUserOptedIn)") print("Fully opted in: \(status.isOptedIn)") } } ``` #### Objective-C > **Note:** This async stream is not available in Objective-C. Use the `userPushNotificationsEnabled` property and check authorization status directly with `UNUserNotificationCenter`. ## Understanding Notification Status Fields The `AirshipNotificationStatus` struct provides detailed information about why push might not be working: | Field | Description | |-------|-------------| | `isUserNotificationsEnabled` | Whether `Airship.push.userPushNotificationsEnabled` is set to `true` | | `areNotificationsAllowed` | Whether the user has granted notification permissions (at least one authorized type) | | `isPushPrivacyFeatureEnabled` | Whether the push privacy feature is enabled in `AirshipPrivacyManager` | | `isPushTokenRegistered` | Whether a push token has been successfully registered with the system | | `displayNotificationStatus` | The system permission status (`.granted`, `.denied`, `.notDetermined`, `.ephemeral`) | | `isUserOptedIn` | `true` if user notifications are enabled, privacy feature is enabled, notifications are allowed, and display status is granted | | `isOptedIn` | `true` if `isUserOptedIn` is `true` AND a push token is registered | ## Common Status Scenarios **Status:** `isUserNotificationsEnabled = false` - **Cause:** `Airship.push.userPushNotificationsEnabled` has not been set to `true`. - **Solution:** Enable user notifications in your app code. **Status:** `areNotificationsAllowed = false` - **Cause:** User denied notification permissions or permissions not yet requested. - **Solution:** Request notification permissions or guide user to system settings. **Status:** `isPushPrivacyFeatureEnabled = false` - **Cause:** Push privacy feature is disabled in Privacy Manager. - **Solution:** Enable the push privacy feature: `Airship.privacyManager.enabledFeatures = [.push]`. **Status:** `isPushTokenRegistered = false` - **Cause:** Device hasn't received a push token from APNs yet. - **Solution:** Check network connectivity, APNs certificate configuration, and device/simulator limitations. **Status:** `isUserOptedIn = true` but `isOptedIn = false` - **Cause:** Push token registration is pending or failed. - **Solution:** Check console logs for APNs registration errors, verify network connectivity, and ensure proper entitlements. # Troubleshooting Notification Service Extensions > Verify notification service extension setup and debug issues. If the basic verification steps do not resolve your [notification service extension](https://www.airship.com/docs/developer/sdk-integration/apple/push-notifications/notification-service-extension/) (NSE) issue, use the advanced debugging steps to isolate the cause. ## Basic verification First, perform basic verifications: 1. Verify the deployment target includes the device you are testing it against. Airship supports 16.0+. ![Deployment target setting in Xcode](https://www.airship.com/docs/images/nse-troubleshoot-deployment-version_hu_5e5f95dfb9a1775e.webp) *Deployment target setting in Xcode* 1. Verify the extension target links the `AirshipNotificationServiceExtension` framework and none of the other Airship frameworks. ![Extension target linked to AirshipNotificationServiceExtension only](https://www.airship.com/docs/images/extension-target-links-airship-notification-service-extension_hu_a3504fa3a2c481b6.webp) *Extension target linked to AirshipNotificationServiceExtension only* 1. Verify the main app target links to your extension. ![Main app target embedding the extension](https://www.airship.com/docs/images/main-app-target-extension_hu_fed870069ef4ef29.webp) *Main app target embedding the extension* 1. Verify the extension bundle ID follows the app bundle ID with a suffix. For example, `com.example.app.NotificationServiceExtension`. ## Advanced debugging If issues persist, isolate the problem by building up from Apple's default implementation: 1. Replace your NSE implementation with Apple's default NSE that only modifies the notification title. If that fails, recreate the extension and repeat the basic verification steps. #### Swift ```swift import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } ``` 1. Confirm the extension can modify the title when using Apple's default implementation. 1. Link the Airship NSE framework to your extension and have your NSE extend the Airship NSE class instead of `UNNotificationServiceExtension`. Keep your existing title-modification logic for now. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } } ``` 1. Confirm you still receive the modified title. This verifies the Airship NSE framework is linked correctly. 1. Remove the title-modification test code and implement the full Airship NSE integration. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { } ``` 1. Test that rich media, such as images, displays correctly. 1. Add verbose logging to troubleshoot the issue if none of these steps resolved the issue. #### Swift ```swift import UserNotifications import AirshipNotificationServiceExtension class NotificationService: UANotificationServiceExtension { override var airshipConfig: AirshipExtensionConfig { return AirshipExtensionConfig( logLevel: .verbose, logHandler: .publicLogger ) } } ``` ## Data Collection Overview of data collection and controls provided by the Airship SDK. # Privacy Manager > Use Privacy Manager to enable or disable Airship SDK features for privacy and consent management. Privacy Manager allows you to control which Airship SDK features are enabled. This is particularly useful for consent opt-in flows where you need to disable all features initially, then enable them as users grant consent. For information about what data is collected for each Privacy Manager flag, see [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/). When all features are disabled, the SDK operates in a no-op mode—it doesn't store data or make network requests. Once features are enabled, you can enable or disable specific features at runtime based on user consent. ## Privacy Manager flags Each Privacy Manager flag controls a group of related Airship features. Enabling a flag enables all features within that group: #### Swift | Swift Constant | Features | Config Value | |----------------|----------|--------------| | `AirshipFeature.push` | Push notifications | `push` | | `AirshipFeature.inAppAutomation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | `in_app_automation` | | `AirshipFeature.messageCenter` | Message Center | `message_center` | | `AirshipFeature.tagsAndAttributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | `tags_and_attributes` | | `AirshipFeature.contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | `contacts` | | `AirshipFeature.analytics` | Associated identifiers, Custom events, Screen tracking, Surveys (questions and NPS surveys in [Scenes](https://www.airship.com/docs/reference/glossary/#scene)), email address (via form inputs in Scenes), [Feature Flag](https://www.airship.com/docs/reference/glossary/#feature_flag) interaction | `analytics` | | `AirshipFeature.featureFlags` | Feature Flag evaluation and interaction | `feature_flags` | | `AirshipFeature.all` | All features | `all` | | `[]` | No features | `none` | #### Objective-C | Objective-C Constant | Features | Config Value | |----------------------|----------|--------------| | `UAFeatures.push` | Push notifications | `push` | | `UAFeatures.inAppAutomation` | In-App Automation, In-App Messages, Scenes, and Landing Pages | `in_app_automation` | | `UAFeatures.messageCenter` | Message Center | `message_center` | | `UAFeatures.tagsAndAttributes` | [Tags](https://www.airship.com/docs/guides/audience/tags/), [Attributes](https://www.airship.com/docs/guides/audience/attributes/about/), Subscription Lists, and Preference Center | `tags_and_attributes` | | `UAFeatures.contacts` | Contact Tags, Attributes, and Subscription Lists; Named User; and Associated Channels | `contacts` | | `UAFeatures.analytics` | Associated identifiers, Custom events, Screen tracking, Surveys (questions and NPS surveys in [Scenes](https://www.airship.com/docs/reference/glossary/#scene)), email address (via form inputs in Scenes), [Feature Flag](https://www.airship.com/docs/reference/glossary/#feature_flag) interaction | `analytics` | | `UAFeatures.featureFlags` | Feature Flag evaluation and interaction | `feature_flags` | | `UAFeatures.all` | All features | `all` | | `UAFeatures.none` | No features | `none` | ## Configuring default enabled features You can configure which features are enabled by default when the SDK initializes. This is done in your Airship config during [takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff). ### Default configuration (all features enabled) By default, all features are enabled. The SDK will collect data and make network requests as configured. ### Disabling all features To disable all features by default (useful for consent opt-in flows), set the enabled features to an empty array: #### Swift **Config** ```swift let config = AirshipConfig() config.enabledFeatures = [] try! Airship.takeOff(config) ``` **AirshipConfig.plist** ```xml ... enabledFeatures ... ``` #### Objective-C **UAConfig** ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; [UAirship takeOff:config error:&airshipError]; ``` **AirshipConfig.plist** ```xml ... enabledFeatures ... ``` ### Enabling specific features To enable only specific features by default: #### Swift **Config** ```swift let config = AirshipConfig() config.enabledFeatures = [.push, .analytics] try! Airship.takeOff(config) ``` **AirshipConfig.plist** ```xml ... enabledFeatures push analytics ... ``` #### Objective-C **UAConfig** ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesPush | UAFeaturesAnalytics; [UAirship takeOff:config error:&airshipError]; ``` **AirshipConfig.plist** ```xml ... enabledFeatures push analytics ... ``` ### Resetting features on each app start If you need to gather consent on each app start, enable `resetEnabledFeatures` to reset enabled features to the config defaults on each `takeOff`, ignoring any previously persisted settings: #### Swift ```swift var config = AirshipConfig() config.enabledFeatures = [] config.resetEnabledFeatures = true try! Airship.takeOff(config) ``` #### Objective-C ```objc UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; config.resetEnabledFeatures = YES; [UAirship takeOff:config error:&airshipError]; ``` ## Modifying enabled features at runtime You can enable or disable features at any time after takeOff. Once you modify the enabled features from the default, those settings are persisted between app launches (unless `resetEnabledFeatures` is enabled in your config). ### Enabling features #### Swift ```swift Airship.privacyManager.enableFeatures([.push, .analytics]) ``` #### Objective-C ```objc UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager enableFeatures:features]; ``` ### Disabling features #### Swift ```swift Airship.privacyManager.disableFeatures([.push, .analytics]) ``` #### Objective-C ```objc UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager disableFeatures:features]; ``` ## Consent opt-in flow example A common use case is to start with all features disabled, then enable them as users grant consent: #### Swift ```swift // Start with all features disabled var config = AirshipConfig() config.enabledFeatures = [] try! Airship.takeOff(config) // Later, when user grants consent: func userGrantedConsent() { // Enable features based on user's consent choices Airship.privacyManager.enableFeatures([.push, .analytics]) } ``` #### Objective-C ```objc // Start with all features disabled UAConfig *config = [UAConfig config]; config.enabledFeatures = UAFeaturesNone; [UAirship takeOff:config error:&airshipError]; // Later, when user grants consent: - (void)userGrantedConsent { // Enable features based on user's consent choices UAFeature *features = [[UAFeature alloc] initFrom:@[UAFeature.push, UAFeature.analytics]]; [UAirship.privacyManager enableFeatures:features]; } ``` > **Note:** If features are disabled after being previously enabled, the SDK may make a few network requests to opt the channel out to prevent notifications. ## Related documentation - [SDK Data Collection](https://www.airship.com/docs/reference/data-collection/sdk-data-collection/) - Comprehensive overview of what data Airship collects for each Privacy Manager flag - [Apple Privacy Manifest](https://www.airship.com/docs/reference/data-collection/apple-privacy-manifest/) - Declare data collection practices to Apple - [Analytics](https://www.airship.com/docs/developer/sdk-integration/apple/analytics/) - Track user engagement with custom events, screen tracking, and associated identifiers - [Permission Gathering](https://www.airship.com/docs/developer/sdk-integration/apple/data-collection/permission-gathering/) - Request additional permissions from users # Permission Prompts > Request additional system permissions (e.g., location) from users using Opt-in Actions.The Airship SDK automatically handles push notification permissions. For additional permissions like location, you can use Opt-in Actions to prompt users using native permission prompts. Opt-in Actions are a special type of [Action](https://www.airship.com/docs/reference/glossary/#action) that are handled by `PermissionsManager`. For an overview of all supported actions and where they are available, see the [Actions](https://www.airship.com/docs/guides/messaging/messages/actions/) guide. ## Supported Opt-in Types * Push — Handled automatically by the SDK (no implementation needed) * Location — Requires implementing a custom `PermissionDelegate` ## Implementing Location Opt-in To implement Location Opt-in, create a custom `PermissionDelegate` and register it with `PermissionsManager` to handle location permissions. ### Create a Location Permission Delegate #### Swift ```swift import Foundation import CoreLocation import AirshipCore import Combine class LocationPermissionDelegate: AirshipPermissionDelegate { let locationManager = CLLocationManager() @MainActor func checkPermissionStatus() async -> AirshipCore.AirshipPermissionStatus { return self.status } @MainActor func requestPermission() async -> AirshipCore.AirshipPermissionStatus { guard (self.status == .notDetermined) else { return self.status } guard (AppStateTracker.shared.state == .active) else { return .notDetermined } locationManager.requestAlwaysAuthorization() await waitActive() return self.status } var status: AirshipPermissionStatus { switch(locationManager.authorizationStatus) { case .notDetermined: return .notDetermined case .restricted: return .denied case .denied: return .denied case .authorizedAlways: return .granted case .authorizedWhenInUse: return .granted @unknown default: return .notDetermined } } } @MainActor private func waitActive() async { var subscription: AnyCancellable? await withCheckedContinuation { continuation in subscription = NotificationCenter.default.publisher(for: AppStateTracker.didBecomeActiveNotification) .first() .sink { _ in continuation.resume() } } subscription?.cancel() } ``` #### Objective-C ```objc #import #import @interface LocationPermissionDelegate : NSObject @property (nonatomic, strong) CLLocationManager *locationManager; @end @implementation LocationPermissionDelegate - (instancetype)init { self = [super init]; if (self) { _locationManager = [[CLLocationManager alloc] init]; } return self; } - (UAPermissionStatus)checkPermissionStatus { return [self status]; } - (void)requestPermissionWithCompletionHandler:(void (^)(UAPermissionStatus))completionHandler { if ([self status] != UAPermissionStatusNotDetermined) { completionHandler([self status]); return; } if ([UAAppStateTracker shared].state != UAAppStateActive) { completionHandler(UAPermissionStatusNotDetermined); return; } [self.locationManager requestAlwaysAuthorization]; // Wait for app to become active and check status dispatch_async(dispatch_get_main_queue(), ^{ completionHandler([self status]); }); } - (UAPermissionStatus)status { switch (self.locationManager.authorizationStatus) { case kCLAuthorizationStatusNotDetermined: return UAPermissionStatusNotDetermined; case kCLAuthorizationStatusRestricted: case kCLAuthorizationStatusDenied: return UAPermissionStatusDenied; case kCLAuthorizationStatusAuthorizedAlways: case kCLAuthorizationStatusAuthorizedWhenInUse: return UAPermissionStatusGranted; default: return UAPermissionStatusNotDetermined; } } @end ``` ### Register the Permission Delegate After creating a location `PermissionDelegate`, register it with `PermissionsManager` [after takeOff](https://www.airship.com/docs/developer/sdk-integration/apple/installation/getting-started/#calling-takeoff): #### Swift ```swift Airship.permissionsManager.setDelegate( LocationPermissionDelegate(), permission: .location ) ``` #### Objective-C ```objc LocationPermissionDelegate *delegate = [[LocationPermissionDelegate alloc] init]; [[UAirship permissionsManager] setDelegate:delegate permission:UAPermissionLocation]; ```