7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Swift]NSLayoutConstraintとNSLayoutAnchorを学ぶ過程を記録してみた

Posted at

コードでAutoLayoutを実装するには?

実装するためには下記2つの知識が必要そうだ。

  • NSLayoutConstraint
  • NSLayoutAnchor

NSLayoutConstraintとは?

公式には下記のように記載があった。

The relationship between two user interface objects that must be satisfied by the constraint-based layout system.

NSLayoutConstraintを使用すると、オブジェクト間の配置やサイズを設定することができる。
使い方については、下記記事がとてもわかりやすかった!
特に"制約"と"制約を考える上で"はNSLayoutConstraintを理解しやすくしてくれる!

NSLayoutAnchorとは?

公式には下記のように記載があった。

Use these constraints to programatically define your layout using Auto Layout. Instead of creating NSLayoutConstraint objects directly, start with a UIView, NSView, or UILayoutGuide object you wish to constrain, and select one of that object’s anchor properties. These properties correspond to the main NSLayoutConstraint.Attribute values used in Auto Layout, and provide an appropriate NSLayoutAnchor subclass for creating constraints to that attribute. Use the anchor’s methods to construct your constraint.

NSLayoutAnchorを使用することで、より簡潔にオブジェクト間の制約を作成することができる。
使い方については、下記記事がとてもわかりやすかった!
"NSLayoutAnchorの使い方、制約の作り方"の部分でNSLayoutConstraintと比較して書かれているので違いがわかりやすい!

NSLayoutConstraintとNSLayoutAnchorの違いとは?

ChatGPTに違いを聞いてみた際の返答がとてもわかりやすかったので記録しておく!

NSLayoutConstraintとNSLayoutAnchorは、どちらもiOSやMacアプリケーション開発において制約ベースのレイアウトを実現するためのツールですが、使用方法や表現方法においていくつかの違いがあります。

NSLayoutConstraintは、制約を作成するためのクラスです。制約を作成する際には、NSLayoutConstraintクラスのインスタンスを生成し、制約を指定します。制約は、ビューやコンポーネントの属性(位置、サイズなど)を他のビューやコンポーネントに関連付けることで定義されます。

一方、NSLayoutAnchorは、制約を作成するための便利なメソッドを提供するクラスです。NSLayoutAnchorを使用すると、より簡潔かつ直感的なコードで制約を指定することができます。NSLayoutAnchorは、ビューやコンポーネントの特定の属性(たとえば、topAnchor、leadingAnchor、widthAnchorなど)を表すアンカープロパティを提供します。これらのアンカープロパティを使用して、ビュー間の関係を記述する制約を簡単に作成できます。

NSLayoutConstraintとNSLayoutAnchorは、同じ目的を達成するための異なるアプローチです。NSLayoutConstraintはより柔軟で広範な制約を作成するために使用されますが、コードがやや冗長になる場合があります。一方、NSLayoutAnchorは、より簡潔で読みやすいコードを実現するために使用されますが、いくつかの制約の種類に制約されることがあります。

どちらを使用するかは、個々の開発者やプロジェクトの好みや要件によって異なります。NSLayoutConstraintはより広範な制約を作成するための柔軟性がありますが、NSLayoutAnchorはコードの可読性を向上させることができます。

実際にコードを書いていく

使い方を知れたところで実際に使って慣らしていく!

準備

新規プロジェクトを作成した場合、Storyboardが初期画面になっているので、作成したViewControllerを初期画面に設定する必要がある。設定手順は下記になる。

1. Storyboardを削除する
2. Main Interfaceを空白にする
3. Info.plistのStoryboard Nameの項目を削除する
4. SceneDelegate.swiftを編集する

手順1から手順4については、こちらを参考にする。
同じ題名の箇所をそのまま追っていけば設定が完了する。写真もついているのでわかりやすい!

手順4について、ViewControllerを新規作成した場合は、下記のコードを適宜変更する。

SceneDelegate
if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    // 新規作成の場合
    window.rootViewController = SampleViewControlle()
    self.window = window
    window.makeKeyAndVisible()
}

