Swizzling CocoaTouch class in Swift 3.1 - ios

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?

Related

How to track changes in currently visible ViewController as user navigating the screens in iOS?

I'm trying to replicate Firebase Analytics behaviour, which automatically fire screen events whenever ViewController screen get's changed with another.
Though I'm able to find currently visible ViewController using :
UIApplication.shared.windows.first?.rootViewController?.presentedViewController
But I need some way to get notified for any change in rootViewController. I tried to observe this rootViewController using KVO, but I don't get any callback. I found that KVO only works on NSObject with dynamic properties.
Is there any way I could receive callback for change in ViewController? Since this will be a library project, I couldn't make changes in main code to support the feature.
Following solution worked for me:-
import Foundation
import UIKit
public extension UIViewController {
#objc dynamic func _tracked_viewWillAppear(_ animated: Bool) {
UserActivityTracker.startTracking(viewController: self)
}
static func swizzle() {
//Make sure This isn't a subclass of UIViewController,
//So that It applies to all UIViewController childs
if self != UIViewController.self {
return
}
let _: () = {
let originalSelector =
#selector(UIViewController.viewWillAppear(_:))
let swizzledSelector =
#selector(UIViewController._tracked_viewWillAppear(_:))
let originalMethod =
class_getInstanceMethod(self, originalSelector)
let swizzledMethod =
class_getInstanceMethod(self, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!);
}()
}
}
In above code _tracked_viewWillAppear() is my custom function which I want to call my implementation before actual implementation called.
Then in AppDeligate class, call UIViewController.swizzle() method, as follows:-
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UIViewController.swizzle()
return true
}

Set global default back button display mode for all UIViewController instances

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()

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.

Swift override function in extension

If I have a class:
class Spaceship<FuelType> {
function prepareForLiftoff() throws {
//Start the countdown!
}
}
I originally assumed that I would be able to override prepareForLiftoff without subclassing by adding an extension:
extension Spaceship where FuelType: CollectionType {
func prepareForLiftoff() throws {}
}
This code doesn't compile though, the error says invalid redeclaration of the function, which makes sense.
My question is: Is there anyway to override a function of a particular class? In other words can I replace the functionality under certain conditions like the example above where FuelType: CollectionType. If not, is there any other workaround or way to achieve that behavior (maybe declaring another protocol, idk)
Now that I think about it more, I would have to say that's not possible because what's to stop someone from overriding any of the standard library functions?
From the documentation:
Extensions can add new functionality to a type, but they cannot override existing functionality.
The documentation lists carefully and precisely what an extension is allowed to do.
As to your question:
Is there anyway to override a function of a particular class
Yes, it's called subclassing.
Instead of overriding, you may like to try swizzling. For example the following code allows you to run your own viewWillAppear. Swift 3:
extension UIViewController {
open override class func initialize() {
// make sure this isn't a subclass
guard self === UIViewController.self else { return }
DispatchQueue.once(token: "viewWillAppear") {
let originalSelector = #selector(self.viewWillAppear(_:))
let swizzledSelector = #selector(self.proj_viewWillAppear1(animated:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
func proj_viewWillAppear1(animated: Bool) {
self.proj_viewWillAppear1(animated: animated)
let viewControllerName = NSStringFromClass(type(of: self))
print("viewWillAppear: \(viewControllerName)")
}
}
Update 20170621
public extension DispatchQueue {
private static var _onceTracker = [String]()
public class func once(file: String = #file, function: String = #function, line: Int = #line, block:(Void)->Void) {
let token = file + ":" + function + ":" + String(line)
once(token: token, block: block)
}
public class func once(token: String, block:(Void)->Void) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}

Resources