How to Inspect SwiftUI.TextField.onSubmit and Figure Out You Can't Programmatically Use It
I wanted to know how a SwiftUI TextField gets to know its onSubmit handler to wire my own NSViewRepresentable to it (would be the same for a UIViewRepresentable).
Naively (spoilers!), I thought that you could maybe send a private selector somewhere. But that went absolutely nowhere.
But first, to inspect this, I added a breakpoint inside the .onSubmit {...} block. This is triggered when e.g. pressing enter/return inside the text field.
Inspecting the backtrace of onSubmit
The relevant top of the stack
The stack trace via (lldb) thread backtrace all prints nothing new if you know what to expect from AppKit:
-[NSInputContext doCommandBySelector:completionHandler:] (#20) is somewhat interesting (or at least new to me), but I assume this corresponds to NSTextInputClient.doCommand(by:) eventually. It’s private API.
-[NSTextView doCommandBySelector:] (#17) reacts to the enter key. The window’s field editor will then end editing for the text field.
-[NSTextField textDidEndEditing:] (#10) reacts to the field editor sending its NSTextDelegate.textDidEndEditing(_:) message. (The text field receives delegate messages from the field editor.)
-[NSControl sendAction:to:] (#9) is the text field informing the app that something happened. NSTextField inherits from NSControl, that’s why the type isn’t NSTextField here. It’s the same widget, though.
Before the onSubmit breakpoint is reached, seven unnamed symbols from the SwiftUI module are used. I’d like to know more about these.
I’d also like to know which selector is being passed. Maybe I can send that as well?
Inspecting the action
Next, I add a symbolic breakpoint for -[NSApplication sendAction:to:from:] in Xcode and hit the Enter key in the text field again.
Keep in mind that while the app is running, the object addresses will be the same, so frame #8 will always be 0x00000001abe57a50 (AppKit-[NSApplication(NSResponder) sendAction:to:from:]). I'm pointing this out because 0x1abe57a50` is quite the ways down in the assembly:
$arg2 and $arg3 are useless as numbers; so I tried to interpret the number as a C string via po (char *)$arg2. This works, but Daniel Jalkut taught me that it’s even simpler to use x/s $arg2 as a shorthand for memory read (x) and to display it as a string (/s):
So $arg2 is the message sent to self, and $arg3 is the first parameter, anAction. (controlActionWithSender: is private API as well, though.)
That means $arg4 is the aTarget parameter, which is the SwiftUI.PlatformTextFieldCoordinator instance, and $arg5 the sender. The sender’s po output (its debugDescription) is a bit weird, but we can check what type it conforms to:
(lldb) po ((NSObject *)$arg5).class
SwiftUI.AppKitTextField
(lldb) po ((NSObject *)$arg5).superclass
NSTextField
(lldb) po ((NSObject *)$arg5).description
<SwiftUI.AppKitTextField: 0x158f20770>
(lldb) po ((NSObject *)$arg5).debugDescription
Bordered: false, bezeled: false, bezel: NSTextFieldBezelStyle(rawValue: 0)
So it’s an AppKitTextField of the SwiftUI module – also private API! Its base class isn’t NSViewRepresentable, to my surprise, but NSTextField itself. Maybe the SwiftUI adaptation is implemented by making AppKitTextField conform to SwiftUI.View, the protocol?
Knowing that the sender is an AppKit text field, we can check out it components, the delegate that handles events, and the cell that draws stuff:
(lldb) po ((NSTextField *)$arg5).cell
<_TtC7SwiftUIP33_C58093E7172B0A541A997680E343D0D520_SystemTextFieldCell: 0x600003976580>
(lldb) po ((NSTextField *)$arg5).delegate
<SwiftUI.PlatformTextFieldCoordinator: 0x158f20460>
The cell doesn’t seem to help. And it’s that private PlatformTextFieldCoordinator again.
One last experiment using fp_methodDescription: what’s its API?
(lldb) po ((NSObject *) $arg4).class
SwiftUI.PlatformTextFieldCoordinator
(lldb) po ((NSObject *) $arg4).superclass
SwiftUI.PlatformViewCoordinator
(lldb) po [((NSObject *) $arg4) performSelector:@selector(fp_methodDescription)]
<SwiftUI.PlatformTextFieldCoordinator: 0x158f20460>:
in SwiftUI.PlatformTextFieldCoordinator:
Instance Methods:
- (id) init; (0x1d1ffe5c8)
- (void) .cxx_destruct; (0x1d1ffe6c4)
- (void) controlTextDidBeginEditing:(id)arg1; (0x1d1ffde68)
- (void) controlTextDidChange:(id)arg1; (0x1d1ffe1e8)
- (void) controlTextDidEndEditing:(id)arg1; (0x1d1ffe4c4)
- (void) controlActionWithSender:(id)arg1; (0x1d1ffdd2c)
in SwiftUI.PlatformViewCoordinator:
Instance Methods:
- (id) init; (0x1d1956d4c)
in NSObject:
Class Methods:
... default interfaces follow ...
How can I send messages to the PlatformTextFieldCoordinator from my own NSTextField?
SwiftUI view representation of AppKit views
PlatformTextFieldCoordinator is an NSObject, but not an NSResponder. So some other component needs to have a reference to this coordinator in order to wire AppKitTextField instances to it.
The NSViewRepresentable API expects this interesting makeNSView + makeCoordinator dance – maybe AppKitTextField is the NSView here, and the (aptly named) PlatformTextFieldCoordinator is the, well, coordinator.
There is no way for me to hook into the NSViewRepresentable setup to check out how the coordinator ever gets to know the onSubmit block. The NSViewRepresentable doesn’t live inside the AppKit NSObject space and so it is neither hooked to the text field nor to the coordinator.
None of types are available publicly. And EnvironmentValues is keyed by a key’s type, not by strings, as far as I can tell.
So I can’t get to the TriggerSubmitAction.
Dead end?
This appears to be a dead end:
TriggerSubmitAction is not public API.
The coordinator also is private API and I can’t create my own instance to forward the submit action to, either.
The responder chain doesn’t include the default text field delegate (or at least I couldn’t find it), so I can’t forward the submit action from my code.
I can’t pluck the delegate of the SwiftUI.TextField. I could cast the text field to any NSViewRepresentable, but can’t get to the makeNSView(context:) result because I can’t create the context object.
For all intents and purposes, the .onSubmit modifier won’t work with a custom NSViewRepresentable that produces a NSTextField.