UIWindowScene、UIWindow、Sceneとは?
何をしているのかが知りたい場合は、こちらを参考にすると良い!
難しく感じてしまうけど、知っておいた方が良い情報がたくさん詰まっている!

5. ViewControllerのViewに背景色をつける
ViewControllerを継承すると、Viewの背景色がUIColor.clearに設定されているので、起動時に画面が黒くなってしまう。(xibファイルを使用している場合はUIColor.systemBackgroundに設定してくれてたみたい。)
それを回避するためにView.backgroundColorを下記のように設定する。

SampleViewController
// rootViewControllerに設定したクラス
class SampleViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // ここで背景色を設定
        self.view.backgroundColor = UIColor.systemBackground
    }
}

UIViewControllerを継承すると、なぜUIViewがデフォルトで設定されるのかについて知りたい場合は、こちらを参考にすると良い!特に図1-1 ビュー コントローラーとそのビューの関係をみるとイメージがつきやすい!

6. ビルドしてViewControllerが表示されているか確認する
実際にビルドして確認してみる。指定した背景色で画面が表示されていれば初期画面の変更が成功している。

実践1

画面上部に引っ付いている、横幅いっぱいのredView(参考はこちら)

Sample1ViewController
import UIKit

class Sample1ViewController: UIViewController {
    
    private var redView: UIView = UIView()
    
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        self.view.backgroundColor = UIColor.systemBackground
        setUpRedView()
    }
    
    private func setUpRedView() {
        // ①
        redView.translatesAutoresizingMaskIntoConstraints = false
        redView.backgroundColor = UIColor.red
        // ②
        self.view.addSubview(redView)
        
        self.view.addConstraints([
            // self.viewの上から0pxの位置に配置
            NSLayoutConstraint(item: redView,
                               attribute: .top,
                               relatedBy: .equal,
                               toItem: self.view,
                               attribute: .top,
                               multiplier: 1,
                               constant: 0
                              ),
            // self.viewの横幅いっぱいにする
            NSLayoutConstraint(item: redView,
                               attribute: .width,
                               relatedBy: .equal,
                               toItem: self.view,
                               attribute: .width,
                               multiplier: 1,
                               constant: 0
                              ),
            // self.viewのレイアウトに関わらず高さは64px
            NSLayoutConstraint(item: redView,
                               attribute: .height,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .height,
                               multiplier: 1,
                               constant: 64
                              )
        ])
    }
}

制約をつける処理については、説明箇所で紹介した記事を参考にしてもらえれば問題ないと思う。他の大事な箇所について番号をつけたので、順番に説明していく。
①は、AutoLayoutを設定する場合に必須となる。下記は公式に記載されている内容である。

translatesAutoresizingMaskIntoConstraints

If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.

プログラムで作成したViewのtranslatesAutoresizingMaskIntoConstraintsプロパティはtrueに設定される。 Interface BuilderでViewを追加すると、システムはこのプロパティを自動的にfalseに設定する。つまり、今回はコードで作成したViewになるので、translatesAutoresizingMaskIntoConstraintstrueの状態である。trueのままAutoLayoutを設定すると、デフォルトで設定されるAutoResizingMaskの
制約とコンフリクトしてしまい、レイアウトに関して問題が発生する可能性がある。そのため、falseに設定し、AutoResizingMaskの
制約を無効化する必要がある。

AutoresizingMaskについては、公式に下記のように記載されている。

autoresizingMask

When a view’s bounds change, that view automatically resizes its subviews according to each subview’s autoresizing mask. You specify the value of this mask by combining the constants described in UIView.AutoresizingMask using the C bitwise OR operator. Combining these constants lets you specify which dimensions of the view should grow or shrink relative to the superview. The default value of this property is none, which indicates that the view should not be resized at all.

For example, suppose this property includes the flexibleWidth and flexibleRightMargin constants but does not include the flexibleLeftMargin constant, thus indicating that the width of the view’s left margin is fixed but that the view’s width and right margin may change. Thus, the view appears anchored to the left side of its superview while both the view width and the gap to the right of the view increase.

