Set global default back button display mode for all UIViewController instances - ios

iOS 14 introduced configurable Back button mode.
Eg. you can have "Back" label text on the button, but in the compact history menu you can still see the proper title of previous controllers.
I'm looking for a easy, pleasant way to configure the default mode, so all UIViewController instances during runtime will have default mode set, like UINavigationItemBackButtonDisplayModeGeneric
I wonder if there is a way to do that without subclassing UIViewController or remember to always configure manually every instance of UIViewController
(via viewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeGeneric).
Any handy method which does not require extensive refactoring of hundreds of UIViewController instances greatly appreciated!

Without subclassing I think is not possible since navigationItem requires an instance to work with and can't be modified directly from extensions
class GenericViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// your code here
viewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeGeneric
}
}
And use that where ever you need
class viewController: GenericViewController
This is a really good approach since you have control over what implements it and what's not considering that it might not be there in all scenes

To solve the same problem, I used the swizzling technique
import UIKit
private let swizzling: (UIViewController.Type, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
let didAddMethod = class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
extension UIViewController {
static func swizzle() {
let originalSelector1 = #selector(viewDidLoad)
let swizzledSelector1 = #selector(swizzled_viewDidLoad)
swizzling(UIViewController.self, originalSelector1, swizzledSelector1)
}
#objc open func swizzled_viewDidLoad() {
if let _ = navigationController {
if #available(iOS 14.0, *) {
navigationItem.backButtonDisplayMode = .generic
} else {
// Fallback on earlier versions
navigationItem.backButtonTitle = "Back"
}
}
swizzled_viewDidLoad()
}
}
And in application(_:didFinishLaunchingWithOptions:) call
UIViewController.swizzle()

Related

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.
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

Repeat same statement in all viewDidLoad functions possible extension file

