Given an enumeration such as MPMusicShuffleMode, which has 4 cases, what's the best way *cycle* through them?

With x: Bool, one can changes states with x.toggle(). Is there a similar feature for Enums, such that one could (with mode: MPMusicShuffleMode) do something like mode.nextCase(), and have it keep going around?

1 Like

You may do smth like this:

protocol Toggleable: RawRepresentable where RawValue == Int {
    static var minRawValue: RawValue { get }
    static var maxRawValue: RawValue { get }
    mutating func toggle()
}

extension Toggleable {
    mutating func toggle() {
        let v = rawValue == Self.maxRawValue ? Self.minRawValue : rawValue + 1
        self = .init(rawValue: v)!
    }
}

extension MPMusicShuffleMode: Toggleable {
    static var minRawValue: RawValue { MPMusicShuffleMode.albums.rawValue }
    static var maxRawValue: RawValue { MPMusicShuffleMode.default.rawValue }
}

but I don't think it is a good idea: could be non integer raw values.. or gaps in values.. compiler won't warn you when the underlying enum got new cases or deprecated old cases.. or you want to be more flexible about the toggle order, you haven't won many lines of code (in fact if you use this just for one type you probably lost).

Do KISS:

extension MPMusicShuffleMode {
    var next: Self {
        switch self {
        case .default: return .off
        case .off: return .songs
        case .songs: return .albums
        case .albums: return .default
        @unknown default: fatalError("TODO: new case")
        }
    }
    mutating func toggle() {
        self = next
    }
}
1 Like

I like keeping it simple, thanks. I do wonder whether there is a more general way to do this. I messed around with this idea for a few minutes, but it's a weird one. I got to the compiler complaining that CaseIterable isn't Equatable and decided to go back to simple.

Thanks!

You wanted to autogenerate CaseIterable for this enum? via mirroring?

Yep, I usually found KISS the best approach.

protocol Cyclic: CaseIterable, Equatable {}
extension Cyclic {
  var next: Self {
    let all = Self.allCases
    let index = all.firstIndex(of: self)!
    let next = all.index(after: index)
    if next == all.endIndex {
      return all.first!
    } else {
      return all[next]
    }
  }
  mutating func cycle() {
    self = next
  }
}

This assumes the CaseIterable conformance corresponds to the intended order. Further optimizations are possible, but this is the simplest.

2 Likes

Lovely! Thank you so much, this gives me a path forward.

Unfortunately, the Enum in question is one translated from Obj.c, so no quick CaseIterable conformance to be had. I seem to recall someone pondering that problem, I shall go hunt for it.

Never the less, I have learned things her today, so I count that as a good day.

CaseIterable can be implemented manually even when the compiler cannot synthesize it. If you are worried about owning neither the type nor the protocol, then duplicate the protocol requirements without inheriting from it:

protocol Cyclic: Equatable {
  associatedtype AllCases: Collection where AllCases.Element == Self
  static var allCases: AllCases { get }
}
extension Cyclic {
  var next: Self {
    let all = Self.allCases
    let index = all.firstIndex(of: self)!
    let next = all.index(after: index)
    if next == all.endIndex {
      return all.first!
    } else {
      return all[next]
    }
  }
  mutating func cycle() {
    self = next
  }
}

enum MyEnumeration: CaseIterable, Cyclic { // ← with synthesis
  case a, b, c, d
}
extension EnumerationFromSomeoneElse: Cyclic { // ← without synthesis
  static var allCases: [EnumerationFromSomeoneElse] {
    return [.α, .β, .γ, .δ]
  }
}
1 Like

This is what I'll probably need to do with MPMusicShuffleMode, yep.

The issues are still present:

  • next Xcode version might introduce a new case but compiler doesn't alert you about that.
  • OS doesn't have CaseIterable and you've chosen a particular order in allCases. Then next OS version changes the type to be CaseIterable which may lead to a different order compared to what your app was previously using, but you've "overridden" allCases, compiler doesn't warn you and some possibly unrelated new parts of the app (including third party components?) might unknowingly use your version of allCases.
  • related to the previous: harder to make the order you want.
  • this one matters only for a big number of cases like 100, and if the call used frequently enough: next has O(n) performance, n = number of cases.
  • one other issue I won't mention, as insignificantly small fraction of people uses swift to write realtime code

