swiftのenumでStringがカスタムなときでもenumを諦めない

  • 29
    いいね
  • 0
    コメント

はじめに

アプリ内の状態をenumで表す時、それをサーバーに伝える際に文字列として表現するようなケースがあった時、その中にカスタムな文字列を使いたい場合にenumを諦めてないだろうか。

例えば次のような感じ。SampleEventはeventで列挙できるパターン3つしかないんだけど、文字列はカスタムしたいみたいな。

// case customのStringが変わる場合は使いづらい
enum SampleEvent: String {
    case event1 = "e1"
    case event2 = "e2"
    case custom = "このStringが変わるようにしたい"
}

enumと固定されたStringが1対1で対応しているならその時はそれでいいけど、カスタムな要素があるとenum :Stringはそのままでは使えない。なので、enum :stringは諦める。ただし、enumを使う事自体は諦めなくていいっていう話です。

その際のやり方は色々あるだろうけど、次の項目を知ってると無理なくできる

  • Associated Value を使う
  • RawRepresentable のプロトコルに沿ってrawValueを実装する

本題: カスタムな文字列があるenumを実装する

Associated Value を使う

まずAssociated Value を使う。そもそもswiftでenumを利用する際にAssociated Valueを知ってるから知らないかでだいぶ変わる。困ったら大抵 Associated Valueが解決してくれる。

例えば次のようなenum AppEventNameで 3つのEventが有るとして、case customにカスタムな文字列をStringを使いたいので Associated Valueを使う。

public enum AppEventName {

    // MARK: General
    case completedRegistration
    case completedTutorial

    // MARK: Custom
    case custom(String)

    public init(_ string: String) {
        self = .custom(string)
    }
}

2つの固定のcaseに続いて.customはenumを利用する側がカスタムなStringに使いたいというわけ。

ちなみにinit(_ string: String) 使って初期化できるようにしておくと便利

let event = AppEventName.custom("お前が好きなカスタムなイベントをキメろ!")

↓ initがあるので

let event = AppEventName("お前が好きなカスタムなイベントをキメろ!")

RawRepresentableのプロトコルに沿ってrawValueを実装する

次にカスタムなこの文字列を無理なく取り出したいじゃないですか。なのでRawRepresentableのプロトコルに沿ってメソッドを実装する。

先述したenum AppEventName からStringの値を利用するならrawValueを使えるようにするのが自然に思えるので、protocolRawRepresentableに準拠してあげる。

extension AppEventName: RawRepresentable {
    public init?(rawValue: String) {
        self = .custom(rawValue)
    }

    /// The corresponding `String` value.
    public var rawValue: String {
        switch self {
        case .completedRegistration: return "登録完了"
        case .completedTutorial: return "チュートリアルオワタ"

        case .custom(let string): return string
        }
    }
}

rawValueで該当する文字列定数を返したりカスタムなStringを返す具体的な利用例は次

let event0 = AppEventName.completedRegistration
print(event0.rawValue)   // => 登録完了

let event0_1 = AppEventName.completedTutorial
print(event0_1.rawValue) // => チュートリアルオワタ

let event0_2 = AppEventName("お前が好きなカスタムなイベントをキメろ!")
print(event0_2.rawValue) // => お前が好きなカスタムなイベントをキメろ!

すごい、これで自然な感じがしてくる。

switch文を書きたいならこんな感じ

もしswitchを使うとしてもstringはrawValueで取り出せるし、case .custom(let string):で取り出してもいい。

let event = AppEventName("お前が好きなカスタムなイベントをキメろ!")
switch event {
case .completedRegistration:
    // ...いろいろな処理...
    print(event.rawValue) 
case .completedTutorial:
    // ...いろいろな処理...
    print(event.rawValue) 
case .custom(let string):
    // ...いろいろな処理...
    print(string)         // => お前が好きなカスタムなイベントをキメろ!
}

ここまでのまとめ

以上でだいたいやりたいことが出来た。

さて、こういったやり方は一体どっから知ったかというと、facebook-ios-sdkのswift版が2016年の夏に公開されていて、そこで使われていただけ。

https://github.com/facebook/facebook-sdk-swift/blob/0.2.0/Sources/Core/AppEvents/AppEventName.swift#L75-L77

もうだいぶ前に公開されていたため大概みんな知ってるかと思ってたんだけど、これを知らずにenumを使わない方法をやってたりするのを目にしたので、こういう文章にしておいた。

その他

facebook-ios-sdkのAppEventNameには、その他の役に立つような立たないような実装もあるのでそれも書いておく。

protocol ExpressibleByStringLiteralに準拠するように実装しておく

カスタムな文字列でswitchしたいなら、protocol ExpressibleByStringLiteralに準拠するように次の3つのメソッドを実装しておく

extension AppEventName: ExpressibleByStringLiteral {
    public init(stringLiteral value: StringLiteralType) {
        self = .custom(value)
    }

    public init(unicodeScalarLiteral value: String) {
        self.init(stringLiteral: value)
    }

    public init(extendedGraphemeClusterLiteral value: String) {
        self.init(stringLiteral: value)
    }
}

雑だけど利用例は次のようになる。

let event = AppEventName("お前が好きなカスタムなイベントをキメろ!")

switch event {
case "登録完了":
    break
case "チュートリアルオワタ":
    break
case "お前が好きなカスタムなイベントをキメろ!":
    print("このやり方が役に立つ日が来るのだろう") // => このやり方が役に立つ日が来るのだろう
default:
    break
}

protocol CustomStringConvertibleに準拠してdescriptionも実装

もう一つ、主に開発中にprintを実行したときにenumの値よりStringを表示して欲しいのでprotocol CustomStringConvertibleに準拠してdescriptionも実装している

extension AppEventName: CustomStringConvertible {
    /// Textual representation of an app event name.
    public var description: String {
        return rawValue
    }
}

書くまでもないけど利用例としては次の通り

let event = AppEventName("お前が好きなカスタムなイベントをキメろ!")
print(event)                     // => お前が好きなカスタムなイベントをキメろ!
print(String(describing: event)) // => お前が好きなカスタムなイベントをキメろ!

これはお作法としてやっておくのはまあ良いとは思う。実際にアプリ内で処理に使うのはrawValueで充分だけど、CustomStringConvertibleを実装しておくと、String(describing: event)が使えるのでそれはまあ何かに使えるかもしれない。

おわりに

enumは強い制約を与えるので仕様を見える化する際にそれが適切であればenumを使うのを諦めない方向で設計を考えてくれると、後でコードを修正する際に助かる。enumを使うケースで安易にStructや文字列定数を並べたものを利用してしまうと制約を与えられないため、複数のパターンを考慮しないといけなくなる。