つまり、コードを下記のように記述することによって、親Viewに合わせてViewの幅や高さを変更することができるということ。下記のコードでは、高さと幅を親Viewに合わせて変更できるようにしている。

view.autoresizingMask = [.flexibleHeight, .flexibleWidth]

もっと詳しく動きを知りたい場合は、下記記事を読んでみると良い!
https://www.advancedswift.com/autolayout-vs-autoresizing-masks/
https://developer.apple.com/documentation/uikit/uiview/1622559-autoresizingmask

②は、Viewに対してredViewを追加している。必ずAutoLayoutの制約をつける前に、この処理を行うこと!
前後が逆になってしまうとエラーが出てしまうので注意!

実践2

redViewの上から10px左から10pxの位置にあり、縦横44pxのblueButton(参考はこちら)

Sample1ViewController
class Sample1ViewController: UIViewController {
    // 省略
    private var greenView: UIView = UIView()
    
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        // 省略
        setUpBlueButton()
    }
    private func setUpBlueButton() {
        blueButon.translatesAutoresizingMaskIntoConstraints = false
        blueButon.backgroundColor = UIColor.blue
        redView.addSubview(blueButon)
        
        redView.addConstraints([
            // 上から10px
            NSLayoutConstraint(item: blueButon,
                               attribute: .top,
                               relatedBy: .equal,
                               toItem: redView,
                               attribute: .top,
                               multiplier: 1,
                               constant: 10
                              ),
            // 左から10px
            NSLayoutConstraint(item: blueButon,
                               attribute: .leading,
                               relatedBy: .equal,
                               toItem: redView,
                               attribute: .leading,
                               multiplier: 1,
                               constant: 10
                              ),
            // 幅44px
            NSLayoutConstraint(item: blueButon,
                               attribute: .width,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .width,
                               multiplier: 1.0,
                               constant: 44
                              ),
            // 高さ44px
            NSLayoutConstraint(item: blueButon,
                               attribute: .height,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .height,
                               multiplier: 1.0,
                               constant: 44)
        
        ])
    }
}

実践1までの内容がわかっていれば問題なくできる!

実践3

redViewの中央にある、横幅がredViewの横幅によって動的に変化し、高さが44pxのgreenView(参考はこちら)
redView.width:320px、greenView.width:100pxの比率を保ような横幅

Sample1ViewController
class Sample1ViewController: UIViewController {
    // 省略
    private var greenView: UIView = UIView()
    
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        // 省略
        setUpGreenView()
    }

    private func setUpGreenView() {
        greenView.translatesAutoresizingMaskIntoConstraints = false
        greenView.backgroundColor = UIColor.green
        redView.addSubview(greenView)
        
        redView.addConstraints([
            // CenterXが同じ
            NSLayoutConstraint(item: greenView,
                               attribute: .centerX,
                               relatedBy: .equal,
                               toItem: redView,
                               attribute: .centerX,
                               multiplier: 1,
                               constant: 0
                              ),
            // CenterYが同じ
            NSLayoutConstraint(item: greenView,
                               attribute: .centerY,
                               relatedBy: .equal,
                               toItem: redView,
                               attribute: .centerY,
                               multiplier: 1,
                               constant: 0
                              ),
            // redView.width:320のときgreenView:100の比率
            NSLayoutConstraint(item: greenView,
                               attribute: .width,
                               relatedBy: .equal,
                               toItem: redView,
                               attribute: .width,
                               multiplier: 100.0 / 320.0,
                               constant: 0
                              ),
            // 高さ44px
            NSLayoutConstraint(item: greenView,
                               attribute: .height,
                               relatedBy: .equal,
                               toItem: nil,
                               attribute: .height,
                               multiplier: 1,
                               constant: 44
                              )
        ])
    }
}

実践1までの内容がわかっていれば問題なくできる!

実践4

完成図(図のようにコーディングできればOK)

Sample2ViewController
import UIKit

