Enum with instance-variable? (in Swift)

1k Views Asked by At

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?

1

There are 1 best solutions below

0
Top-Master On

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?