iOS 開發中 Storyboard 的運用非常的常見,所以通過代碼設置 AutoLayout 的場景會比較少。但是不代表沒有。
需要純代碼構建界面(公司要求,創建 UI 控件) 需要根據屏幕方向或數據內容提供不同的約束方案。可是蘋果提供的 Autolayout 語法真是要讓人抓狂的節奏。於是衍生了很多類庫,如
Stevia Masonry …另外還有 MyLinearLayout ,這是基於 frame 的自動布局方案。
每一個方案都有自己不同的解決方法,有側重於代碼直觀可讀的,有側重於代碼簡潔的。我自己比較喜歡能讓代碼變得更加簡潔,當然這是在保證可讀性的情況下的。
所以我就這一塊進行比較深入的了解,並造一個輪子作為自己的學習成果。
NSLayoutConstraint(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute, multiplier multiplier: CGFloat, constant c: CGFloat)
整個輪子就圍繞著這一個核心方法來進行。
基礎屬性將多個相關操作通過 (.) 號連接起來,增加代碼塊的邏輯性以及可讀性。最簡單的實現辦法,就是將函數的返回值設置為返回對象本身。
infix operator +-: AdditionPrecedence
操作符的位置(前中後綴) operator 操作符: 操作符優先級組
extension Vector2D { static func +- (left: Vector2D, right: Vector2D) -> Vector2D { return Vector2D(x: left.x + right.x, y: left.y - right.y) } }
在 Swift 3 之前,操作符的運算還只能添加到全局, Swift 3 已經可以添加到類當中了,不過需要用 static 前綴。(那麼還是一個全局……但是這樣的改進對代碼的易讀性會更好,畢竟一看就知道這是有關於哪一個類的運算。)
不過差別還是有的,添加到類當中的運算必須要有該類作為參數之一。這其實還是很好理解其中的邏輯的,畢竟跟它沒關系你添加到它裡面干嘛呢……
Apple: The Swift Programming Language(Swift 3) -> Language Guide -> Advanced Operators -> Custom Operators
操作符優先級蘋果的標准操作符等級
Swift Standard Library Operators
添加自定義操作符等級
precedencegroup precedence group name {
higherThan: lower group names
lowerThan: higher group names
associativity: associativity
assignment: assignment
}
Apple: The Swift Programming Language(Swift 3) -> Language Peference -> Declarations -> Precedence Group Declaration
進行 AutoLayout 的時候,有幾個元素是需要考慮清楚的。
Who對此我做了兩方面的嘗試:
鏈式:通過鏈式給視圖添加約束,大概是這樣Layout(view, item, second).leading().top().trailing().bottom()重寫操作符:通過操作符直觀的表達約束關系
Layout(view).auto { item.top == second.top * 1.5 + 10 ... }
兩者的優缺點都有吧,鏈式可以極大的縮短代碼的長度,操作符可以直觀的“看到”約束是怎麼樣的,而且也比較好玩,但是代碼量卻不少。到底是更短的代碼,還是更易讀的代碼……這是一個需要好好平衡的問題。
super: UIView? weak var view: UIView! var attribute: NSLayoutAttribute var constant: CGFloat = 0 var multiplier: CGFloat = 1 var priority: UILayoutPriority? Init 重寫操作符
== <= >= : 添加運算 / : 設置 multiplier : 設置 constant >>> : 設置 priority 擴展 UIView 添加相應約束的屬性,造成可以直接 UIView 設置約束的錯覺。
實現
//
// Layout.swift
// Layout
//
// Created by 黃穆斌 on 16/9/24.
// Copyright ? 2016年 MuBinHuang. All rights reserved.
//
import UIKit
// MARK: - Layout
class Layout {
// MARK: Views
weak var view: UIView!
weak var first: UIView!
weak var second: UIView!
// MARK: Init
init(_ view: UIView) {
self.view = view
}
// MARK: Set View
func view(_ first: UIView) -> Layout {
self.first = first
self.first.translatesAutoresizingMaskIntoConstraints = false
if self.second == nil {
self.second = self.view
}
return self
}
func to(_ second: UIView) -> Layout {
self.second = second
return self
}
// MARK: Constraints
var constrants: [NSLayoutConstraint] = []
@discardableResult
func get(layouts: ([NSLayoutConstraint]) -> Void) -> Layout {
layouts(constrants)
return self
}
@discardableResult
func clear() -> Layout {
constrants.removeAll(keepingCapacity: true)
return self
}
// MARK: Custom Edge Methods
@discardableResult
func layout(edge: NSLayoutAttribute, to: NSLayoutAttribute? = nil, constant: CGFloat = 0, multiplier: CGFloat = 1, priority: UILayoutPriority = UILayoutPriorityDefaultHigh, related: NSLayoutRelation = .equal) -> Layout {
let layout = NSLayoutConstraint(item: first, attribute: edge, relatedBy: related, toItem: second, attribute: to ?? edge, multiplier: multiplier, constant: constant)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func equal(edges: NSLayoutAttribute...) -> Layout {
for edge in edges {
let layout = NSLayoutConstraint(
item: first,
attribute: edge,
relatedBy: .equal,
toItem: second,
attribute: edge,
multiplier: 1,
constant: 0
)
view.addConstraint(layout)
constrants.append(layout)
}
return self
}
// MARK: - Size Edge Methods
@discardableResult
func size(height: CGFloat, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: height
)
layout.priority = priority
first.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func size(width: CGFloat, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1,
constant: width
)
layout.priority = priority
first.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func size(ratio: CGFloat, constant: CGFloat = 0, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .width,
relatedBy: .equal,
toItem: first,
attribute: .height,
multiplier: ratio,
constant: constant
)
layout.priority = priority
first.addConstraint(layout)
constrants.append(layout)
return self
}
// MARK: - One Edge Methods
@discardableResult
func leading(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .leading,
relatedBy: relate,
toItem: second,
attribute: .leading,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func trailing(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .trailing,
relatedBy: relate,
toItem: second,
attribute: .trailing,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func top(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .top,
relatedBy: relate,
toItem: second,
attribute: .top,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func bottom(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .bottom,
relatedBy: relate,
toItem: second,
attribute: .bottom,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func centerX(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerX,
relatedBy: relate,
toItem: second,
attribute: .centerX,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func centerY(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerY,
relatedBy: relate,
toItem: second,
attribute: .centerY,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func width(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .width,
relatedBy: relate,
toItem: second,
attribute: .width,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
@discardableResult
func height(_ constant: CGFloat = 0, multiplier: CGFloat = 1, relate: NSLayoutRelation = .equal, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> Layout {
let layout = NSLayoutConstraint(
item: first,
attribute: .height,
relatedBy: relate,
toItem: second,
attribute: .height,
multiplier: multiplier,
constant: constant
)
layout.priority = priority
view.addConstraint(layout)
constrants.append(layout)
return self
}
// MARK: Two Edge Methods
/// width and height
@discardableResult
func size(_ constant: CGFloat = 0, multiplier: CGFloat = 1) -> Layout {
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .width,
relatedBy: .equal,
toItem: second,
attribute: .width,
multiplier: multiplier,
constant: constant
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .height,
relatedBy: .equal,
toItem: second,
attribute: .height,
multiplier: multiplier,
constant: constant
)
view.addConstraint(layout)
constrants.append(layout)
}()
return self
}
/// centerX and centerY
@discardableResult
func center(_ constant: CGFloat = 0, multiplier: CGFloat = 1) -> Layout {
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerX,
relatedBy: .equal,
toItem: second,
attribute: .centerX,
multiplier: multiplier,
constant: constant
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerY,
relatedBy: .equal,
toItem: second,
attribute: .centerY,
multiplier: multiplier,
constant: constant
)
view.addConstraint(layout)
constrants.append(layout)
}()
return self
}
// MARK: Four Edge Methods
/// top, bottom, leading, trailing
@discardableResult
func edges(zoom: CGFloat = 0) -> Layout {
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .top,
relatedBy: .equal,
toItem: second,
attribute: .top,
multiplier: 1,
constant: zoom
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .bottom,
relatedBy: .equal,
toItem: second,
attribute: .bottom,
multiplier: 1,
constant: -zoom
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .leading,
relatedBy: .equal,
toItem: second,
attribute: .leading,
multiplier: 1,
constant: zoom
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .trailing,
relatedBy: .equal,
toItem: second,
attribute: .trailing,
multiplier: 1,
constant: -zoom
)
view.addConstraint(layout)
constrants.append(layout)
}()
return self
}
/// width, height, centerX, centerY
@discardableResult
func align(offset: CGFloat = 0) -> Layout {
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .width,
relatedBy: .equal,
toItem: second,
attribute: .width,
multiplier: 1,
constant: 0
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .height,
relatedBy: .equal,
toItem: second,
attribute: .height,
multiplier: 1,
constant: 0
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerX,
relatedBy: .equal,
toItem: second,
attribute: .centerX,
multiplier: 1,
constant: offset
)
view.addConstraint(layout)
constrants.append(layout)
}()
let _ = {
let layout = NSLayoutConstraint(
item: first,
attribute: .centerY,
relatedBy: .equal,
toItem: second,
attribute: .centerY,
multiplier: 1,
constant: offset
)
view.addConstraint(layout)
constrants.append(layout)
}()
return self
}
}
// MARK: - Advanced Using
// MAKR: Custom Operation
/// Using to set the layout's priority.
infix operator >>>: AdditionPrecedence
// MARK: Safe Call Interface
extension Layout {
static weak var `super`: UIView?
@discardableResult
func auto(_ layouts: () -> Void) -> Layout {
DispatchQueue.main.sync {
Layout.super = self.view
layouts()
Layout.super = nil
}
return self
}
}
// MARK: - Layout.View
extension Layout {
class View {
weak var `super`: UIView?
weak var view: UIView!
var attribute: NSLayoutAttribute
var constant: CGFloat = 0
var multiplier: CGFloat = 1
var priority: UILayoutPriority?
init(view: UIView, attribute: NSLayoutAttribute) {
self.view = view
self.attribute = attribute
}
}
}
// MARK: - Custom Operations
extension Layout.View {
// MARK: Results
@discardableResult
static func == (left: Layout.View, right: Layout.View) -> NSLayoutConstraint {
let layout = NSLayoutConstraint(item: left.view, attribute: left.attribute, relatedBy: .equal, toItem: right.view, attribute: right.attribute, multiplier: right.multiplier, constant: right.constant)
if right.priority != nil {
layout.priority = right.priority!
}
if right.super == nil {
Layout.super?.addConstraint(layout)
} else {
right.super?.addConstraint(layout)
}
return layout
}
@discardableResult
static func == (left: Layout.View, right: CGFloat) -> NSLayoutConstraint {
let layout = NSLayoutConstraint(item: left.view, attribute: left.attribute, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: right)
left.view.addConstraint(layout)
return layout
}
@discardableResult
static func <= (left: Layout.View, right: Layout.View) -> NSLayoutConstraint {
let layout = NSLayoutConstraint(item: left.view, attribute: left.attribute, relatedBy: .lessThanOrEqual, toItem: right.view, attribute: right.attribute, multiplier: right.multiplier, constant: right.constant)
if right.priority != nil {
layout.priority = right.priority!
}
if right.super == nil {
Layout.super?.addConstraint(layout)
} else {
right.super?.addConstraint(layout)
}
return layout
}
@discardableResult
static func >= (left: Layout.View, right: Layout.View) -> NSLayoutConstraint {
let layout = NSLayoutConstraint(item: left.view, attribute: left.attribute, relatedBy: .greaterThanOrEqual, toItem: right.view, attribute: right.attribute, multiplier: right.multiplier, constant: right.constant)
if right.priority != nil {
layout.priority = right.priority!
}
if right.super == nil {
Layout.super?.addConstraint(layout)
} else {
right.super?.addConstraint(layout)
}
return layout
}
// MARK: Values Set
static func + (left: Layout.View, right: CGFloat) -> Layout.View {
left.constant = right
return left
}
static func - (left: Layout.View, right: CGFloat) -> Layout.View {
left.constant = -right
return left
}
static func * (left: Layout.View, right: CGFloat) -> Layout.View {
left.multiplier = right
return left
}
static func / (left: Layout.View, right: CGFloat) -> Layout.View {
left.multiplier = 1/right
return left
}
static func >>> (left: Layout.View, right: UILayoutPriority) -> Layout.View {
left.priority = right
return left
}
}
// MARK: - Extension UIView
extension UIView {
var leading: Layout.View {
return Layout.View(view: self, attribute: .leading)
}
var trailing: Layout.View {
return Layout.View(view: self, attribute: .trailing)
}
var top: Layout.View {
return Layout.View(view: self, attribute: .top)
}
var bottom: Layout.View {
return Layout.View(view: self, attribute: .bottom)
}
var centerX: Layout.View {
return Layout.View(view: self, attribute: .centerX)
}
var centerY: Layout.View {
return Layout.View(view: self, attribute: .centerY)
}
var width: Layout.View {
return Layout.View(view: self, attribute: .width)
}
var height: Layout.View {
return Layout.View(view: self, attribute: .height)
}
}
總結
這篇明顯的帶有水分……可能是因為我對 AutoLayout 的封裝還停留在非常淺層次的緣故。所謂的封裝也只是寫了一堆的方法,然後打包起來等著使用。唯一比較能說得上的學習點,就是對於鏈式編程的嘗試以及自定義操作符的學習。
希望在接下來的使用中,可以對它進行改進。