ランタイムより動的にクラスを取得
Runtime中に特定のサブクラスのインスタンスを生成したいとします。UITableViewCell
などのようにidentifier
のようなキーワードから、どのサブクラスをインスタンス化させたいかを指定する方法も良いが、この手のものは大概、identifier
とクラスを紐づける為に、UITableView
のようにクラスを登録させるのが一般的です。
func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)
それはそれでいいと思うのですが、やはり別のケースではクラス名の文字列からオブジェクトのインスタンスを生成したいと思う場合があります。できれば、identifier
とクラスの組み合わせの登録を行わずに。
そこで、ランタイムより以下のクラスAnyClass
を取得するユーティリティコードを書いてみました。取得できるクラスは以下の通りです。
- 特定のクラスとそのサブクラス
AnyClass
の列 - Objective-C のプロトコル
Protocol
に準拠したクラスAnyClass
の列 - Swift の特定のプロトコルに準拠したクラス
AnyClass
の列
コードの取得先は以下のURLより可能です。
https://gist.github.com/codelynx/bb72bf0bed58a327ce1dbe6639d0369b
特定のクラスとそのサブクラス列の取得
protocol MyProtocol {}
class MyClass1: MyProtocol {}
class MyClass2: MyClass1 {}
class MyClass3: MyClass2 {}
let myClasses = Runtime.subclasses(of: MyClass2.self)
print(myClasses.map { String(describing: $0) })
// ["MyClass2", "MyClass3"]
Objective-Cのプロトコルに準拠するクラス列の取得
@objc protocol YourProtocol {}
class YourClass1: NSObject, YourProtocol {}
class YourClass1b: YourClass1 {}
class YourClass2: NSObject, YourProtocol {}
let yourClasses = Runtime.classes(conformToProtocol: YourProtocol.self)
print(yourClasses.map { String(describing: $0) })
// ["YourClass2", "YourClass1", "YourClass1b"]
Swiftのプロトコルに準拠するクラス列の取得
protocol ThierProtocol {}
class ThierClass1: ThierProtocol {}
class ThierClass1b: ThierClass1 {}
class ThierClass2: ThierProtocol {}
let theirClasses = Runtime.classes(conformTo: ThierProtocol.Type.self)
print(theirClasses.map { String(describing: $0) })
// ["ThierClass2", "ThierClass1", "ThierClass1b"]
プロトコルを利用してAnyClass
から特定のオブジェクトを生成
AnyClass
のクラスのオブジェクトを生成するには、プロトコルに初期化用のメソッドを用意します。そのプロトコルに準拠したクラスのそれぞれには、そのプロトコルに準拠した初期化メソッドを用意します。
protocol ItemProtocol {
init()
}
class Item1: ItemProtocol {
required init() {}
}
class Item1b: Item1 {
required init() {}
}
class Item2: ItemProtocol {
required init() {}
}
次にクラスの文字列からオブジェクトを生成するユーティリティ関数を用意します。他の方法でも同等であれば良いかと思います。
func makeItem(named name: String) -> ItemProtocol? {
if let aClass = Runtime.classes(conformTo: ItemProtocol.Type.self).filter({ String(describing: $0) == name }).first,
let type = aClass as? ItemProtocol.Type {
return type.init()
}
return nil
}
これで特定のクラスの名前から、そのクラスのオブジェクトを生成する事が可能になります。初期化にさらなる引数を必要とする時でもinit()
に必要なだけ引数を用意するだけで、対応可能となります。
if let item = makeItem(named: "Item2") {
print(type(of: item)) // "Item2"
}
応用
今回紹介した方法を応用すれば、外部よりJSONなどにクラスの名前を指定して、特定のクラスのインスタンスを生成する事が可能になったり。ランタイムから網羅的に適合するクラスの一覧を抽出して、ユーザーに表示したり選ばせたり、コードによるランライム時の挙動の自由度が上がるものと考えています。
課題
Runtime.allClasses()
は objc_getClassList()
よりすべてのクラスのAnyClass
の列を取得してくるので、as?
オペレータを使って、次のように書きたいと思うかもしれませんが、CNZombie
(要調査) など特殊なクラスのAnyClass
が潜り込んでいて、as?
オペレータがクラッシュする場合があります。allClasses()
メソッドがそれら問題のAnyClass
を排除してクラス列を戻せればいいのですが、現状では良い方法が見つかっていません。特定のサブクラス群や特定のプロトコルに準拠したクラス群の取得の際には、それら問題を起こすクラスは事前にふるいにかけられているので、as?
オペレータを利用しても、クラッシュしない筈です。
for aClass in Runtime.allClasses() {
if let viewClass = aClass as? NSView.Type { // may cause crash on certain classes
// found a subclass of NSView
}
}
また、将来のSwiftのバージョン、またはOSのバージョンにおける互換性の問題なども考えられますので、注意が必要と言えます。
環境に関する表記
swift --version
Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1)
Target: x86_64-apple-darwin18.2.0