SwiftでObjective-Cの黒魔術ってどうなった?

  • 86
    いいね
  • 0
    コメント

この記事はSwift Advent Calendar 17日目の記事です。

Swiftを始めて、最近では仕事でもいくつか徐々にSwiftに移行し始めています。その中で、時折ObjCを使った黒魔術に出会うことがあります。これらは、ObjCの文化であり、Swiftでもきのこって行けるのだろうかと思いましたので、今回試してみました。

Objective-Cの黒魔術

諸説ありますが、自分の中ではObjecitive-Cの黒魔術といえば、つまりObjective-Cのランタイム関数です。
SwiftでもObjective-Cのランタイム関数は利用可能です。ランタイム関数はC言語の関数ですが、Swift自体はC言語との互換があるので使えるんですね。
ランタイム関数を利用するにはObjectiveCという名前のモジュールをインポートします。

import UIKit
import ObjectiveC // これをインポート

ランタイム関数には色々な関数が用意されています。
今回はそのうち最もよく使われているであろう(自分調べ)下記の2つを試しました。

  • オブジェクトに保持可能なメンバ変数を追加する Associated Object
  • メソッドの挙動を差し替える Method Swizzling

Associated Object

Associated Objectはオブジェクトに対して、関連したオブジェクトを保存しておく事ができるランタイム関数の機能です。
関連元のオブジェクトが開放されると、関連したオブジェクトも開放されます。Objective-Cではカテゴリ拡張でさもメンバ変数であるかのようなプロパティを作るときに用いました。
具体的な利用例だと、UIAlertViewのdelegateをUIAlertViewの生存期間に合わせて開放したい場合などです。

Swiftで利用できるかという点については、条件付きで使えます。その条件とは、NSObjectのサブクラスであるということです。純Swiftのコードでは、これらの機能は実装しても機能しません。

利用方法

Associated Objectは設定にobjc_setAssociatedObject()、取得にobjc_getAssociatedObject()を用います。実装すると次のようになります。

class MyObject: NSObject {}

private var key = 0
extension MyObject {
    var str:String {
        get {
           return objc_getAssociatedObject(self, &key) as String
        }
        set {
            objc_setAssociatedObject(self, &key, newValue, objc_AssociationPolicy(OBJC_ASSOCIATION_COPY_NONATOMIC))
        }
    }
}

SwiftでのAssociated Objectの利用にはいくつか注意が必要です。
Associated Objectの関連付けに必要な第二引数のキーですが、UnsafeMutablePointer<Void>型です。そのため、キーに指定する変数はMutableであるvar宣言が必要です。let宣言ではコンパイルエラーとなります。そして、ポインタで指定するため変数の前に&が必要です。
また、設定と取得で同じポインタを示すため、エクステンションの外のグローバル領域で変数keyを用意しています。
この変数keyは外から触れられるとまずいので、privateを指定しておきます。

Method Swizzling

Method Swizzlingは既存の2つのメソッドの挙動を入れ替える黒魔術です。
Apple標準のメソッドの挙動を上書きしたりなど、少々手荒な真似をして対応したいときに用います。

こちらの実装も、Associated Objectと同様NSObjectのサブクラスに限定されます。やはり純Swiftのコードではこれを利用できません。

次のコードは、description()メソッド実行時に"swizzed"と表示されるようにする例です。

import UIKit
import ObjectiveC

extension NSObject {
    func swizzDescription() -> String {
        println("swizzed")
        return self.swizzDescription()
    }
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    override class func initialize() {
        var m1 = class_getInstanceMethod(NSObject.self, "description")
        var m2 = class_getInstanceMethod(NSObject.self, "swizzDescription")

        method_exchangeImplementations(m1, m2)
    }

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.        
        println(self)
        return true
    }
}

まず準備として、エクステンションでNSObjectにswizzlingしたいメソッドを拡張しておきます。その中で追加したメソッドを呼ぶようにします。
コードだけ見ると再起呼び出しで、無限ループするように見えますが、これで問題ありません。Method Swizzling後は元の実装と名前が入れ替わっているので、元のdescriptionメソッドを呼び出すことになります。

次にMethod Swizzlingを実行します。Swizzling操作は、エクステンションでNSObjectinitializeメソッドをオーバーライドできなかったので、やむなくAppDelegateで行いました。

まずclass_getInstanceMethod関数を利用してdescriptionswizzDescriptionの実装を取得します。この時、クラスを指定する必要がありますが、Swiftではクラス名.selfで指定できます。

交換する2つのメソッドの実装を取得したら、method_exchangeImplementations関数を用いて、2つの実装を入れ替えるだけです。

AppDelegateでselfをプリントすると

swizzed
<SwizzTrial.AppDelegate: 0x7fdf99611da0>

と、descriptionの表示前にswizzedが表示されます。

このようにSwiftでもMethod Swizzlingは可能です。

まとめ

どちらの操作も、NSObjectを継承したクラスに対して利用可能です。
現状提供されているSDKはすべてNSObjectを継承して作られているので、問題無く利用できます。
しかし、この先追加されるSDKが純Swiftだったり、既存のSDKがSwiftに書き換えられたりすると、これらの方法は使えなくなる可能性が高いと考えられます。
もしかすると、iOS9以降そういう未来が待っているかもしれませんね。

P.S.

誠に恐縮ながら、この度縁がありまして、Swiftの本を執筆させていただきました!

swift_mynavi.jpeg
開発のプロが教える Swift標準ガイドブック

この本は、Swiftの文法に始まり、Optional型の説明、UIKit・Foundationの利用やObjective-CからSwiftへの移行など盛りだくさんで、実践で使えることをテーマに詰め込みました!

Swiftやってみたいけど、うまいこと手を出せていないという人から、Swiftもうバリバリやっているよという人まで、役に立つ本になっていますので、是非一度手にとっていただきたいと思います。

12月25日発売なので来週木曜日、クリスマス発売です!

年末年始のお供に 開発のプロが教える Swift標準ガイドブック をどうぞよろしくお願いします!

この投稿は Swift Advent Calendar 201417日目の記事です。