Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 543
bevoy Avatar asked Oct 03 '19 12:10

bevoy


1 Answers

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:

  • https://nshipster.com/method-swizzling/
  • https://nshipster.com/swift-objc-runtime/
  • https://medium.com/rocknnull/ios-to-swizzle-or-not-to-swizzle-f8b0ed4a1ce6
like image 194
bevoy Avatar answered Oct 12 '22 02:10

bevoy