Search code examples
iosobjective-cswiftios13

Change modalPresentationStyle on iOS13 on all UIViewController instances at once using method swizzling


[Q&A] Is it possible to change UIViewController.modalPresentationStyle value globally on iOS 13 so it behaves like it used to on iOS 12 (or earlier)?


Why?

In iOS 13 SDK the default value of UIViewController.modalPresentationStyle property has been changed from UIModalPresentationFullScreen to UIModalPresentationAutomatic which is, as far as I know, resolved to UIModalPresentationPageSheet on iOS devices or at least on iPhones.

Since the project I've been working for several years has become quite big, there are tens of places where a view controller is presented. The new presentation style doesn't always match our app designs and sometimes it causes the UI to fall apart. Which is why, we deciced to change UIViewController.modalPresentationStyle back to UIModalPresentationFullScreen as it was is pre-iOS13 SDK's versions.

But adding viewController.modalPresentationStyle = UIModalPresentationFullScreen before calling presentViewController:animated:completion: in every single place where a controller is presented seemed like an overkill. Moreover, we had more serious matters to deal with at that time, which is why, for the time being or at least until we update our designs and fix all the UI issues, we decided to go with method swizzling approach.

The working solution is presented in my answer, but I would appreciate any feedback telling me what downsides or consequences of such an approach might be.


Solution

  • Here's how we achieved that by using method swizzling:


    Objective-C

    UIViewController+iOS13Fixes.h

    #import <Foundation/Foundation.h>
    
    @interface UIViewController (iOS13Fixes)
    @end
    

    UIViewController+ iOS13Fixes.m

    #import <objc/runtime.h>
    
    @implementation UIViewController (iOS13Fixes)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            SEL originalSelector = @selector(presentViewController:animated:completion:);
            SEL swizzledSelector = @selector(swizzled_presentViewController:animated:completion:);
    
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
            BOOL methodExists = !class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
            if (methodExists) {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            } else {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }
        });
    }
    
    - (void)swizzled_presentViewController:(nonnull UIViewController *)viewController animated:(BOOL)animated completion:(void (^)())completion {
    
        if (@available(iOS 13.0, *)) {
            if (viewController.modalPresentationStyle == UIModalPresentationAutomatic || viewController.modalPresentationStyle == UIModalPresentationPageSheet) {
                viewController.modalPresentationStyle = UIModalPresentationFullScreen;
            }
        }
    
        [self swizzled_presentViewController:viewController animated:animated completion:completion];
    }
    
    @end
    

    Swift

    UIViewController+iOS13Fixes.swift

    import UIKit
    
    @objc public extension UIViewController {
    
        private func swizzled_present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) {
    
            if #available(iOS 13.0, *) {
                if viewControllerToPresent.modalPresentationStyle == .automatic || viewControllerToPresent.modalPresentationStyle == .pageSheet {
                    viewControllerToPresent.modalPresentationStyle = .fullScreen
                }
            }
    
            self.swizzled_present(viewControllerToPresent, animated: animated, completion: completion)
        }
    
        @nonobjc private static let _swizzlePresentationStyle: Void = {
            let instance: UIViewController = UIViewController()
            let aClass: AnyClass! = object_getClass(instance)
    
            let originalSelector = #selector(UIViewController.present(_:animated:completion:))
            let swizzledSelector = #selector(UIViewController.swizzled_present(_:animated:completion:))
    
            let originalMethod = class_getInstanceMethod(aClass, originalSelector)
            let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector)
    
            if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
                if !class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) {
                    method_exchangeImplementations(originalMethod, swizzledMethod)
                } else {
                    class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
                }
            }
        }()
    
        @objc static func swizzlePresentationStyle() {
            _ = self._swizzlePresentationStyle
        }
    }
    

    and in AppDelegate, in application:didFinishLaunchingWithOptions: invoke the swizzling by calling (swift version only):

    UIViewController.swizzlePresentationStyle()
    

    Make sure it's called only once (use dispatch_once or some equivalent).


    More on method swizzling here: