iOS/Auto Layout – Phần 3: Anatomy of a Constraint, cách để tạo constraints bằng code

by DaoNM2
1.1K views

Lời mở đầu

Bài viết này của mình có 2 nội dung chính. Đầu tiên mình sẽ nói về cấu tạo của một constraint để các bạn có thể hình dung được nó hoạt động như thế nào. Thứ hai mình sẽ nói về việc làm thế nào để tạo một constraint sử dụng code mà không cần dùng đến Interface buider.



Anatomy of a Constraint

Layout của view hierarchy được định nghĩa là một chuỗi các phương trình tuyến tính. Mỗi ràng buộc đại diện cho một phương trình duy nhất. Mục tiêu của bạn là khai báo một loạt các phương trình có một và chỉ một giải pháp khả thi.

Giờ chúng ta sẽ xem cấu tạo của một constraint nó như nào nhé:

Constraint này nói rằng cạnh trái của view màu đỏ(Red view’ leading edge) phải là 8 points sau cạnh phải của view màu xanh(Blue view’s trailing edge). Phương trình của nó có các phần như sau:

  • Item 1: Đây là phần đầu tiên trong phương trình, trong trường hợp này nó là View màu đỏ. Phần này phải là một View hoặc là một layout guide.
  • Attribute 1: Là thuộc tính được constraint trên Item 1. Trường hợp này nó là cạnh trái của Red View(Red view’s leading edge).
  • Relationship: Mối quan hệ giữa bên trái và bên phải của phương trình. Giá trị của relationship có thể thuộc 1 trong 3 giá trị: bằng, lớn hơn hoặc nhỏ hơn. Trong trường hợp này bên trái và bên phải bẳng nhau.
  • Multiplier: Hệ số giá trị của Attribute được nhân với số này. Trong trường hợp này hệ số nhân là 1.0. Trong các trường hợp mặc định hệ số này là 1.0, nên ta có thể bỏ đi nếu không muốn thay đổi giá trị này.
  • Item 2: Là phần thứ 2 trong phương trình. Trong trường hợp này nó là Blue View. Không giống với Item 1, nó có thể để trống.
  • Attribute 2: Là thuộc tính được constraint trên Item 2. Trong trường hợp này là cạnh phải của BlueView. Nếu Item 2 để trống thì Attribute 2 sẽ không tồn tại
  • Constant: Một hằng số bù trừ, trong trường hợp này nó là 8.0. Giá trị này được thêm vào giá trị của thuộc tính 2(Value of Attribute 2).

Hầu hết các constraint xác định một mối quan hệ giữa hai items trong giao diện người dùng. Những constraint này có thể đại diện cho các View hoặc Layout guide. Các Constraint cũng có thể xác đinh mối quan hệ giữa hai thuộc tính khác nhau của một item.
Ví dụ: bạn có thể đặt Aspect ratio giữa chiều cao của 1 item với chiều rộng của nó. Bạn cũng có thể gán giá trị hằng số cho chiều cao hoặc chiều rộng của Item đó. Khi làm việc với giá trị Constant, Item 2 được để trống thì attribute 2 sẽ được gán là không phải thuộc tính và multiplier được gán là 0.0

Auto Layout Attributes

Trong Auto Layout, các thuộc tính được xác định là một tính năng có thể được ràng buộc(Constraint). Thông thường, nó bao gồm 4 cạnh: Trên(Top), Dưới(bottom), Trái(leading), Phải(trailing), cũng như chiều cai, chiều rộng và căn giữa theo chiều dọc và ngang. Các text cũng có một hoặc nhiều thuộc tính baseline.

Các bạn có thể xem thêm NSLayoutAttribute enum.

Cách tạo constraint bằng code

Auto layout constraints cho phép chúng ta tạo ra các view tự động điều chỉnh theo các Size Class và Vị trí khác nhau. Các constraint sẽ đảm bảo view của bạn điều chỉnh phù hợp với bất kỳ thay đổi kích thước nào mà không phải cập nhật thủ công các khung hoặc vị trí của nó.

Tuy nhiên, ngoài việc sử dụng Interface Builder để Auto Layout thì chúng ta có thể sử dụng code để Auto Layout.

Việc viết AutoLayout có một số ưu điểm và nhược điểm như sau:
Ưu điểm:
– Dễ dàng merge code khi dùng Git
– Dễ debug
– Các constraint có thể dễ nhìn hơn
Nhược điểm:
– Không có đại diện trực quan. Việc này khiến bạn phải tưởng tượng thay vì như IB cho ta nhìn thấy Views của mình trên màn hình ở nhiều kích thước khác nhau.
– Bạn có thể sẽ phải viết nhiều code layout lên trên ViewController của mình.

Theo mình để có thể làm tốt Auto Layout bằng code thì mình nghĩ các bạn nên sử dụng thành thạo Interface Builder trước.
Việc kết hợp giữa Interface Builder và constraint trong code cũng có thể là một giải pháp tốt. Tuy nhiên, hãy luôn nhớ rằng việc đó sẽ khiến nó trở nên khó hiểu hơn.

Tạo constraints bằng cách sử dụng Layout Anchors

Việc đầu tiên chúng ta cần làm là phải set thuộc tính translatesAutoresizingMaskIntoConstraints thành false. Việc này nhằm mục đích ngăn các view’s auto-resizing mask được chuyển sang Auto Layout constraints và ảnh hưởng đến các constraint của bạn.

Tiếp theo chúng ta sẽ bắt đầu tạo một mảng chứa các constraints. Trong mảng này chúng ta sẽ định nghĩa các constraints mà bạn muốn gắn nó cho một View nào đó.

Trong trường hợp này mình muốn tạo ra 1 view màu đỏ có width = 200, height = 200 và căn giữa màn hình. Vì vậy mình sẽ tạo ra một mảng constraint như dưới đây.

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        let myView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
        myView.backgroundColor = .red
        view.addSubview(myView)
        myView.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            myView.centerYAnchor.constraint(equalTo: view!.centerYAnchor),
            myView.centerXAnchor.constraint(equalTo: view!.centerXAnchor),
            myView.widthAnchor.constraint(equalToConstant: 200.0),
            myView.heightAnchor.constraint(equalToConstant: 200.0)
        ]

        NSLayoutConstraint.activate(constraints)
    }

Đó là những dòng code cơ bản để có thể tạo constraint cho 1 view bằng code. Và nó khá dễ đọc và dễ hiểu.
Dòng cuối cùng của đoạn code trên nhằm mục đích active một chuỗi các constraint mà bạn tạo ra.

Kết quả chúng ta thu được sẽ như hình dưới đây:

UIView cung cấp cho chúng ta 1 tập hợp các thuộc tính neo cho phép bạn thiết lập các mối quan hệ giữa các View với nhau.
Dưới đây là danh sách các thuộc tính neo:

extension UIView {

    /* Constraint creation conveniences. See NSLayoutAnchor.h for details.
     */
    @available(iOS 9.0, *)
    open var leadingAnchor: NSLayoutXAxisAnchor { get }

    @available(iOS 9.0, *)
    open var trailingAnchor: NSLayoutXAxisAnchor { get }

    @available(iOS 9.0, *)
    open var leftAnchor: NSLayoutXAxisAnchor { get }

    @available(iOS 9.0, *)
    open var rightAnchor: NSLayoutXAxisAnchor { get }

    @available(iOS 9.0, *)
    open var topAnchor: NSLayoutYAxisAnchor { get }

    @available(iOS 9.0, *)
    open var bottomAnchor: NSLayoutYAxisAnchor { get }

    @available(iOS 9.0, *)
    open var widthAnchor: NSLayoutDimension { get }

    @available(iOS 9.0, *)
    open var heightAnchor: NSLayoutDimension { get }

    @available(iOS 9.0, *)
    open var centerXAnchor: NSLayoutXAxisAnchor { get }

    @available(iOS 9.0, *)
    open var centerYAnchor: NSLayoutYAxisAnchor { get }

    @available(iOS 9.0, *)
    open var firstBaselineAnchor: NSLayoutYAxisAnchor { get }

    @available(iOS 9.0, *)
    open var lastBaselineAnchor: NSLayoutYAxisAnchor { get }
}

Với mỗi Anchor nó trả về các subclass từ NSLayoutAnchor đi kèm với một số phương thức phổ biến để thiết lập mối quan hệ. Nó bao gồm =, >, <, >=, <=. Cũng giống như khi chúng ta dùng Interface Builder. Đây là tài liệu để làm quen về nó Documents.

NOTE: Như các bạn đã thấy, nó chỉ hỗ trợ từ iOS 9, Hầu hết các ứng dụng bây giờ đều sẽ hỗ trợ từ iOS 9 trở lên, vì vậy chúng ta không cần phải lo lắng quá nhiều về nó.

Order of constraints

Khi các bạn bắt đầu viết các constraint của mình, điều quan trọng là bạn phải nhớ thứ tự của các constraint của bạn khi bạn làm việc với các constants.

let innerView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
let outerView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
innerView.backgroundColor = .red
outerView.backgroundColor = .blue

view.addSubview(outerView)
outerView.addSubview(innerView)
innerView.translatesAutoresizingMaskIntoConstraints = false
outerView.translatesAutoresizingMaskIntoConstraints = false
let constraintsOuter = [
    outerView.centerYAnchor.constraint(equalTo: view!.centerYAnchor),
    outerView.centerXAnchor.constraint(equalTo: view!.centerXAnchor),
    outerView.widthAnchor.constraint(equalToConstant: 200.0),
    outerView.heightAnchor.constraint(equalToConstant: 200.0)
]

NSLayoutConstraint.activate(constraintsOuter)

let constraints = [
    innerView.topAnchor.constraint(equalTo: outerView.topAnchor),
    innerView.leftAnchor.constraint(equalTo: outerView.leftAnchor, constant: 40),
    innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor),
    innerView.rightAnchor.constraint(equalTo: outerView.rightAnchor, constant: -40)
]

NSLayoutConstraint.activate(constraints)

Kết quả như hình dưới đây:

Mục đích của đoạn code này là tạo ra mảng constraint cho innerView sao cho top của nó bằng top của của outerView, bot = bot của outerView, và cách đều 2 bên = 40 points.
let constraints = [
innerView.topAnchor.constraint(equalTo: outerView.topAnchor),
innerView.leftAnchor.constraint(equalTo: outerView.leftAnchor, constant: 40),
innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor),
innerView.rightAnchor.constraint(equalTo: outerView.rightAnchor, constant: -40)
]
Ở dòng được highlight kia giá trị của constant phải = -40. Vì lúc này vị trí của outerView đã được cố định, và outerView.rightAnchor lúc này được coi là gốc(Ox) của trục tọa độ. Vậy nên vị trí bên trái của gốc tọa độ Ox ta hiểu là giá trị âm và ngược lại.
Ta có thể đổi ngược lại vị trí của innerView và outerView để được kết quả như trên hình theo đoạn có dưới đây:( Không nên viết theo kiểu này mà nên viết theo kiểu trên để dễ nhìn hơn)
outerView.rightAnchor.constraint(equalTo: innerView.rightAnchor, constant: 40)

Tương tự đối với top và bottom là trục Oy hướng xuống dưới, phía trên của gốc Oy là giá trị âm và ngược lại.

NOTE: Khi các bạn thêm các constraint bằng code, các bạn nên làm theo thứ tự để code của ta dễ kiểm soát và dễ hiểu hơn.

Một số Layout guides hay sử dụng

UIVew cũng có một vài Layout guides có thể được sử dụng làm neo như sau:

  • layoutMarginGuide: Đặt các constraint và giữ lề của layout bằng 1 khoảng trống cố định (16 points).
  • readableContentGuide: Constraints chiều rộng của view giúp người dùng dễ đọc.
  • safeAreaLayoutGuide: Đặt các constraint giúp view của bạn không bị che bởi các thanh và nội dung khác.

Đây là đoạn code mẫu sử dụng Layout guide(safeAreaLayoutGuide)

let constraints = [
    outerSquare.topAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.topAnchor),
    outerSquare.leftAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.leftAnchor),
    outerSquare.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    outerSquare.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)
]

Hỗ trợ ngôn ngữ từ Phải qua trái

Có vẻ rất rõ ràng khi bạn sử dụng leftAnchor và rightAnchor, nhưng bạn sẽ phải nghĩ về việc sử dụng leadingAnchor và trailingAnchor để thay thế. Nhằm mục đích hỗ trợ các ngôn ngữ từ phải qua trái. Điều này rất quan trọng khi sử dụng views như là Labels trong trường hợp bạn muốn chúng được lật lại cho các ngôn ngữ từ phải sang trái.



Phần kết

Mình hi vọng bài viết này giúp các bạn có thể tạo các constraints bằng code sẽ dễ dàng hơn.
Nó sẽ là một giải pháp thay thế tuyệt vời để thiết lâp các constraints khi bạn không muốn dùng Interface Builder.

1 comment

Đỗ Thành Đạt October 18, 2022 - 10:42 AM

Rất hay thank bạn.

Reply

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

%d bloggers like this: