21

I have an object FormField which has two properties: a String name, and a value which can accept any type--hence I've made it Any!. However, I've been told in a separate question to use an enum with associated values instead of Any!.

enum Value {
    case Text(String!)
    case CoreDataObject(NSManagedObject!)
}

class FormField {
    var name: String
    var value: Value?
    // initializers...
}

This approach makes it awfully verbose to check for nullity however. If I wanted to display an alert view for all the missing fields in the form, I'll have to repeat a nil check for every case in a switch statement:

for field in self.fields {
    if let value = field.value {
        switch value {
        case .Text(let text):
            if text == nil {
                missingFields.append(field.name)
            }
        case .CoreDataObject(let object):
            if object == nil {
                missingFields.append(field.name)
            }
        }
    }
}

Is there a shorter way of accessing the enum's associated value, regardless of the type? If I make FormField.value an Any! the above code would be as easy as:

for field in self.fields {
    if field.value == nil {
        missingFields.append(field.name)
    }
}
Community
  • 1
  • 1
Matthew Quiros
  • 13,385
  • 12
  • 87
  • 132
  • 1
    You can remove a few lines by using a `where` clause in your `case` statements: `case .Text(let text) where text == nil:`. – Mike S Sep 17 '14 at 01:38
  • @MikeS Great tip, thanks. That way I can have a `where` clause for every case that can be nil and let it fallthrough. I'm going back with `Any!` though. Making it an enum requires me to write code that's too verbose. – Matthew Quiros Sep 17 '14 at 01:49
  • Isn't that just moving the verbosity around though? I'd presume that at some point you'll have to know the actual type of those `Any`s, and then you'll be doing a bunch of `if value is String`, `if value is NSManagedObject`, etc. – Mike S Sep 17 '14 at 02:05
  • 1
    Also, `value` is already an optional in your `FormField` class. If you're only setting the `FormField`'s `value` when there is one, you can already just just check `if field.value == nil`. That means you also don't need to make your `enum`'s associated values implicitly unwrapped optionals. – Mike S Sep 17 '14 at 02:21
  • @MikeS Unless I make the associated values an optional, I can't compare them to nil. The compiler complains about how the associated value's type doesn't conform to `NilLiteralConvertible`. – Matthew Quiros Sep 17 '14 at 03:03
  • @MikeS And true, `Any!` just moves the verbosity around but with way less lines of code. I don't have to check `if value is String` or `if value is NSManagedObject` but I just downcast them when assigning to the corresponding parameter; eg `expense.description = fields[0].value as String` then `expense.category = fields[1].value as SPRCategory` etc. – Matthew Quiros Sep 17 '14 at 03:06
  • What does it mean to be `.Text(nil)` in your data model? Why not just be `.Nothing`? Since `FormField.value()` is already optional, why do you need another layer of "emptiness" at all? If they really can be empty, it is much better to use an explicit optional (`?`) rather than an implicit one (`!`). A lot of what you're doing here is fighting the type-safety, which is going to cause you to have to check things constantly. Often a simpler data model will give you the conciseness you want. – Rob Napier Sep 17 '14 at 14:45

3 Answers3

11

Define a method isMissing() inside the enum - write it once and only once. Then you get nearly exactly what you prefer:

for field in self.fields {
    if field.value.isMissing() {
        missingFields.append(field.name)
    }
}

It would look something like this (from the Swift Interpreter):

  1> class Foo {}
   >
  2> enum Value { 
  3.     case One(Foo!) 
  4.     case Two(Foo!) 
  5.      
  6.     func isMissing () -> Bool { 
  7.         switch self { 
  8.         case let .One(foo): return foo == nil 
  9.         case let .Two(foo): return foo == nil 
 10.         } 
 11.     } 
 12. }    
 13> let aVal = Value.One(nil)
aVal: Value = One {
  One = nil
}
 14> aVal.isMissing()
