// // UIMenuController.swift // MenuItemKit // // Created by CHEN Xian’an on 1/17/16. // Copyright © 2016 lazyapps. All rights reserved. // import UIKit import ObjectiveC.runtime // This is inspired by https://github.com/steipete/PSMenuItem private func swizzle(class klass: AnyClass) { objc_sync_enter(klass) defer { objc_sync_exit(klass) } let key: StaticString = #function guard objc_getAssociatedObject(klass, key.utf8Start) == nil else { return } if true { // swizzle canBecomeFirstResponder let selector = #selector(getter: UIResponder.canBecomeFirstResponder) let block: @convention(block) (AnyObject) -> Bool = { _ in true } setNewIMPWithBlock(block, forSelector: selector, toClass: klass) } if true { // swizzle canPerformAction:withSender: let selector = #selector(UIResponder.canPerformAction(_:withSender:)) let origIMP = class_getMethodImplementation(klass, selector) typealias IMPType = @convention(c) (AnyObject, Selector, Selector, AnyObject) -> Bool let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (AnyObject, Selector, AnyObject) -> Bool = { return UIMenuItem.isMenuItemKitSelector($1) ? true : origIMPC($0, selector, $1, $2) } setNewIMPWithBlock(block, forSelector: selector, toClass: klass) } if true { // swizzle methodSignatureForSelector: let selector = NSSelectorFromString("methodSignatureForSelector:") let origIMP = class_getMethodImplementation(klass, selector) typealias IMPType = @convention(c) (AnyObject, Selector, Selector) -> AnyObject let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (AnyObject, Selector) -> AnyObject = { if UIMenuItem.isMenuItemKitSelector($1) { // `NSMethodSignature` is not allowed in Swift, this is a workaround return NSObject.perform(NSSelectorFromString("_mik_fakeSignature")).takeUnretainedValue() } return origIMPC($0, selector, $1) } setNewIMPWithBlock(block, forSelector: selector, toClass: klass) } if true { // swizzle forwardInvocation: // `NSInvocation` is not allowed in Swift, so we just use AnyObject let selector = NSSelectorFromString("forwardInvocation:") let origIMP = class_getMethodImplementation(klass, selector) typealias IMPType = @convention(c) (AnyObject, Selector, AnyObject) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (AnyObject, AnyObject) -> () = { if UIMenuItem.isMenuItemKitSelector($1.selector) { guard let item = UIMenuController.shared.findMenuItemBySelector($1.selector) else { return } item.actionBox.value?(item) } else { origIMPC($0, selector, $1) } } setNewIMPWithBlock(block, forSelector: selector, toClass: klass) } objc_setAssociatedObject(klass, key.utf8Start, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } private extension UIMenuController { @objc class func _mik_load() { if true { let selector = #selector(setter: menuItems) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (AnyObject, Selector, AnyObject) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (AnyObject, AnyObject) -> () = { if let firstResp = UIResponder.mik_firstResponder { swizzle(class: type(of: firstResp)) } origIMPC($0, selector, makeUniqueImageTitles($1)) } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } if true { let selector = #selector(setTargetRect(_:in:)) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (AnyObject, Selector, CGRect, UIView) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (AnyObject, CGRect, UIView) -> () = { if let firstResp = UIResponder.mik_firstResponder { swizzle(class: type(of: firstResp)) } else { swizzle(class: type(of: $2)) // Must call `becomeFirstResponder` since there's no firstResponder yet $2.becomeFirstResponder() } origIMPC($0, selector, $1, $2) } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } } static func makeUniqueImageTitles(_ itemsObj: AnyObject) -> AnyObject { guard let items = itemsObj as? [UIMenuItem] else { return itemsObj } var dic = [String: [UIMenuItem]]() items.filter { $0.title.hasSuffix(imageItemIdetifier) }.forEach { item in if dic[item.title] == nil { dic[item.title] = [] } dic[item.title]?.append(item) } dic.filter { $1.count > 1 }.flatMap { $1 }.enumerated().forEach { index, item in item.title = (0...index).map { _ in imageItemIdetifier }.joined(separator: "") } return items as AnyObject } func findImageItemByTitle(_ title: String?) -> UIMenuItem? { guard title?.hasSuffix(imageItemIdetifier) == true else { return nil } return menuItems?.lazy.filter { $0.title == title }.first } func findMenuItemBySelector(_ selector: Selector?) -> UIMenuItem? { guard let selector = selector else { return nil } return menuItems?.lazy.filter { sel_isEqual($0.action, selector) }.first } func findMenuItemBySelector(_ selector: String?) -> UIMenuItem? { guard let selStr = selector else { return nil } return findMenuItemBySelector(NSSelectorFromString(selStr)) } } private extension UILabel { @objc class func _mik_load() { if true { let selector = #selector(drawText(in:)) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (UILabel, Selector, CGRect) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (UILabel, CGRect) -> () = { label, rect in guard let item = UIMenuController.shared.findImageItemByTitle(label.text), let _ = item.imageBox.value else { return origIMPC(label, selector, rect) } } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } if true { let selector = #selector(layoutSubviews) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (UILabel, Selector) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (UILabel) -> () = { label in guard let item = UIMenuController.shared.findImageItemByTitle(label.text), let image = item.imageBox.value else { return origIMPC(label, selector) } // Workaround for #9: https://github.com/cxa/MenuItemKit/issues/9 let point = CGPoint( x: (label.bounds.width - image.size.width) / 2, y: (label.bounds.height - image.size.height) / 2) let imageView: Box = label.associatedBoxForKey(#function, initialValue: { [weak label] in let imgView = UIImageView(frame: .zero) label?.addSubview(imgView) return imgView }()) imageView.value.image = image imageView.value.frame = CGRect(origin: point, size: image.size) } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } if true { let selector = #selector(setter: frame) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (UILabel, Selector, CGRect) -> () let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (UILabel, CGRect) -> () = { label, rect in let isImageItem = UIMenuController.shared.findImageItemByTitle(label.text)?.imageBox.value != nil let rect = isImageItem ? label.superview?.bounds ?? rect : rect origIMPC(label, selector, rect) } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } } } private extension NSString { @objc class func _mik_load() { let selector = #selector(size) let origIMP = class_getMethodImplementation(self, selector) typealias IMPType = @convention(c) (NSString, Selector, AnyObject) -> CGSize let origIMPC = unsafeBitCast(origIMP, to: IMPType.self) let block: @convention(block) (NSString, AnyObject) -> CGSize = { str, attr in guard let item = UIMenuController.shared.findImageItemByTitle(str as String), let image = item.imageBox.value else { return origIMPC(str, selector, attr) } return image.size } setNewIMPWithBlock(block, forSelector: selector, toClass: self) } } // MARK: Helper to find first responder // Source: http://stackoverflow.com/a/14135456/395213 private weak var _currentFirstResponder: UIResponder? = nil private extension UIResponder { static var mik_firstResponder: UIResponder? { _currentFirstResponder = nil UIApplication.shared.sendAction(#selector(mik_findFirstResponder(_:)), to: nil, from: nil, for: nil) return _currentFirstResponder } @objc func mik_findFirstResponder(_ sender: AnyObject) { _currentFirstResponder = self } }