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.NSTextFieldinherits fromNSControl, that’s why the type isn’tNSTextFieldhere. 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,Rmperforms anRn - Rmoperation, 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,
onSubmitadds aTriggerSubmitActionvalue 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:
TriggerSubmitActionis 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
delegateof 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.