2 // UIMenuController.swift
5 // Created by CHEN Xian’an on 1/17/16.
6 // Copyright © 2016 lazyapps. All rights reserved.
10 import ObjectiveC.runtime
12 // This is inspired by https://github.com/steipete/PSMenuItem
13 private func swizzle(class klass: AnyClass) {
14 objc_sync_enter(klass)
15 defer { objc_sync_exit(klass) }
16 let key: StaticString = #function
17 guard objc_getAssociatedObject(klass, key.utf8Start) == nil else { return }
19 // swizzle canBecomeFirstResponder
20 let selector = #selector(getter: UIResponder.canBecomeFirstResponder)
21 let block: @convention(block) (AnyObject) -> Bool = { _ in true }
22 setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
26 // swizzle canPerformAction:withSender:
27 let selector = #selector(UIResponder.canPerformAction(_:withSender:))
28 let origIMP = class_getMethodImplementation(klass, selector)
29 typealias IMPType = @convention(c) (AnyObject, Selector, Selector, AnyObject) -> Bool
30 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
31 let block: @convention(block) (AnyObject, Selector, AnyObject) -> Bool = {
32 return UIMenuItem.isMenuItemKitSelector($1) ? true : origIMPC($0, selector, $1, $2)
35 setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
39 // swizzle methodSignatureForSelector:
40 let selector = NSSelectorFromString("methodSignatureForSelector:")
41 let origIMP = class_getMethodImplementation(klass, selector)
42 typealias IMPType = @convention(c) (AnyObject, Selector, Selector) -> AnyObject
43 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
44 let block: @convention(block) (AnyObject, Selector) -> AnyObject = {
45 if UIMenuItem.isMenuItemKitSelector($1) {
46 // `NSMethodSignature` is not allowed in Swift, this is a workaround
47 return NSObject.perform(NSSelectorFromString("_mik_fakeSignature")).takeUnretainedValue()
50 return origIMPC($0, selector, $1)
53 setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
57 // swizzle forwardInvocation:
58 // `NSInvocation` is not allowed in Swift, so we just use AnyObject
59 let selector = NSSelectorFromString("forwardInvocation:")
60 let origIMP = class_getMethodImplementation(klass, selector)
61 typealias IMPType = @convention(c) (AnyObject, Selector, AnyObject) -> ()
62 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
63 let block: @convention(block) (AnyObject, AnyObject) -> () = {
64 if UIMenuItem.isMenuItemKitSelector($1.selector) {
65 guard let item = UIMenuController.shared.findMenuItemBySelector($1.selector) else { return }
66 item.actionBox.value?(item)
68 origIMPC($0, selector, $1)
72 setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
75 objc_setAssociatedObject(klass, key.utf8Start, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
78 private extension UIMenuController {
80 @objc class func _mik_load() {
82 let selector = #selector(setter: menuItems)
83 let origIMP = class_getMethodImplementation(self, selector)
84 typealias IMPType = @convention(c) (AnyObject, Selector, AnyObject) -> ()
85 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
86 let block: @convention(block) (AnyObject, AnyObject) -> () = {
87 if let firstResp = UIResponder.mik_firstResponder {
88 swizzle(class: type(of: firstResp))
91 origIMPC($0, selector, makeUniqueImageTitles($1))
94 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
98 let selector = #selector(setTargetRect(_:in:))
99 let origIMP = class_getMethodImplementation(self, selector)
100 typealias IMPType = @convention(c) (AnyObject, Selector, CGRect, UIView) -> ()
101 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
102 let block: @convention(block) (AnyObject, CGRect, UIView) -> () = {
103 if let firstResp = UIResponder.mik_firstResponder {
104 swizzle(class: type(of: firstResp))
106 swizzle(class: type(of: $2))
107 // Must call `becomeFirstResponder` since there's no firstResponder yet
108 $2.becomeFirstResponder()
111 origIMPC($0, selector, $1, $2)
114 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
118 static func makeUniqueImageTitles(_ itemsObj: AnyObject) -> AnyObject {
119 guard let items = itemsObj as? [UIMenuItem] else { return itemsObj }
120 var dic = [String: [UIMenuItem]]()
121 items.filter { $0.title.hasSuffix(imageItemIdetifier) }.forEach { item in
122 if dic[item.title] == nil { dic[item.title] = [] }
123 dic[item.title]?.append(item)
126 dic.filter { $1.count > 1 }.flatMap { $1 }.enumerated().forEach { index, item in
127 item.title = (0...index).map { _ in imageItemIdetifier }.joined(separator: "")
130 return items as AnyObject
133 func findImageItemByTitle(_ title: String?) -> UIMenuItem? {
134 guard title?.hasSuffix(imageItemIdetifier) == true else { return nil }
135 return menuItems?.lazy.filter { $0.title == title }.first
138 func findMenuItemBySelector(_ selector: Selector?) -> UIMenuItem? {
139 guard let selector = selector else { return nil }
140 return menuItems?.lazy.filter { sel_isEqual($0.action, selector) }.first
143 func findMenuItemBySelector(_ selector: String?) -> UIMenuItem? {
144 guard let selStr = selector else { return nil }
145 return findMenuItemBySelector(NSSelectorFromString(selStr))
150 private extension UILabel {
152 @objc class func _mik_load() {
154 let selector = #selector(drawText(in:))
155 let origIMP = class_getMethodImplementation(self, selector)
156 typealias IMPType = @convention(c) (UILabel, Selector, CGRect) -> ()
157 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
158 let block: @convention(block) (UILabel, CGRect) -> () = { label, rect in
160 let item = UIMenuController.shared.findImageItemByTitle(label.text),
161 let _ = item.imageBox.value
162 else { return origIMPC(label, selector, rect) }
165 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
169 let selector = #selector(layoutSubviews)
170 let origIMP = class_getMethodImplementation(self, selector)
171 typealias IMPType = @convention(c) (UILabel, Selector) -> ()
172 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
173 let block: @convention(block) (UILabel) -> () = { label in
175 let item = UIMenuController.shared.findImageItemByTitle(label.text),
176 let image = item.imageBox.value
177 else { return origIMPC(label, selector) }
179 // Workaround for #9: https://github.com/cxa/MenuItemKit/issues/9
181 x: (label.bounds.width - image.size.width) / 2,
182 y: (label.bounds.height - image.size.height) / 2)
183 let imageView: Box<UIImageView> = label.associatedBoxForKey(#function, initialValue: { [weak label] in
184 let imgView = UIImageView(frame: .zero)
185 label?.addSubview(imgView)
189 imageView.value.image = image
190 imageView.value.frame = CGRect(origin: point, size: image.size)
193 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
197 let selector = #selector(setter: frame)
198 let origIMP = class_getMethodImplementation(self, selector)
199 typealias IMPType = @convention(c) (UILabel, Selector, CGRect) -> ()
200 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
201 let block: @convention(block) (UILabel, CGRect) -> () = { label, rect in
202 let isImageItem = UIMenuController.shared.findImageItemByTitle(label.text)?.imageBox.value != nil
203 let rect = isImageItem ? label.superview?.bounds ?? rect : rect
204 origIMPC(label, selector, rect)
207 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
213 private extension NSString {
215 @objc class func _mik_load() {
216 let selector = #selector(size)
217 let origIMP = class_getMethodImplementation(self, selector)
218 typealias IMPType = @convention(c) (NSString, Selector, AnyObject) -> CGSize
219 let origIMPC = unsafeBitCast(origIMP, to: IMPType.self)
220 let block: @convention(block) (NSString, AnyObject) -> CGSize = { str, attr in
222 let item = UIMenuController.shared.findImageItemByTitle(str as String),
223 let image = item.imageBox.value
225 return origIMPC(str, selector, attr)
231 setNewIMPWithBlock(block, forSelector: selector, toClass: self)
236 // MARK: Helper to find first responder
237 // Source: http://stackoverflow.com/a/14135456/395213
238 private weak var _currentFirstResponder: UIResponder? = nil
240 private extension UIResponder {
242 static var mik_firstResponder: UIResponder? {
243 _currentFirstResponder = nil
244 UIApplication.shared.sendAction(#selector(mik_findFirstResponder(_:)), to: nil, from: nil, for: nil)
245 return _currentFirstResponder
248 @objc func mik_findFirstResponder(_ sender: AnyObject) {
249 _currentFirstResponder = self