8
2

More than 3 years have passed since last update.

enum Associated Values と Raw Values を両立させる

Posted at

背景

Swift の enum においては、 Associated Values と Raw Values を利用することができます。

これらはそれぞれ単独での利用は問題ないのですが、両方を同時に利用するとすると、コンパイルエラーとなってしまいます。

それぞれ単独での利用は OK

Associated Values
enum Screen {
  case home(tabIndex: Int)
  case search
  case setting
}
Raw Values
enum Screen: String {
  case home = "Home"
  case search = "Search"
  case setting = "Setting"
}

同時利用が NG

Associated Values & Raw Values
enum Screen: String {
  case home(tabIndex: Int)  // コンパイルエラー "Enum with raw type cannot have cases with arguments"
  case search
  case setting
}

コンパイルエラーを回避して、 Associated Values と Raw Values を両立させようというのが今回の内容です。

ユースケースとしては、次のように iOS アプリの画面を enum で表現することを想定します。

  • ホーム/検索/設定の3画面をタブ( UITabbar を利用した下タブ)で構成
  • ホーム画面は可変個のタブストリップ(上タブ)で子画面を保持
    • Associated Values を利用
  • スクリーンビュー測定のため、 enum 毎に特定の文字列を定義
    • Raw Valuesを利用

どうするのか

Associated Values はそのまま。
Raw Values を protocol RawRepresentable に適合させることで実現します。

For any enumeration with a string, integer, or floating-point raw type, the Swift compiler automatically adds RawRepresentable conformance. When defining your own custom enumeration, you give it a raw type by specifying the raw type as the first item in the enumeration’s type inheritance list. You can also use literals to specify values for one or more cases.

とあるように、 Raw Values 自体は Swift コンパイラが protocol RawRepresentable に自動的に適合してくれた結果なので、これを自分で実装することとなります。

具体的には、以下のイニシャライザとプロパティ実装が要求されます。
associatedtype RawValue については、型推論が効くため省略可能です)

init?(rawValue: Self.RawValue)
var rawValue: Self.RawValue

上述の想定ユースケースにおける enum Screen を例にした実装だと、次の通りです。

enum Screen: RawRepresentable {
    case home(tabIndex: Int)
    case search
    case setting

    init?(rawValue: String) {
        switch rawValue {
        case Screen.search.rawValue:
            self = .search
        case Screen.setting.rawValue:
            self = .setting
        default:
            if let regex = try? NSRegularExpression(pattern: "^Home_Tab(.+)$"),
               let match = regex.firstMatch(in: rawValue, range: NSRange(location: 0, length: rawValue.count)),
               let range = Range(match.range(at: 1), in: rawValue),
               let tabIndex = Int(rawValue[range]) {
                // `case home(tabIndex: Int)` の rawValue フォーマットに合致
                self = .home(tabIndex: tabIndex)
            } else {
                return nil
            }
        }
    }

    var rawValue: String {
        switch self {
        case .home(let tabIndex):
            return "Home_Tab\(tabIndex)"
        case .search:
            return "Search"
        case .setting:
            return "Setting"
        }
    }
}

メリット・デメリット

メリットとしては、 Associated Values と Raw Values を両立可能という結果そのものがあります。

もう少し具体的な例だと、 Raw Values 実装に依存した既存コードが大量にあるプロジェクトにおいて、 enum に Associated Values を利用した case を追加する必要性が発生した場合に、影響を最小限に抑えることが可能となってきます。

デメリットとしては、

  • init?(rawValue: Self.RawValue)の実装が煩雑になる
  • enum の表現力 / 記述容易性が失われる

といったことが挙げられます。

特に後者において、通常の enum 実装では、列挙子と rawValue の一意性を少ない記述で簡潔に表現でき、コンパイラの静的解析による保証もついてくる

enum Screen: String {
  case home = "Home"
  case search = "Search"
  case setting = "Search"   // コンパイルエラー "Raw value for enum case is not unique"
}

のですが、自分で protocol RawRepresentable への適合を実装するとなると、このあたりの恩恵が失われてしまいます。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2