Explicit switch while "less clever" and quite boring is free from all these issues.

1 Like

Yeah, the ergonomic here are key to me. What I really want is a universal way to find out the cases, which is also available to the LSP server, so that I can easily construct a switch if I need to control the ordering, or do it automatically if I don't care.

Even better would be for the result to be a compiled constant or a linked list, so the O(n) could be short circuited somehow.

I'm curious about the realtime issue you don't mention.

So you are doing this en masse not just for one or three types? :thinking: What exactly is your app doing?

Ideally we could add CaseIterable on Obj-C imported types, but there must be a (good?) reason it is currently impossible.

I'm rather curious what you app is doing. If O(n) behaviour is a potential concern, that means either N is big, or next is called frequently, (or both); what is it you are using "cycling through enums" for?

One possible solution - during the first call next may iterate through all items in O(n) time and construct "array of next elements", so the subsequent next calls work in O(1) time.

See this thread.

I tend to think in terms of for-all-time. In general, every widget which has user selectable states might want a related state chooser. I'm a fan of making it extremely easy to maintain those widgets. If one could have the entire chain reflect the possible states with nothing more than adding the new case to an enumeration, it'd be ideal.

That or those types would get migrated to Swift, I'm not picky. :wink:

I only mention it because you did, and because I tend to think in terms of for-all-time.

  1. Nobody's going to change MPMusicShuffleMode. It's 13 years old.
  2. Sometimes you want to cycle; sometimes you don't.
MPMusicShuffleMode.albums.next() as Optional // nil
MPMusicShuffleMode.albums.next() as MPMusicShuffleMode // .default
extension MPMusicShuffleMode: CaseIterable & IteratorProtocol {
  public static let allCases = (0...3).map(Self.init) as! [Self]
}
import Algorithms

extension IteratorProtocol where Self: CaseIterable & Equatable {
  /// The case after this one, in `Self.allCases`.
  public func next() -> Self? {
    .allCases(after: self)
  }

  /// The case after this one, in `Self.allCases.cycled()`.
  public func next() -> Self {
    .allCases.cycled()(after: self)!
  }
}
public extension Sequence where Element: Equatable {
  /// The element that follows.
  func callAsFunction(after element: Element) -> Element? {
    var iterator = makeIterator()
    while iterator.next() != element { }
    return iterator.next()
  }
}

Without any custom code, when I do:

p MPMusicShuffleMode(rawValue: 2)!

in the debugger it shows correct "songs". This works in both debug and release builds.

Trying an invalid constant expectedly shows:

p MPMusicShuffleMode(rawValue: 0x42)!
<invalid> (0x42)

(for those unfamiliar, Obj-C imported enums are different from swift native enums in that their init?(rawValue:) despite being typed optional, never returns nil).

I tried another random guinea pig type that has "holes" in its range, which has this Obj-c definition:

typedef NS_ENUM(NSUInteger, AVAudioSessionPortOverride) {
    AVAudioSessionPortOverrideNone = 0,
    AVAudioSessionPortOverrideSpeaker = 'spkr'
};

when converted to swift it becomes AVAudioSession.PortOverride:

p AVAudioSession.PortOverride(rawValue: 0)!
none

p AVAudioSession.PortOverride(rawValue: 1)!
<invalid> (0x1)

p AVAudioSession.PortOverride(rawValue: 0x73706B72)! // 'spkr'
speaker

But how does this magic work? Can I replicate this in my app?

PS. Unless there's another magic feature, this per se won't help with automatic case enumeration for types like AVAudioSessionPortOverride unless you want to test all underlying RawValue values, and RawValue can be UInt32, UInt64, etc. For more standard enums like the above's MPMusicShuffleMode it would be possible to enumerate from, say, 0 up until it says "invalid".

1 Like