Description
Previous ID | SR-14975 |
Radar | rdar://problem/81115919 |
Original Reporter | @CharlesJS |
Type | Bug |
Environment
macOS 21A5284e
Xcode Version 13.0 beta 3 (13A5192i)
Swift version 5.5 (swiftlang-1300.0.24.13 clang-1300.0.25.10)
Additional Detail from JIRA
Votes | 0 |
Component/s | Foundation |
Labels | Bug |
Assignee | None |
Priority | Medium |
md5: f34c7dac79752ed46ed183110bcd487a
Issue Description:
So, let's say we have an enum that conforms to `LocalizedError`, which contains logic to bridge nicely to `NSError` so that Cocoa UIs will present our errors nicely:
import Foundation
enum Plague: LocalizedError {
case fires
case floods
case frogs
case flies
var failureReason: String? {
switch self {
case .fires:
return "We didn't start the fire! But this error did"
case .floods:
return "Ya got trouble, right here in River City"
case .frogs:
return "Ribbit Ribbit"
case .flies:
return "Shoo fly, don't bother me"
}
}
}
let err = Plague.frogs
print(
"Before encoding/decoding: localizedDescription is '\((err as NSError).localizedDescription)', ",
"userInfo is \((err as NSError).userInfo)"
)
Running that, we get:
Before encoding/decoding: localizedDescription is 'The operation couldn’t be completed. Ribbit Ribbit', userInfo is [:]
The `userInfo` dictionary is empty, which is odd, but the `localizedDescription` does get displayed correctly after we bridge to `NSError`. So far so good.
But what if it goes through an `NSKeyedArchiver` (for example, if it gets sent over an XPC connection)?
let encoded = try! NSKeyedArchiver.archivedData(withRootObject: err, requiringSecureCoding: true)
let decoded = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: encoded)!
print("After: localizedDescription is '\(decoded.localizedDescription)', userInfo is \(decoded.userInfo)")
Suddenly, the `failureReason` string is lost:
After: localizedDescription is 'The operation couldn’t be completed. (main.Plague error 2.)', userInfo is [:]
However, since `Error` contains a `_userInfo` property, we can add an implemention for that ourselves:
var _userInfo: AnyObject? { [NSLocalizedFailureReasonErrorKey : self.failureReason!] as NSDictionary }
And that fixes it:
After: localizedDescription is 'The operation couldn’t be completed. Ribbit Ribbit', userInfo is ["NSLocalizedFailureReason": Ribbit Ribbit]
However: this only seems to work on error classes defined in the current module. If I try to add `_userInfo` to something defined in another module via an extension, it doesn't work:
extension POSIXError {
var _userInfo: AnyObject? { [NSLocalizedFailureReasonErrorKey : "Hello"] as NSDictionary }
}
let err = POSIXError(.EINVAL)
print(
"Before encoding/decoding: localizedDescription is '\((err as NSError).localizedDescription)', ",
"userInfo is \((err as NSError).userInfo)"
)
let encoded = try! NSKeyedArchiver.archivedData(withRootObject: err, requiringSecureCoding: true)
let decoded = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: encoded)!
print("After: localizedDescription is '\(decoded.localizedDescription)', userInfo is \(decoded.userInfo)")
yields:
Before encoding/decoding: localizedDescription is 'The operation couldn’t be completed. Invalid argument', userInfo is [:]
After: localizedDescription is 'The operation couldn’t be completed. Invalid argument', userInfo is [:]
This is not exclusive to Foundation-defined things like `POSIXError`; I've tried it with errors I've defined myself in other modules, and the behavior is the same. I can put a log in the `_userInfo` accessor, and it never gets called.
It seems to me that the expected behavior should be:
-
`LocalizedString` includes a default implementation for `_userInfo` that provides the various strings corresponding to the properties it supports.
-
We have logic in place to make sure `_userInfo` always gets propagated across when bridging to `NSError`, or at least when we're running it through an encoder.
Otherwise, the `userInfo` field is always unpopulated for bridged Swift errors, and in the end, all I have is an arbitrary integer and I don't know whether I need to get an extinguisher, an umbrella, some bug spray, or... whatever you do for frogs (a cat maybe?).