$R0: Bool = true
GoZoner
  • 67,920
  • 20
  • 95
  • 145
  • Great idea! Might be I'll use this in some other situation. Right now, for cases where the associated value isn't nil, I can't pinpoint which `FormField` object it is an associated value for if I have a form that has multiple `UITextField`s (i.e., all of them will hold values of type `.Text(String!)` and accessing the `String!` alone doesn't tell me which field it's for). – Matthew Quiros Sep 17 '14 at 02:01
  • @Cristik Hm... I think at that time I wanted to have some reflection API (e.g. as seen here https://medium.com/@swiftthesorrow/reflection-in-swift-958824116b07) – Klaas Feb 25 '19 at 14:12
7

With Swift 2 it's possible to get the associated value using reflection.

To make that easier just add the code below to your project and extend your enum with the EVAssociated protocol.

    public protocol EVAssociated {
    }

    public extension EVAssociated {
        public var associated: (label:String, value: Any?) {
            get {
                let mirror = Mirror(reflecting: self)
                if let associated = mirror.children.first {
                    return (associated.label!, associated.value)
                }
                print("WARNING: Enum option of \(self) does not have an associated value")
                return ("\(self)", nil)
            }
        }
    }

Then you can access the .asociated value with code like this:

    class EVReflectionTests: XCTestCase {
            func testEnumAssociatedValues() {
                let parameters:[EVAssociated] = [usersParameters.number(19),
usersParameters.authors_only(false)]
            let y = WordPressRequestConvertible.MeLikes("XX", Dictionary(associated: parameters))
            // Now just extract the label and associated values from this enum
            let label = y.associated.label
            let (token, param) = y.associated.value as! (String, [String:Any]?)

            XCTAssertEqual("MeLikes", label, "The label of the enum should be MeLikes")
            XCTAssertEqual("XX", token, "The token associated value of the enum should be XX")
            XCTAssertEqual(19, param?["number"] as? Int, "The number param associated value of the enum should be 19")
            XCTAssertEqual(false, param?["authors_only"] as? Bool, "The authors_only param associated value of the enum should be false")

            print("\(label) = {token = \(token), params = \(param)")
        }
    }

    // See http://github.com/evermeer/EVWordPressAPI for a full functional usage of associated values
    enum WordPressRequestConvertible: EVAssociated {
        case Users(String, Dictionary<String, Any>?)
        case Suggest(String, Dictionary<String, Any>?)
        case Me(String, Dictionary<String, Any>?)
        case MeLikes(String, Dictionary<String, Any>?)
        case Shortcodes(String, Dictionary<String, Any>?)
    }

    public enum usersParameters: EVAssociated {
        case context(String)
        case http_envelope(Bool)
        case pretty(Bool)
        case meta(String)
        case fields(String)
        case callback(String)
        case number(Int)
        case offset(Int)
        case order(String)
        case order_by(String)
        case authors_only(Bool)
        case type(String)
    }

The code above is now available as a cocoapod susbspec at https://github.com/evermeer/Stuff#enum It also has an other nice enum extension for enumerating all enum values.

Edwin Vermeer
  • 13,017
  • 2
  • 34
  • 58
0

If the associated values were of the same type for all enum cases the following approach could help.

enum Value {
    case text(NSString!), two(NSString!), three(NSString!) // This could be any other type including AnyClass
}

// Emulating "fields" datastruct for demo purposes (as if we had struct with properties).
typealias Field = (en: Value, fieldName: String)
let fields: [Field] = [(.text(nil),"f1"), (.two(nil), "f2"), (.three("Hey"), "f3")] // this is analog of "fields"

let arrayOfFieldNamesWithEmptyEnums: [String] = fields.compactMap({
    switch $0.en {
    case let .text(foo), let .two(foo), let .three(foo): if foo == nil { return $0.fieldName } else { return nil }}
})
print("arrayOfFieldNamesWithEmptyEnums \(arrayOfFieldNamesWithEmptyEnums)")

Many other things can be obtained similarly.

let arrayOfEnumsWithoutValues: [Value] = fields.compactMap({
    switch $0.en {
    case let .text(foo), let .two(foo), let .three(foo): if foo == nil { return $0.en } else { return nil }}
})
print("arrayOfEnumsWithoutValues \(arrayOfEnumsWithoutValues)")

// just to check ourselves
if let index = arrayOfEnumsWithoutValues.index(where: { if case .two = $0 { return true }; return false }) {
    print(".two found at index \(index)")
}
Paul B
  • 3,989
  • 33
  • 46