added iOS source code
[wl-app.git] / iOS / Pods / MenuItemKit / MenuItemKit / Swizzlings.swift
1 //
2 //  UIMenuController.swift
3 //  MenuItemKit
4 //
5 //  Created by CHEN Xian’an on 1/17/16.
6 //  Copyright © 2016 lazyapps. All rights reserved.
7 //
8
9 import UIKit
10 import ObjectiveC.runtime
11
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 }
18   if true {
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)
23   }
24
25   if true {
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)
33     }
34
35     setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
36   }
37
38   if true {
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()
48       }
49
50       return origIMPC($0, selector, $1)
51     }
52
53     setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
54   }
55
56   if true {
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)
67       } else {
68         origIMPC($0, selector, $1)
69       }
70     }
71
72     setNewIMPWithBlock(block, forSelector: selector, toClass: klass)
73   }
74
75   objc_setAssociatedObject(klass, key.utf8Start, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
76 }
77
78 private extension UIMenuController {
79
80   @objc class func _mik_load() {
81     if true {
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))
89         }
90
91         origIMPC($0, selector, makeUniqueImageTitles($1))
92       }
93
94       setNewIMPWithBlock(block, forSelector: selector, toClass: self)
95     }
96
97     if true {
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))
105         } else {
106           swizzle(class: type(of: $2))
107           // Must call `becomeFirstResponder` since there's no firstResponder yet
108           $2.becomeFirstResponder()
109         }
110
111         origIMPC($0, selector, $1, $2)
112       }
113
114       setNewIMPWithBlock(block, forSelector: selector, toClass: self)
115     }
116   }
117
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)
124     }
125
126     dic.filter { $1.count > 1 }.flatMap { $1 }.enumerated().forEach { index, item in
127       item.title = (0...index).map { _ in imageItemIdetifier }.joined(separator: "")
128     }
129
130     return items as AnyObject
131   }
132
133   func findImageItemByTitle(_ title: String?) -> UIMenuItem? {
134     guard title?.hasSuffix(imageItemIdetifier) == true else { return nil }
135     return menuItems?.lazy.filter { $0.title == title }.first
136   }
137
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
141   }
142
143   func findMenuItemBySelector(_ selector: String?) -> UIMenuItem? {
144     guard let selStr = selector else { return nil }
145     return findMenuItemBySelector(NSSelectorFromString(selStr))
146   }
147
148 }
149
150 private extension UILabel {
151
152   @objc class func _mik_load() {
153     if true {
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
159         guard
160           let item = UIMenuController.shared.findImageItemByTitle(label.text),
161           let _ = item.imageBox.value
162         else { return origIMPC(label, selector, rect) }
163       }
164
165       setNewIMPWithBlock(block, forSelector: selector, toClass: self)
166     }
167
168     if true {
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
174         guard
175           let item = UIMenuController.shared.findImageItemByTitle(label.text),
176           let image = item.imageBox.value
177         else { return origIMPC(label, selector) }
178
179         // Workaround for #9: https://github.com/cxa/MenuItemKit/issues/9
180         let point = CGPoint(
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)
186           return imgView
187         }())
188
189         imageView.value.image = image
190         imageView.value.frame = CGRect(origin: point, size: image.size)
191       }
192
193       setNewIMPWithBlock(block, forSelector: selector, toClass: self)
194     }
195
196     if true {
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)
205       }
206
207       setNewIMPWithBlock(block, forSelector: selector, toClass: self)
208     }
209   }
210
211 }
212
213 private extension NSString {
214
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
221       guard
222         let item = UIMenuController.shared.findImageItemByTitle(str as String),
223         let image = item.imageBox.value
224       else {
225         return origIMPC(str, selector, attr)
226       }
227
228       return image.size
229     }
230
231     setNewIMPWithBlock(block, forSelector: selector, toClass: self)
232   }
233
234 }
235
236 // MARK: Helper to find first responder
237 // Source: http://stackoverflow.com/a/14135456/395213
238 private weak var _currentFirstResponder: UIResponder? = nil
239
240 private extension UIResponder {
241
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
246   }
247
248   @objc func mik_findFirstResponder(_ sender: AnyObject) {
249     _currentFirstResponder = self
250   }
251
252 }