One technique that I like to use that involves an init-restricted .struct is to design a robust API that involves temporary objects with strict life-time limitations (like some sort of transaction or some temporary accessor to something). Take, for example this API:
public final class DataBase {
// ...
public func withTransaction<Success>(call function: @escaping (Transaction) throws -> Success) -> Success {
// ...
}
}
In a normal scenario, the Transaction object can be either easily copied (if it is a struct) or retained (if it is a class), which is not what you’d want, given that the transaction has a very specific semantic attached to its life-time (for instance, the same transaction object might be reused for nested transaction-wrapped code, which would coalesce them into a single transaction, which, in turn, would commit the transaction at the end of the outermost call):
func foo() {
myDataBase.withTransaction { transaction in
// The `transaction` object was just created (a transaction has begun).
transaction.changeSomething()
bar()
}
}
func bar() {
myDataBase.withTransaction { transaction in
// The `transaction` object is the same as the one in `foo`.
transaction.changeSomething()
}
}
In this scenario, the transaction either has to be a class object or a struct that wraps a class object. If we simply pass around the class object itself, then foo or bar could accidentally escape the reference to it, thus disrupting the transaction logic and causing the transaction (and all subsequent transactions) to hang indefinitely.
One could argue that it’s a programming error to escape the reference, but it’s easy to accidentally retain it in a closure and it’s incredibly hard to track down the cause of the bug.
So, in order to avoid this problem, I’d make Transaction a struct that stores a weak reference to a class object that actually implements the transactional functionality. I’d then pass around that wrapper struct instead of the object, thus, guaranteeing that the transaction will live exactly as long as we need it to. Any “escaped” references will always be weak, so at the end of the transaction, all references will expire and the worst possible outcome is a force-unwrapping crash, instead of a mysterious hang...
Naturally, you’d make all inits of the struct non-public, so that the only way to get it would be to actually start a trabsaction.