Search code examples
iosapple-push-notificationsmultiple-versions

How implement Apple Push Notifications for iOS versions 9 + 10 (and perhaps 8)?


I have not found any official Apple document that discusses correctly implementing push notification simultaneously for old iOS versions, as well as iOS 10. And the independent tutorials I have seen, likewise cover a single iOS version.

I see this official document for iOS 10: Local and Remote Notifications Overview But it doesn't comment on supporting earlier iOS versions.

And a tutorial for iOS 9: Push Notifications Tutorial - Ray Wenderlich

And I see various stackoverflow threads about changes people had to make to get their old solutions to work on newer versions:

Push notifications are not working in iOS 9 Which does show code for handling 6 - 9.

didReceiveRemoteNotification not called , iOS 10


BUT what I don't see, is what is correct to do, starting today (with iOS 10), but also supporting older devices. ** UPDATE ** App Store says only 6% of devices downloading apps are older than ios 9, so if it is easier to just support 9 + 10, i'll just do that.

(I tried starting with an iOS 10 example, but it immediately crashed on iOS 9.3 emulated device, though it works fine in iOS 10. So I conclude that I should be starting with information about correctly setting up for the different versions. I could post that code, but I think that leads this thread in the wrong direction. I would rather start with what "should" work on multiple versions of iOS, including 10.)

If I don't find some solution, I'll start combining code from different stackoverflow code snippets ... but seriously? I must be missing something, as presumably every iOS developer has this issue.


Conversely, I could start with an older example, and then follow the changes to get that to work with iOS 10 - but will that take full advantage of iOS 10?

NOTE: I'm programming in Xamarin C#, but Objective-C or Swift answer is just as useful.


