React-native-permissions: Handler being ARC'd on iOS

Created on 31 Dec 2019  路  3Comments  路  Source: zoontek/react-native-permissions

Bug

This is a problem only happens in a release app
I am experiencing issue that when I try to request for Location When In Use permission on iOS, the dialog pop up and close suddenly before user click the buttons

I have research that, this might be caused by iOS ARC.
https://stackoverflow.com/questions/27048995/alert-view-disappears-on-its-own-when-calling-locationmanager-requestwheninusea

I checked the source code, locationManager is already a strong reference in LocationWhenInUse class. Then I tried to make handler in RNPermission.m to a member variable of the class instead of a local variable in request(). The problem solved immediately.

Though, I am not an expert of iOS, I think my fix is not a proper fix, thus I hope someone could help on fixing this.

Environment info

React native info output:

System:
    OS: macOS 10.15.1
    CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
    Memory: 3.34 GB / 32.00 GB
    Shell: 5.7.1 - /bin/zsh
  Binaries:
    Node: 12.13.0 - ~/.nvm/versions/node/v12.13.0/bin/node
    npm: 6.12.0 - ~/.nvm/versions/node/v12.13.0/bin/npm
  SDKs:
    iOS SDK:
      Platforms: iOS 13.2, DriverKit 19.0, macOS 10.15, tvOS 13.2, watchOS 6.1
  IDEs:
    Android Studio: 3.5 AI-191.8026.42.35.5977832
    Xcode: 11.2.1/11B500 - /usr/bin/xcodebuild
  npmPackages:
    @react-native-community/cli: 2.9.0 => 2.9.0 
    react: 16.9.0 => 16.9.0 
    react-native: 0.61.5 => 0.61.5 
  npmGlobalPackages:
    react-native-cli: 2.0.1

Library version: 2.0.8

Steps To Reproduce

  1. Make your app a bit more bucky (Eating more ram)
  2. Request location when in use permission on iOS
  3. The dialog shown but closed itself suddenly

Describe what you expected to happen:

  1. The dialog should show and wait for users to click on the option they want

Reproducible sample code

Permission.request(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE)

bug

Most helpful comment

@booker-dragon Indeed, this happens because of ARC being a little too aggressive. But the solution is too fragile and will not work with concurrent checks / requests.

A solution could be a NSMutableArray<id<RNPermissionHandler>>, then adding handlers instances in it / set them to nil when the job is done (using the array index)

All 3 comments

I have made a patch that fixed the issue but I am not sure whether it is a good fix

