LoginSignup
13
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-16

名前付き UIView

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

// 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 を書いたので紹介いたします。

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」経由でも結構です。

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

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
    }
}

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

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

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 にサンプルを用意いたしました。

Swift

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

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
13
4
2

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
13
4