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 relevant top of the stack

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 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.
  • 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 an Rn - Rm operation, and places the result in Rd.

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 a TriggerSubmitAction 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 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.