I used method swizzling to get a hold of the keyboard VC. Hacky as hell of course, but that's what it takes sometimes... I just use it to fix a glitch/bug with the keyboard that Apple never bothered to fix for years...
Post
Replies
Boosts
Views
Activity
Add this somewhere:
extension UIViewController {
static func swizzleLifecycleMethods() {
//this makes sure it can only swizzle once
_ = self.actuallySwizzleLifecycleMethods
}
private static let actuallySwizzleLifecycleMethods: Void = {
let originalVdlMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidLoad))
let swizzledVdlMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidLoad))
method_exchangeImplementations(originalVdlMethod!, swizzledVdlMethod!)
let originalVwaMethod = class_getInstanceMethod(UIViewController.self, #selector(viewWillAppear(_:)))
let swizzledVwaMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewWillAppear(_:)))
method_exchangeImplementations(originalVwaMethod!, swizzledVwaMethod!)
let originalVdaMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))
let swizzledVdaMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidAppear(_:)))
method_exchangeImplementations(originalVdaMethod!, swizzledVdaMethod!)
let originalVddMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidDisappear(_:)))
let swizzledVddMethod = class_getInstanceMethod(UIViewController.self, #selector(swizzledViewDidDisappear(_:)))
method_exchangeImplementations(originalVddMethod!, swizzledVddMethod!)
}()
@objc private func swizzledViewDidLoad() -> Void {
swizzledViewDidLoad() //run original implementation
print("swizzledViewDidLoad \(self)")
}
@objc private func swizzledViewWillAppear(_ animated: Bool) -> Void {
swizzledViewWillAppear(animated) //run original implementation
print("swizzledViewWillAppear \(self)")
if type(of: self).description() == "UICompatibilityInputViewController" {
self.view.printSubViews()
if (self.view?.subviews.count == 0) {
self.view?.backgroundColor = .red
}
}
}
@objc private func swizzledViewDidAppear(_ animated: Bool) -> Void {
swizzledViewDidAppear(animated) //run original implementation
print("swizzledViewDidAppear \(self)")
}
@objc private func swizzledViewDidDisappear(_ animated: Bool) -> Void {
swizzledViewDidDisappear(animated) //run original implementation
print("swizzledViewDidDisappear \(self)")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
if let self {
print("swizzled VC stil in memory after disappearing for 1s: \(self)")
}
}
}
}
extension UIView {
/**
Recursively prints all subviews to console, indented according to level for easy view tree assessment.
- Parameter level: initial indentation level, default is 0
*/
func printSubViews(level : UInt = 0) {
var tabs = ""
for _ in 0..<level {
tabs += "\t"
}
//do your print() here of whatever you'd like to know of each view:
print("\(tabs)\(self)")
for subview in self.subviews {
subview.printSubViews(level: level+1)
}
}
}
Then call UIViewController.swizzleLifecycleMethods() once somewhere to activate the swizzling, I suggest in didFinishLaunchingWithOptions. All keyboards should look red now :). It should print lifecycle events of all VCs to the console, including system ones like the keyboard.
iOS 17 still has the same issue but at least now there is a way to avoid it:
The leak will clear once you set isSelectable to false. So I'd advice to do this before removeFromSuperview() or in VC's deinit.
myTextView.isSelectable = false
myTextView.removeFromSuperview()
//in ViewController
deinit {
myTextView.isSelectable = false
}
Or even better is to just subclass UITextView and override removeFromSuperview():
//in UITextView subclass
override func removeFromSuperview() {
let wasSelectable = isSelectable
isSelectable = false
super.removeFromSuperview()
isSelectable = wasSelectable
}
Even setting isSelectable directly back to true the next line will work (at least in debug builds). This way the links still work if you decide to reuse the view.
I noticed UITextFields don't always deinit when closing its VC (or using removeFromSuperview()), but only if it has become first responder at least once.
Actually, if you have a VC with multiple UITextFields, let one become first responder, then close the VC, ALL UITextFields won't deinit! Then if you activate another UITextField in another VC they will deinit after all. I believe it happens more often when textContentType isn't nil.
To check yourself just subclass UITextField and check the deinit calls in the console:
class TestTextField: UITextField {
deinit {
print("deinitted textfield \(self.placeholder ?? "") \(self.text ?? "")")
}
}