0

I define all custom Errors using enum(s), like:

public enum MyErrorEnum: String, LocalizedError {
    case FileNotFound = "Failed to find file."

    public var errorDescription: String? { rawValue }
}

But some errors require additional context, like adding file-path to the message.

Unfortunately, because Swift enums don't support instance variables, I tried workarounds, like:

private var KEY_CONTEXT: UInt8 = 0;

public enum MyErrorEnum: String, LocalizedError {
    case FileNotFound = "Failed to find file."

    public var errorDescription: String? { rawValue }

    public var context: String {
        return objc_getAssociatedObject(self as NSObject, &KEY_CONTEXT)
            as? String ?? "";
    }

    @discardableResult
    public mutating func withContext(_ value: String) -> Self {
        objc_setAssociatedObject(
            self as NSObject, &KEY_CONTEXT, value as NSString,
            .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return self;
    }
}

Note that above does not raise any compile and/or runtime error in Xcode 12, but just did not work.

And later, I was notified that Xcode 13 raises compile error below: "Cannot convert value of type 'MyErrorEnum' to type 'NSObject' in coercion"

I also already tried simply changing String to a custom StringWithContext class which implements ExpressibleByStringLiteral, but NOT even that did work (probably because enum's rawValue is somehow change protected).

Is there any way to add additional variable to said enum?

Top-Master
  • 7,611
  • 5
  • 39
  • 71
  • Now with a little more context than your previous question, it doesn't look like enums are suitable for this use case at all. Use a struct with two properties - `context` and `errorType`, where `errorType` is a simple enum. If each case is associated with different types of contexts however, consider using [associated values](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html#ID148). – Sweeper Dec 21 '21 at 18:36
  • @Sweeper the problem is clean throwing and catching without rethrow and copy/paste need. **Firstly,** I want messages without copy/pasting (`enum` is pretty unique without copy/pasting), but somehow need to have variable context/details. **Secondly,** I want to be able to `catch` each `case` separately, NOT catching entire `struct` then doing `switch` inside the `catch`, and want to avoid forgeting to rethrow cases we don't handle. – Top-Master Dec 21 '21 at 19:21

1 Answers1

-1

No, currently there is no way to add instance variable to enum directly in Swift.

(in Java that's trivial, as there enum is yet another class).

However, if you insist on using Swift's enum, use associative-enum with default value as message.

Example

Simply, copy and add LocalizedErrorEnum from below into your project once, and reuse as many times as required with associative-enums.

public protocol LocalizedErrorEnum: LocalizedError {
    var errorDescription: String? { get }
}

extension LocalizedErrorEnum {
    public var errorDescription: String? {
        if let current = Mirror(reflecting: self).children.first {
            let mirror = Mirror(reflecting: current.value);
            // Initial error description.
            let message = mirror.children.first?.value as? String
                ?? current.label ?? "Unknown-case";
            var context = "";
            // Iterate additional context.
            var i = 0;
            for associated in mirror.children {
                if i >= 1 {
                    if let text = associated.value as? String {
                        context += "\n  ";
                        if let label: String = associated.label {
                            context += "\(label): "
                        }
                        context += text;
                    }
                }
                i += 1;
            }
            return context.isEmpty ? message : (
                message + " {" + context + "\n}"
            );
        }
        return "\(self)";
    }
}

Usage:

public enum MyErrorEnum: LocalizedErrorEnum {
    case FileNotFound(String = "Failed to find file.", file: String)
}

// ...

do {
    let path = "/path/to/file.txt";
    throw MyErrorEnum.FileNotFound(
        file: path
    );
} catch {
    print(error.localizedDescription);
}

Output:

Failed to find file. {
  file: /path/to/file.txt
}

See also: Simplest way to throw custom error in Swift?

Top-Master
  • 7,611
  • 5
  • 39
  • 71