Skip to content

Commit 74bd4bd

Browse files
[Super Clipboard][iOS] - Swizzle Flutter method to say that we can paste even when binary data is on the clipboard (#2903)
1 parent 1bdbd02 commit 74bd4bd

File tree

1 file changed

+88
-22
lines changed

1 file changed

+88
-22
lines changed

super_editor_clipboard/ios/Classes/SuperEditorClipboardPlugin.swift

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public class SuperEditorClipboardPlugin: NSObject, FlutterPlugin {
1616
let instance = SuperEditorClipboardPlugin()
1717
registrar.addMethodCallDelegate(instance, channel: channel)
1818

19+
// Swizzle both the action execution (paste) and the validation (canPerformAction)
1920
swizzleFlutterPaste()
21+
swizzleCanPerformAction()
2022
}
2123

2224
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -33,63 +35,127 @@ public class SuperEditorClipboardPlugin: NSObject, FlutterPlugin {
3335
}
3436
}
3537

38+
// MARK: - Swizzling Logic
39+
3640
private static func swizzleFlutterPaste() {
37-
// 1. Locate the private Flutter engine class
38-
guard let flutterClass = NSClassFromString("FlutterTextInputView") else {
39-
log("RichPastePlugin: Could not find FlutterTextInputView")
41+
swizzle(
42+
clsName: "FlutterTextInputView",
43+
originalSelector: #selector(UIResponder.paste(_:)),
44+
customSelector: #selector(customPaste(_:))
45+
)
46+
}
47+
48+
private static func swizzleCanPerformAction() {
49+
swizzle(
50+
clsName: "FlutterTextInputView",
51+
originalSelector: #selector(UIResponder.canPerformAction(_:withSender:)),
52+
customSelector: #selector(customCanPerformAction(_:withSender:))
53+
)
54+
}
55+
56+
private static func swizzle(clsName: String, originalSelector: Selector, customSelector: Selector) {
57+
guard let flutterClass = NSClassFromString(clsName) else {
58+
log("Could not find \(clsName)")
4059
return
4160
}
4261

43-
let originalSelector = #selector(UIResponder.paste(_:))
44-
let swizzledSelector = #selector(customPaste(_:))
45-
46-
// 2. Get the methods
4762
guard let originalMethod = class_getInstanceMethod(flutterClass, originalSelector),
48-
let swizzledMethod = class_getInstanceMethod(SuperEditorClipboardPlugin.self, swizzledSelector) else {
63+
let swizzledMethod = class_getInstanceMethod(SuperEditorClipboardPlugin.self, customSelector) else {
64+
log("Could not find methods to swizzle for \(clsName)")
4965
return
5066
}
5167

52-
// 3. Inject our custom method into the Flutter engine class
68+
// Add the custom method to the Flutter class
5369
let didAddMethod = class_addMethod(
5470
flutterClass,
55-
swizzledSelector,
71+
customSelector,
5672
method_getImplementation(swizzledMethod),
5773
method_getTypeEncoding(swizzledMethod)
5874
)
5975

6076
if didAddMethod {
61-
// 4. Swap the pointers
62-
let newMethod = class_getInstanceMethod(flutterClass, swizzledSelector)!
77+
// Exchange implementations so 'originalSelector' calls our custom code,
78+
// and 'customSelector' calls the original code.
79+
let newMethod = class_getInstanceMethod(flutterClass, customSelector)!
6380
method_exchangeImplementations(originalMethod, newMethod)
81+
log("Successfully swizzled \(originalSelector) in \(clsName)")
82+
} else {
83+
log("Failed to add method \(customSelector) to \(clsName)")
6484
}
6585
}
6686

67-
// This method is "moved" into FlutterTextInputView at runtime.
68-
// 'self' inside this method will actually be the FlutterTextInputView instance.
87+
// MARK: - Custom Implementations
88+
89+
/// This method replaces `paste(_:)` at runtime.
6990
@objc func customPaste(_ sender: Any?) {
7091
if (!SuperEditorClipboardPlugin.doCustomPaste) {
7192
SuperEditorClipboardPlugin.log("Running regular Flutter paste")
72-
// FALLBACK:
73-
// This calls the ORIGINAL paste logic.
74-
// Because we swapped the methods, calling 'customPaste' on 'self'
75-
// now triggers the engine's original 'insertText' flow.
93+
// FALLBACK: Call original implementation (which is now mapped to customPaste)
7694
if self.responds(to: #selector(customPaste(_:))) {
7795
self.perform(#selector(customPaste(_:)), with: sender)
7896
}
79-
80-
return;
97+
return
8198
}
8299

83100
SuperEditorClipboardPlugin.log("Running custom paste")
84101
SuperEditorClipboardPlugin.channel?.invokeMethod("paste", arguments: nil)
85102
}
86103

104+
/// This method replaces `canPerformAction(_:withSender:)` at runtime.
105+
@objc func customCanPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
106+
let isPasteAction = action == #selector(UIResponderStandardEditActions.paste(_:))
107+
108+
// 1. If it is the PASTE action AND we are in custom mode, check our broader conditions.
109+
if isPasteAction && SuperEditorClipboardPlugin.doCustomPaste {
110+
// Check for ANY pasteable content (Images, Colors, URLs, Strings)
111+
// Note: Flutter only checks `hasStrings`.
112+
if UIPasteboard.general.hasStrings ||
113+
UIPasteboard.general.hasImages ||
114+
UIPasteboard.general.hasURLs ||
115+
UIPasteboard.general.hasColors {
116+
return true
117+
}
118+
}
119+
120+
// 2. Otherwise (or if the custom check failed), fall back to the ORIGINAL logic.
121+
// Because we exchanged implementations, calling 'customCanPerformAction' here
122+
// actually invokes the original Flutter engine logic.
123+
124+
// We cannot use 'perform' for Bool return types, so we use IMP casting.
125+
return SuperEditorClipboardPlugin.callOriginalCanPerformAction(
126+
instance: self,
127+
selector: #selector(customCanPerformAction(_:withSender:)),
128+
action: action,
129+
sender: sender
130+
)
131+
}
132+
133+
// MARK: - Helpers
134+
135+
/// Safely invokes the original implementation of `canPerformAction` (which is now swapped).
136+
private static func callOriginalCanPerformAction(instance: Any, selector: Selector, action: Selector, sender: Any?) -> Bool {
137+
guard let method = class_getInstanceMethod(object_getClass(instance), selector) else {
138+
return false
139+
}
140+
141+
let imp = method_getImplementation(method)
142+
143+
// Define the C function signature for (BOOL)objc_msgSend(id, SEL, SEL, id)
144+
typealias CanPerformActionFunction = @convention(c) (AnyObject, Selector, Selector, Any?) -> Bool
145+
146+
let originalFunction = unsafeBitCast(imp, to: CanPerformActionFunction.self)
147+
148+
// 'instance' is 'self' (FlutterTextInputView)
149+
// 'selector' is the selector triggering this IMP (customCanPerformAction)
150+
// 'action' is the argument (e.g., paste:)
151+
return originalFunction(instance as AnyObject, selector, action, sender)
152+
}
153+
87154
public static let isLoggingEnabled = false
88155

89156
internal static func log(_ message: String) {
90157
if isLoggingEnabled {
91158
print("[SuperEditorClipboardPlugin] \(message)")
92159
}
93160
}
94-
}
95-
161+
}

0 commit comments

Comments
 (0)