# SwiftTrace, tracing Swift and Objective-C method calls at runtime

> johnno1962/SwiftTrace swizzles method dispatch in a running app so calls to non-final Swift and Objective-C classes are logged, and it can go further by adding aspects that run a closure before or after a method and modify the arguments or the return value. The constraint that shapes everything is Swift's direct dispatch for final and internal types, which cannot be intercepted this way.

**johnno1962/SwiftTrace** — Trace Swift and Objective-C method invocations

- Repository: https://github.com/johnno1962/SwiftTrace
- Stars: 750 · Forks: 53
- Language: Swift
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/johnno1962-swifttrace

## What the package does and the limit that defines it

SwiftTrace traces Swift and Objective-C method invocations of non-final classes in an app bundle or framework. The README frames it as the equivalent of the author's Xtrace project but for Swift and Objective-C.

The interesting second half of its capability is that it can add aspects to member functions of non-final Swift classes, so a closure is called before or after a function implementation executes and can modify the incoming arguments or the return value. That turns it from a logging tool into an interception tool, which is what makes it useful for experimenting with behaviour rather than only observing it.

The README notes that with binary distribution of Swift frameworks on the horizon, the approach may be useful in the way swizzling was in days of yore, which is a useful framing for anyone arriving from an Objective-C background where swizzling was routine.

The constraint is stated plainly and it is not a small one. None of these features work on a class or method that is final, or internal in a module compiled with whole module optimisation, because the dispatch of the method is then direct, linked to a symbol at the call site rather than going through the class's vtable. So the tool can only see methods that are dispatched dynamically, which in modern Swift is a shrinking set.

There is a partial exception and the README is precise about it. It is possible to trace calls to methods of a struct, but only if they are referenced through a protocol, because protocol dispatch goes through a witness table, which can be patched. That distinction between a vtable and a witness table is the technical detail that makes the whole thing work, and understanding it tells you which calls you will see and which you will not.

## Adding it to a project and the first call

SwiftTrace can be used with the Swift Package Manager or as a CocoaPod, and the Podfile line is:

```Swift
    pod 'SwiftTrace'
```

Once the project has rebuilt, you import SwiftTrace into the application's AppDelegate and add a call to the beginning of the didFinishLaunchingWithOptions method:

```Swift
    SwiftTrace.traceBundle(containing: type(of: self))
```

That traces all classes defined in the main application bundle. The bundle being traced is identified by a class instance from it, which is a slightly unusual but compact way of saying "the bundle this class lives in".

To trace classes in a third-party library, the same call is used with a class from that library:

```Swift
    SwiftTrace.traceBundle(containing: RxSwift.DisposeBase.self)
```

which produces output in the Xcode debug console.

For a system framework such as UIKit, you trace by pattern instead:

```Swift
    SwiftTrace.traceClasses(matchingPattern:"^UI")
```

The individual APIs go down from there. SwiftTrace.trace(aClass:) traces a single class. SwiftTrace.traceInstances(ofClass:) traces all methods of instances of a class including those of their superclasses, which matters when the behaviour you care about is inherited. SwiftTrace.trace(anInstance:) traces one particular instance, which is the tool for the case where you have one object in a test or a reproduction and do not want the noise of the whole class.

There is one mode that needs an extra build setting. If you have specified -Xlinker -interposable in the project's Other Linker Flags, it is possible to trace all methods in the application's main bundle at once, which the README says is useful for profiling SwiftUI:

```Swift
    SwiftTrace.traceMainBundleMethods()
```

The interposable linker flag is what makes those symbols patchable, and it is a build-level change rather than a runtime one, so it has to be in place before you can use that entry point.

## Protocols, filtering and the noise problem

Tracing protocols is available at the bundle level, where the bundle is specified with a class instance and the trace can be filtered by an optional regular expression:

```Swift
SwiftTrace.traceProtocolsInBundle(containing: AClassInTheBundle.self, matchingPattern: "regexp")
```

The example in the README traces internal calls made in the SwiftUI framework:

```Swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    SwiftTrace.traceProtocolsInBundle(containing: UIHostingController<HomeView>.self)
    return true
}
```

That last example is the one that matters for anyone working with SwiftUI, because SwiftUI's own internals are largely internal and dynamically dispatched through protocols, so the vtable problem that limits ordinary class tracing largely does not apply there.

The other lever is filtering by method name, which is what turns an unreadable flood of log lines into something usable. There are two patterns:

```swift
    SwiftTrace.methodInclusionPattern = "TestClass"
    SwiftTrace.methodExclusionPattern = "init|"+SwiftTrace.defaultMethodExclusions
```

The inclusion pattern keeps only the methods you care about, and the exclusion pattern drops noise. The default exclusions are a constant on SwiftTrace itself, so appending to them rather than replacing them is the right move, and the init prefix in the example is there because initialisers are called constantly and would otherwise dominate the output.

The README is clear that these must be set before you start the trace, because the patterns are applied during the setup rather than at log time. That is worth remembering, because setting them afterwards appears to do nothing.

The combination of an inclusion pattern with protocol tracing is what makes this practical on a real framework. Tracing everything in a large bundle will produce output no one can read; tracing one class, or one protocol, with a name filter produces something you can actually use to answer a question.

