名前付き UIView と後付けストアドプロパティもどき

## 名前付き UIView 

Storyboard などで配置した UIView に `@IBOutlet` を使わないでアクセスしたいとします。なぜ、`@ IBOutlet` を使わないかは大人の事情で割愛するとして、そうした事情でコードなどで特定の UIView をアクセスしたい場合に、Storyboard で配置した UIView やそのサブクラスに、tag に数字を仕込み、 `viewWithTag()` で 特定のViewにアクセスする事ができます。

```.swift
// An integer that you can use to identify view objects in your application.
var tag: Int

// Returns the view whose tag matches the specified value.
func viewWithTag(Int) -> UIView?
```

最近、またこの大人の事情で、`@IBOutlet` を使わないで、特定のViewにアクセスする必要が出てきました。以前と同様`tag`を使ってもいいのですが、時代的には `String` を使いたいと思いました。そこで、Storyboard の User Runtime Attribute を利用して、名前 `name` でアクセスできるような extension を書いたので紹介いたします。

```.swift
import UIKit

fileprivate var viewNameMap = NSMapTable<UIView, NSString>.weakToStrongObjects()

extension UIView {
	
	@IBInspectable dynamic public var name: String? {
		get {
			return viewNameMap.object(forKey: self) as String?
		}
		set {
			if let name = newValue {
				viewNameMap.setObject(name as NSString, forKey: self)
			}
			else {
				viewNameMap.removeObject(forKey: self)
			}
		}
	}

	public func view(named name: String) -> UIView? {
		if self.name == name {
			return self
		}
		for subview in self.subviews {
			if let view = subview.view(named: name) {
				return view
			}
		}
		return nil
	}
}
```

実は全ての UIView に `name` プロパティを用意しているわけではなく、`name` にwriteアクセスがあった場合に同`UIView`とその名前の関係を記憶します。そして、同 View がリリースされる時に、`name`もリリースされます。仕組みは、`NSMapTable.weakToStrongObjects`を利用してます。

使い方は、先のコードをプロジェクトに組み込んだ状態で、Storyboard から目的の View に名前をつけます。「Identity Inspector」からでも結構ですし、「Attributes Inspector」経由でも結構です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65634/4b4b8d46-f607-160b-d59e-c289390d595a.png" width="640"/>

クライアント側のコードはこんな感じです。`.view(named:)` でアクセス可能です。目的の`UIView` のサブクラス特有のメソッドやプロパティにアクセスする場合は適切にキャストしてあげる必要があります。

```.swift
class ViewController: UIViewController {
	// ...
	override func viewDidLoad() {
		super.viewDidLoad()
		let cyanView = self.view.view(named: "cyan")
		let magentaView = self.view.view(named: "magenta")
		let yellowView = self.view.view(named: "yellow")
		cyanView?.backgroundColor = UIColor.cyan
		magentaView?.backgroundColor = UIColor.magenta
		yellowView?.backgroundColor = UIColor.yellow
	}
}
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65634/74c26a32-547d-2fde-3376-5facf72ccb2c.png" width="260"/>

## ストアドプロパティもどき

応用すれば、自分で好きなストアドプロパティもどきを好きな View に追加可能です。試しに、dictionary プロパティを追加できる extension を用意してみましょう。

```.swift
import UIKit

fileprivate var viewDictionaryMap = NSMapTable<UIView, NSDictionary>.weakToStrongObjects()

extension UIView {
	public var configuration: NSDictionary? {
		get {
			return viewDictionaryMap.object(forKey: self)
		}
		set {
			if let newValue = newValue {
				viewDictionaryMap.setObject(newValue, forKey: self)
			}
			else {
				viewDictionaryMap.removeObject(forKey: self)
			}
		}
	}
}
``` 

これは、User Runtime Attribute で Dictionary を指定できないので、Storyboard 自体に情報を仕込む事はできませんが、コードで特定の view に `NSDictionary` を　`set`/`get` できるようになります。

時々、こんなようにストアドプロパティを追加する為だけにサブクラスを作りたくなる場合もあるかと思いますが、それだけの理由であれば、こんなトリックを使う方法もあるので紹介しておきます。

また、名前はあまり汎用的な名前を使うと、iOSの将来のバージョンに同じ名前のプロパティが追加される可能性があるので、注意して命名しましょう。

### Git

まぁ、小ネタなので、必要ないかもしれませんが、参考までに Git にサンプルを用意いたしました。

https://github.com/codelynx/NamedView


### Swift

執筆時に環境を記しておくとします。

```.console
Xcode Version 11.3 (11C29)
$ swift --version
Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15)
Target: x86_64-apple-darwin19.0.0
```