19

Note: This is basically the same question as another one I've posted on Stackoverflow yesterday. However, I figured that I used a poor example in that question that didn't quite boil it down to the essence of what I had in mind. As all replies to that original post refer to that first question I thought it might be a better idea to put the new example in a separate question — no duplication intended.


Model Game Characters That Can Move

Let's define an enum of directions for use in a simple game:

enum Direction {
    case up
    case down
    case left
    case right
}

Now in the game I need two kinds of characters:

  • A HorizontalMover that can only move left and right.
  • A VerticalMover that can only move up and down.

They can both move so they both implement the

protocol Movable {
    func move(direction: Direction)
}

So let's define the two structs:

struct HorizontalMover: Movable {
    func move(direction: Direction)
    let allowedDirections: [Direction] = [.left, .right]
}

struct VerticalMover: Movable {
    func move(direction: Direction)
    let allowedDirections: [Direction] = [.up, .down]
}

The Problem

... with this approach is that I can still pass disallowed values to the move() function, e.g. the following call would be valid:

let horizontalMover = HorizontalMover()
horizontalMover.move(up) // ⚡️

Of course I can check inside the move() funtion whether the passed direction is allowed for this Mover type and throw an error otherwise. But as I do have the information which cases are allowed at compile time I also want the check to happen at compile time.

So what I really want is this:

struct HorizontalMover: Movable {
    func move(direction: HorizontalDirection)
}

struct VerticalMover: Movable {
    func move(direction: VerticalDirection)
}

where HorizontalDirection and VerticalDirection are subset-enums of the Direction enum.

It doesn't make much sense to just define the two direction types independently like this, without any common "ancestor":

enum HorizontalDirection {
    case left
    case right
}

enum VerticalDirection {
    case up
    case down
}

because then I'd have to redefine the same cases over and over again which are semantically the same for each enum that represents directions. E.g. if I add another character that can move in any direction, I'd have to implement the general direction enum as well (as shown above). Then I'd have a left case in the HorizontalDirection enum and a left case in the general Direction enum that don't know about each other which is not only ugly but becomes a real problem when assigning and making use of raw values that I would have to reassign in each enumeration.


So is there a way out of this?

Can I define an enum as a subset of the cases of another enum like this?

enum HorizontalDirection: Direction {
    allowedCases:
        .left
        .right
}
Community
  • 1
  • 1
Mischa
  • 15,816
  • 8
  • 59
  • 117
  • 1
    Related: [Can an enum contain another enum values in Swift?](http://stackoverflow.com/questions/40665509/can-an-enum-contain-another-enum-values-in-swift) – Hamish Nov 20 '16 at 11:19

4 Answers4

4

You probably solved your issue, but to anyone looking for an answer, for some time now (not sure when Apple introduced it) you can use associated values inside enum cases to model these kinds of states.

enum VerticalDirection {
    case up
    case down
}

enum HorizontalDirection {
    case left
    case right
}

enum Direction {
    case vertical(direction: VerticalDirection)
    case horizontal(direction: HorizontalDirection)
}

So you can use a method like this:

func move(_ direction: Direction) {
    print(direction)
}

move(.horizontal(.left))

And if you conform to Equatable protocol:

extension Direction: Equatable {
    static func ==(lhs: Direction, rhs: Direction) -> Bool {
        switch (lhs, rhs) {
        case (.vertical(let lVertical), .vertical(let rVertical)):
            switch (lVertical, rVertical) {
            case (.up, .up):
                return true
            case (.down, .down):
                return true
            default:
                return false
            }
        case (.horizontal(let lHorizontal), .horizontal(let rHorizontal)):
            switch (lHorizontal, rHorizontal) {
            case (.left, .left):
                return true
            case (.right, .right):
                return true
            default:
                return false
            }
        default:
            return false
        }
    }
}

you can do something like this:

func isMovingLeft(direction: Direction) -> Bool {
    return direction == .horizontal(.left)
}

let characterDirection: Direction = .horizontal(.left)
isMovingLeft(direction: characterDirection) // true
isMovingLeft(direction: characterDirection) // false
ejovrh
  • 189
  • 1
  • 5
3

No. This is currently not possible with Swift enums.

The solutions I can think of:

  • Use protocols as I outlined in your other question
  • Fallback to a runtime check
naglerrr
  • 2,809
  • 1
  • 12
  • 24
2

Here's a possible compile-time solution:

enum Direction: ExpressibleByStringLiteral {

  case unknown

  case left
  case right
  case up
  case down

  public init(stringLiteral value: String) {
    switch value {
    case "left": self = .left
    case "right": self = .right
    case "up": self = .up
    case "down": self = .down
    default: self = .unknown
    }
  }

  public init(extendedGraphemeClusterLiteral value: String) {
    self.init(stringLiteral: value)
  }

  public init(unicodeScalarLiteral value: String) {
    self.init(stringLiteral: value)
  }
}

enum HorizontalDirection: Direction {
  case left = "left"
  case right = "right"
}

enum VerticalDirection: Direction {
  case up = "up"
  case down = "down"
}

Now we can define a move method like this:

func move(_ allowedDirection: HorizontalDirection) {
  let direction = allowedDirection.rawValue
  print(direction)
}

The drawback of this approach is that you need to make sure that the strings in your individual enums are correct, which is potentially error-prone. I have intentionally used ExpressibleByStringLiteral for this reason, rather than ExpressibleByIntegerLiteral because it is more readable and maintainable in my opinion - you may disagree.

You also need to define all 3 of those initializers, which is perhaps a bit unwieldy, but you would avoid that if you used ExpressibleByIntegerLiteral instead.

I'm aware that you're trading compile-time safety in one place for another, but I suppose this kind of solution might be preferable in some situations.

To make sure that you don't have any mistyped strings, you could also add a simple unit test, like this:

XCTAssertEqual(Direction.left, HorizontalDirection.left.rawValue)
XCTAssertEqual(Direction.right, HorizontalDirection.right.rawValue)
XCTAssertEqual(Direction.up, VerticalDirection.up.rawValue)
XCTAssertEqual(Direction.down, VerticalDirection.down.rawValue)
ganzogo
  • 2,516
  • 24
  • 36
0

Use Swift protocol OptionSet

struct Direction: OptionSet {
let rawValue: int
static let up = Direction(rawValue: 1<<0)
static let right = Direction(rawValue: 1<<1)
static let down = Direction(rawValue: 1<<2)
static let left = Direction(rawValue: 1<<3)
static let horizontal = [.left, .right]
static let vertical = [.up, down]
}
Nagaraj
  • 23
  • 1
  • 5