Adopting CustomNSError in DecodingError
NSError
bridging is an interesting beast in the Swift compiler. On the one hand, NSError
comes from the Foundation framework, which your application may or may not use; on the other, the actual bridging mechanics need to be performed in the compiler, and rightfully, the compiler should have as little knowledge of "high-level" libraries above the standard library as possible.
As such, the compiler has very little knowledge of what NSError
actually is, and instead, Error
exposes three properties which provide the entirety of the underlying representation of NSError
:
public protocol Error {
var _domain: String { get }
var _code: Int { get }
// Note: _userInfo is always an NSDictionary, but we cannot use that type here
// because the standard library cannot depend on Foundation. However, the
// underscore implies that we control all implementations of this requirement.
var _userInfo: AnyObject? { get }
// ...
}
NSError
, then, has a Swift extension which conforms to Error
and implements those three properties:
extension NSError : Error {
@nonobjc
public var _domain: String { return domain }
@nonobjc
public var _code: Int { return code }
@nonobjc
public var _userInfo: AnyObject? { return userInfo as NSDictionary }
// ...
}
With this, when you import Foundation
, any Error
can be cast to an NSError
and vice versa, as both expose _domain
, _code
, and _userInfo
(which is what the compiler actually uses to perform the bridging).
The CustomNSError
protocol plays into this by allowing you to supply an errorDomain
, errorCode
, and errorUserInfo
, which are then exposed by various extensions as their underscore versions:
public extension Error where Self : CustomNSError {
/// Default implementation for customized NSErrors.
var _domain: String { return Self.errorDomain }
/// Default implementation for customized NSErrors.
var _code: Int { return self.errorCode }
// ...
}
So, how are EncodingError
and DecodingError
different? Well, since they're both defined in the standard library (which is present regardless of whether or not you use Foundation, and cannot depend on Foundation), they hook into the system by providing implementations of _domain
, _code
, and _userInfo
directly.
Since both types provide the direct underscore versions of those variables, they don't call in to the non-underscore versions to get the domain, code, and user info — the values are used directly (rather than rely on var _domain: String { return Self.errorDomain }
).
So, in effect, you can't override the behavior because EncodingError
and DecodingError
already provide this info. Instead, if you want to provide different codes/domains/user info dictionaries, you're going to need to write a function which takes an EncodingError
/DecodingError
and returns your own NSError
, or similar.
Swift 3: How to catch error if NSCoder decodeX function decodes wrong value type?
Using decodeInteger
on a non-integer key would raise an exception. Sadly, it's an NSException
which Swift cannot handle directly (see references below).
You need to first write a wrapper to handle ObjC exceptions in ObjC and bridge it over to Swift (inspired by this answer):
/// -------------------------------------------
/// ObjC.h
/// -------------------------------------------
#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error;
@end
/// -------------------------------------------
/// ObjC.m
/// -------------------------------------------
#import "ObjC.h"
@implementation ObjC
+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[userInfo setValue:exception.reason forKey:NSLocalizedDescriptionKey];
[userInfo setValue:exception.name forKey:NSUnderlyingErrorKey];
*error = [[NSError alloc] initWithDomain:exception.name
code:0
userInfo:userInfo];
return NO;
}
}
@end
Now you can catch the exception in Swift:
do {
try ObjC.catchException {
let age = aDecoder.decodeInteger(forKey: "firstName")
}
} catch {
print(error.localizedDescription)
}
References: Using ObjectiveC with Swift: Adopting Cocoa Design Patterns
Although Swift error handling resembles exception handling in Objective-C, it is entirely separate functionality. If an Objective-C method throws an exception during runtime, Swift triggers a runtime error. There is no way to recover from Objective-C exceptions directly in Swift. Any exception handling behavior must be implemented in Objective-C code used by Swift.
Asymmetric Encoding/Decoding with Codable and Firestore?
I don't get where you are seeing that error from - a line reference would be useful - but it's not directly related to your en/decoding. (As an aside, that method really is not a data model)
As you want to decode both properties with JSON keys that match their property names, there is no need to specify CodingKeys
or write a custom decoder; you can rely on the synthesised decoder and let Codable
do the work for you.
For the decoding you will need a custom solution else Decodable
will decode both fields. This will require both a CodingKey
enum (note the singular, i.e. the protocol, not the defaut enun name) and a custom encoder to use that.
You end up with a far simpler implementation of your struct. I've also added a simple initialiser as you lose the synthesised memberwise initialiser as soon as you define the init(from:)
. This was just so I could test it.
struct Recipe: Codable {
var id: String?
var vegetarian: Bool?
init(id: String, vegetarian: Bool){
self.id = id
self.vegetarian = vegetarian
}
init(from decoder: Decoder) throws {
enum DecodingKeys: CodingKey {
case vegetarian
}
let container = try decoder.container(keyedBy: DecodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
}
If you test this you will find that it will just decode the vegetarian
property but encode both. Simple testing shows:
let recipe = Recipe(id: "1", vegetarian: true)
let data = try! JSONEncoder().encode(recipe)
print(String(data: data, encoding: .utf8)!) //{"id":"1","vegetarian":true}
let decodedRecipe = try! JSONDecoder().decode(Recipe.self, from: data)
print(decodedRecipe) //Recipe(id: nil, vegetarian: Optional(true))
Why does my special Codable protocol work differently than Swift's Codable with Array?
The error message might be more useful if it used the unsugared typename:
Type 'Array<Person>' has no member 'decode';
Person
may conform to your protocol, but Array
does not. Swift explicitly declares that Array
s are Decodable
if their elements are. You just need to do the same:
extension Array : JsonDecodable where Element : JsonDecodable {
static func decode(data: Data?, decoder: JSONDecoder) -> Self? {
// Decode each element and return an array
}
}
This uses a feature called "Conditional Conformance", which allows containers generally to conform to a protocol if the type they hold also does.
Related Topics
How to Build a Swift Executable for Linux on Macos
Xcode - Build Setting "Excluded_Source_File_Names" Not Working
How to Set Alignment for Wkinterface Label Using Setattributedtext
Arkit/Realitykit - People Occlusion Config Not Working
Swiftui: @Observedobject Redraws Every View
Are Built-In Intrinsic Functions Available in Swift 3
Can't Create Default Closure Parameter in Array Extension Method in Swift
Swift Generics Protocols: Can Only Be Used as a Generic Constraint Problem
Adopting Customnserror in Decodingerror
Swift Protocol as Generic Parameter
Gcdasyncsocket Multiple Connections Wont Accept Data from Multiple Sockets
Why Are Objects in the Same Sknode Layer Not Interacting with Each Other
Play Sound with a Little Delay