Notification.Nameの独自定義をタイプセーフなSwift enumにしたかった
モチベーション
Notification.Nameの独自定義において…
- Stringで値も直接記述するのが微妙(タイプセーフにしたい)
- 同じ名前を2回書くのが微妙
- シンプルに定義したい
- 使いやすくしたい
- 名前空間的な定義をしたい
- Stringのenumにすると一見使いやすそうだが、caseの拡張(extension)はSwiftの仕様上できないらしい
一般的な「Notification.Nameのextensionによってstatic let
で追加定義する方法」だと、変数名とStringで2回似たような名前を書く必要があって微妙。しかもNotification.Nameに直接グローバルな拡張が施されるので、ランタイム内ならどこからでもアクセスできてしまう(それが良いと見る場合もある)。
これはAppleが案内している方法でもある。
extension Notification.Name {
// この記述内に`objectWillUpdateState`を2回書く必要があり、書くのが面倒くさい。
// (実際には名前はなんでも良いのだが、メンテ性から定義と値は同一名にした方が無難)
// タイプセーフではないので、値の一意性はコンパイラには保証されない。
static let objectWillUpdateState = Self("objectWillUpdateState")
static let objectDidUpdateState = Self("objectDidUpdateState")
}
protocol extension + enumで使いやすくする
enumの拡張でcaseの追加定義はできないが、Notification.Nameをどうにかする共通の仕組みをprotocol extensionでまとめて、それを装着するenum定義を都度宣言する形ならうまくいきそうである。
まず以下のようにprotocol extensionの形で「Notification.Nameをどうにかする共通の仕組み」を実装しておく。プロトコル名はNotificationTypeDeclaration
と名付けておくが、ここは好みで。Notification.NameのrawValueはStringなので、Stringで扱えるようRawRepresentableに準拠させる。
定義名にカスタム接頭辞を付けられるとStringにダンプした時に扱いやすいと考え、prefix()
getterメソッドも用意した。これの中身は装着するenum側で任意に実装するものだが、不要なら空文字を返すでも良い。
実際にNotification.Nameを使いたい時には、name
プロパティ経由で得られる。
print()時などのdescription
でも同じ値を出力したいため、CustomStringConvertible
に準拠させてdescriptionプロパティの実装を上書きする。
これをライブラリ化して、プロジェクトごとに都度enumに装着する方針をとる。
protocol NotificationTypeDeclaration: CustomStringConvertible, RawRepresentable where RawValue == String {
static var prefix: String { get }
}
extension NotificationTypeDeclaration {
var name: Notification.Name {
let prefix = Self.prefix()
let dot = prefix.isEmpty ? "" : "."
return Notification.Name("\(prefix)\(dot)\(Self.self).\(self.rawValue)")
}
var description: String {
name.rawValue
}
}
enum側実装(例)
enum NotificationType1: String, NotificationTypeDeclaration {
static func prefix() -> String {
"PROJECT_PREFIX" // NS, UI, CG, CTみたいな適当な接頭辞を付けたい場合
}
// case名を書くだけで実質Notification.Nameを定義できる
case scrollViewOffsetDidChange
case scrollViewScaleDidChange
}
enum NotificationType2: String, NotificationTypeDeclaration {
static func prefix() -> String {
"" // 接頭辞なしにもできる
}
case scrollViewOffsetDidChange // NotificationType1と別空間なので、重複しない
case scrollViewScaleDidChange // 〃
case buttonStateWillChange
case buttonStateDidChange
}
// --- 使用例 ---
let notifType1: Notification.Name = NotificationType1.scrollViewOffsetDidChange.name
// dump: -> NSNotificationName(_rawValue: "PROJECT_PREFIX.NotificationType1.scrollViewOffsetDidChange")
let notifType2: Notification.Name = NotificationType2.buttonStateWillChange.name
// dump: -> NSNotificationName(_rawValue: "NotificationType2.buttonStateWillChange")
let notifType3: NotificationType1 = NotificationType1.scrollViewScaleDidChange
// dump: -> "PROJECT_PREFIX.NotificationType1.scrollViewScaleDidChange"
Discussion