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
).
There’s no key for that block in the EnvironmentValues
structure that you get from the “context”, though.
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 stack trace via (lldb) thread backtrace all
prints nothing new if you know what to expect from AppKit:
frame #0: 0x0000000100827f14 TestApp`closure #4 in MyView.body.getter(self=TestApp.MyView @ 0x000000016f7b7730) at MyView.swift:38:19
frame #1: 0x00000001d205b060 SwiftUI`___lldb_unnamed_symbol220589 + 128
frame #2: 0x00000001d1893958 SwiftUI`___lldb_unnamed_symbol167203 + 44
frame #3: 0x00000001d1899ef4 SwiftUI`___lldb_unnamed_symbol167312 + 24
frame #4: 0x00000001d1893908 SwiftUI`___lldb_unnamed_symbol167202 + 60
frame #5: 0x00000001d187f998 SwiftUI`___lldb_unnamed_symbol166820 + 116
frame #6: 0x00000001d1ffdca4 SwiftUI`___lldb_unnamed_symbol217870 + 124
frame #7: 0x00000001d1ffdd64 SwiftUI`___lldb_unnamed_symbol217871 + 56
frame #8: 0x00000001abe57a50 AppKit`-[NSApplication(NSResponder) sendAction:to:from:] + 440
frame #9: 0x00000001abe57868 AppKit`-[NSControl sendAction:to:] + 72
frame #10: 0x00000001abef5dc4 AppKit`-[NSTextField textDidEndEditing:] + 460
frame #11: 0x00000001a8a87254 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 148
frame #12: 0x00000001a8b22f30 CoreFoundation`___CFXRegistrationPost_block_invoke + 88
frame #13: 0x00000001a8b22e78 CoreFoundation`_CFXRegistrationPost + 440
frame #14: 0x00000001a8a58580 CoreFoundation`_CFXNotificationPost + 704
frame #15: 0x00000001a99b59e4 Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 88
frame #16: 0x00000001abf8d3d4 AppKit`-[NSTextView(NSPrivate) _giveUpFirstResponder:] + 308
frame #17: 0x00000001abf0f88c AppKit`-[NSTextView doCommandBySelector:] + 176
frame #18: 0x00000001d1fff8c0 SwiftUI`___lldb_unnamed_symbol217942 + 144
frame #19: 0x00000001d1fff8fc SwiftUI`___lldb_unnamed_symbol217943 + 44
frame #20: 0x00000001abf0f79c AppKit`-[NSTextInputContext(NSInputContext_WithCompletion) doCommandBySelector:completionHandler:] + 228
-[NSInputContext doCommandBySelector:completionHandler:]
(#20) is somewhat interesting (or at least new to me), but I assume this corresponds toNSTextInputClient.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 itsNSTextDelegate.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 fromNSControl
, that’s why the type isn’tNSTextField
here. It’s the same widget, though.NSApp.sendAction(_:to:from:)
(#8) is invoked. That’s basically passing a selector through the responder chain.
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:
AppKit`-[NSApplication(NSResponder) sendAction:to:from:]:
-> 0x1abe57898 <+0>: pacibsp
0x1abe5789c <+4>: sub sp, sp, #0xc0
0x1abe578a0 <+8>: stp x24, x23, [sp, #0x80]
0x1abe578a4 <+12>: stp x22, x21, [sp, #0x90]
0x1abe578a8 <+16>: stp x20, x19, [sp, #0xa0]
0x1abe578ac <+20>: stp x29, x30, [sp, #0xb0]
0x1abe578b0 <+24>: add x29, sp, #0xb0
0x1abe578b4 <+28>: mov x20, x4
0x1abe578b8 <+32>: mov x1, x3
0x1abe578bc <+36>: mov x21, x2
0x1abe578c0 <+40>: mov x3, x0
0x1abe578c4 <+44>: adrp x8, 341711
0x1abe578c8 <+48>: ldr x8, [x8, #0xce8]
0x1abe578cc <+52>: ldr x8, [x8]
0x1abe578d0 <+56>: stur x8, [x29, #-0x38]
0x1abe578d4 <+60>: mov x0, x2
0x1abe578d8 <+64>: mov x2, x4
0x1abe578dc <+68>: bl 0x1abd7fbb4 ; _NSTargetForSendAction
...
0x1abe57a44 <+428>: ldr x8, [sp, #0x38]
0x1abe57a48 <+432>: add x0, sp, #0x28
0x1abe57a4c <+436>: blraa x8, x23
0x1abe57a50 <+440>: sub x0, x29, #0x48
0x1abe57a54 <+444>: bl 0x1ac75b3a0 ; symbol stub for: os_activity_scope_leave
From the “ARM Developer Suite Assembler Guide” (adapted to ignore add
):
sub Rd,Rn,Rm
performs anRn - Rm
operation, and places the result inRd
.
So the onSubmit
block is somehow called by subtracting x29 - #0x48
, and storing the result in x0
. Is changing x0
similar to “calling a function”?
But we start at the top, 0x1abe57898
, so what do we know there, at the beginning of the method call?
We can inspect all the arguments.
-[NSApplication(NSResponder) sendAction:to:from:]
has 3 named parameters (anAction
, aTarget
, and sender
), preceded by the self
assignment that makes Objective-C methods know what the self
object reference actually is:
(lldb) po $arg1
<SwiftUI.AppKitApplication: 0x15a00f5a0>
(lldb) po $arg2
8464438324
(lldb) po $arg3
8481212643
(lldb) po $arg4
<SwiftUI.PlatformTextFieldCoordinator: 0x158f20460>
(lldb) po $arg5
Bordered: false, bezeled: false, bezel: NSTextFieldBezelStyle(rawValue: 0)
So self
is an instance of AppKitApplication
. That’s private API.
I assume AppKitApplication
is a subclass of NSApplication
. Let’s try that. We can inspect the current AppKit NSEvent
:
(lldb) po ((NSApplication*)$arg1).currentEvent
NSEvent: type=KeyDown loc=(295.051,397.113) time=1069934.5 flags=0x100 win=0x158f34090 winNum=48627 ctxt=0x0 chars="
" unmodchars="
" repeat=0 keyCode=36
Checks out: key code 36 is #define KEY_RETURN 36
according to this ancient map of virtual key codes.
$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
):
(lldb) x/s $arg2
0x1f8851434: "sendAction:to:from:"
(lldb) x/s $arg3
0x1f98508e3: "controlActionWithSender:"
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 ...
All right, so there’s just some NSControlTextEditingDelegate
stuff.
The AppKitApplication
type, by the way, isn’t much more interesting:
(lldb) po [((NSObject *)$arg1) performSelector:@selector(fp_methodDescription)]
<SwiftUI.AppKitApplication: 0x15a00f5a0>:
in SwiftUI.AppKitApplication:
Instance Methods:
- (id) init; (0x1d1e3d17c)
- (id) initWithCoder:(id)arg1; (0x1d1e3d1f4)
- (BOOL) _shouldLoadMainStoryboardNamed:(id)arg1; (0x1d1e3d27c)
- (BOOL) _shouldLoadMainNibNamed:(id)arg1; (0x1d1e3d120)
in NSApplication:
... 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.
Federico Zanetello (@zntfdr) wrote in 2021 about SwiftUI’s new onSubmit modifier:
Behind the scenes,
onSubmit
adds aTriggerSubmitAction
value into the environment.
That’s not a public EnvironmentKey
.
Can we inspect this? Chris Eidhof says we can:
As an interesting aside, it’s possible to inspect the current environment for a view using the following wrapper:
struct DumpingEnvironment<V: View>: View { @Environment(\.self) var env let content: V var body: some View { dump(env) return content } }
That dumps a lot of information; too much to parse in the view hierarchy.
It gets better when I hook into makeNSView
and just dump(context.environment)
. That’s still a lot:
▿ after: Optional(EnvironmentPropertyKey<TriggerSubmissionKey> = Optional(SwiftUI.TriggerSubmitAction(onSubmit: (Function))))
▿ some: EnvironmentPropertyKey<TriggerSubmissionKey> = Optional(SwiftUI.TriggerSubmitAction(onSubmit: (Function))) #6
// 2000 lines later ...
▿ value: Optional(SwiftUI.TriggerSubmitAction(onSubmit: (Function)))
▿ some: SwiftUI.TriggerSubmitAction
- onSubmit: (Function)
Marco Eidinger shares a shorter dump, and that does indeed print easier to parse results:
EnvironmentPropertyKey<TriggerSubmissionKey> = Optional(SwiftUI.TriggerSubmitAction(onSubmit: (Function)))
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 theSwiftUI.TextField
. I could cast the text field toany NSViewRepresentable
, but can’t get to themakeNSView(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
.