flowkey / react-native-home-indicator

A <PrefersHomeIndicatorAutoHidden /> component for react-native
MIT License
79 stars 24 forks source link

iOS crashes on launch (expo) #43

Open ideopunk opened 2 years ago

ideopunk commented 2 years ago

I'm attempting to create a config plugin so that this library will work with Expo. However, on app launch I get this error:

DevLauncher tries to handle uncaught exception: rootViewController is not of type HomeIndicatorViewController as expected.

My AppDelegate.m didn't have UIViewController *rootViewController = [UIViewController new]; as a line to replace, it had UIViewController *rootViewController = [self.reactDelegate createRootViewController]; instead, so I replaced it. I suspect this has something to do with the problem.

albinorats1 commented 2 years ago

I am also having this problem, will this be addressed soon?

jakeorbtl commented 2 years ago

Also having this problem. I tried typecasting the the rootViewController like this:

UIViewController *rootViewController = (HomeIndicatorViewController *) [self.reactDelegate createRootViewController];

and this:

UIViewController *rootViewController = [HomeIndicatorViewController new]; rootViewController = (HomeIndicatorViewController *) [self.reactDelegate createRootViewController];

but they both got the same error. Bear in mind, I don't know the first thing about Objective-C, or Swift, or managing a bare react-native workflow, so I don't know if I typecasted correctly, or if it even makes sense to typecast in this context. I'm using an expo managed project that has been ejected with expokit.

jakeorbtl commented 2 years ago

Well the package is working as intended now, and the reason is that I replaced

UIViewController *rootViewController = [self.reactDelegate createRootViewController];

with

UIViewController *rootViewController = [HomeIndicatorViewController new];

and it worked. I didn't try it before since @ideopunk said they did the same and ran into a problem. I checked the issues before ever trying it myself and just assumed I would run into the same problem. That said, I'm not sure why it worked for me and not them. Still, I think the installation guide should address this case for people using expo projects.

michaelknoch commented 2 years ago

Casting will not work because we need to create the rootView from the HomeIndicatorViewController class. This is mandatory for the functionality of the library. Thats also why the error explicitly gets thrown when the rootViewController of the app does not have the correct type.

I'm unsure if replacing the init of the *rootViewController will reliably work for expo projects. I can see here that it is possible that the rootViewController is supposed to come from self.handlers which probably has a reason. When we replace the controllers in AppDelegate we ignore controllers which may have been initialised and stored in self.handlers before.

I don't have any experience with expo so I don't have a feeling for the downsides of this approach. Does it still work for you @jakeorbtl ?

syropian commented 2 years ago

@ideopunk Did you have any luck ever getting this to work?

markobalogh commented 1 year ago

Also wondering :-)

syropian commented 1 year ago

Here's a working config plugin for anyone who needs it.

Note: This simply turns it off entirely. It does not provide the option to set it manually.

const { withAppDelegate } = require('@expo/config-plugins')

