The issue is that the data types being accessed being Sendable isn't sufficient to ensure the type is being accessed correctly in aggregate. You can implement plenty of unsafe behavior using purely Sendable types.
A toy example:
Let's say that I'm working with extremely large datatypes in my app — structs which have to represent lots of data, and are expensive to compute and pass around. Instead of incurring large copies, I may decide to write a ThreadLocalDataStorage type which stores the data, and allows me to access individual fields on those structs via thread-local ID and keypath:
struct ThreadLocalDataStorage {
// Every thread gets its own pool of IDs.
// IDs from one pool are not safe to use from another thread.
nonisolated static func get<T>(_ property: KeyPath<MyDataType, T>, forId: Int) -> T
}
To make access a little nicer, I might write a small DataProxy struct which makes those fields easier to access:
@dynamicMemberLookup
struct DataProxy {
let id: Int
nonisolated subscript<T>(dynamicMember keyPath: KeyPath<MyDataType, T>) -> T
ThreadLocalDataStorage.get(keyPath, forId: id)
}
}
extension ThreadLocalDataStorage {
nonisolated static func insert(_ data: MyDataType) -> DataProxy
}
Now, even though DataProxy contains only Sendable data, and ThreadLocalDataStorage is safe to access from any thread, the Int identifiers only identify data accessible to the thread that inserted the data. As such, I don't want DataProxy to be Sendable, because that might cause the data to be accessed from the wrong thread:
/// DataProxy is only safe to access on the thread it was created on!!
@available(*, unavailable)
extension DataProxy: Sendable {}
Later, I see that I need to interface MyDataType with an API requiring IdRequirement, and I don't notice IdRequirement: Sendable, so I write
extension DataProxy: IdRequirement {
func provideId() -> String {
// MyDataType has an `id` field itself.
self[\.id]
}
}
If the compiler doesn't prevent this access, because all of the data types are Sendable and functions are nonisolated... Then I'm in for a fun surprise if I pass a DataProxy as an IdRequirement to somewhere that then shuffles it across threads.
At a higher level, again: protocol conformances in Swift are intended to mean something at the type level. Either a whole type is Sendable, or it isn't, because of what it means to be Sendable; it doesn't make sense to say "if I look only at the parts of a type which are Sendable then it's Sendable" any more than it would to say "if I look only at the parts of a type which are Comparable then it's Comparable":
// Not Comparable because the operation wouldn't make sense.
struct WebPage {
let url: URL
let lastUpdated: Date
let contents: Data
// ...
}
protocol URLProvider: Comparable {
func getURL() -> URL
}
// Conforming only because I can provide a URL
extension WebPage: URLProvider {
func getURL() -> URL { url }
}
let googleDotCom: WebPage = /* ... */
let amazonDotCom: WebPage = /* ... */
// Now this is possible based on URL?
if googleDotCom < amazonDotCom { ... }
The semantics of a protocol need to apply to the whole type, as defined by the author.