Swift Digest
SE-0064 | Swift Evolution

Referencing the Objective-C selector of property getters and setters

Proposal
SE-0064
Authors
David Hart
Review Manager
Doug Gregor
Status
Implemented (Swift 3.0)

01 何が問題だったのか

SE-0022 で導入された #selector 式により、Objective-C メソッドのセレクタを型安全に参照できるようになりました。しかし、#selector がカバーするのはメソッドだけで、プロパティのゲッター・セッター に対応するセレクタを参照する手段は用意されていませんでした。

Cocoa の API にはプロパティのゲッター・セッターをセレクタとして受け取るものが少なくありません。たとえば NSUndoManager の取り消し登録や、NSTimer のコールバック指定、KVO 経由の操作などで、「このプロパティのセッターを呼ぶ」といった指定が必要になる場面があります。#selector がプロパティを扱えないため、これらのケースでは従来どおり文字列リテラルでセレクタを書き下すしかなく、#selector 導入の動機だったスペルミスやリネームへの追従の問題がそのまま残っていました。

class Person: NSObject {
  dynamic var firstName: String
  // ...
}

// ゲッター・セッターのセレクタは文字列で書くしかなかった
let getter: Selector = "firstName"
let setter: Selector = "setFirstName:"

Swift のプロパティ名から Objective-C のセッター名(setFirstName: のように set + 大文字始まり + :)を手で導出しなければならず、メソッドの場合と同様に間違いやすい書き方でした。

02 どのように解決されるのか

#selector に、プロパティのゲッター・セッターを参照するための2つのオーバーロードを追加します。新しい構文は導入せず、#selector 式の中で getter: または setter: のラベルを付けてプロパティ参照を渡す形です。コンパイラは対象が実在する @objc プロパティであることを静的に検証したうえで、対応する Objective-C セレクタ(ゲッターならプロパティ名、セッターなら set + 大文字始まりの : 付きセレクタ)を生成します。

class Person: NSObject {
  dynamic var firstName: String
  dynamic let lastName: String
  dynamic var fullName: String {
    return "\(firstName) \(lastName)"
  }

  init(firstName: String, lastName: String) {
    self.firstName = firstName
    self.lastName = lastName
  }
}

let firstNameGetter = #selector(getter: Person.firstName)
let firstNameSetter = #selector(setter: Person.firstName)

これにより、プロパティ名のスペルミスや存在しないプロパティへの参照はコンパイル時に検出できるようになり、プロパティをリネームしてもセレクタ側が自動的に追従します。

セッターを取れるのはミュータブルなプロパティのみ

#selector(setter:) に渡せるのは var で宣言されたプロパティに限られます。let で宣言された immutable なプロパティや、ゲッターのみの computed property に対してセッターを要求するとコンパイルエラーになります。

let lastNameSetter = #selector(setter: Person.lastName)
// Argument of #selector(setter:) must refer to a variable property

一方、ゲッターは letvar・computed property のいずれに対しても取得できます。上の例では fullName のような読み取り専用の computed property に対しても #selector(getter: Person.fullName) と書けます。