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

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

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

```swift
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

###特定のクラスとそのサブクラス列の取得

```swift
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のプロトコルに準拠するクラス列の取得

```swift
@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のプロトコルに準拠するクラス列の取得

```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`のクラスのオブジェクトを生成するには、プロトコルに初期化用のメソッドを用意します。そのプロトコルに準拠したクラスのそれぞれには、そのプロトコルに準拠した初期化メソッドを用意します。

```swift:
protocol ItemProtocol {
	init()
}

class Item1: ItemProtocol {
	required init() {}
}
class Item1b: Item1 {
	required init() {}
}
class Item2: ItemProtocol {
	required init() {}
}
```

次にクラスの文字列からオブジェクトを生成するユーティリティ関数を用意します。他の方法でも同等であれば良いかと思います。

```swift
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()`に必要なだけ引数を用意するだけで、対応可能となります。 

```swift
if let item = makeItem(named: "Item2") {
	print(type(of: item)) // "Item2"
}
```

#応用

今回紹介した方法を応用すれば、外部よりJSONなどにクラスの名前を指定して、特定のクラスのインスタンスを生成する事が可能になったり。ランタイムから網羅的に適合するクラスの一覧を抽出して、ユーザーに表示したり選ばせたり、コードによるランライム時の挙動の自由度が上がるものと考えています。

#課題

`Runtime.allClasses()` は　`objc_getClassList()` よりすべてのクラスの`AnyClass`の列を取得してくるので、`as?` オペレータを使って、次のように書きたいと思うかもしれませんが、`CNZombie`（要調査） など特殊なクラスの`AnyClass`が潜り込んでいて、`as?` オペレータがクラッシュする場合があります。`allClasses()`メソッドがそれら問題の`AnyClass`を排除してクラス列を戻せればいいのですが、現状では良い方法が見つかっていません。特定のサブクラス群や特定のプロトコルに準拠したクラス群の取得の際には、それら問題を起こすクラスは事前にふるいにかけられているので、`as?`オペレータを利用しても、クラッシュしない筈です。

```swift
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
```