anorskdev
anorskdev

Reputation: 1925

How to reduce Swift enum conversion code

I have a variety of enums such as below.

enum PaperOrientation : Int { case portrait, landscape }
enum MetricType : Int { case inches, metric }

I made the enums of type Int, so that the values of instances could be saved as numbers with CoreData.

When retrieving the values from CoreData to use in the program, I end up with very similar conversion routines, like those shown below.

Typically, I want some default value - such as for the case where it is a new enum for the latest version of the program, and a value for that variable may not actually have been saved in CoreData. For example, the MetricType was added for the second rev of the program. Retrieving a paper created in rev 1 will not have a metric value saved. For the nil value, I want to use a default value the paper was originally assumed to have.

class ConversionRoutine {

    class func orientationFor(_ num: NSNumber?) -> PaperOrientation {
        if let iVal = num?.intValue {
            if let val = PaperOrientation(rawValue: iVal) {
                return val
            }
        }
        return PaperOrientation(rawValue: 0)!
    }

    class func metricTypeFor(_ num: NSNumber?) -> MetricType {
        if let iVal = num?.intValue {
            if let val = MetricType(rawValue: iVal) {
                return val
            }
        }
        return MetricType(rawValue: 0)!
    }
}

Is there a way to reduce the redundancy?

I present a way below that works pretty well. But welcome more refinements or improvements.

Upvotes: 1

Views: 541

Answers (2)

anorskdev
anorskdev

Reputation: 1925

The Swift 4 example below uses a Defaultable protocol based on RawRepresentable. The first step is creating a defaultValue that can be used when the initializer fails. Note that the Defaultable protocol is not limited to Int enums. A String enum could also use it.

protocol Defaultable : RawRepresentable {
    static var defaultValue : Self { get }
}

protocol IntDefaultable : Defaultable where RawValue == Int {
}

extension IntDefaultable {
    static func value(for intValue : Int) -> Self {
        return Self.init(rawValue: intValue) ?? Self.defaultValue
    }

    static func value(for num : NSNumber?) -> Self {
        if let iVal = num?.intValue {
            return self.value(for: iVal)
        }
        return Self.defaultValue
    }
}

After the Defaultable protocol is defined, I can create an IntDefaultable protocol that will be used for Int enums.

In an extension to IntDefaultable, I can create the generic code to handle the conversion. First, I create a function that takes an Int. Then I create a function that takes an NSNumber optional.

Next, look at how one of the enums is built:

enum MetricType : Int, Codable, IntDefaultable { case inches, metric
    static var defaultValue: MetricType = .inches
}

I also decided to declare the enum Codable, which may be useful. When I add the IntDefaultable protocol, it becomes fairly easy to add the defaultValue line of code with code completion - go to the new line and type “def”-tab, then “ = .”, and then choose one of the values from the popup. Note that often I want to pick the first enum value, but the default value could be any one.

The last thing is calling the conversion routine for getting a value from CoreData

let units = MetricType.value(for: self.metricType)  // where self.metricType is the NSManagedObject member.

Upvotes: 3

Ryan
Ryan

Reputation: 4884

You can add an initializer in enum.

enum PaperOrientation : Int { 
    case portrait, landscape

    init(number: NSNumber) {
        self = PaperOrientation(rawValue: number.intValue) ?? .portrait
    }
}

Upvotes: 1

Related Questions