4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

SwiftでRuntimeから特定のクラスのAnyClass列を取得しそのインスタンスを生成する

Last updated at Posted at 2019-01-24

ランタイムより動的にクラスを取得

Runtime中に特定のサブクラスのインスタンスを生成したいとします。UITableViewCellなどのようにidentifierのようなキーワードから、どのサブクラスをインスタンス化させたいかを指定する方法も良いが、この手のものは大概、identifierとクラスを紐づける為に、UITableViewのようにクラスを登録させるのが一般的です。

func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: String)

それはそれでいいと思うのですが、やはり別のケースではクラス名の文字列からオブジェクトのインスタンスを生成したいと思う場合があります。できれば、identifierとクラスの組み合わせの登録を行わずに。

そこで、ランタイムより以下のクラスAnyClassを取得するユーティリティコードを書いてみました。取得できるクラスは以下の通りです。

  1. 特定のクラスとそのサブクラスAnyClassの列
  2. Objective-C のプロトコルProtocolに準拠したクラスAnyClassの列
  3. 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
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?