PinLayout이란?
왜 PinLayout을 쓰지 않는지?
PinLayout이 Auto Layout보다 성능이 좋다고는 하나, 좋은 성능의 폰이 보급됨에 따라 그 성능이 차이가 크지 않다고 느끼며, 그에 따라 굳이 PinLayout에서 제공하는 수많은 기능을 공부하기엔 시간 낭비같았다.
실제로 PinLayout에서 제공하는 퍼포먼스 표를 보면 5s부터 X까지만 비교가 되어있다.
아마 최신 기종일수록 그 격차가 좁혀져서 유의미한 차이는 없을거라고 본다.
하지만 one view, one line 은 인상깊었다.
나는 뷰를 만들때 스토리보드를 쓰지 않고, 코드로 작성한다.
간단하게 UIImageView와 UILabel 두개만 있는 뷰를 구성한다고 가정해보자.
그러면 보통 다음과 같이 작성할 것이다.
import UIKit
import Then
final class TestView: UIView {
private let imageView = UIImageView().then {
$0.layer.cornerRadius = 12
$0.backgroundColor = .lightGray
$0.clipsToBounds = true
}
private let label = UILabel().then {
$0.text = "레이블입니다."
$0.font = .systemFont(ofSize: 20)
}
override init(frame: CGRect) {
super.init(frame: frame)
configureLayout()
}
required init?(coder: NSCoder) {
fatalError("not support")
}
private func configureLayout() {
addSubview(imageView)
addSubview(label)
imageView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.widthAnchor.constraint(equalToConstant: 200),
imageView.heightAnchor.constraint(equalToConstant: 200),
label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
label.centerYAnchor.constraint(equalTo: imageView.centerYAnchor)
])
}
}

그러면 다음과 같은 View가 만들어진다.
되게 간단한 구성임에도 여러 줄을 작성해야 해서 디자인 작업을 하다보면 타이핑에 지칠 때도 있다.
또 다음에 보면 줄이 너무 길어서 가독성이 떨어질 때도 많았다.
그래서 PinLayout의 one view, one line을 도입해보기로 했다.
UIView Extension
최초의 아이디어는 UIView를 리턴해주는 메소드를 만들고, 그 안에서 코드를 작성해두면 체이닝으로 쓸 수 있겠다고 생각했다.
그리고 UIView를 만들게 되면 translatesAutoresizingMaskIntoConstraints를 false로 줘야해서 관련 extension도 추가했다.
extension UIView {
func addSubviews(_ views: UIView...) {
views.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
}
@discardableResult
func top(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> UIView {
topAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func leading(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> UIView {
leadingAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func bottom(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> UIView {
bottomAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func trailing(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> UIView {
trailingAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func width(constant: CGFloat) -> UIView {
widthAnchor.constraint(equalToConstant: constant).isActive = true
return self
}
@discardableResult
func width(to anchor: NSLayoutAnchor<NSLayoutDimension>, constant: CGFloat = 0) -> UIView {
widthAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func height(constant: CGFloat) -> UIView {
heightAnchor.constraint(equalToConstant: constant).isActive = true
return self
}
@discardableResult
func height(to anchor: NSLayoutAnchor<NSLayoutDimension>, constant: CGFloat = 0) -> UIView {
heightAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func centerX(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> UIView {
centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func centerY(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> UIView {
centerYAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
}
덕분에 이렇게 코드를 작성할 수 있게 됐다.
private func configureLayout() {
backgroundColor = .white
addSubviews(imageView, label)
imageView.top(to: topAnchor).leading(to: leadingAnchor).bottom(to: bottomAnchor).width(constant: 200).height(constant: 200)
label.leading(to: imageView.trailingAnchor, constant: 10).trailing(to: trailingAnchor, constant: -10).centerY(to: imageView.centerYAnchor)
}
처음 코드와 비교해보면 비약적으로 단축되었고, one view, one line이 되었다. 그런데 사용하면서 조금 불편한 점이 있었다. 바로 topAnchor, leadingAnchor 등 UIView의 Property와 네이밍이 중복되어 같은 레이어에서 보여지는 것이 불편했고, 가끔 실수나 잘못사용하게 될 수 있다고 생각했다.

그래서 PinLayout 처럼 계층을 분리하고, 내가 새로 추가한 코드만 쓸 수 있게 따로 클래스를 정의하기로 했다.
우선 나는 Hook 라는 네이밍을 사용하기로 했고 Property를 추가했다.
extension UIView {
var hook: Hook {
Hook(view: self)
}
}
그리고 Hook 클래스를 정의했다.
final class Hook {
private var view: UIView
init(view: UIView) {
self.view = view
}
// MARK: - Top, Leading, Bottom, Trailing
@discardableResult
func top(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.topAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func leading(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.leadingAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func bottom(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.bottomAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func trailing(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.trailingAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
// MARK: - Combination
@discardableResult
func all(to superView: UIView, topConstant: CGFloat = 0, leadingConstant: CGFloat = 0, bottomConstant: CGFloat = 0, trailingConstant: CGFloat = 0) -> Hook {
view.hook
.top(to: superView.topAnchor, constant: topConstant)
.leading(to: superView.leadingAnchor, constant: leadingConstant)
.bottom(to: superView.bottomAnchor, constant: bottomConstant)
.trailing(to: superView.trailingAnchor, constant: trailingConstant)
return self
}
// MARK: - Width & Height
@discardableResult
func width(constant: CGFloat) -> Hook {
view.widthAnchor.constraint(equalToConstant: constant).isActive = true
return self
}
@discardableResult
func width(to anchor: NSLayoutAnchor<NSLayoutDimension>, constant: CGFloat = 0) -> Hook {
view.widthAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func height(constant: CGFloat) -> Hook {
view.heightAnchor.constraint(equalToConstant: constant).isActive = true
return self
}
@discardableResult
func height(to anchor: NSLayoutAnchor<NSLayoutDimension>, constant: CGFloat = 0) -> Hook {
view.heightAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
// MARK: - Center
@discardableResult
func centerX(to anchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
@discardableResult
func centerY(to anchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, constant: CGFloat = 0) -> Hook {
view.centerYAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
return self
}
}
이제는 UIView의 hook 프로퍼티에서만 내가 추가한 코드를 쓸 수 있어 기존 코드와 분리가 되었고, 더욱 명확하고 안전하게 쓸 수 있게 되었다.

결과
자 이제 코드가 이렇게 바뀌었다.
private func configureLayout() {
backgroundColor = .white
addSubviews(imageView, label)
imageView.hook.top(to: topAnchor).leading(to: leadingAnchor).bottom(to: bottomAnchor).width(constant: 200).height(constant: 200)
label.hook.leading(to: imageView.trailingAnchor, constant: 10).trailing(to: trailingAnchor, constant: -10).centerY(to: imageView.centerYAnchor)
}
CocoaPods & SPM
이렇게 쓰다가 이 글을 남긴지 6개월이 지나고 점점 이것저것 추가해서 쓰면서 NSLayoutConstraint를 Wrapping해서 아예 라이브러리를 만들게 되었다.
CocoaPods와 SPM 둘다 지원합니다.