Comparing between two errors - ios

In my app, I am throwing around errors by using the Error protocol. And there are many of these error types in my app such as AuthError, ApiError, etc. Not to mention the third-party frameworks's errors such as RxCocoaURLError, Twilio's NSError, etc. And then there is an enum to show/hide the error messages on the view:
enum ErrorMessageEvent {
case hide
case show(error: Error)
}
Now, in the unit test, I have a need to compare between two errors like so:
func testGetDataButtonTappedFailed_ShouldShowError() {
let error = ApiError(type: .serviceUnavailable, message: "ABC")
let viewModel = createViewModel(dataProvider: { _ in .error(error) })
var actualErrorMessageEvent: ErrorMessageEvent?
viewModel.errorMessageEvent
.emit(onNext: { actualErrorMessageEvent = $0 })
.disposed(by: disposeBag)
viewModel.getDataButtonTapped(id: 1)
XCTAssertEqual(.show(error: error), actualErrorMessageEvent)
}
Now when I added the Equatable to the ErrorMessageEvent and added this for the comparison func, but it always failed:
static func == (lhs: ErrorMessageEvent, rhs: ErrorMessageEvent) -> Bool {
switch (lhs, rhs) {
case (.hide, .hide): return true
case (.show(let lError), .show(let rError)):
return lError as AnyObject === rError as AnyObject
default: return false
}
}
What is the right way to compare those two errors and fix this? Thanks.

First, make sure that each kind of error that you are dealing with properly conforms to Equatable. Why are we using Equatable rather than ===? Because the error types value types. RxCocoaURLError is an enum for example. And === never works on structs, even if you cast to AnyObject, because two different objects will be created when you do. Example:
enum Foo {
case a(Int)
case b(String)
}
(Foo.a(1) as AnyObject) === (Foo.a(1) as AnyObject) // false
The next step is to make ErrorMessageEvent generic (kind of like Optional, or half of Result):
enum ErrorMessageEvent<Failure: Error> {
case hide
case show(error: Failure)
// this (tries to) changes the type of the error
func mapError<T: Error>(toType type: T.Type) -> ErrorMessageEvent<T>? {
switch self {
case .hide:
return .hide
case .show(error: let e):
return (e as? T).map(ErrorMessageEvent<T>.show)
}
}
}
// messages with an equatable error are also equatable
// the compiler will synthesise the implementation for us
extension ErrorMessageEvent : Equatable where Failure : Equatable {}
You can use ErrorMessageEvent<Error> for the type of observable for viewModel.errorMessageEvent. Then, when you want to compare errors in a test, you'd know which type of error you are comparing at that point, so you can do:
let error = ApiError(type: .serviceUnavailable, message: "ABC")
// ...
// I *think* it can infer the types. If not, write out the full "ErrorMessageEvent<ApiError>.show"
XCTAssertEqual(
.show(error: error),
actualErrorMessageEvent?.mapError(toType: ApiError.self))
At this point, you might have realised. This is just reinventing the wheel of Optional<T>. So rather than ErrorMessageEvent, you can also consider an using an optional. Your ErrorMessageEvent might be overengineering.
let error = ApiError(type: .serviceUnavailable, message: "ABC")
let viewModel = createViewModel(dataProvider: { _ in .error(error) })
var actualErrorMessage: ApiError?
// viewModel.errorMessageEvent would be an Observable<Error?>
viewModel.errorMessageEvent
.emit(onNext: { actualErrorMessage = $0 as? ApiError })
.disposed(by: disposeBag)
viewModel.getDataButtonTapped(id: 1)
XCTAssertEqual(error, actualErrorMessageEvent)
It doesn't have as descriptive names as show and hide (It uses none and some instead), so if that's what you care about, I can understand.

