SDK 17.x now requires Xcode 14.3 or newer.
SDK 17.x is compatible with iOS 14+. Apps using Airship will need to update the minimum deployment version.
The following modules are no longer supported and have been removed from the SDK:
Users of Accengage should remove the AirshipAccengage
module from their project after completing the migration process.
For further information about migration and removal, see the Accengage Migration guide.
The Airship Chat module is no longer supported and has been removed from the SDK.
The Airship Location module is no longer supported and has been removed from the SDK. If you want to continue prompting users for location permissions, you must update your integration to set a location permission delegate on the PermissionsManager
:
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()
}
Then after takeOff, register the permission delegate for the location permission:
Airship.shared.permissionsManager.setDelegate(
LocationPermissionDelegate(),
permission: .location
)
The Airship Extended Actions only contained the RateAppAction which is now available in the core module.
The URL allow list configuration has been changed to an opt-out process, rather than an opt-in process like previous SDK versions.
By default, all URLs are allowed by SDK 17, unless explicitly disallowed by the app via the urlAllowList
or urlAllowListScopeOpen
config options.
Allow list behavior changes:
- If neither
urlAllowList
orurlAllowListScopeOpenURL
are set in your Airship config, the SDK will default to allowing all URLs and an error message will be logged. - To suppress the error message, set
urlAllowList
orurlAllowListScopeOpenURL
to[*]
to your config to adopt the new allow-all behavior, or customize the allowed URLs as needed. - URLs for media displayed within in-app messages will no longer be checked against the URL allow lists.
- YouTube has been removed from the default allow list. If your application makes use of opening links to YouTube from Airship messaging, you will need to update your allow list to explicitly allow
youtube.com
, or allow all URLs with[*]
.
Some common class names have been renamed to prevent collisions with other libraries/apps:
Legacy class name | New class name |
---|---|
Config | AirshipConfig |
Channel | AirshipChannel |
Contact | AirshipContact |
Push | AirshipPush |
PrivacyManager | AirshipPrivacyManager |
Analytics | AirshipAnalytics |
Action | AirshipAction |
Situation | ActionSituation |
Features | AirshipFeature |
Event | AirshipEvent |
The new default display interval for in-app messages is now set to 0 seconds. Apps that wish to maintain the previous default display interval of 30 seconds should set the display interval manually, after takeOff:
InAppAutomation.shared.inAppMessageManager.displayInterval = 30.0
Deep link delegate is now async.
SDK 16:
deepLinkDelegate.receivedDeepLink(deepLink) {
completionHandler(true)
}
SDK 17:
await deepLinkDelegate.receivedDeepLink(deepLink) {
completionHandler(true)
}
Contact conflict event is now available as a NSNotification or using Airship.contact.conflictEventPublisher
to listen for events:
SDK 16:
conflictDelegate?.onConflict(anonymousContactData: anonData, namedUserID: namedUserID)
SDK 17:
Airship.contact.conflictEventPublisher.sink { event in
// ...
}
NotificationCenter.default.addObserver(
self,
selector: #selector(conflictEventReceived),
name: AirshipContact.contactConflictEvent
)
Named User ID access is now an async property:
SDK 16:
let namedUserID = Airship.contact.namedUserID
SDK 17:
let namedUserID = await Airship.contact.namedUserID
Subscription list is now an async method:
SDK 16:
Airship.contact.fetchSubscriptionLists { contactSubscriptionLists, error in
// Use the contactSubscriptionLists
}
SDK 17:
let contactSubscriptions = try await Airship.contact.fetchSubscriptionLists
Subscription list is now an async method:
SDK 16:
Airship.channel.fetchSubscriptionLists { channelSubscriptionLists, error in
// Use the channelSubscriptionLists
}
SDK 17:
let channelSubscriptions = try await Airship.channel.fetchSubscriptionLists
The API's to track and restore live activity tracking have been updated to no longer require a Task:
SDK 16:
Task {
await Airship.channel.trackLiveActivity(
activity,
name: "order-1234"
)
Task {
await Airship.channel.restoreLiveActivityTracking { restorer in
await restorer.restore(
forType: Activity<DeliveryAttributes>.self
)
await restorer.restore(
forType: Activity<SomeOtherAttributes>.self
)
}
}
SDK 17:
Airship.channel.trackLiveActivity(
activity,
name: "order-1234"
)
Airship.channel.restoreLiveActivityTracking { restorer in
await restorer.restore(
forType: Activity<DeliveryAttributes>.self
)
await restorer.restore(
forType: Activity<SomeOtherAttributes>.self
)
}
The MessageCenter module has been rewritten in Swift and the OOTB UI in SwiftUI. With the rewrite, we are providing a new set of APIs that take advantage of Swift's structured concurrency.
All methods on the Message Center for listing are now async, and the listing is no longer stored in memory.
SDK 16:
let messages = MessageCenter.shared.messageList.messages
SDK 17:
let messages = await MessageCenter.shared.inbox.messages
SDK 16:
MessageCenter.shared.messageList.markMessagesDeleted([message]) { // completed }
SDK 17:
// by message ID
await MessageCenter.shared.inbox.delete(messageIDs: ["messageID"])
// by message
await MessageCenter.shared.inbox.delete(messages: [message])
SDK 16:
MessageCenter.shared.messageList.markMessagesRead([message]) { // completed }
SDK 17:
// by message ID
await MessageCenter.shared.inbox.markRead(messageIDs: ["messageID"])
// by message
await MessageCenter.shared.inbox.markRead(messages: [message])
SDK 16:
MessageCenter.shared.messageList.retrieveMessageList(successBlock: {
// handle success
}, withFailureBlock: {
// handle failure
})
SDK 17:
await MessageCenter.shared.inbox.refreshMessages()
SDK 16:
NotificationCenter.default.addObserver(self,
selector: #selector(messageListWillUpdate),
name: UAInboxMessageListWillUpdateNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(messageListUpdated),
name: UAInboxMessageListUpdatedNotification,
object: nil)
SDK 17:
MessageCenter.shared.inbox.messagePublisher
.receive(on: RunLoop.main)
.sink(receiveValue: { messages in
// Latest messages
})
.store(in: &self.subscriptions)
Now our Message Center View has been rewritten using SwiftUI. The UIKit based views have been removed.
struct CustomMessageCenter: View {
let controller = MessageCenterController()
var body: some View {
MessageCenterView(controller: controller)
}
}
SDK 16:
import AirshipKit
class MessageCenterViewController : DefaultMessageCenterSplitViewController {
}
SDK 17:
// 1
let messageCenterviewController = MessageCenterViewControllerFactory.make(
controller: controller
)
if let messageCenterView = messageCenterviewController.view {
// 2
// Add the message center view controller to the destination view controller.
addChild(messageCenterviewController)
view.addSubview(messageCenterView)
// 3
// Create and activate the constraints.
messageCenterView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageCenterView.topAnchor.constraint(equalTo: view.topAnchor),
messageCenterView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
messageCenterView.leftAnchor.constraint(equalTo: view.leftAnchor),
messageCenterView.rightAnchor.constraint(equalTo: view.rightAnchor),
])
}
MessageCenterStyle
has been renamed to MessageCenterTheme
to avoid confusing with SwiftUI style patterns.
SDK 16:
let style = MessageCenterStyle()
MessageCenter.shared.defaultUI.style = style
SDK 17:
var messageCenterTheme = MessageCenterTheme()
MessageCenter.shared.theme = messageCenterTheme
You can also set the theme on the MessageCenterView directly:
Example:
MessageCenterView(controller: controller)
.messageCenterTheme(CustomMessageCenter.messageCenterTheme)
SDK 16:
MessageCenter.shared.user.getData { user in
// ...
}
SDK 17:
let user = await MessageCenter.shared.inbox.user
The example below shows how to fetch the credentials, set auth on the request, and load a message into the webview.
This code assumes a custom view controller with an embedded WKWebView, as well as a MessageCenterMessage
ready to be loaded.
SDK 16:
let requestObj = NSMutableURLRequest(url:message.messageURL)
MessageCenter.shared.user.getData { data in
// set the auth
let auth = Utils.authHeaderString(withName: data.username, password: data.password)
requestObj.setValue(auth, forHTTPHeaderField:"Authorization")
// load the request
self.webView.load(requestObj)
}, queue: DispatchQueue.main)
SDK 17:
var request = URLRequest(url: message.bodyURL)
let user = await MessageCenter.shared.inbox.user
// set the auth
request.setValue(user.basicAuthString, forHTTPHeaderField: "Authorization")
// load the request
self.webView.load(request)
The Preference Center UI has been rewritten in SwiftUI.
PreferenceCenterView(preferenceCenterID: "preferenceCenter-ID")
// 1
let preferenceCenterviewController = PreferenceCenterViewControllerFactory.makeViewController(preferenceCenterID: "neat")
if let preferenceCenterView = preferenceCenterviewController.view {
// 2
// Add the prefrence center view controller to the destination view controller.
addChild(preferenceCenterviewController)
view.addSubview(preferenceCenterView)
// 3
// Create and activate the constraints.
preferenceCenterView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
preferenceCenterView.topAnchor.constraint(equalTo: view.topAnchor),
preferenceCenterView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
preferenceCenterView.leftAnchor.constraint(equalTo: view.leftAnchor),
preferenceCenterView.rightAnchor.constraint(equalTo: view.rightAnchor),
])
}
PreferenceCenterStyle
has been replaced by PreferenceCenterTheme
.
SDK 16:
let style = PreferenceCenterStyle()
PreferenceCenter.shared.style = style
SDK 17:
var theme = PreferenceCenterTheme()
PreferenceCenter.shared.theme = theme
A theme can also be set directly on the PreferenceCenterView:
PreferenceCenterView(preferenceCenterID: "preferenceCenterID")
.preferenceCenterTheme(theme)
Actions have been rewritten to use async/await and be Sendable.
SDK 16:
Airship.shared.actionRegistry.register(action, names: ["action_name", "action_alias"])
SDK 17:
Airship.shared.actionRegistry.registerEntry(names: ["action_name", "action_alias"]) {
return ActionEntry(action: action)
}
SDK 16:
let customAction = BlockAction { args, completionHandler in
print("Action is performing with args: \(args)")
completionHandler(ActionResult.empty())
}
SDK 17:
let customAction = BlockAction { args in
print("Action is performing with args: \(args)")
return nil
}
SDK 16:
// Run an action by name
ActionRunner.run("action_name", value: "action_value", situation: .manualInvocation) { result in
print("Action finished!")
}
// Run an action directly
ActionRunner.run(action, value: "action_value", situation: .manualInvocation) { result in
print("Action finished!")
}
SDK 17:
// Run an action by name
let result = await ActionRunner.run(
actionName: "action_name",
arguments: ActionArguments(
string: "action_value",
situation: .manualInvocation
)
)
// Run an action directly
let result = await ActionRunner.run(
action: action,
arguments:ActionArguments(
string: "action_value",
situation: .manualInvocation
)
)