入門
Swift

Swiftとキー値コーディング

More than 3 years have passed since last update.

「ヒレガス本」ことアーロン・ヒレガス著「Mac OS X Cocoa プログラミング」の例題をSwiftで書いてみようと思っているのですが、Cocoaバインディングまわりで思ったように動かず停滞中です。Cocoaバインディングではキー値コーディングと呼ばれるオブジェクトのプロパティを文字列で指定して操作する技術が用いられているのですが、Objective-CとSwiftではいろいろ違いがあるのかもしれません。

先日発売された萩原剛志著「詳解Swift」読んでもキー値コーディングについては解説がなかったので、基本的なトコを簡単にはですがちょっと調べてみました。

まず、Swiftで単純なクラスを定義してキー値コーディングに対応しているか調べてみました。

class Person
{
    var name = ""
    var email = ""
    var age = 0
}

var p1 = Person()
p1.setValue("Jhone Doe", forKey: "name")  // エラー!!

setValueというメソッドがないとエラーになってしまいました。
Objective-Cでは全てのクラスはNSObjectの派生クラスでしたが、Swiftでは当たり前といえば当たり前ですが違うようです。

そこで、PersonクラスをNSObjectから派生するように変更してみます。

class Person : NSObject  // ← NSObjectから派生させる
{
    var name = ""
    var email = ""
    var age = 0
}

var p1 = Person()
p1.setValue("Jhone Doe", forKey: "name")    // 成功!!

今度は成功しました。SwiftではNSObjectからクラスを派生させることで、プロパティへのキー値コーディングが可能になるようです。

念のため値の取得も試してみます。

let name = p1.valueForKey("name") as String?

こちらも問題ないようです。valueForKeyはAnyObject?を返すのでダウンキャストが必要になります。

p1.setValue(14, forKey: "age")
let age = p1.valueForKey("age") as Int?

文字列以外、Int型のプロパティも問題ありません。Objecttive-Cだと、直接整数リテラルを渡すことはできずNSNumberにラップしてやる必要がありましたが、Swiftでは適切に変換されるためリテラル値を直接渡すことが可能です。

setValuevalueForKeyに対して存在しないキー値を指定した場合、setValue: forUndefinedKeyvalueForUndefinedKeyが呼び出されます。これらのメソッドをPersonクラスに実装して呼び出されるか確認します。

class Person : NSObject
{
    var name = ""
    var email = ""
    var age = 0

    override func setValue(value: AnyObject?, forUndefinedKey key: String) {
        println("\(key)は存在しません")
    }
    override func valueForUndefinedKey(key: String) -> AnyObject? {
        println("\(key)は存在しません")
        return nil
    }
}

p1.setValue("Hoge", forKey: "foo")
p1.valueForKey("bar")

実行すると「fooは存在しません」「barは存在しません」と表示され、それぞれのメソッドが呼び出されていることが確認できます。

setValueメソッドは設定する値をAnyObject?で受け取るので、どのような値でも渡すことができてしまいます。では異なる型の値を渡すとどうなるでしょうか?

p1.setValue(100, forKey: "email")   // エラー!!

実行時エラーが発生しプログラムが終了してしまいます。Swiftには例外処理がないのでエラーをトラップすることができません。したがって異なる型の値を渡さないようにプログラマが保証してやる必要があります。

ただ、スカラ型のプロパティにnilを設定しようとした場合は、setNilValueForKeyが呼び出されるとなっています。PersonクラスにsetNilValueForKeyを実装して試してみます。

class Person : NSObject
{
    var name = ""
    var email = ""
    var age = 0

    override func setValue(value: AnyObject?, forUndefinedKey key: String) {
        println("\(key)は存在しません")
    }
    override func valueForUndefinedKey(key: String) -> AnyObject? {
        println("\(key)は存在しません")
        return nil
    }

    override func setNilValueForKey(key: String) {
        if key == "age"
        {
            age = 0
        }
        else
        {
            setValue("", forKey: key)
        }
    }
}

p1.setValue(nil, forKey: "email") // エラー!!

何故かsetNilValueForKeyは呼び出されずに、不正アクセスでエラーになってしまいました。
自分のコードの問題なのか、Swiftのバグなのか仕様上の制限なのか今の所良く判りません。

ちなみに、プロパティをオプショナル型にすれば(当たり前ですが)エラーにはなりません。

とりあえず、文字列や数値といった単純な値型のプロパティ値に関していえば、SwiftでもクラスをNSObjectから派生させることでキー値コーディングに対応させることができること判りました。ただ、キー値コーディングでは値をAnyObject?としてやりとりするので型については注意が必要。でないとエラーで落ちる。というトコでしょうか?

次はより複雑な場合について試してみようと思います。