1

We just implemented a new push notification feature for our Xamarin.forms app using azure notification hub as provider. On iOS the notifications work perfectly fine when app is fresh installed on iOS device, i.e. no prior version of the app is installed on device. But when I try to upgrade the existing app on iOS device by installing the newer version on top, then I don't get any notifications.

When I debugged, I found that only during app upgrade scenario, the iOS app throws a UIKit.UIKitThreadAccessException: 'UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.' Due to this exception the device doesn't get registered with Apple Push Notification Services and thus doesn't get any push notifications.

Below is the full exception with stacktrace:

UIKit.UIKitThreadAccessException: 'UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.'

UIKit.UIKitThreadAccessException
  Message=UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.
  Source=mscorlib
  StackTrace:
  at UIKit.UIApplication.EnsureUIThread () [0x00020] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.18.2.1/src/Xamarin.iOS/UIKit/UIApplication.cs:96 
  at UIKit.UIGestureRecognizer..ctor (Foundation.NSObject target, System.IntPtr action) [0x00016] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.18.2.1/src/Xamarin.iOS/UIGestureRecognizer.g.cs:102 
  at UIKit.UIGestureRecognizer..ctor (System.IntPtr sel, UIKit.UIGestureRecognizer+Token token) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.18.2.1/src/Xamarin.iOS/UIKit/UIGestureRecognizer.cs:66 
  at UIKit.UITapGestureRecognizer..ctor (System.Action`1[T] action) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.18.2.1/src/Xamarin.iOS/UIKit/UIGestureRecognizer.cs:208 
  at Xamarin.Forms.Platform.iOS.EventTracker.CreateTapRecognizer (System.Int32 numTaps, System.Action`1[T] action, System.Int32 numFingers) [0x00000] in D:\a\1\s\Xamarin.Forms.Platform.iOS\EventTracker.cs:462 
  at Xamarin.Forms.Platform.iOS.EventTracker.GetNativeRecognizer (Xamarin.Forms.IGestureRecognizer recognizer) [0x00049] in D:\a\1\s\Xamarin.Forms.Platform.iOS\EventTracker.cs:258 
  at Xamarin.Forms.Platform.iOS.EventTracker.LoadRecognizers () [0x00042] in D:\a\1\s\Xamarin.Forms.Platform.iOS\EventTracker.cs:572 
  at Xamarin.Forms.Platform.iOS.EventTracker.ModelGestureRecognizersOnCollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) [0x00000] in D:\a\1\s\Xamarin.Forms.Platform.iOS\EventTracker.cs:624 
  at (wrapper delegate-invoke) <Module>.invoke_void_object_NotifyCollectionChangedEventArgs(object,System.Collections.Specialized.NotifyCollectionChangedEventArgs)
  at System.Collections.ObjectModel.ObservableCollection`1[T].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x00018] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.ObservableCollection`1[T].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedAction action, System.Object item, System.Int32 index) [0x00009] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.ObservableCollection`1[T].InsertItem (System.Int32 index, T item) [0x0001a] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.Collection`1[T].Add (T item) [0x00020] in <cbddc4225b2f45f09f3a1d43a1268bc0>:0 
  at Xamarin.Forms.View.<.ctor>g__AddItems|14_1 (Xamarin.Forms.View+<>c__DisplayClass14_0& ) [0x00032] in D:\a\1\s\Xamarin.Forms.Core\View.cs:85 
  at Xamarin.Forms.View.<.ctor>b__14_0 (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs args) [0x0002f] in D:\a\1\s\Xamarin.Forms.Core\View.cs:101 
  at System.Collections.ObjectModel.ObservableCollection`1[T].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x00018] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.ObservableCollection`1[T].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedAction action, System.Object item, System.Int32 index) [0x00009] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.ObservableCollection`1[T].InsertItem (System.Int32 index, T item) [0x0001a] in <b912bfaf235d4ed8af62226c84967349>:0 
  at System.Collections.ObjectModel.Collection`1[T].Add (T item) [0x00020] in <cbddc4225b2f45f09f3a1d43a1268bc0>:0 
  at SWAPA.MemberMobileApp.UI.Views.Login.CommonSetup () [0x002a1] in C:\MemberMobileApp\SWAPA.MemberMobileApp.UI\SWAPA.MemberMobileApp.UI\SWAPA.MemberMobileApp.UI\Views\Login.xaml.cs:369 
  at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__7_1 (System.Object state) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs:1037 
  at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context (System.Object state) [0x0000d] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/threadpool.cs:1370 
  at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00071] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/executioncontext.cs:968 
  at System.Threading.ExecutionContext.Run (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/executioncontext.cs:910 
  at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem () [0x00021] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/threadpool.cs:1341 
  at System.Threading.ThreadPoolWorkQueue.Dispatch () [0x00074] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/threadpool.cs:899 
  at ObjCRuntime.Runtime.ThreadPoolDispatcher (System.Func`1[TResult] callback) [0x00006] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.18.2.1/src/Xamarin.iOS/ObjCRuntime/Runtime.cs:288 
  at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback () [0x00009] in /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/src/Xamarin.iOS/mcs/class/referencesource/mscorlib/system/threading/threadpool.cs:1258 

The only implementation that I have done in AppDelegate is to register the device with Apple Push Notification Services and then send that device registration id to main UI app to register the device with Azure notification hub.

Below is my code with some comments:

    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate, IUNUserNotificationCenterDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();

            //----Block needed for App Center Testing-----
#if ENABLE_TEST_CLOUD
            //Xamarin.Calabash.Start();
#endif
            //----Block needed for App Center Testing-----

            new SfCalendarRenderer();

            LoadApplication(new App());
            SfImageEditorRenderer.Init();
            SfListViewRenderer.Init();
            SfCheckBoxRenderer.Init();
            SfPdfDocumentViewRenderer.Init();
            SfPickerRenderer.Init();
            Syncfusion.SfChart.XForms.iOS.Renderers.SfChartRenderer.Init();
            new Syncfusion.SfAutoComplete.XForms.iOS.SfAutoCompleteRenderer();

            UITabBar.Appearance.SelectedImageTintColor = UIColor.FromRGB(213, 84, 39);

            // Color of the tabbar background:
            UITabBar.Appearance.BarTintColor = UIColor.LightGray;

            // Color of the selected tab text color:
            UITabBarItem.Appearance.SetTitleTextAttributes(
                new UITextAttributes()
                {
                    TextColor = UIColor.FromRGB(213, 84, 39)
                },
                UIControlState.Selected);

            // Color of the unselected tab icon & text:
            UITabBarItem.Appearance.SetTitleTextAttributes(
                new UITextAttributes()
                {
                    TextColor = UIColor.FromRGB(65, 64, 66)
                },
                UIControlState.Normal);

            base.FinishedLaunching(app, options);

            /*Register device with Apple Push Notification Service*/
            RegisterForRemoteNotifications();

            if (options != null && options.ContainsKey(UIApplication.LaunchOptionsRemoteNotificationKey))
            {
                NSDictionary userInfo = (NSDictionary)options[UIApplication.LaunchOptionsRemoteNotificationKey];
                if (userInfo != null)
                {
                    PushNotifications.IsNotifictaionClick_AppInActive = true;
                    // To remove all delivered notifications
                    RemoveAllDeliveredPushNotifications();
                }
            }
            return true;
        }

        //Register device with Apple Push Notification Services
        void RegisterForRemoteNotifications()
        {
            //register for remote notifications based on system version
            if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
            {
                UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert |
                    UNAuthorizationOptions.Sound |
                    UNAuthorizationOptions.Sound,
                    (granted, error) =>
                    {
                        if (granted)
                            InvokeOnMainThread(UIApplication.SharedApplication.RegisterForRemoteNotifications); // Ask user for permission to receive notifications on device.
                    });
            }
            else if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0))
            {
                var pushSettings = UIUserNotificationSettings.GetSettingsForTypes(
                UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound,
                new NSSet());

                UIApplication.SharedApplication.RegisterUserNotificationSettings(pushSettings);
                UIApplication.SharedApplication.RegisterForRemoteNotifications();
            }
            else
            {
                UIRemoteNotificationType notificationTypes = UIRemoteNotificationType.Alert | UIRemoteNotificationType.Badge | UIRemoteNotificationType.Sound;
                UIApplication.SharedApplication.RegisterForRemoteNotificationTypes(notificationTypes);
            }

            UNUserNotificationCenter.Current.Delegate = this;
        }

        //Send the PNS handle to UI app for device installation with Azure Notificaiton Hub.
        public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
        {
            //Format the pns handle before registering with Azure notification hub.
            PushNotifications.PNSHandle = deviceToken.DebugDescription.Replace("<", string.Empty)
                                                                      .Replace(">", string.Empty)
                                                                      .Replace(" ", string.Empty)
                                                                      .ToUpper();
        }

        // Process notification when received.
        public override void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo)
        {
            ProcessNotification(userInfo);
        }

        void ProcessNotification(NSDictionary options)
        {
            // make sure we have a payload
            if (options != null && options.ContainsKey(new NSString("aps")))
            {
                // get the APS dictionary and extract message payload. Message JSON will be converted
                // into a NSDictionary so more complex payloads may require more processing
                NSDictionary aps = options.ObjectForKey(new NSString("aps")) as NSDictionary;
                NSDictionary alertMessage = aps.ObjectForKey(new NSString("alert")) as NSDictionary;

                //Extract notification content
                NSString messageKey = new NSString("body");
                NSString titleKey = new NSString("title");

                string messageBody = alertMessage.ContainsKey(messageKey) ? alertMessage[titleKey].ToString() : string.Empty;
                string title = alertMessage.ContainsKey(titleKey) ? alertMessage[titleKey].ToString() : string.Empty;

                if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(messageBody))
                {
                    var content = new UNMutableNotificationContent();
                    content.Title = title;
                    content.Body = messageBody;

                    var requestID = title;
                    var request = UNNotificationRequest.FromIdentifier(requestID, content, null);

                    UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) =>
                    {
                        if (err != null)
                        {
                            // TODO: log error messages in error data table.
                            Debug.WriteLine($"Received request to process notification but something went wrong.");
                        }
                    });
                }
            }
            else
            {
                // TODO: log error messages in error data table.
                Debug.WriteLine($"Received request to process notification but there was no payload.");
            }
        }

        [Export("userNotificationCenter:willPresentNotification:withCompletionHandler:")]
        public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
        {
            completionHandler(UNNotificationPresentationOptions.Sound | UNNotificationPresentationOptions.Alert);
        }

        [Export("userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:")]
        public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action
        completionHandler)
        {
            completionHandler();

            /*Logic to open NotificationInboxPage onclick of notification when app was active */
            if (!App.IsAppLaunching)
            {
                var modalStack = App.Current.MainPage.Navigation.ModalStack;

                if (modalStack.Count > 0 && modalStack.Last().ToString().Contains("NotificationInbox"))
                    App.Current.MainPage.Navigation.PopModalAsync();

                App.Current.MainPage.Navigation.PushModalAsync(new NotificationInboxPage());
            }
            App.IsAppLaunching = false;

            //To remove all delivered notifications
            RemoveAllDeliveredPushNotifications();
        }

        private void RemoveAllDeliveredPushNotifications()
        {
            if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
            {
                UNUserNotificationCenter.Current.RemoveAllDeliveredNotifications();
            }
            else
            {
                UIApplication.SharedApplication.CancelAllLocalNotifications();
            }
        }
}

The exception says I am calling UIKit method that should only be invoked from UI thread. The only action I am calling is InvokeOnMainThread(UIApplication.SharedApplication.RegisterForRemoteNotifications); which I am already invoking on main thread. So I am not sure what other function is the exception talking about. And why is it complaining only when I am upgrading the app and not when I am fresh installing the app.

I checked this post here but didn't get much help. At this point I am completely stuck due to this and really need some help on resolving the issue. Any idea what I might be missing?

Thanks in advance.

Saamer
  • 4,687
  • 1
  • 13
  • 55
Naphstor
  • 2,356
  • 7
  • 35
  • 53
  • 1
    The error is actually occuring on line 369 of your `Login.xaml.cs` file. Try to place an "Exception catchpoint" and it should tell you exactly what line is causing that issue, and share that code with us. Besides that, have you tried to use DispatchAsync instead? like this https://stackoverflow.com/questions/39120789/error-in-ui-kit-consistency-error This could also help you a bit with https://github.com/xamarin/ios-samples/blob/6c133faa31d76aaa0fb9e981c33ce4144b87f558/ios12/XamarinShot/XamarinShot/Core/Sounds%20and%20Haptics/HapticsGenerator.cs#L36 – Saamer Aug 23 '20 at 17:35
  • 1
    @Saamer thanks a lot for pointing out. I guess I was burying my head in AppDelegate code and didn't see the login.xaml.cs issue. Putting try-catch in login.xaml.cs helped and I was able to resolve the issue. – Naphstor Aug 24 '20 at 13:15
  • Ah nice! Didn’t know if you had more issues so I didn’t put it in as an answer before. Keep up the good work ! – Saamer Aug 24 '20 at 17:41

1 Answers1

0

As you can read from the stack trace provided by you in the questions, the error is actually occuring on line 369 of your ‘Login.xaml.cs’ file.

If you place an "Exception catchpoint" and it should tell you exactly what line is causing that issue. Placing a try-catch around it should fix the crash, but try to to resolve the issue as well since exceptions in general are bad.

Besides that, you can also to use DispatchAsync instead

Saamer
  • 4,687
  • 1
  • 13
  • 55