Solution

  • This is Xamarin C# code (different syntax and capitalization than Objective-C, but I think it is translatable line-by-line to Objective-C).

    Tested on both iOS 9.3 and iOS 10.2.

    To initialize "local" and "remote" notifications:

    // "UIApplicationDelegate" is for "local" notifications,
    // "IUNUserNotificationCenterDelegate, IMessagingDelegate" for "remote" notifications.
    public class AppDelegate : UIApplicationDelegate,
        IUNUserNotificationCenterDelegate, IMessagingDelegate
    {
        ...
        public override bool FinishedLaunching( UIApplication application, NSDictionary launchOptions )
        {
            ...
            RegisterForOurRemoteNotifications( this );
            RegisterForOurLocalNotifications();
            ...
        }
        ...
    
        // --- Comment out if not using Google FCM. ---
        public override void RegisteredForRemoteNotifications( UIApplication application, NSData deviceToken )
        {
            //base.RegisteredForRemoteNotifications( application, deviceToken );
            Firebase.InstanceID.InstanceId.SharedInstance.SetApnsToken( deviceToken,
                                                                       Firebase.InstanceID.ApnsTokenType.Sandbox );
        }
    
        ...
        // ----- "static"; Could be in another class. -----
    
        // These flags are for our convenience, so we know initialization was done.
        static bool IsRegisteredForNotifications;
        static bool IsRegisteredForRemoteNotifications;
        // Optional - true when we are using Google "Firebase Cloud Messaging".
        static bool HasFCM;
    
        public static void RegisterForOurRemoteNotifications( AppDelegate del )
        {
            // Google "Firebase Cloud Messaging" (FCM) Monitor token generation
            // (Uncomment, if you are using FCM for notifications.)
            //InstanceId.Notifications.ObserveTokenRefresh( TokenRefreshNotification );
    
            if (UIDevice.CurrentDevice.CheckSystemVersion( 10, 0 )) {
                // iOS 10 or later
                var authOptions = UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound;
                UNUserNotificationCenter.Current.RequestAuthorization( authOptions, ( granted, error ) => {
                    Console.WriteLine( granted );
                } );
    
                // For iOS 10 display notification (sent via APNS)
                UNUserNotificationCenter.Current.Delegate = del;
    
                // For iOS 10 data message (sent via Google FCM).
                // (Uncomment, if you are using FCM for notifications.)
                // TBD: If NOT using FCM, you may need some other lines of code here.
                //Messaging.SharedInstance.RemoteMessageDelegate = del;
    
            } else {
                // iOS 9 or before
                var allNotificationTypes = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound;
                var settings = UIUserNotificationSettings.GetSettingsForTypes( allNotificationTypes, null );
                UIApplication.SharedApplication.RegisterUserNotificationSettings( settings );
            }
    
            UIApplication.SharedApplication.RegisterForRemoteNotifications();
            IsRegisteredForRemoteNotifications = true;
    
            // Uncomment if using Google "Firebase Cloud Messaging" (FCM).
            //TokenRefreshNotification( null, null );
            //if (UIDevice.CurrentDevice.CheckSystemVersion( 9, 0 )) // Needed to call this twice on iOS 9 for some reason.
            //  TokenRefreshNotification( null, null );
    
    
            UIApplication.SharedApplication.SetMinimumBackgroundFetchInterval( UIApplication.BackgroundFetchIntervalMinimum );
        }
    
        public static void RegisterForOurLocalNotifications()
        {            
            // --- Our app's notification actions. ---
            UNNotificationAction followAction = UNNotificationAction.FromIdentifier( "follow", PS.LocalizedString( "Follow" ), UNNotificationActionOptions.None );
            UNNotificationAction likeAction = UNNotificationAction.FromIdentifier( "like", PS.LocalizedString( "Like" ), UNNotificationActionOptions.None );
            // ...
    
            // --- Our app's notification categories ---
            UNNotificationCategory followCategory = UNNotificationCategory.FromIdentifier( "followCategory", new UNNotificationAction[] { followAction, likeAction },
                                                                                    new string[] { }, UNNotificationCategoryOptions.None );
            // ...
    
            // --- All of the app's categories from above ---
            var categories = new UNNotificationCategory[] { followCategory /*, ...*/ };
    
    
            // --- Same for all apps ---
            UIUserNotificationSettings settings = UIUserNotificationSettings.GetSettingsForTypes(
                                                      UIUserNotificationType.Alert |
                                                      UIUserNotificationType.Badge |
                                                      UIUserNotificationType.Sound
                , new NSSet( categories ) );
            UIApplication.SharedApplication.RegisterUserNotificationSettings( settings );
    
    
            if (UIDevice.CurrentDevice.CheckSystemVersion( 10, 0 )) {
                UNUserNotificationCenter.Current.SetNotificationCategories( new NSSet<UNNotificationCategory>( categories ) );
    
                UNUserNotificationCenter.Current.RequestAuthorization( UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound | UNAuthorizationOptions.Badge,
                                                                      ( result, err ) => {
                                                                          Console.WriteLine( result.ToString() );
                                                                      } );
            }
    
            IsRegisteredForNotifications = true;
        }
    }
    
    
        // -------------------------------------------------------
        // --- These are for Google "Firebase Cloud Messaging" ---
        // (Comment out if not using FCM.)
    
        public static string Token;
    
        static void TokenRefreshNotification( object sender, NSNotificationEventArgs e )
        {
            // This method will be fired every time a new token is generated, including the first
            // time. So if you need to retrieve the token as soon as it is available this is where that
            // should be done.
            //var refreshedToken = InstanceId.SharedInstance.Token;
    
            ConnectToFCM( UIApplication.SharedApplication.KeyWindow.RootViewController );
    
            // TODO: If necessary send token to application server.
        }
    
    
        public static void ConnectToFCM( UIViewController fromViewController )
        {
            Messaging.SharedInstance.Connect( error => {
                if (error != null) {
                    Helper.logD( "Unable to connect to FCM", error.LocalizedDescription );
                } else {
                    //var options = new NSDictionary();
                    //options.SetValueForKey( DeviceToken, Constants.RegisterAPNSOption );
                    //options.SetValueForKey( new NSNumber( true ), Constants.APNSServerTypeSandboxOption );
    
                    //InstanceId.SharedInstance.GetToken("", InstanceId.ScopeFirebaseMessaging 
                    Token = InstanceId.SharedInstance.Token;
    
                    Console.WriteLine( $"Token: {InstanceId.SharedInstance.Token}" );
                    HasFCM = true;
                }
            } );
        }
        // ------------------ End Google FCM ---------------------
        // -------------------------------------------------------
    }
    

    The above code initializes your app so that it can receive notifications.

    IMPORTANT: You also need to set appropriate permissions on your app; see Apple docs, or links mentioned in the question. And you need this file:

    Entitlements.plist:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>aps-environment</key>
        <string>development</string>
    </dict>
    </plist>
    

    <string> above must contain either "development" or "production". (I don't know the significance of our app still saying "development" here; I haven't examined what is built to see if it gets automatically changed to "production" by Xcode before submission to Apple. According to https://stackoverflow.com/a/40857877/199364 it does.)


    Then you need code to send [e.g. your app tells your server to notify your friends' devices of what you are doing now] and receive a local or remote notification. That code, in our app, is combined with our specific notification actions and categories; I do not have time to extract a concise version to post here. Please see Apple docs, or links mentioned in the original question, for full details.

    Here are the essential methods (Add to class AppDelegate above) to receive notifications:

        public override void ReceivedLocalNotification( UIApplication application, UILocalNotification notification )
        {
            ...
        }
    
        public override void DidReceiveRemoteNotification( UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler )
        {
            ...
        }
    
    
        [Export( "userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:" )]
        public void DidReceiveNotificationResponse( UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler )
        {
            ...
        }
    

    Other methods that you may want/need to override or implement (also see the interfaces declared on class AppDelegate above); some of these might be specific to FCM:

    ApplicationReceivedRemoteMessage
    ReceivedRemoteNotification
    WillPresentNotification
    PerformFetch (for background notifications)
    HandleAction