Skip to content

Instantly share code, notes, and snippets.

@Kyle-Ye
Last active August 3, 2026 20:15
Show Gist options
  • Select an option

  • Save Kyle-Ye/4a7059e78f2797cce225a2c6d0315471 to your computer and use it in GitHub Desktop.

Select an option

Save Kyle-Ye/4a7059e78f2797cce225a2c6d0315471 to your computer and use it in GitHub Desktop.
Print SwiftUI DisplayList.minimalDescription via LLDB
command script import --relative-to-command-file swiftui_displaylist_minimal_description.py

Print SwiftUI's minimal DisplayList description with LLDB

Setting SWIFTUI_PRINT_TREE=1 makes SwiftUI print DisplayList.description. For a large view hierarchy, that output can quickly become difficult to scan.

SwiftUI also contains a compact DisplayList.minimalDescription getter, but there is no environment variable for selecting it.

This LLDB command temporarily redirects description to minimalDescription inside the debugged process. It creates no breakpoint and does not modify SwiftUICore on disk.

Setup

  1. Download .lldbinit-swiftui and swiftui_displaylist_minimal_description.py into the same directory.
  2. In Xcode, open Scheme → Run → Info → LLDB Init File and select .lldbinit-swiftui.
  3. Add SWIFTUI_PRINT_TREE=1 to the scheme's environment variables before launching the app.

Alternatively, import the Python script manually:

(lldb) command script import /path/to/swiftui_displaylist_minimal_description.py

Usage

Launch the app and pause it after SwiftUICore has loaded:

(lldb) swiftui-display-list-minimal enable
(lldb) continue

The display-list output now uses the compact representation:

View 0x... at Time(...):
(DL(I:1(E(I:0 T))))

Inspect or disable the redirect while the process is paused:

(lldb) swiftui-display-list-minimal status
(lldb) swiftui-display-list-minimal disable

Example

The output switches from the full description to the compact minimalDescription after enabling the command:

SwiftUI DisplayList full and minimal descriptions

To explore how the full description maps to each minimalDescription encoding, paste it into DisplayList Explorer. The Swift parser runs locally in your browser via WebAssembly; no DisplayList data leaves the page.

Compatibility

  • Supports arm64 and arm64e targets.
  • Validated with SwiftUI 6.5.4 on an iOS 18.5 arm64 Simulator.
  • The getter symbols are private implementation details and may change between OS releases.
  • Disable the redirect before detaching LLDB if the process will remain alive.

References