So, based on #Sweeper's answer, I came up with this custom assertion function instead:
func XCTAssertEqualGenericError<ErrorType: Equatable>(_ expected: ErrorType,
_ actual: Error?,
file: StaticString = #file,
line: UInt = #line) throws {
let actualError = try XCTUnwrap(actual as? ErrorType, file: file, line: line)
XCTAssertEqual(expected, actualError, file: file, line: line)
}
And use it in this two assertions:
func XCTAssertErrorMessageEventHide(_ actual: ErrorMessageEvent?,
file: StaticString = #file, line: UInt = #line) {
guard let actual = actual else {
XCTFail("Actual ErrorMessageEvent is nil!")
return
}
switch actual {
case .show: XCTFail("Actual ErrorMessageEvent is not of the type .hide!")
case .hide: return
}
}
func XCTAssertErrorMessageEventShow<ErrorType: Equatable>(_ expected: ErrorMessageEvent,
_ actual: ErrorMessageEvent?,
errorType: ErrorType.Type,
file: StaticString = #file,
line: UInt = #line) throws {
guard let actual = actual else {
XCTFail("Actual ErrorMessageEvent is nil!")
return
}
switch (expected, actual) {
case (.hide, _):
XCTFail("Expected ErrorMessageEvent is not of the type .show(_, _)!")
case (_, .hide):
XCTFail("Actual ErrorMessageEvent is not of the type .show(_, _)!")
case (.show(let lError, let lDuration), .show(let rError, let rDuration)):
XCTAssertEqual(lDuration, rDuration, file: file, line: line)
let expectedError = try XCTUnwrap(lError as? ErrorType, file: file, line: line)
try XCTAssertEqualGenericError(expectedError, rError, file: file, line: line)
default: return
}
}
Thanks again.

Related

PHPhoto localIdentifier to cloudIdentifier conversion code improvements?

The two conversion methods below for mapping between the PHPhoto localIdentifier to the corresponding cloudIdentifier work but it feels too heavy. Do you have suggestions on how to rewrite to a more elegant (easier to read) form?
The sample code in the Apple documentation found in PHCloudIdentifier https://developer.apple.com/documentation/photokit/phcloudidentifier/ does not compile in xCode 13.2.1.
It was difficult to rewrite the sample code because I made the mistake of interpreting the Result type as a tuple. Result type is really an enum.
func localId2CloudId(localIdentifiers: [String]) -> [String] {
var mappedIdentifiers = [String]()
let library = PHPhotoLibrary.shared()
let iCloudIDs = library.cloudIdentifierMappings(forLocalIdentifiers: localIdentifiers)
for aCloudID in iCloudIDs {
//'Dictionary<String, Result<PHCloudIdentifier, Error>>.Element' (aka '(key: String, value: Result<PHCloudIdentifier, Error>)')
let cloudResult: Result = aCloudID.value
// Result is an enum .. not a tuple
switch cloudResult {
case .success(let success):
let newValue = success.stringValue
mappedIdentifiers.append(newValue)
case .failure(let failure):
// do error notify to user
let iCloudError = savePhotoError.otherSaveError // need to notify user
}
}
return mappedIdentifiers
}
func cloudId2LocalId(assetCloudIdentifiers: [PHCloudIdentifier]) -> [String] {
// patterned error handling per documentation
var localIDs = [String]()
let localIdentifiers: [PHCloudIdentifier: Result<String, Error>]
= PHPhotoLibrary
.shared()
.localIdentifierMappings(
for: assetCloudIdentifiers)
for cloudIdentifier in assetCloudIdentifiers {
guard let identifierMapping = localIdentifiers[cloudIdentifier] else {
print("Failed to find a mapping for \(cloudIdentifier).")
continue
}
switch identifierMapping {
case .success(let success):
localIDs.append(success)
case .failure(let failure) :
let thisError = failure as? PHPhotosError
switch thisError?.code {
case .identifierNotFound:
// Skip the missing or deleted assets.
print("Failed to find the local identifier for \(cloudIdentifier). \(String(describing: thisError?.localizedDescription)))")
case .multipleIdentifiersFound:
// Prompt the user to resolve the cloud identifier that matched multiple assets.
default:
print("Encountered an unexpected error looking up the local identifier for \(cloudIdentifier). \(String(describing: thisError?.localizedDescription))")
}
}
}
return localIDs
}

Contextual closure type ... expects 2 arguments, but 3 were used in closure body