/// NSLayoutAnchorを学ぶためのクラス
class Sample2ViewController: UIViewController {
    
    private var greenView = UIView()
    private var baseStackView = UIStackView()
    private var buttonStackView = UIStackView()
    private var blueButton = UIButton()
    private var redButton = UIButton()
    private var purpleLabel = UILabel()
    
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        self.view.backgroundColor = UIColor.systemBackground
        setUpBaseStackView()
        setUpButtonStackView()
        setUpBlueButton()
        setUpRedButton()
        setUpGreenView()
        setUpPurpleLabel()
    }
    
    private func setUpBaseStackView() {
        baseStackView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(baseStackView)
        // ①
        baseStackView.addArrangedSubview(greenView)
        baseStackView.addArrangedSubview(buttonStackView)
        // ②
        baseStackView.spacing = 30
        baseStackView.distribution = .fill
        baseStackView.alignment = .fill
        baseStackView.axis = .vertical
        
        baseStackView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100).isActive = true
        baseStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20).isActive = true
        baseStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20).isActive = true
        baseStackView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -100).isActive = true
    }
    
    private func setUpButtonStackView() {
        buttonStackView.translatesAutoresizingMaskIntoConstraints = false
        buttonStackView.addArrangedSubview(blueButton)
        buttonStackView.addArrangedSubview(redButton)
        buttonStackView.spacing = 10
        buttonStackView.distribution = .fillEqually
        buttonStackView.alignment = .fill
        
        buttonStackView.heightAnchor.constraint(equalToConstant: 44).isActive = true
        buttonStackView.leadingAnchor.constraint(equalTo: baseStackView.leadingAnchor).isActive = true
        buttonStackView.trailingAnchor.constraint(equalTo: baseStackView.trailingAnchor).isActive = true
        buttonStackView.bottomAnchor.constraint(equalTo: baseStackView.bottomAnchor).isActive = true
    }
    
    private func setUpBlueButton() {
        blueButton.translatesAutoresizingMaskIntoConstraints = false
        blueButton.backgroundColor = UIColor.blue
        blueButton.tintColor = UIColor.white
        blueButton.setTitle("BlueButton", for: .normal)
    }
    
    private func setUpRedButton() {
        redButton.translatesAutoresizingMaskIntoConstraints = false
        redButton.backgroundColor = UIColor.red
        redButton.tintColor = UIColor.white
        redButton.setTitle("RedButton", for: .normal)
    }
    
    private func setUpGreenView() {
        greenView.translatesAutoresizingMaskIntoConstraints = false
        greenView.backgroundColor = UIColor.green
        
        greenView.topAnchor.constraint(equalTo: baseStackView.topAnchor).isActive = true
        greenView.leadingAnchor.constraint(equalTo: baseStackView.leadingAnchor).isActive = true
        greenView.trailingAnchor.constraint(equalTo: baseStackView.trailingAnchor).isActive = true
    }
    
    private func setUpPurpleLabel() {
        purpleLabel.translatesAutoresizingMaskIntoConstraints = false
        purpleLabel.backgroundColor = UIColor.purple
        purpleLabel.text = "PurpleLable"
        purpleLabel.textAlignment = .center
        purpleLabel.textColor = UIColor.white
        greenView.addSubview(purpleLabel)
        
        purpleLabel.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 20).isActive = true
        purpleLabel.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 100).isActive = true
        purpleLabel.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -100).isActive = true
        purpleLabel.heightAnchor.constraint(equalToConstant: 30).isActive = true
    }
    
}

①について、StackViewにViewを追加する場合は、addSubviewではなく、addArrangedSubviewで追加をする。②について、StackViewの詳細な設定をここで行なっている。垂直にしたい場合や、大きさを同じにしたいなどの希望を叶えてくれる!

まとめ

実践はまだやっていこうと思うので、完成したら都度追加していきます!
ここまでご覧いただきありがとうございました!
また、優れた知識と経験を共有してくださる皆様に感謝です!本当にありがとうございました!

7
8
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?