LoginSignup
13
11

More than 5 years have passed since last update.

TableViewの各Section番号をSwiftのEnumとして持たせた時に、全caseを配列で返すProtocol Extension

Last updated at Posted at 2016-02-29

この記事は以下をがっつり参考にさせていただきました。

enumの列挙子を配列で取得するのをprotocolで

経緯

TableViewやCollctionViewのSectionをInt型のEnumで定義することは多いかと思います。

enum Section: Int {
  case SectionA
  case SectionB
}
switch Section(rawValue: indexPath.seciton) {
  case .SectionA?:
    break
  case .SectionB?:
    break
  case .None:
    break
}

このようにenumで定義すると、トータルのsection数が取れないという問題があります。困ってstack over flowを調べていたら、以下のような解決策を見つけました。その時はまあこれでいいかと思い、それからは全要素を配列で持たせる方法をとっていました。

enum Section: Int {
  case SectionA
  case SectionB

  static var allValue: [Section] {
    return [.SectionA, .SectionB] // enumの各要素を配列で持たせるcomputed valueを追加
  }
}

しかしSectionの数が多い時などやSectionの構成が変わってしまう場合などを考えると極めて保守性が低く、若干後ろめたさも感じます。

他のやり方として、Objective-C時代によく見かけたEnumの一番末尾へ総数カウント用Caseを余計に追加するという方法があるかと思います。

enum Section: Int {
  case SectionA
  case SectionB
  case Count  // この値が自動的にSectionの全要素数になる
}

これはこれでswitch文の中で邪魔な存在として現れてしまいます。せっかくswiftのパターンマッチでいい感じの網羅チェックができるのにこれじゃもったいないと思っていました。

そんな折、ふとなんか他の方法ないんかなと思って調べていたところ以下の記事を見つけました。

EnumのhashValueを辿って順番にCaseをナメるという内容です。そうやってEnumの全ての値の配列を生成しています。
これを読んだ時、「GeneratorやProtocol Extensionをうまく使ってるし、自分の求めていた物そのものだし、イケてる!参考にさせてもらおう!」とテンションがあがっていました。しかし、この記事の元ネタとなっている文章を読むと重要な注意点が書いてあります。

As with @rintaro's answer, this code uses the underlying representation of an enum. This representation isn't documented and might change in the future, which would break it -> I don't recommend the usage of this in production.

http://stackoverflow.com/questions/24007461/how-to-enumerate-an-enum-with-string-type/32429125#32429125

要するに、Enumのハッシュ値の実装に依存しているのだけれどそれはUndocumentedだから将来実装が変わらない保証はない、とのことです。人によるかもしれませんが、個人的には結構躊躇してしまう内容で、利用を見送ることにしました。

けれども折角いい方法なので、同じアイデアを使いつつ多少妥協した解決策を検討してみたいと思います。

実装

前置きが長くなりましたが、タイトルの内容を実現するための実装がここからになります。
上記方法の基本アイデアは、起点となるCaseとそこから全てのCaseをたどっていく方法をProtocol Extensionとして定義する、ということだと思います。そこへhashValueを使うことはUndocumentedで危険なので、代わりにRaw Valueを使いたいと思います。とりあえず自分が解決したい問題は「TableViewのSectionをEnumで定義して、その総数を余計なことを何もせずにとる」ことなので、そこへ特化させます。
Int型のRaw Valueで定義されたEnumを想定して、Protocolを作成します。AnyGeneratorでrawValueを順に取り出す計算手続きを書いて、それをArrayにわたすことで全Caseを持った配列を返します。

// メソッドの型でInt型以外のEnumを防ぐ
protocol IntRawValueEnumerable {
    init?(rawValue: Int) // SomeEnum(rawValue: someInt)でCaseが取得できるようにするために定義しています
    var rawValue: Int { get } // Int型のRaw Value Enumへ適用できるようにするために定義しています
}

// 
extension IntRawValueEnumerable {
    // Enumの各caseを順番に取得するための計算をここで書いています。s
    // tartの値からstepずつ増やしていき、nilが返るまでcaseをとり続けます。
    static var generator: AnyGenerator<Self> {
        var index = start
        return anyGenerator {
            defer { index += step }    
            let next = Self(rawValue: index)
            return next
        }
    }

    // 上のgeneratorを元にArrayを生成しています。これによって全caseを取得しています。
    static var allValues: [Self] {
        return Array(generator)
    }

    var start: Int { return 0 }
    var step:  Int { return 1 }
}

使用方法が以下のようになります。String型のRaw Valueなどにこのプロトコルを使おうとするとエラーになり、Int型のみ利用できるというところまでは実現できました。

// Int型のRaw Value Enumへプロトコルを追加する
enum Section: Int, IntRawValueEnumerable {
    case A
    case B
    case C
    case D
}

print(Section.allValues) // => [.A, .B, .C, .D]
print(Section.allValues.count) // => 4

注意点としては、Raw Valueの値が規則的に増えていかないと期待した結果が得られません。その辺もどうにかしたかったのですが、今回はここで時間切れということで諦めました。

最後に

Enumの各要素にアクセスするProtocol Extensionを作ってみました。問題解決のヒントになれば幸いです。

13
11
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
13
11