"""LLDB command for printing SwiftUI display lists minimally.
SwiftUI's SWIFTUI_PRINT_TREE path calls DisplayList.description. While the
inferior is stopped, this command temporarily replaces that getter's first
arm64 instruction with a direct branch to DisplayList.minimalDescription.
Both getters have the same Swift calling convention, so SwiftUI keeps its
original environment gating, prefix, and printing schedule.
"""
import lldb
import shlex
import struct
_COMMAND_NAME = "swiftui-display-list-minimal"
_DESCRIPTION_SYMBOL = "$s7SwiftUI11DisplayListV11descriptionSSvg"
_MINIMAL_DESCRIPTION_SYMBOL = (
"$s7SwiftUI11DisplayListV18minimalDescriptionSSvg"
)
_SWIFTUI_CORE_MODULE = "SwiftUICore"
_PRINT_TREE_ENVIRONMENT_KEY = "SWIFTUI_PRINT_TREE"
_ARM64_BRANCH_OPCODE = 0x14000000
_ARM64_BRANCH_IMMEDIATE_MASK = 0x03FFFFFF
_INSTRUCTION_SIZE = 4
_GONE_PROCESS_STATES = {
lldb.eStateInvalid,
lldb.eStateUnloaded,
lldb.eStateExited,
}
_WRITABLE_PROCESS_STATES = {
lldb.eStateStopped,
lldb.eStateCrashed,
lldb.eStateSuspended,
}
# Preserve the patch across `command script import` reloads in one LLDB
# session. A process restart restores the framework mapping by itself.
if "_PATCH_STATE" not in globals():
_PATCH_STATE = None
class HookError(RuntimeError):
pass
def _selected_target(debugger):
target = debugger.GetSelectedTarget()
if not target.IsValid():
raise HookError("No LLDB target is selected.")
return target
def _stopped_process(target):
process = target.GetProcess()
if not process.IsValid() or process.GetProcessID() == 0:
raise HookError(
"Launch the process and pause it before enabling the hook."
)
if process.GetState() != lldb.eStateStopped:
raise HookError("Pause the process before changing the hook.")
return process
def _symbol_load_addresses(module, target, name):
contexts = module.FindSymbols(name, lldb.eSymbolTypeCode)
addresses = set()
for index in range(contexts.GetSize()):
symbol = contexts.GetContextAtIndex(index).GetSymbol()
if not symbol.IsValid():
continue
address = symbol.GetStartAddress().GetLoadAddress(target)
if address != lldb.LLDB_INVALID_ADDRESS:
addresses.add(address)
return addresses
def _branch_instruction(source, destination):
if source == destination:
raise HookError("The two DisplayList getters resolve to one address.")
delta = destination - source
if delta % _INSTRUCTION_SIZE != 0:
raise HookError("Getter addresses are not instruction-aligned.")
immediate = delta // _INSTRUCTION_SIZE
if immediate < -(1 << 25) or immediate >= (1 << 25):
raise HookError(
"DisplayList.minimalDescription is outside arm64 branch range."
)
instruction = (
_ARM64_BRANCH_OPCODE
| (immediate & _ARM64_BRANCH_IMMEDIATE_MASK)
)
return struct.pack("<I", instruction)
def _resolve_patch(target):
architecture = (target.GetTriple() or "").split("-", 1)[0]
if architecture not in {"arm64", "arm64e"}:
raise HookError(
"The hook currently supports arm64 and arm64e targets only."
)
modules = [
target.GetModuleAtIndex(index)
for index in range(target.GetNumModules())
if target.GetModuleAtIndex(index).GetFileSpec().GetFilename()
== _SWIFTUI_CORE_MODULE
]
if not modules:
raise HookError(
"SwiftUICore is not loaded. Continue to an early app breakpoint, "
"pause, and try again."
)
candidates = []
for module in modules:
sources = _symbol_load_addresses(module, target, _DESCRIPTION_SYMBOL)
destinations = _symbol_load_addresses(
module,
target,
_MINIMAL_DESCRIPTION_SYMBOL,
)
if len(sources) == 1 and len(destinations) == 1:
candidates.append((sources.pop(), destinations.pop()))
if not candidates:
raise HookError("Could not resolve both DisplayList getters uniquely.")
if len(candidates) != 1:
raise HookError(
"Multiple loaded SwiftUICore images contain the DisplayList "
"getters; refusing to choose one."
)
source, destination = candidates[0]
return source, destination, _branch_instruction(source, destination)
def _read_instruction(process, address):
error = lldb.SBError()
data = process.ReadMemory(address, _INSTRUCTION_SIZE, error)
if error.Fail() or len(data) != _INSTRUCTION_SIZE:
message = error.GetCString() or "short read"
raise HookError(
"Could not read SwiftUICore memory at {:#x}: {}".format(
address,
message,
)
)
return data
def _write_instruction(process, address, instruction):
error = lldb.SBError()
count = process.WriteMemory(address, instruction, error)
if error.Fail() or count != len(instruction):
message = error.GetCString() or "short write"
raise HookError(
"Could not write SwiftUICore memory at {:#x}: {}".format(
address,
message,
)
)
if _read_instruction(process, address) != instruction:
raise HookError(
"SwiftUICore did not retain the instruction written at "
"{:#x}.".format(address)
)
def _breakpoint_locations_at_address(target, address):
for index in range(target.GetNumBreakpoints()):
breakpoint = target.GetBreakpointAtIndex(index)
if not breakpoint.IsValid() or not breakpoint.IsEnabled():
continue
for location_index in range(breakpoint.GetNumLocations()):
location = breakpoint.GetLocationAtIndex(location_index)
if location.GetAddress().GetLoadAddress(target) == address:
yield location
def _disable_conflicting_breakpoints(target, address):
disabled = []
for location in _breakpoint_locations_at_address(target, address):
if location.IsEnabled():
location.SetEnabled(False)
disabled.append(location)
return disabled
def _restore_breakpoints(locations):
for location in locations:
if location.IsValid():
location.SetEnabled(True)
def _print_tree_environment(target):
value = target.GetLaunchInfo().GetEnvironment().Get(
_PRINT_TREE_ENVIRONMENT_KEY
)
if value is not None:
return value
process = target.GetProcess()
if process.IsValid() and process.GetProcessID() != 0:
return "<unknown after launch>"
return "<unset>"
def _new_patch_state(
target,
process,
source,
destination,
original,
instruction,
locations,
uncertain=False,
):
return {
"target": target,
"process": process,
"process_id": process.GetProcessID(),
"process_unique_id": process.GetUniqueID(),
"source": source,
"destination": destination,
"original": original,
"instruction": instruction,
"locations": locations,
"uncertain": uncertain,
}
def _same_process(state, process):
return state["process_unique_id"] == process.GetUniqueID()
def _retire_ended_patch():
global _PATCH_STATE
if _PATCH_STATE is None:
return
process = _PATCH_STATE["process"]
state = process.GetState() if process.IsValid() else lldb.eStateInvalid
if state in _GONE_PROCESS_STATES:
_restore_breakpoints(_PATCH_STATE["locations"])
_PATCH_STATE = None
def _prepare_enable(target, process, source, instruction):
"""Return True when this process already contains the active patch."""
global _PATCH_STATE
_retire_ended_patch()
if _PATCH_STATE is None:
return False
if not _same_process(_PATCH_STATE, process):
raise HookError(
"The hook is still active in process {}. Disable it before "
"enabling another process.".format(_PATCH_STATE["process_id"])
)
if (
_PATCH_STATE["source"] != source
or _PATCH_STATE["instruction"] != instruction
):
raise HookError(
"The active hook no longer matches the resolved SwiftUICore "
"symbols. Disable it before continuing."
)
current = _read_instruction(process, source)
if current == instruction:
_PATCH_STATE["locations"].extend(
_disable_conflicting_breakpoints(target, source)
)
return True
if current != _PATCH_STATE["original"]:
raise HookError(
"DisplayList.description changed after the hook was enabled."
)
# Someone restored the instruction independently. Release our ownership of
# the breakpoints before installing a fresh patch.
_restore_breakpoints(_PATCH_STATE["locations"])
_PATCH_STATE = None
return False
def _enable(debugger, result):
global _PATCH_STATE
target = _selected_target(debugger)
process = _stopped_process(target)
source, destination, instruction = _resolve_patch(target)
if _prepare_enable(target, process, source, instruction):
result.AppendMessage(
"SwiftUI minimal display-list printing is already enabled."
)
return
locations = _disable_conflicting_breakpoints(target, source)
original = None
write_started = False
try:
original = _read_instruction(process, source)
if original == instruction:
raise HookError(
"DisplayList.description is already patched, but its "
"original instruction is unavailable; restart the process "
"to reset it."
)
write_started = True
_write_instruction(process, source, instruction)
except HookError as error:
if write_started:
try:
_write_instruction(process, source, original)
except HookError as rollback_error:
_PATCH_STATE = _new_patch_state(
target,
process,
source,
destination,
original,
instruction,
locations,
uncertain=True,
)
raise HookError(
"{} Rollback also failed: {} The instruction may be "
"partially modified; keep the process paused and run "
"`{} disable` again.".format(
error,
rollback_error,
_COMMAND_NAME,
)
)
_restore_breakpoints(locations)
raise
_PATCH_STATE = _new_patch_state(
target,
process,
source,
destination,
original,
instruction,
locations,
)
result.AppendMessage(
"Enabled SwiftUI minimal display-list printing: {:#x} -> {:#x}.".format(
source,
destination,
)
)
if locations:
result.AppendMessage(
"Disabled {} conflicting description breakpoint(s) for this "
"session.".format(len(locations))
)
if _print_tree_environment(target) == "<unset>":
result.AppendMessage(
"Note: set SWIFTUI_PRINT_TREE=1 in the Scheme before launching "
"the process."
)
def _disable(result):
global _PATCH_STATE
if _PATCH_STATE is None:
result.AppendMessage("SwiftUI minimal display-list printing is disabled.")
return
state = _PATCH_STATE
process = state["process"]
process_state = (
process.GetState() if process.IsValid() else lldb.eStateInvalid
)
if process_state == lldb.eStateDetached:
raise HookError(
"Process {} is detached and may still contain the patch. "
"This LLDB session can no longer restore it; terminate the "
"process to discard the process-local change.".format(
state["process_id"]
)
)
if process_state not in _GONE_PROCESS_STATES:
if process_state not in _WRITABLE_PROCESS_STATES:
raise HookError(
"Pause process {} before disabling the hook.".format(
state["process_id"]
)
)
current = _read_instruction(process, state["source"])
if current != state["original"]:
if current != state["instruction"] and not state["uncertain"]:
raise HookError(
"DisplayList.description contains an unrelated "
"instruction; refusing to overwrite it."
)
_write_instruction(
process,
state["source"],
state["original"],
)
_restore_breakpoints(state["locations"])
_PATCH_STATE = None
result.AppendMessage("Disabled SwiftUI minimal display-list printing.")
def _patch_status(state):
process = state["process"]
process_state = (
process.GetState() if process.IsValid() else lldb.eStateInvalid
)
if process_state in _GONE_PROCESS_STATES:
return "disabled (process ended; breakpoint cleanup pending)"
if process_state == lldb.eStateDetached:
return "enabled (process detached; restoration pending)"
if process_state not in _WRITABLE_PROCESS_STATES:
return "enabled (unverified while process is running)"
try:
current = _read_instruction(process, state["source"])
except HookError as error:
return "unknown ({})".format(error)
if current == state["instruction"]:
return "enabled"
if current == state["original"]:
return "disabled externally (breakpoint cleanup pending)"
return "unknown (description instruction changed externally)"
def _status(debugger, result):
target = _selected_target(debugger)
state = _PATCH_STATE
status = "disabled" if state is None else _patch_status(state)
result.AppendMessage(
"SwiftUI minimal display-list printing: {}".format(status)
)
result.AppendMessage("mode=arm64 branch patch (no breakpoint)")
reporting_target = state["target"] if state is not None else target
result.AppendMessage("target={}".format(reporting_target.GetTriple()))
result.AppendMessage(
"{}={}".format(
_PRINT_TREE_ENVIRONMENT_KEY,
_print_tree_environment(reporting_target),
)
)
if state is not None:
result.AppendMessage(
"process={}, description={:#x}, minimalDescription={:#x}".format(
state["process_id"],
state["source"],
state["destination"],
)
)
result.AppendMessage(
"conflicting_breakpoints_disabled={}".format(
len(state["locations"])
)
)
def swiftui_display_list_minimal(
debugger,
command,
result,
internal_dict,
):
del internal_dict
try:
arguments = shlex.split(command)
if len(arguments) != 1 or arguments[0] not in {
"enable",
"disable",
"status",
}:
raise HookError(
"usage: {} enable|disable|status".format(_COMMAND_NAME)
)
action = arguments[0]
if action == "enable":
_enable(debugger, result)
elif action == "disable":
_disable(result)
else:
_status(debugger, result)
except (HookError, ValueError) as error:
result.SetError(str(error))
def __lldb_init_module(debugger, internal_dict):
del internal_dict
debugger.HandleCommand(
'command script add --overwrite -h "Redirect SwiftUI display-list '
'tree output to minimalDescription without a breakpoint." -f '
'{}.swiftui_display_list_minimal {}'.format(
__name__,
_COMMAND_NAME,
)
)
print(
"Installed LLDB command: {} enable|disable|status".format(
_COMMAND_NAME
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment