動的にAutolayout制約を設定する

エンジニアの徳光です。 最近はあまり使われなくなった技術な気もしますが、動的に生成したViewに対してAuto…

エンジニアの徳光です。

最近はあまり使われなくなった技術な気もしますが、動的に生成したViewに対してAutolayout制約を適用する方法を記述したいと思います。
今回は「DynamicGenerationVW」クラスとして作成し、そこに任意数の子Viewを追加表示できるようにします。
その子VIewに親Viewに対して制約をもたせているので、親Viewのサイズなどが変わっても追随されるという動きにします。

◆まずは子Viewとなるものを作成

生成させる子Viewを見やすくするために、色付き角丸で表示するようにしておきたいと思います。
見た目の初期化を「commonInit()」でやることにして、ソース記述からでも、InterfaceBuilder追加からでも呼び出すようにしています。

class ChiledVW: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }
    private func commonInit() {
        //角丸・枠線・背景色を設定する
        self.layer.cornerRadius = 8.0
        self.layer.borderWidth = 2.0
        self.layer.borderColor = UIColor.red.cgColor
        self.backgroundColor = UIColor.lightGray
    }
}

◆親Viewとなるものを作成

▼Autolayout制約を動的につける

下記の記述で、「btn」の上端と「self」の上端の間に、「sukima」な距離をとるような制約を定義しています。
そしてそれを self.addConstraint() で追加することで、制約を設定することができます。
こちらの引数は、InterfaceBuilderで制約を設定したことがあれば、容易に対応がわかるかと思います。

        let layoutConstraintUp = NSLayoutConstraint(
            item: btn,          attribute: .top,    relatedBy: .equal,
            toItem: self,       attribute: .top,
            multiplier: 1.0,    constant: 123.0 )

 

▼さらっと完成させる

・親Viewの中に10個の子Viewを貼り付けます
・子VIewは左から徐々に幅が広くなっていきます
・最初の子Viewは親との制約関係となりますが、2つ目以降は手前の子Viewとの制約関係となります
 →vwMaeを用いて、最初か2つ目以降かを表すとともに、手前の子Viewを保持させます

class DynamicGenerationVW: UIView {
    override func awakeFromNib() {
        super.awakeFromNib()
        self.backgroundColor = UIColor.darkGray
        self.clipsToBounds = true
        self.dispViews()
    }
    func dispViews() {
        let constraints: NSMutableArray = []
        var vwMae: UIView? = nil
        let sukima = CGFloat(6)  //各部品周囲の余白
        for num in 1...10 {
            let btn = ChiledVW(frame: CGRect.zero)
            self.addSubview(btn) //親(self)に追加しておく
            //======== ここから制約を動的に設定していく
            btn.translatesAutoresizingMaskIntoConstraints = false
            //===上下に張り付かせる(親の上端(下端)と、追加Viewの上端(下端)を、sukimaだけ開けて繋げる)
            constraints.add( NSLayoutConstraint(
                item: btn,       attribute: .top,        relatedBy: .equal,
                toItem: self,    attribute: .top,
                multiplier: 1.0,    constant: sukima) )
            constraints.add( NSLayoutConstraint(
                item: self,      attribute: .bottom,     relatedBy: .equal,
                toItem: btn,     attribute: .bottom,
                multiplier: 1.0,    constant: sukima) )
            //===左側に張り付かせる
            if vwMae == nil { //最初の項目は、親の左端と追加Viewの左端を、sukimaだけ開けて繋げる
                constraints.add( NSLayoutConstraint(
                    item: btn,       attribute: .leading,    relatedBy: .equal,
                    toItem: self,    attribute: .leading,
                    multiplier: 1.0,    constant: sukima) )
            } else { //2つ目以降の項目は、手前のViewの右端と追加Viewの左端を、sukimaだけ開けて繋げる
                constraints.add( NSLayoutConstraint(
                    item: btn,       attribute: .leading,    relatedBy: .equal,
                    toItem: vwMae,   attribute: .trailing,
                    multiplier: 1.0,    constant: sukima) )
            }
            //===追加Viewの横幅を設定する
            constraints.add( NSLayoutConstraint(
                item: btn,   attribute: .width,  relatedBy: .equal,
                toItem: nil, attribute: .notAnAttribute,
                multiplier: 1.0,    constant: CGFloat(num*5)) )
            vwMae = btn
        }
        self.addConstraints(constraints as! [NSLayoutConstraint]) //制約の追加
    }
}


 

▼ちょこっと修正する

「任意数の子View」とか書いちゃったのを思い出したので、ちょっこし追加します。
「@IBInspectable」でInterfaceBuilder側でも個数を定義可能にしつつ、dispViewに引数での指定も可能としています。
※親Viewをソース記述で追加した場合などに、個数をを設定するとともに描画させられるようにしています
self.numViewsに0が指定されていた場合には子Viewを追加しないためにreturnさせています。
(というか、returnしないと「for num in 1…0」で例外発生してしまいますし…)
キャプチャ画像では、2つの親ViewをIBで定義するとともに、それぞれ子Viewを7個、14個に指定しています。

class DynamicGenerationVW: UIView {
    @IBInspectable var numViews: Int = 0

    override func awakeFromNib() {
        super.awakeFromNib()
        //self.backgroundColor = UIColor.darkGray
        self.clipsToBounds = true
        self.dispViews()
    }
    func dispViews(_ num: Int? = nil) {
        if let _num = num { self.numViews = _num }
        if self.numViews <= 0 { return } //1以上ない場合にはViewを生成しniない
        let constraints: NSMutableArray = []
        var vwMae: UIView? = nil
        let sukima = CGFloat(6)  //各部品周囲の余白
        for num in 1...self.numViews {
            let btn = ChiledVW(frame: CGRect.zero)


 

※なんで ChiledVW が btn なのかというと、ボタンを動的生成させるために作っていた名残なのでした 😉

◆おまけ

画面を回転させると、下記のような警告がでてウザい場合には、Xcodeメニュー「Product」→「Scheme」→「Edit Scheme…」で「Run」を選び「Aguments」タブに含まれる「Environment Variables」に「OS_ACTIVITY_MODE」を追加して「disable」を設定しましょう。

if we’re in the real pre-commit handler we can’t actually add any new fences due to CA restriction