Dear Advanced Programmers,
Could you please assist a fairly new programmer into editing this code. Seems that the new version of Xcode does not support the below code and displays the error:
**"Contextual closure type '(Directions.Session, Result<RouteResponse, DirectionsError>) -> Void' (aka '((options: DirectionsOptions, credentials: DirectionsCredentials), Result<RouteResponse, DirectionsError>) -> ()') expects 2 arguments, but 3 were used in closure body"**
The code is copied directly from the mapbox documentation website. Any form of assistance would be appreciated. Thanks in advance.
func getRoute(from origin: CLLocationCoordinate2D,
to destination: MGLPointFeature) -> [CLLocationCoordinate2D]{
var routeCoordinates : [CLLocationCoordinate2D] = []
let originWaypoint = Waypoint(coordinate: origin)
let destinationWaypoint = Waypoint(coordinate: destination.coordinate)
let options = RouteOptions(waypoints: [originWaypoint, destinationWaypoint], profileIdentifier: .automobileAvoidingTraffic)
_ = Directions.shared.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
guard let route = routes?.first else { return }
routeCoordinates = route.coordinates!
self.featuresWithRoute[self.getKeyForFeature(feature: destination)] = (destination, routeCoordinates)
}
return routeCoordinates
}
Please, developers, learn to read error messages and/or the documentation.
The error clearly says that the type of the closure is (Directions.Session, Result<RouteResponse, DirectionsError>) -> Void (2 parameters) which represents a session (a tuple) and a custom Result type containing the response and the potential error.
You have to write something like
_ = Directions.shared.calculate(options) { (session, result) in
switch result {
case .failure(let error): print(error)
case .success(let response):
guard let route = response.routes?.first else { return }
routeCoordinates = route.coordinates!
self.featuresWithRoute[self.getKeyForFeature(feature: destination)] = (destination, routeCoordinates)
}
}
Apart from the issue it's impossible to return something from an asynchronous task, you have to add a completion handler for example
func getRoute(from origin: CLLocationCoordinate2D,
to destination: MGLPointFeature,
completion: #escaping ([CLLocationCoordinate2D]) -> Void) {
and call completion(route.coordinates!) at the end of the success case inside the closure

How can I remove the warning "Cannot match several associated values at once"

I have a helper method in my unit tests:
func expect(_ sut: CompanyStore, toRetrieve expectedResult: RetrieveCacheResult, when action: #escaping (() -> Void), file: StaticString = #file, line: UInt = #line) {
let exp = expectation(description: "await completion")
sut.retrieve { retrievedResult in
switch (expectedResult, retrievedResult) {
case (.empty, .empty), (.failure, .failure):
break
case let (.found(retrieved), .found(expected)):
XCTAssertEqual(retrieved.item, expected.item, file: file, line: line)
XCTAssertEqual(retrieved.timestamp, expected.timestamp, file: file, line: line)
default:
XCTFail("Expected to retrieve \(expectedResult), got \(retrievedResult) instead", file: file, line: line)
}
exp.fulfill()
}
action()
wait(for: [exp], timeout: 1.0)
}
It allows me to create tests such as:
func test_retrieve_delivers_empty_on_empty_cache() {
let sut = makeSUT()
expect(sut, toRetrieve: .empty, when: {
// some action to perform
})
}
Since upgrading to Swift 5.1 I am getting the following warning:
Cannot match several associated values at once, implicitly tupling the
associated values and trying to match that instead
On the following line
case let (.found(retrieved), .found(expected)):
These values are a tuple (item: LocalCompany, timestamp: Date)
I haven't been able to work out how to clear this warning.
Edit:
.found is an enum:
public enum RetrieveCacheResult {
case empty
case found(item: LocalCompany, timestamp: Date)
case failure(Error)
}
You have two options:
Redefine the associated value to be a tuple. 🤮
case found( (item: Int, timestamp: Bool) )
Extract all the members individually. 🤮
case let (
.found(retrievedItem, retrievedTimestamp),
.found(expectedItem, expectedTimestamp)
):
Example you have this : case todo(shoppingCount: Int, eating: Bool)
In usage:
case .todo(let shoppingCount, _):
print("Shopping count = ", shoppingCount)
case .todo(_, let eating):
print("Eating = ", eating)

Accessing values in swift enums

I am having trouble figuring out the syntax for accessing the raw value of an enum. The code below should clarify what I am trying to do.
enum Result<T, U> {
case Success(T)
case Failure(U)
}
struct MyError: ErrorType {
var status: Int = 0
var message: String = "An undefined error has occured."
init(status: Int, message: String) {
self.status = status
self.message = message
}
}
let goodResult: Result<String, MyError> = .Success("Some example data")
let badResult: Result<String, MyError> = .Failure(MyError(status: 401, message: "Unauthorized"))
var a: String = goodResult //<--- How do I get the string out of here?
var b: MyError = badResult //<--- How do I get the error out of here?
You can make it without switch like this:
if case .Success(let str) = goodResult {
a = str
}
It's not the prettiest way, but playing around in a playground I was able to extract that value using a switch and case:
switch goodResult {
case let .Success(val):
print(val)
case let .Failure(err):
print(err.message)
}
EDIT:
A prettier way would be to write a method for the enum that returns a tuple of optional T and U types:
enum Result<T, U> {
case Success(T)
case Failure(U)
func value() -> (T?, U?) {
switch self {
case let .Success(value):
return (value, nil)
case let .Failure(value):
return (nil, value)
}
}
}

Convert array of custom object to AnyObject in Swift

In my app I am doing something like this:
struct Record {
var exampleData : String
}
class ExampleClass : UIViewController {
let records = [Record]()
override func viewDidLoad() {
super.viewDidLoad()
let data = NSKeyedArchiver.archivedDataWithRootObject(self.records) as! NSData
}
...
}
But in the last line of viewDidLoad() I got this error:
Argument type '[Record]' does not conform to expected type 'AnyObject'
How can I fix this? Thanks.
If you want to keep struct, you can encode data using withUnsafePointer(). Here's an example, which I adapted from this Gist:
import UIKit
enum EncodingStructError: ErrorType {
case InvalidSize
}
func encode<T>(var value: T) -> NSData {
return withUnsafePointer(&value) { p in
NSData(bytes: p, length: sizeofValue(value))
}
}
func decode<T>(data: NSData) throws -> T {
guard data.length == sizeof(T) else {
throw EncodingStructError.InvalidSize
}
let pointer = UnsafeMutablePointer<T>.alloc(1)
data.getBytes(pointer, length: data.length)
return pointer.move()
}
enum Result<T> {
case Success(T)
case Failure
}
I added some error handling and marked the method as throws. Here's one way you can use it, in a do…catch block:
var res: Result<String> = .Success("yeah")
var data = encode(res)
do {
var decoded: Result<String> = try decode(data)
switch decoded {
case .Failure:
"failure"
case .Success(let v):
"success: \(v)" // => "success: yeah"
}
} catch {
print(error)
}
The error handling I added will not decode if the NSData length doesn't match the type size. This can commonly happen if you write the data to disk, the user updates to a newer version of the app with a different-sized version of the same type, and then the data is read in.
Also note that sizeof() and sizeofValue() may return different values on different devices, so this isn't a great solution for sending data between devices (NSJSONSerialization might be better for that).
AnyObject means any reference type object, primarily a class. A struct is a value type and cannot be passed to a function needing an AnyObject. Any can be used to accept value types as well as reference types. To fix your code above, change struct Record to class Record. But I have a feeling you may want to use a struct for other reasons. You can create a class wrapper around Record that you can convert to and from to use for functions that need an AnyObject.
I did a similar thing:
static func encode<T>(value: T) -> NSData {
var val = value
return withUnsafePointer(to: &val) { pointer in
NSData(bytes: pointer, length: MemoryLayout.size(ofValue: val))
}
}
static func decode<T>(data: NSData) -> T {
guard data.length == MemoryLayout<T>.size.self else {
fatalError("[Credential] fatal unarchiving error.")
}
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
data.getBytes(pointer, length: data.length)
return pointer.move()
}

Resources