That is correct to a very large extent. There is some minor nuances, but those don't apply here.
Escaping a closure by itself doesn't cause a reference cycle. Here are two simpler scenarios:
class A {
var closure: () -> () = {}
deinit { print("Deinit-ing A") }
}
var escapedClosure: (() -> ())?
func good(a: A, x: @escaping () -> ()) {
escapedClosure = x
}
func bad(a: A, x: @escaping () -> ()) {
a.closure = x
}
// Scenario 1
var a: Optional = A()
good(a: a!) { [a] in print(a!) } // No cycle
a = nil
print("A not deinited yet")
escapedClosure = nil // Deinit A
print("A is deinited")
// Scenario 2
a = A()
bad(a: a!) { [a] in print(a!) } // Has cycle
a = nil // A not deinited
In the first scenario, closure x captures a, and outlive the good function lifetime, hence @escaping. Still, the lifetime of x is still as long as escapedClosure. That's why, when we set escapedClosure to nil, x is destroyed, releasing the only reference to a, and a is also destroyed.
In the second scenario, however, x is stored into a.closure. So when a is set to nil, a still holds reference to x, which holds a reference to a. This is very problematic because we can no longer access from anywhere in the program, but a is still referenced (by itself), and so are not deinit-ed. That's why the reference cycle is a problem. It prevents the program from destroying an instance that's no longer used by anyone.
Your MyAPICaller is a good scenario:
a is the caller
x is test(data:resp:err:)
escapedClosure is stored somewhere by URLSession.shared
So while the MyAPICaller instance can outlive the variable caller, it will still get destroyed once URLSession.shared use the closure and stop referencing them.
UserAPI itself doesn't store the declared closure. You need to assign it to a variable, which is presumably done somewhere inside .shared. So .shared has a reference to the closure, but UserAPI doesn't.