diff --git a/node_modules/react-native-permissions/ios/RNPermissions.h b/node_modules/react-native-permissions/ios/RNPermissions.h
index 3314426..921d129 100644
--- a/node_modules/react-native-permissions/ios/RNPermissions.h
+++ b/node_modules/react-native-permissions/ios/RNPermissions.h
@@ -79,6 +79,8 @@ typedef enum {

 @interface RNPermissions : NSObject <RCTBridgeModule>

+@property (nonatomic, strong, nullable) id<RNPermissionHandler> handler;
+
 + (bool)isFlaggedAsRequested:(NSString * _Nonnull)handlerId;

 + (void)flagAsRequested:(NSString * _Nonnull)handlerId;
diff --git a/node_modules/react-native-permissions/ios/RNPermissions.m b/node_modules/react-native-permissions/ios/RNPermissions.m
index 7affed9..6939e29 100644
--- a/node_modules/react-native-permissions/ios/RNPermissions.m
+++ b/node_modules/react-native-permissions/ios/RNPermissions.m
@@ -116,83 +116,83 @@ - (dispatch_queue_t)methodQueue {
   return dispatch_get_main_queue();
 }

-- (id<RNPermissionHandler> _Nullable)handlerForPermission:(RNPermission)permission {
-  id<RNPermissionHandler> handler = nil;
+- (void)handlerForPermission:(RNPermission)permission {
+  self.handler = nil;

   switch (permission) {
 #if __has_include("RNPermissionHandlerBluetoothPeripheral.h")
     case RNPermissionBluetoothPeripheral:
-      handler = [RNPermissionHandlerBluetoothPeripheral new];
+      self.handler = [RNPermissionHandlerBluetoothPeripheral new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerCalendars.h")
     case RNPermissionCalendars:
-      handler = [RNPermissionHandlerCalendars new];
+      self.handler = [RNPermissionHandlerCalendars new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerCamera.h")
     case RNPermissionCamera:
-      handler = [RNPermissionHandlerCamera new];
+      self.handler = [RNPermissionHandlerCamera new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerContacts.h")
     case RNPermissionContacts:
-      handler = [RNPermissionHandlerContacts new];
+      self.handler = [RNPermissionHandlerContacts new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerFaceID.h")
     case RNPermissionFaceID:
-      handler = [RNPermissionHandlerFaceID new];
+      self.handler = [RNPermissionHandlerFaceID new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerLocationAlways.h")
     case RNPermissionLocationAlways:
-      handler = [RNPermissionHandlerLocationAlways new];
+      self.handler = [RNPermissionHandlerLocationAlways new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerLocationWhenInUse.h")
     case RNPermissionLocationWhenInUse:
-      handler = [RNPermissionHandlerLocationWhenInUse new];
+      self.handler = [RNPermissionHandlerLocationWhenInUse new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerMediaLibrary.h")
     case RNPermissionMediaLibrary:
-      handler = [RNPermissionHandlerMediaLibrary new];
+      self.handler = [RNPermissionHandlerMediaLibrary new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerMicrophone.h")
     case RNPermissionMicrophone:
-      handler = [RNPermissionHandlerMicrophone new];
+      self.handler = [RNPermissionHandlerMicrophone new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerMotion.h")
     case RNPermissionMotion:
-      handler = [RNPermissionHandlerMotion new];
+      self.handler = [RNPermissionHandlerMotion new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerPhotoLibrary.h")
     case RNPermissionPhotoLibrary:
-      handler = [RNPermissionHandlerPhotoLibrary new];
+      self.handler = [RNPermissionHandlerPhotoLibrary new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerReminders.h")
     case RNPermissionReminders:
-      handler = [RNPermissionHandlerReminders new];
+      self.handler = [RNPermissionHandlerReminders new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerSiri.h")
     case RNPermissionSiri:
-      handler = [RNPermissionHandlerSiri new];
+      self.handler = [RNPermissionHandlerSiri new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerSpeechRecognition.h")
     case RNPermissionSpeechRecognition:
-      handler = [RNPermissionHandlerSpeechRecognition new];
+      self.handler = [RNPermissionHandlerSpeechRecognition new];
       break;
 #endif
 #if __has_include("RNPermissionHandlerStoreKit.h")
     case RNPermissionStoreKit:
-      handler = [RNPermissionHandlerStoreKit new];
+      self.handler = [RNPermissionHandlerStoreKit new];
       break;
 #endif
     case RNPermissionUnknown:
@@ -200,15 +200,13 @@ - (dispatch_queue_t)methodQueue {
   }

 #if RCT_DEV
-  for (NSString *key in [[handler class] usageDescriptionKeys]) {
+  for (NSString *key in [[self.handler class] usageDescriptionKeys]) {
     if (![[NSBundle mainBundle] objectForInfoDictionaryKey:key]) {
       RCTLogError(@"Cannot check or request permission without the required \"%@\" entry in your app \"Info.plist\" file", key);
-      return nil;
+      self.handler = nil;
     }
   }
 #endif
-
-  return handler;
 }

 - (NSString *)stringForStatus:(RNPermissionStatus)status {
@@ -269,14 +267,14 @@ + (void)flagAsRequested:(NSString * _Nonnull)handlerId {
                  checkWithPermission:(RNPermission)permission
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
-  id<RNPermissionHandler> handler = [self handlerForPermission:permission];
+  [self handlerForPermission:permission];

-  [handler checkWithResolver:^(RNPermissionStatus status) {
+  [self.handler checkWithResolver:^(RNPermissionStatus status) {
     NSString *strStatus = [self stringForStatus:status];
-    NSLog(@"[react-native-permissions] %@ permission checked: %@", [[handler class] handlerUniqueId], strStatus);
+    NSLog(@"[react-native-permissions] %@ permission checked: %@", [[self.handler class] handlerUniqueId], strStatus);
     resolve(strStatus);
   } rejecter:^(NSError *error) {
-    NSLog(@"[react-native-permissions] %@ permission failed: %@", [[handler class] handlerUniqueId], error.localizedDescription);
+    NSLog(@"[react-native-permissions] %@ permission failed: %@", [[self.handler class] handlerUniqueId], error.localizedDescription);
     reject([NSString stringWithFormat:@"%ld", (long)error.code], error.localizedDescription, error);
   }];
 }
@@ -285,14 +283,14 @@ + (void)flagAsRequested:(NSString * _Nonnull)handlerId {
                  requestWithPermission:(RNPermission)permission
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
-  id<RNPermissionHandler> handler = [self handlerForPermission:permission];
+  [self handlerForPermission:permission];

-  [handler requestWithResolver:^(RNPermissionStatus status) {
+  [self.handler requestWithResolver:^(RNPermissionStatus status) {
     NSString *strStatus = [self stringForStatus:status];
-    NSLog(@"[react-native-permissions] %@ permission checked: %@", [[handler class] handlerUniqueId], strStatus);
+    NSLog(@"[react-native-permissions] %@ permission checked: %@", [[self.handler class] handlerUniqueId], strStatus);
     resolve(strStatus);
   } rejecter:^(NSError *error) {
-    NSLog(@"[react-native-permissions] %@ permission failed: %@", [[handler class] handlerUniqueId], error.localizedDescription);
+    NSLog(@"[react-native-permissions] %@ permission failed: %@", [[self.handler class] handlerUniqueId], error.localizedDescription);
     reject([NSString stringWithFormat:@"%ld", (long)error.code], error.localizedDescription, error);
   }];
 }

@booker-dragon Indeed, this happens because of ARC being a little too aggressive. But the solution is too fragile and will not work with concurrent checks / requests.

A solution could be a NSMutableArray<id<RNPermissionHandler>>, then adding handlers instances in it / set them to nil when the job is done (using the array index)

It's available: https://github.com/react-native-community/react-native-permissions/releases/tag/2.0.9

I added identical logical for notification permission. Thanks again!

Was this page helpful?
0 / 5 - 0 ratings