はじめに
アプリ内の状態を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年の夏に公開されていて、そこで使われていただけ。
もうだいぶ前に公開されていたため大概みんな知ってるかと思ってたんだけど、これを知らずに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や文字列定数を並べたものを利用してしまうと制約を与えられないため、複数のパターンを考慮しないといけなくなる。