Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Increment through Enum

Tags:

enums

swift

I love that Swift allows for the use of enum methods. I'm trying to work with a method but am looking for a more extensible method of doing this:

enum CopyState{
    case binary, hex, both
    init(){
        self = .both
    }
    mutating func next() {
        if self == .binary{
            self = .hex
        } else if self == .hex {
            self = .both
        } else if self == .both{
            self = .binary
        }
    }
}

var state = CopyState()

state.next()

I'd like to essentially cast the enum to an integer and increment it modulo the number of total enum options

Adding or removing enum options is a hassle (I'm using a last() and a next() method).

like image 831
Jim from Princeton Avatar asked Feb 04 '18 19:02

Jim from Princeton


People also ask

How to iterate through enum in Swift?

You can iterate over all of the possible cases of an enumeration in Swift by making it conform to the CaseIterable protocol. When using CaseIterable, you can access a collection of all enum cases by using a allCases property and then iterate over it as an array.

Can we loop an enum?

Enumeration (enum) in Java is a datatype which stores a set of constant values. You can use enumerations to store fixed values such as days in a week, months in a year etc. You can iterate the contents of an enumeration using for loop, using forEach loop and, using java.

How do I use enum in Swift 5?

To define an enum in Swift, use the keyword enum followed by the name of the enum. The name of an enum in Swift should follow the PascalCase naming convention in which the first letter of each word in a compound word is capitalized.


2 Answers

Update Starting with Swift 4.2 you can make use of the newly added support CaseIterable protocol, which adds compiler support for generating a list of all cases for an enum. Though @ninestones's comment pointed put that we are not guaranteed for allCases to return the cases in the same order as defined, the synthesized implementation does this, and it's unlikely that definition will change.

Your enum could then look something like this (no more hardcoded start value):

enum CopyState: CaseIterable {
    case binary, hex, both

    mutating func next() {
        let allCases = type(of: self).allCases
        self = allCases[(allCases.index(of: self)! + 1) % allCases.count]
    }
}

You can make this piece of functionality available to all CaseIterable enums:

extension CaseIterable where Self: Equatable {
    mutating func next() {
        let allCases = Self.allCases
        // just a sanity check, as the possibility of a enum case to not be
        // present in `allCases` is quite low
        guard let selfIndex = allCases.index(of: self) else { return }
        let nextIndex = Self.allCases.index(after: selfIndex)
        self = allCases[nextIndex == allCases.endIndex ? allCases.startIndex : nextIndex]
    }
}

enum CopyState: CaseIterable {
    case binary, hex, both
}

var state = CopyState.hex
state.next()
print(state) // both
state.next()
print(state) // binary

Or, a little bit more verbose, but with a better separation of concerns:

extension Collection {
    // adding support for computing indexes in a circular fashion
    func circularIndex(after i: Index) -> Index {
        let nextIndex = index(after: i)
        return nextIndex == endIndex ? startIndex : nextIndex
    }
}

extension Collection where Element: Equatable {
    // adding support for retrieving the next element in a circular fashion
    func circularElement(after element: Element) -> Element? {
        return index(of: element).map { self[circularIndex(after: $0)] }
    }
}

// Protocol to allow iterating in place (similar to a type conforming to both Sequence and IteratorProtocol)
protocol InPlaceIterable {
    mutating func next()
}

extension InPlaceIterable where Self: CaseIterable, Self: Equatable {
    // adding default implementation for enums
    mutating func next() {
        self = type(of: self).allCases.circularElement(after: self)!
    }
}

// now the enums need only the protocol conformances, they get the
// functionalities for free
enum CopyState: CaseIterable, InPlaceIterable {
    case binary, hex, both
}

You could use Int as raw value for your enum (note that this is also the default raw value if you don't specify it), and use it like this:

enum CopyState: Int {
    case binary, hex, both

    mutating func next(){
        self = CopyState(rawValue: rawValue + 1) ?? .binary
    }
}

var state = CopyState.hex
state.next()
print(state) // both
state.next()
print(state) // binary

This works fine as long as you have the raw values of the enum cases in consecutive order. By default the compiler assigns consecutive raw values.

You'd also need to keep in mind to update the next() method if the first case changes, otherwise it will no longer correctly work.

An alternative to the above limitation, suggested by @MartinR, is to force unwrap the raw value zero:

mutating func next(){
    self = CopyState(rawValue: rawValue + 1) ?? CopyState(rawValue: 0)!
}

The above code won't require updating the method when the first enum case changes, however it has the potential of crashing the app if the starting raw value of the enum changes.

like image 153
Cristik Avatar answered Nov 12 '22 08:11

Cristik


Swift doc says

When you’re working with enumerations that store integer or string raw values, you don’t have to explicitly assign a raw value for each case. When you don’t, Swift automatically assigns the values for you.

For example, when integers are used for raw values, the implicit value for each case is one more than the previous case. If the first case doesn’t have a value set, its value is 0.

So this is safe (Swift5)

enum CopyState: Int {
    case binary, hex, both

    mutating func next(){
        self = CopyState(rawValue: rawValue + 1) ?? CopyState(rawValue: 0)!
    }
}
like image 36
Andy Dent Avatar answered Nov 12 '22 07:11

Andy Dent