10

I am running this code in both xcode 9.3 and xcode 10 beta 3 playground

import Foundation

public protocol EnumCollection: Hashable {
    static func cases() -> AnySequence<Self>
}

public extension EnumCollection {

    public static func cases() -> AnySequence<Self> {
        return AnySequence { () -> AnyIterator<Self> in
            var raw = 0
            return AnyIterator {
                let current: Self = withUnsafePointer(to: &raw) { $0.withMemoryRebound(to: self, capacity: 1) { $0.pointee } }

                guard current.hashValue == raw else {
                    return nil
                }

                raw += 1
                return current
            }
        }
    }
}

enum NumberEnum: EnumCollection{
    case one, two, three, four
}

Array(NumberEnum.cases()).count

even though both are using swift 4.1 they are giving me different results for the

on xcode 9.3 the size of array is 4

and on xcode 10 beta 3 the size of array is 0

I don't understand this at all.

kr15hna
  • 529
  • 1
  • 5
  • 12

3 Answers3

18

That is an undocumented way to get a sequence of all enumeration values, and worked only by chance with earlier Swift versions. It relies on the hash values of the enumeration values being consecutive integers, starting at zero.

That definitely does not work anymore with Swift 4.2 (even if running in Swift 4 compatibility mode) because hash values are now always randomized, see SE-0206 Hashable Enhancements:

To make hash values less predictable, the standard hash function uses a per-execution random seed by default.

You can verify that with

print(NumberEnum.one.hashValue)
print(NumberEnum.two.hashValue)

which does not print 0 and 1 with Xcode 10, but some other values which also vary with each program run.

For a proper Swift 4.2/Xcode 10 solution, see How to enumerate an enum with String type?:

extension NumberEnum: CaseIterable  { }
print(Array(NumberEnum.allCases).count) // 4
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • thanks for the correction. just for my understanding so the logic hash function is in swift right? how does it change with xcode version? – kr15hna Jul 06 '18 at 08:21
  • @kr15hna: It is part of the Swift standard library or the Swift runtime, and both come with the Xcode application. – Martin R Jul 06 '18 at 08:27
  • so even if I run 4.1 in xcode 10 it isn't working, so this 4.1 is not the same as xcode 9 4.1? – kr15hna Jul 06 '18 at 08:42
  • 1
    @kr15hna: No. Xcode 10 comes with Swift 4.2, and that has *compatibility modes* for Swift 3 and 4. – Martin R Jul 06 '18 at 09:06
2

The solution for this is below for Xcode 10 and Swift 4.2 and above.

Step 1: Create Protocol EnumIterable.

protocol EnumIterable: RawRepresentable, CaseIterable {
    var indexValue: Int { get }
}

extension EnumIterable where Self.RawValue: Equatable {
    var indexValue: Int {
        var index = -1
        let cases = Self.allCases as? [Self] ?? []
        for (caseIndex, caseItem) in cases.enumerated() {
            if caseItem.rawValue == self.rawValue {
                index = caseIndex
                break
            }
        }
        return index
    }
}

Step 2: Extend EnumIterator Protocol to your enums.

enum Colors: String, EnumIterable {
    case red = "Red"
    case yellow = "Yellow"
    case blue = "Blue"
    case green = "Green"
}

Step 3: Use indexValue property like using hashValue.

Colors.red.indexValue
Colors.yellow.indexValue
Colors.blue.indexValue
Colors.green.indexValue

Sample Print statement and Output

print("Index Value: \(Colors.red.indexValue), Raw Value: \(Colors.red.rawValue), Hash Value: \(Colors.red.hashValue)")

Output: "Index Value: 0, Raw Value: Red, Hash Value: 1593214705812839748"

print("Index Value: \(Colors.yellow.indexValue), Raw Value: \(Colors.yellow.rawValue), Hash Value: \(Colors.yellow.hashValue)")

Output: "Index Value: 1, Raw Value: Yellow, Hash Value: -6836447220368660818"

print("Index Value: \(Colors.blue.indexValue), Raw Value: \(Colors.blue.rawValue), Hash Value: \(Colors.blue.hashValue)")

Output: "Index Value: 2, Raw Value: Blue, Hash Value: -8548080225654293616"

print("Index Value: \(Colors.green.indexValue), Raw Value: \(Colors.green.rawValue), Hash Value: \(Colors.green.hashValue)") 

Output: "Index Value: 3, Raw Value: Green, Hash Value: 6055121617320138804"

0

If you use hashValue of an enum to determine case values (position or id), it's a wrong approach since it doesn't guarantee to return sequential int values, 0,1,2... It is not working anymore from swift 4.2

For example, If you use an enum like this :

enum AlertResultType {
    case ok, cancel 
}

hashValue of this enum might return a large int value instead of 0 (ok) and 1 (cancel).

So you may need to declare the enum type more precisely and use rowValue. For example

enum AlertResultType : Int {
    case ok = 0, cancel = 1
}
Abhijith
  • 3,094
  • 1
  • 33
  • 36