avatarMatt Croak Code

Summary

The article discusses an improved method for closing a UIViewController from a React Native app on iOS, focusing on a more reliable approach than previously outlined.

Abstract

In a follow-up to a previous blog post, the author revisits the topic of closing a UIViewController within a React Native application on iOS. The initial solution involved using the rootViewController, which, while functional, posed risks of unintended side effects due to its role as the foundational view controller in the app's UI hierarchy. The author explains why this method was unpredictable and could potentially dismiss more than the intended view controller. The article then introduces a refined solution that checks for the specific class of the view controller to be dismissed, ensuring that only the targeted UIViewController instance, in this case, the SocketCam view controller, is dismissed without affecting other parts of the app. This approach leverages the RCTPresentedViewController method and the isKindOfClass check to provide a safer and more predictable way to dismiss modally presented view controllers from React Native.

Opinions

  • The author believes that using the rootViewController to dismiss a view controller is not an ideal solution due to the risk of affecting the entire view hierarchy.
  • The article suggests that the initial method worked unexpectedly due to the specific context of the SocketCam view controller being presented as a modal over the full screen, which isolated it from the main React Native view hierarchy.
  • The author emphasizes the importance of executing the dismissal method within the correct context, specifically after data scanning and when the correct view controller class is present.
  • The improved method is presented as a safer alternative that avoids the potential pitfall of rendering the app blank by ensuring that only the intended UIViewController is dismissed.
  • The author invites readers to share their insights or alternative methods for achieving the same logic in the comments, indicating an openness to further discussion and improvement.

How to Close A UIViewController From Your React Native App — The BETTER Way

Refining our previous solution

Image from Thinkwik

Previously, I wrote a blog post about how to close a UIViewController from your React Native app on iOS.

The solution I provided makes use of the rootViewController. This is the UIViewController instance that is first displayed on the iOS platform of a React Native app.

Now, this worked for us, but it’s not always the ideal solution. This post will cover a safer way to do it. But first, let’s talk about why it’s unpredictable to do it this way, and why it worked for our use case.

Why It’s Unpredictable

The problem with using the rootViewController is that it is, when talking about the context of a RN app on iOS, the first UIViewController instance that is placed in the UI of the app (hence the term, “root”).

By grabbing the rootViewController, you run the risk of grabbing the first parent view, if you will. When you dismiss this view, you run the risk of subsequently dismissing all of the views within the root view.

In theory, what should have happened in our app is the screen should have been rendered completely blank. Yet, when I tested it numerous times as I always do before committing, it worked completely as expected.

Why?

Why It Worked

The solution worked because we were using Apple’s dismissViewControllerAnimated method to properly “dismiss” a view controller. The rootViewController is just that — a UIViewController.

Now, the reason it didn’t render the app blank, I think can be boiled down to one of two reasons (or both concurrently).

The Native iOS Context

First, the SocketCamC820 view UIViewController was being “injected” into the React Native UI. This is because we were piggy backing off of the iOS SDK’s native code.

So, in essence, we had two “root” views in the same UI: the React Native context and the iOS context. The method used to dismiss the UIViewController was being called from the React Native side, but within the context of the Native Module (i.e. the piggy-backed iOS UI).

The Modal Presentation Style

The second reason is because of modal behavior. In the Native Modules, I was presenting the SocketCam UI window as a modal. This was because in our native code, we were instantiating a UIViewController for SocketCam, and then we assigned the value UIModalPresentationOverFullScreen to the modalPresentationStyle property.

socketCamViewController.modalPresentationStyle = UIModalPresentationOverFullScreen;

This presentation style could in a sense pull the UIViewController instance for the iOS code further out of the context of the rootViewController of the React Native app.

Another important consideration for why this worked is to discuss how/when the dismiss method was executed.

How and When

You already know this was being invoked within the React Native UI, but I was using it in a function that gets executed after data is scanned.

I won’t dive too far into the specifics, but if you scan any data, there is a condition that checks to make sure the device type is SocketCam instance (either C820 or C860 for advanced camera scanning). If it is, then it runs this command to remove the view controller. If it’s not (i.e. there is no view controller) then this method is ignored.

The only issue is, if I, say, added a button to call the native method on it’s own — like even when there is no SocketCam UIViewController — it would probably find the React Native root view controller. This would ultimately render the React Native app blank.

So how was I able to refine this and make it more predictable?

The Solution

Below is how we initially got the rootViewController.

  UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController;

However, our SocketCam UIViewController was an instance of the Socket Cam view controller class — provided by our iOS SDK. So in the dismissViewController method, I was able to check to see if this class existed on the ReactPresentedViewController — the currently presented view used in our React Native app.

If it existed, then remove it. See below.

RCT_EXPORT_METHOD(dismissViewController) {
    dispatch_async(dispatch_get_main_queue(), ^{
        UIViewController* currentViewController = RCTPresentedViewController();
        if ([currentViewController isKindOfClass:NSClassFromString(@"SOCKET_CAM_VIEW_CONTROLLER_CLASS")]) {
            [currentViewController dismissViewControllerAnimated:YES completion:nil];
        }
    });
} 

If the current view is not of this class type, then the dismissal code is ignored. This way we only dismiss the UIViewController associated with SocketCam and not any other important UIViewController instance — such as the one associated with the React Native app’s root view!

Do you have another way of achieving this logic? Or any other reason why the first method worked at all? Let me know in the comments?

🙏 You can also subscribe via email and get notified whenever I post something new!

References

React Native
React
iOS
Code
Software Development
Recommended from ReadMedium