## Where this fits against the alternatives

The standard alternatives in the Apple toolchain are the debugger and the profiler.

The debugger is the first thing to reach for. LLDB can set breakpoints on any method, including final ones, because it operates at the symbol level rather than by patching dispatch tables. For a question like which caller invokes this method, a breakpoint with a backtrace is more direct than a trace log, and it does not have the final and internal restrictions. The advantage of SwiftTrace is that it is non-interactive and continuous: you can leave it running through a whole user flow and read the log afterwards, rather than stopping at each call.

Instruments and the os signpost work are the second alternative, and they are faster and more structured. Signposts give you intervals with names you chose, which you can then aggregate. What they do not give you is the ability to change behaviour at the call site, which is the aspect feature.

The distinctive capability here is the aspect, the closure that runs before or after a method and can modify the arguments or the return value. That is closer to a mocking library than to a logger, and it lets you test how your code behaves when a dependency returns something unexpected, or force a branch that is hard to reach. Xtrace, the author's earlier project, is the direct ancestor and does the same thing for the older Objective-C runtime.

The cost of all of this is the same cost the Swift runtime's optimisations impose. Swift is designed so that the compiler can dispatch directly, and anything that intercepts dispatch has to prevent that. Final types, internal types in whole module optimised modules, and any call the compiler turned into a direct symbol reference are invisible to this technique. As Swift adoption grows and more code is built with aggressive optimisations, the surface this tool can see shrinks.

The other cost is that this is a debugging and research tool rather than a production monitoring one. It patches dispatch tables in a running process, which makes it a poor thing to ship in a released app, and the licence field on the repository reports it as a custom licence GitHub cannot classify, so the terms are in the LICENSE file rather than in a standard identifier.

## What aspects add beyond logging

The aspect capability is worth separating out, because it is what makes this more than a logger, and it is also what makes it a research tool rather than something you ship.

An aspect is a closure that runs before or after the traced method executes. Because it runs on either side of the original implementation, it can read the incoming arguments, modify them before the call proceeds, and replace the return value on the way out. That is the same shape as a mock, except the mock is at the call site and the aspect is at the definition.

The practical use is answering what your code does when a dependency misbehaves. If a network layer is supposed to return a particular error and you want to know how your UI handles it, you can install an aspect on that method, force the error, and watch. It is also useful for exploring a codebase you do not own, since you can observe the values crossing a boundary without a debugger attached.

There is a caveat that comes with it. Because an aspect runs inside the traced process, a bug in your closure is a bug in the application, and the trace output becomes harder to read rather than easier if the closure is chatty. The logging mode and the aspect mode share the same machinery, so the practical discipline is to keep aspects minimal and specific.

The combination of the two features is what gives the package its range. A trace alone tells you what was called. A trace with an aspect tells you what was called and lets you perturb it. And the filtering by method name keeps both manageable, which is the difference between a tool you use and a tool you try once and abandon.

## Conclusion

Adopt SwiftTrace if you are debugging a Swift or Objective-C app and need to see which methods are called, with the option to alter arguments or return values on the way through. Skip it if you need to trace final types or internal methods in a module built with whole module optimisation, because those are dispatched directly and cannot be intercepted. Verify first by tracing a class you know is non-final and confirming the log lines appear, then check the linker flag requirement for the whole-bundle mode.

## FAQ

### What does SwiftTrace trace?

It traces Swift and Objective-C method invocations of non-final classes in an app bundle or framework, and it can add aspects to member functions so a closure runs before or after the implementation and can modify the incoming arguments or the return value.

### Why can SwiftTrace not trace every method?

Because a final class or method, or an internal method in a module compiled with whole module optimisation, is dispatched directly to a symbol at the call site rather than through the class's vtable, so there is nothing to patch. Struct methods can be traced only when they are referenced through a protocol, since protocol dispatch goes through a witness table.

### How do I add SwiftTrace to a project?

Through Swift Package Manager or as a CocoaPod with the pod 'SwiftTrace' line in the Podfile. After the project rebuilds, import it in the AppDelegate and call SwiftTrace.traceBundle(containing: type(of: self)) at the start of didFinishLaunchingWithOptions.

### How do I reduce the amount of output SwiftTrace produces?

Set SwiftTrace.methodInclusionPattern to keep only the classes you want and SwiftTrace.methodExclusionPattern to drop noise, appending to SwiftTrace.defaultMethodExclusions rather than replacing it. Both must be set before the trace starts, because the patterns are applied during setup.

### What linker flag does SwiftTrace need for whole bundle tracing?

Adding -Xlinker -interposable to the project's Other Linker Flags makes the symbols patchable, which allows SwiftTrace.traceMainBundleMethods() to trace every method in the main bundle at once, a mode the README suggests for profiling SwiftUI.

## Sources

- [Issues](https://github.com/johnno1962/SwiftTrace/issues)
- [johnno1962/SwiftTrace on GitHub](https://github.com/johnno1962/SwiftTrace)
- [README](https://github.com/johnno1962/SwiftTrace/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/johnno1962-swifttrace