I want this same statement repeated in all of my projects' viewDidLoad functions. I know I can just manually type it in but I am trying to find a way to increase my code speed. I don't know if I can use a extension file in this.
override func viewDidLoad() {
super.viewDidLoad()
let myswitchBoolValuefromFirstVc : Bool = UserDefaults.standard.bool(forKey: "mySwitch")// this is how you retrieve the bool value
// to see the value, just print those with conditions. you can use those for your things.
if myswitchBoolValuefromFirstVc == true {
print("true")
rosaryCounterLabel.isHidden = false
}
else {
print("false")
rosaryCounterLabel.isHidden = true
}
If you are prepared to abuse the Objective-C runtime that UIViewController still uses, you can use method swizzling to do what you ask. https://medium.com/#abhimuralidharan/method-swizzling-in-ios-swift-1f38edaf984f
let aClass: AnyClass! = object_getClass(instance)
let originalMethod = class_getInstanceMethod(aClass, #selector(viewDidLoad))
let swizzledMethod = class_getInstanceMethod(aClass, #selector(newViewDidLoad))
if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
but I wouldn't recommend it. You trade between discoverability and repetition. In the case where you control all the code, it is going to be easier to maintain using a solution like subclassing that will still require some changes in every view controller.
Create a "master" view controller.
class MasterViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//"Global" code here
}
}
And then inherit in all your other view controllers
class ViewController: MasterViewController{
override func viewDidLoad() {
super.viewDidLoad()
//controller specific code here
}
}

Swift - method swizzling

This is the method swizzling code written in Objective-C. I am having a hard time converting this in Swift.
void MPApplicationDidRegisterForRemoteNotificationsWithDeviceToken(id self, SEL _cmd, UIApplication *application, NSData *deviceToken) {
[[MPPush shared] appRegisteredForRemoteNotificationsWithDeviceToken:deviceToken];
IMP original = [MPAppDelegateProxy originalImplementation:_cmd class:[self class]];
if (original)
((void(*)(id, SEL, UIApplication *, NSData*))original)(self, _cmd, application, deviceToken);
}
Swiftify isn't converting the above code correctly.
I tried to do this but I am not sure how to pass parameters and use the exact above parameters in Swift swizzling method. This is my fail attempt to convert the above in Swift (code doesn't even compile) :
var MPApplicationDidRegisterForRemoteNotificationsWithDeviceToken: Void {
// TODO: MPPush.shared.app
let original = MPAppDelegateProxy.proxyAppDelegate.originalImplementation(selector: cmd, forClass: type(of: self))
}(self: Any, _cmd: Selector, application: UIApplication, deviceToken: Data)
Extend your class:
extension YourClassName {
static let classInit: () -> () = {
let originalSelector = #selector(originalFunction)
let swizzledSelector = #selector(swizzledFunction)
swizzle(YourClassName.self, originalSelector, swizzledSelector)
}
#objc func swizzledFunction() {
//Your new implementation
}
}
Your class (YourClassName) should inherit from NSObject and originalSelector should be a dynamic method.
swizzle is a closure that exchanges the implementations:
private let swizzle: (AnyClass, Selector, Selector) -> () = { fromClass, originalSelector, swizzledSelector in
guard
let originalMethod = class_getInstanceMethod(fromClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(fromClass, swizzledSelector)
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}
You could define swizzle in the class where you are going to do the swizzling. For example the AppDelegate class definition. And then do the swizzling in AppDelegate.init():
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
override init() {
super.init()
YourClassName.classInit()
}
}
Example
Here is a concrete example of swizzling, it exchanges the implementation of two methods that take one argument:
class Example: NSObject {
#objc dynamic func sayHi(to name: String) {
print("Hi", name)
}
}
extension Example {
public class func swizzleMethod() {
guard
let originalMethod = class_getInstanceMethod(Example.self, #selector(Example.sayHi(to:))),
let swizzledMethod = class_getInstanceMethod(Example.self, #selector(Example.sayHello(to:)))
else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}
#objc func sayHello(to name: String) {
print("Hello", name)
}
}
Example.swizzleMethod()
let a = Example()
a.sayHi(to: "Nitish") //Hello Nitish
Swift Native swizzling
As of Swift 5.1, there is a native version of method swizzling that does not rely on Objective-C’s message passing. The #_dynamicReplacement modifier can be used upon a replacement function, and takes as an argument the name of the function that it should replace. The function that is being replaced must be marked with the #dynamic modifier, unless -enable-implicit-dynamic compilation flag is used, which makes the compiler assume that every eligible entity has been marked with the modifier.
For example:
dynamic func original() {
print("I am the original")
}
#_dynamicReplacement(for: original)
func replacement() {
print("I am the replacement")
}
original() // prints "I am the replacement"
For more details on dynamic method replacement, visit this Swift forum page.

Swizzle Tap Gesture Action in iOS Cocoa Touch Framework

I am developing an Cocoa Touch Framework or a dynamic Library which would capture user actions using Swift.
E.g. When a user taps on a label, my framework should know about it.
For achieving this behaviour, I am using method swizzling.
I am creating an extension for the UITapGestureRecognizer.In this extension, in initialize method , I am swizzling the init :
open override class func initialize() {
guard self === UITapGestureRecognizer.self else { return }
let originalSelector = #selector(self.init(target:action:))
let swizzledSelector = #selector(swizzled_init(target:action:))
swizzling(self, originalSelector, swizzledSelector)
}
func swizzled_init(target: Any, action: Selector) {
swizzled_init(target : target,action: action)
print("swizzled_init"+action.description)
//This part is for swizzling the the action which is being provided by the developer at the time of init.
guard self === UITapGestureRecognizer.self else { return }
let originalSelector = action
let swizzledSelector = #selector(swizzled_action(sender:))
swizzling(self.classForCoder, originalSelector, swizzledSelector)
}
func swizzled_action(sender : UITapGestureRecognizer? = nil){
print("swizzled_action"+(sender?.description)!)
}
After swizzling init, the instance of tap is nil in my ViewController.
let tap = UITapGestureRecognizer.init(target: self, action: #selector(buttonTapped(sender:)))
And the second issue is this in the swizzled_init method:
guard self === UITapGestureRecognizer.self else { return }
which is getting failed.
Could you please direct me in resolving these issues?

Swizzling CocoaTouch class in Swift 3.1

I use Swift 3.1 in XCode 8.3 and see that warning:
Method 'initialize()' defines Objective-C class method 'initialize',
which is not guaranteed to be invoked by Swift and will be disallowed
in future versions
I use Swizzling CocoaTouch class and have an issue with that part:
extension UIViewController {
open override class func initialize() {
// make sure this isn't a subclass
guard self === UIViewController.self else { return }
swizzling(self)
}
// MARK: - Method Swizzling
func proj_viewWillAppear(animated: Bool) {
self.proj_viewWillAppear(animated: animated)
let viewControllerName = NSStringFromClass(type(of: self))
print("viewWillAppear: \(viewControllerName)")
}
}
How to rewrite that part of the code?
open override class func initialize()
for fix new warning?
I saw that link, but I don't understand how to use info in my code.
I had same problem and fix it.
My Solution
1. Change swizzling() method to public from private
public let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in
let originalMethod = class_getInstanceMethod(forClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
2. Remove initialize() method at extension UIViewController
// open override class func initialize() {
// // make sure this isn't a subclass
// guard self === UIViewController.self else { return }
// swizzling(self)
// }
3. Add swizzling() at AppDelegate::didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// some code
swizzling(UIViewController.self)
return true
}
This is simply/easy solution. (this is not Elegant)
If you want to get more information, please see this:
Swift 3.1 deprecates initialize(). How can I achieve the same thing?

Resources