xcodeswiftuiapple-push-notificationsappdelegatesalesforce-marketing-cloud

MarketingCloudSDK Crashes SwiftUI App in Beta TestFlight: "_dispatch_once"


MarketingCloudSDK Crashes SwiftUI App in Beta TestFlight

We've implemented Push Notifications through SalesForce Marketing Cloud and after some beta testing with internal testers it seems the SFMCSDK is causing our app to Crash intermittently due to "_dispatch_once". Not sure if this is an issue with the way we've implemented the SDK or simply a bug from the SDK itself, when we open the crash in project we only get the following information:

TestFlight metrics include # crashes

crash details from Xcode Organizer

Screenshot of crash in project

We've followed the documentation to setup the SDK / Push Notifications and standard push notifications are working as expected. We have an issue receiving/reading openDirect urls from a terminated app state (works in foreground/background) but we removed those urls in the hopes that they were causing the crash, it seems they were not.

I also found an article that said setting 'Other Linker Flags' in Xcode Build Settings can cause the "_dispatch_once" crash but we checked and we don't define any.

We implement everything through AppDelegate in SwiftUI:

import SFMCSDK
import MarketingCloudSDK
import Foundation
import SwiftUI
import UIKit

class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, URLHandlingDelegate {

    let appId = "correct appId String"
    let accessToken = "correct accessToken String"
    let appEndpoint = URL(string:"correct marketingCloud appEndpoint URL")!
    let mid = "correct marketing cloud ID"

    func configureSDK() {
        #if DEBUG
            SFMCSdk.setLogger(logLevel: .debug)
        #endif

        let mobilePushConfiguration = PushConfigBuilder(appId: appId)
            .setAccessToken(accessToken)
            .setMarketingCloudServerUrl(appEndpoint)
            .setDelayRegistrationUntilContactKeyIsSet(true)
            .setMid(mid)
            .setInboxEnabled(false)
            .setLocationEnabled(false)
            .setAnalyticsEnabled(true)
            .build()

        let completionHandler: (OperationResult) -> () = { result in
            if result == .success {
                // module is fully configured and ready for use
                SFMCSdk.mp.setURLHandlingDelegate(self)
            } else if result == .error {
                // module failed to initialize, check logs for more details
            } else if result == .cancelled {
                // module initialization was cancelled
            } else if result == .timeout {
                // module failed to initialize due to timeout, check logs for more details
            }
        }

        SFMCSdk.initializeSdk(ConfigBuilder().setPush(config: mobilePushConfiguration,
          onCompletion: completionHandler).build())
    }
    
    func registerPush(contactID:String?) {
        #if !targetEnvironment(simulator)
            #if DEBUG
            SFMCSdk.identity.setProfileId("developer@developer.com")
            setupMobilePush()
            #else
            if let contactKey = contactID {
                SFMCSdk.identity.setProfileId(contactKey)
                setupMobilePush()
            }
            #endif
        #endif
    }
    
    func sfmc_handleURL(_ url: URL, type: String) {
        print(url.description)
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func setupMobilePush() {

        DispatchQueue.main.async {
            UNUserNotificationCenter.current().delegate = self

            // Request authorization from the user for push notification alerts.
            UNUserNotificationCenter.current().requestAuthorization(
             options: [.alert, .sound, .badge], completionHandler: {
                (_ granted: Bool, _ error: Error?) -> Void in
                if error == nil {
                    if granted == true {
                        // Logic if authorized
                    }
                } else {
                    print(error!.localizedDescription)
                }
            })

            UIApplication.shared.registerForRemoteNotifications()
        }

    }
    
    // SDK: REQUIRED IMPLEMENTATION
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        self.configureSDK()
        return true
    }

    
    // MobilePush SDK: REQUIRED IMPLEMENTATION
    func application(_ application: UIApplication,
      didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        SFMCSdk.mp.setDeviceToken(deviceToken)
    }

    // MobilePush SDK: REQUIRED IMPLEMENTATION
    func application(_ application: UIApplication,
     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print(error)
    }

    // MobilePush SDK: REQUIRED IMPLEMENTATION
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo:
     [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping
    (UIBackgroundFetchResult) -> Void) {
        SFMCSdk.mp.setNotificationUserInfo(userInfo)
        completionHandler(.newData)
    }
    
    // MobilePush SDK: REQUIRED IMPLEMENTATION
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response:
     UNNotificationResponse, withCompletionHandler completionHandler: @escaping () ->
     Void) {
        SFMCSdk.mp.setNotificationRequest(response.notification.request)
        completionHandler()
    }

    // MobilePush SDK: REQUIRED IMPLEMENTATION
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent
     notification: UNNotification, withCompletionHandler completionHandler: @escaping
      (UNNotificationPresentationOptions) -> Void) {
        completionHandler(.alert)
    }
}

Any insight would be most appreciated!


Solution

  • This is because of accessing keychain from the background when data protection is enabled. Add the below code before SDK init and we should not be seeing the crash anymore. SFMCSdk.setKeychainAccessErrorsAreFatal(errorsAreFatal: false)

    Please take a look our Learning app: https://github.com/salesforce-marketingcloud/MarketingCloudSDK-iOS/tree/spm/examples/LearningApp to see how this implemented in detail.

    Thanks Prakashini