Let's say I have a dependency on A having a dependency on B. There imagine a reason I'd like to change something in B and I have no control over B, so I fork B (B').
In order to use B', I believe I now also have to fork A (A') to point to B', or is there a way to convince SwiftPM to replace B throughout the dependency graph to use B'?
There could be other dependencies (out of my control) that also have a dependency on B, which would then also have to use B' instead (otherwise SwiftPM throws "version solving failed").
AFAIK the only way to achieve this is forking all of the packages as you described. (I just did this exact thing a few weeks ago to workaround a bug that upstream was slow accepting patches for). You can use swift package edit or Xcode's native SPM workspace integration to "override" a package with any source code you want, but that only works locally.
That's a possibility, however I'm looking for a situation that doesn't involve forking everything. With a C library, I would just replace the shared library for example.
I'm specifically looking for a solution with SwiftPM. I'm publishing a library that is consumed using SwiftPM, and while I rely on the forked dependency, I'd rather not enforce my fork for the consumers of my package. (The fork is API compatible.)
In addition to edit mode, you have the option of configuring a mirror such that SwiftPM will treat the original package as mirrored by the fork. But there is no way of automating such an override for consumers.