function withHiddenHomeIndicator(expoConfig) {
  return withAppDelegate(expoConfig, (config) => {
    const { modResults } = config
    const { contents } = modResults
    const lines = contents.split('\n')

    const importIndex = lines.findIndex((line) => /^#import "AppDelegate.h"/.test(line))

    modResults.contents = [
      '#import <objc/runtime.h>',
      ...lines.slice(0, importIndex + 1),
      ...lines.slice(importIndex + 1),
      '@implementation UIViewController (Swizzling)',
      '+ (void)load',
      '{',
      '    static dispatch_once_t onceToken;',
      '    dispatch_once(&onceToken, ^{',
      '        Class classVC = [self class];',
      '        SEL originalSelector = @selector(prefersHomeIndicatorAutoHidden);',
      '        SEL swizzledSelector = @selector(swizzledPrefersHomeIndicatorAutoHidden);',
      '        Method originalMethod = class_getInstanceMethod(classVC, originalSelector);',
      '        Method swizzledMethod = class_getInstanceMethod(classVC, swizzledSelector);',
      '        const BOOL didAdd = class_addMethod(classVC, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));',
      '        if (didAdd)',
      '            class_replaceMethod(classVC, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));',
      '        else',
      '            method_exchangeImplementations(originalMethod, swizzledMethod);',
      '    });',
      '}',
      '- (BOOL)prefersHomeIndicatorAutoHidden {',
      '    return YES;',
      '}',
      '- (BOOL)swizzledPrefersHomeIndicatorAutoHidden {',
      '    return YES;',
      '}',
      '@end',
    ].join('\n')

    return config
  })
}

module.exports = withHiddenHomeIndicator
ephemer commented 1 year ago

Wow, I'm impressed and terrified that it's possible to alter AppDelegate.h so drastically without even (re)building a native app.

I knew objective c was quite dynamic but I didn't realise it was that dynamic.

Thanks for your contribution!

StackSamurai123 commented 6 months ago

Here's a working config plugin for anyone who needs it.

Note: This simply turns it off entirely. It does not provide the option to set it manually.

const { withAppDelegate } = require('@expo/config-plugins')

function withHiddenHomeIndicator(expoConfig) {
  return withAppDelegate(expoConfig, (config) => {
    const { modResults } = config
    const { contents } = modResults
    const lines = contents.split('\n')

    const importIndex = lines.findIndex((line) => /^#import "AppDelegate.h"/.test(line))

    modResults.contents = [
      '#import <objc/runtime.h>',
      ...lines.slice(0, importIndex + 1),
      ...lines.slice(importIndex + 1),
      '@implementation UIViewController (Swizzling)',
      '+ (void)load',
      '{',
      '    static dispatch_once_t onceToken;',
      '    dispatch_once(&onceToken, ^{',
      '        Class classVC = [self class];',
      '        SEL originalSelector = @selector(prefersHomeIndicatorAutoHidden);',
      '        SEL swizzledSelector = @selector(swizzledPrefersHomeIndicatorAutoHidden);',
      '        Method originalMethod = class_getInstanceMethod(classVC, originalSelector);',
      '        Method swizzledMethod = class_getInstanceMethod(classVC, swizzledSelector);',
      '        const BOOL didAdd = class_addMethod(classVC, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));',
      '        if (didAdd)',
      '            class_replaceMethod(classVC, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));',
      '        else',
      '            method_exchangeImplementations(originalMethod, swizzledMethod);',
      '    });',
      '}',
      '- (BOOL)prefersHomeIndicatorAutoHidden {',
      '    return YES;',
      '}',
      '- (BOOL)swizzledPrefersHomeIndicatorAutoHidden {',
      '    return YES;',
      '}',
      '@end',
    ].join('\n')

    return config
  })
}

module.exports = withHiddenHomeIndicator

I want to ask if it is possible to do with this preferredScreenEdgesDeferringSystemGestures method what i tried -

const { withAppDelegate } = require("@expo/config-plugins");

function withDeferredSystemGestures(expoConfig) {
  return withAppDelegate(expoConfig, (config) => {
    const { modResults } = config;
    const { contents } = modResults;
    const lines = contents.split("\n");

    const importIndex = lines.findIndex((line) =>
      /^#import "AppDelegate.h"/.test(line)
    );

    // Inject Objective-C code to override preferredScreenEdgesDeferringSystemGestures
    modResults.contents = [
      ...lines.slice(0, importIndex + 1),
      "#import <objc/runtime.h>",
      ...lines.slice(importIndex + 1),
      "@implementation UIViewController (DeferredSystemGestures)",
      "+ (void)load",
      "{",
      "    static dispatch_once_t onceToken;",
      "    dispatch_once(&onceToken, ^{",
      "        Class class = [self class];",
      "        SEL originalSelector = @selector(preferredScreenEdgesDeferringSystemGestures);",
      "        SEL swizzledSelector = @selector(swizzled_preferredScreenEdgesDeferringSystemGestures);",
      "        Method originalMethod = class_getInstanceMethod(class, originalSelector);",
      "        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);",
      "        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));",
      "        if (didAddMethod) {",
      "            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));",
      "        } else {",
      "            method_exchangeImplementations(originalMethod, swizzledMethod);",
      "        }",
      "    });",
      "}",
      "",
      // This method will return the edges you want to defer system gestures for.
      // Here, we're deferring the bottom edge as an example.
      // You can change UIRectEdgeBottom to UIRectEdgeLeft, UIRectEdgeRight, or UIRectEdgeTop as needed.
      "- (UIRectEdge)swizzled_preferredScreenEdgesDeferringSystemGestures {",
      "    return UIRectEdgeBottom;",
      "}",
      "@end",
    ].join("\n");

    return config;
  });
}

module.exports = withDeferredSystemGestures;

but i got error... I am trying to modify the iOS behavior where the home indicator dims, then requires two swipes to activate: the first swipe to brighten the indicator and the second swipe to invoke the home action. This is to mimic the behavior of preferredScreenEdgesDeferringSystemGestures in a native iOS app

Ever-It-Lazy commented 5 months ago

@syropian Are you using this in any of your repos?

How does one instantiate your plugin? I've tried importing it and adding a <withHiddenHomeIndicator /> tag to my Expo app, but no dice: "Unable to resolve module assert from ...@expo/config-plugins/build/android/Manifest.js: assert could not be found"

syropian commented 5 months ago

@Ever-It-Lazy It's a pretty quick-n-dirty solution so keep in mind it will apply globally to your app. All you need to do is add the code I wrote to a file called something like withHiddenHomeIndicator.js in a plugins directory at the root, and then reference it in your app.json plugins section

// ...
"plugins": [
  "./plugins/withHiddenHomeIndicator.js",
],
//...
Ever-It-Lazy commented 5 months ago

@syropian Global is fine. An ever-present Home Indicator kind of ruins the app I'm trying to build.

I've attempted what you said in a bare bones Expo app. It no longer errs, but the Home Indicator is just as visible as ever, at the screen's bottom.

Ever-It-Lazy commented 5 months ago

I guess I'd also be cool with the workaround devised by @jakeorbtl...if I could understand how to apply and deploy it?

It seems like the file being changed is node modules > react-native > Libraries > AppDelegate > RCTAppDelegate.mm?

  1. Changing "UIViewController" to "HomeIndicatorViewController" in the one place indicated didn't help. Expo still throws errors.
  2. Aren't files in the node modules directory handled by a package manager like npm or yarn? This wouldn't be part of the project that deploys. How would I then reapply this change across different deployments? (Hence, @syropian's contribution seems like a more portable, sound execution.)
syropian commented 4 months ago

It no longer errs, but the Home Indicator is just as visible as ever

@Ever-It-Lazy FYI this will only work in the context of a development build using expo-dev-client or a production build. It will not work in Expo Go. That may be why you're still seeing it?

Ever-It-Lazy commented 4 months ago

@syropian It took me a while to learn expo-dev-client. My discoveries:

I had to eventually learn deployment, anyway, so I welcome the experiment. But I did include a repo above, to make this trial-and-error a bit more tangible. So please, do let me know if you ever post a simple, functional example of your plugin (or get my example working).

*correction: your plugin has roughly the same effect as the autoHideHomeIndicator screenOption of the Stack.Navigator tag. (My repo makes no use of the Navigator API.) This is more than no effect. But the Home Indicator is still only optionally hidden, returning whenever a finger gestures across the screen.