🐰

Notification.Nameの独自定義をタイプセーフなSwift enumにしたかった

2024/12/30に公開

モチベーション

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