[swift] UIView의 속성확장

개요top

UIView는 별도의 클래스를 만들지 않는 이상 고작 tag라는 Int값 하나를 추가적인 식별자로 줄 수 있을 뿐 일체의 정보를 저장할 수 있는 룸이 없습니다.
이를 어떻게든 극복하여 UIView에 추가정보를 기술하고 더 나아가 이를 IB에서 처리할 수 있게 하는데 목표를 두고 있습니다.

extension의 제약 벗어나기top

extension은 이미 정의된 클래스에 기능을 붙이기 좋은 방법이지만 값 속성을 추가할 수 없다는 제약이 있습니다. 대신 계산된 속성은 추가할 수 있으므로 별도의 저장소를 만들어서 이를 연동하게 처리할 수 있습니다. 우선 UIView의 속성을 확장하기 위해서는 이 점을 이용해서 벗어나야합니다.
즉 직접 UIView에 쓰는 것은 불가능하므로 다른 공간에 값을 기록하되 개별 UIView의 고유한 값을 이용해 연결하는 방식을 사용해야합니다.
다행히 UIView는 NSObject이므로 hashtable을 구상하고 있습니다. 모든 UIView의 인스턴스는 개별로 고유한 Int값을 갖고 있는 것이죠.

위의 이야기에 근거해 일단 어떤 타입의 값을 저장한다고 하면 [Int:T] 를 생각해볼 수 있습니다. 하지만 추가할 필드가 여러 개일 것을 예상한다면 필드명마다 저걸 저장해야하니 [String:[Int:T]] 가 될 걸로 예상이 되죠.

헌데 약간의 문제가 있습니다. 딕셔너리를 값이므로 [String]을 통해 접근하는 [Int:T]는 매번 복사본을 반환하게 됩니다. 이를 방지하려면 어쩔 수 없이 NSMutableDictionary에 [Int:T]를 넣는 방식으로 처리해야합니다…만!

NSMutableDictionary는 Any타입을 받아주지 않으므로 딕셔너리를 그냥 넣을 수는 없습니다. 따라서 이를 다시 래핑한 클래스를 통해 넣어줘야겠죠.

이상에서 두 개의 클래스를 도출할 수 있고 [Int:T]로부터 제네릭클래스가 되어야함을 알 수 있습니다.

//[Int:T] 를 대체하는 클래스
class ViewValue<T>{
	var prop = [Int:T]()
	subscript(key:Int)->T?{
		get{return prop[key]} //제네릭이므로 이시점에 ??패치는 불가능
		set{prop[key] = newValue}
	}
}

//속성명별로 저장하는 클래스
class ViewProp<T>{
	var prop = NSMutableDictionary()
	subscript(key:String)->ViewValue<T>{
		get{
			if prop[key] == nil{prop[key] = ViewValue<T>()}
			return prop[key] as! ViewValue<T>
		}
	}
}

위 주석에서 언급한 것처럼 T로 형이 오기 때문에 안타깝게 ViewValue시점에 옵셔널을 제거할 수는 없습니다. 제네릭 클래스는 내부 클래스를 포함할 수 없으므로 extension이나 ViewProp내부에 ViewValue를 둘 수는 없습니다. 어쩔 수 없이 전역공간에 클래스가 선언되어야 합니다.

속성 추가 및 IB에서 편집하기top

이제 속성을 저장할 수 있는 저장소를 UIView 내부에 마련해줍니다.

extension UIView{
	static private let customProp = ViewProp<String>()	
}

이제 내부에 마련된 custromProp를 저장소로 해서 계산된 속성을 추가할 수 있습니다. IB에서도 편집가능하도록 @IBInspectable 어노테이션을 붙여줍니다. 간단히 name과 comment 속성을 추가해보죠.

extension UIView{

	@IBInspectable var name:String{
		//이 시점에서 옵셔널언랩을 처리함
		get{return UIView.customProp["name"][hash] ?? ""}
		set{UIView.customProp["name"][hash] = newValue}
	}

	@IBInspectable var comment:String{
		get{return UIView.customProp["comment"][hash] ?? ""}
		set{UIView.customProp["comment"][hash] = newValue}
	}
}

IB를 열어 아무 컴포넌트나 뷰에 끌어와서 인스펙터를 보면 다음과 같은 그림이 될 것입니다.

1

IB와 연동하지 않는 다양한 사용top

IB와 연동하는 경우는 String으로 한정되지만 실은 일반적인 목적으로 저장하기 위해서는 다양한 형을 제네릭으로 연동하면 됩니다. 예를들어 개별 UIView에 딕셔너리를 저장하고 싶다면 다음과 같이 확장할 수 있을 것입니다.

extension UIView{
	static private let customPropDic = ViewProp<[String:String]>()
	static private let emptyDic = ["":""] //옵셔널 언래핑용
 
	var dic:[String:String]{
		get{return UIView.customPropDic["name"][hash] ?? UIView.emptyDic}
		set{UIView.customPropDic["name"][hash] = newValue}
	}
}

class ViewController: UIViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
		
		view.subviews[0].dic = ["a":"test1", "b":"test2"]

		print("\(view.subviews[0].dic["a"])") //"test1"
	}
}

IB와 연동하지 않더라도 다양한 목적으로 특정 UIView를 확장하여 사용할 수 있게 합니다.

결론top

IB에서 설정한 값은 subviews를 순회하면서 뷰를 특정짓거나 컨트롤러에서 후처리작업의 힌트가 되는 등 다양한 용도로 사용됩니다.
특히 제 경우는 action이나 outlet의 정적 바인딩을 피하고 유연한 동적 바인딩을 위한 힌트로 사용합니다. 전체 코드는 다음과 같습니다.

class ViewValue<T>{
	var prop = [Int:T]()
	subscript(key:Int)->T?{
		get{return prop[key]}
		set{prop[key] = newValue}
	}
}
class ViewProp<T>{
	var prop = NSMutableDictionary()
	subscript(key:String)->ViewValue<T>{
		get{
			if prop[key] == nil{prop[key] = ViewValue<T>()}
			return prop[key] as! ViewValue<T>
		}
	}
}
extension UIView{
	static private let customProp = ViewProp<String>()
	@IBInspectable var name:String{
		get{return UIView.customProp["name"][hash] ?? ""}
		set{UIView.customProp["name"][hash] = newValue}
	}

	static private let customPropDic = ViewProp<[String:String]>()
	static private let emptyDic = ["":""]
 
	var dic:[String:String]{
		get{return UIView.customPropDic["name"][hash] ?? UIView.emptyDic}
		set{UIView.customPropDic["name"][hash] = newValue}
	}
}