PinLayout, FlexLayout 사용하여 뷰 그리기(2)
머리말
- PinLayout, FlexLayout 사용하여 뷰 그리기(1) 과 이어지는 글입니다.
오토 레이아웃을 공부하면서 어떻게 UIKit 환경에서 뷰를 그릴 수 있을지 알아봤다. 오토 레이아웃을
사용하면서 느낀 점은, 코드 생산성이 뛰어나지 않다는 점과 원하는 뷰를 자유롭게 그릴 수 있다는 점이다. 장
단점이 있지만, UI디자인 및 퍼블리싱을 경험했던 나로서는 더 사용하기 편하고 관리하기 편한 방법을 고민하게
되었다.
오토 레이아웃을 대체할만한 레이아웃 관련한 패키지를 찾다가 크게 두 가지의 옵션을 알게 되었는데, 바로
Snapkit 과 PinLayout & FlexLayout 이다. 얘네 외에도 다양한 레이아웃 패키지가 있지만, 필요하게되면
찾아볼지도?[1]
Snapkit
Snapkit 은 UIKit 을 사용할 때 오토 레이아웃을 쉽게 사용할 수 있게 도와주는 패키지다. 요구사항을
살펴보면 여러 번 버전업이 되었음에도 상당히 이전 버전부터 지원되어 왔던 것을 알 수 있다. 19.7k 의
어마어마한 스타 수 ㄷㄷ
버전 요구사항
- iOS 8.0+ / Mac OS X 10.11+ / tvOS 9.0+
- Xcode 9.0+
- Swift 4.0+
스냅킷은 SnapKit is a DSL to make Auto Layout easy on both iOS and OS X. 라고 소개하고 있는데, 그 말 처럼 오토 레이아웃의 작성방식과 유사하면서도 사용하기 편한 구문을 제공하고 있다. 오토 레이아웃에 익숙하다면 스냅킷의 작성방식을 보고 직관적으로 오토 레이아웃에서 대입할 수 있을 것이다.
let box = UIView()
let container = UIView()
container.addSubview(box)
box.snp.makeConstraints { (make) -> Void in
make.size.equalTo(50)
make.center.equalTo(container)
}
위의 코드를 순정 오토 레이아웃으로 작성한다 하면 다음과 같을 것이다. 비교해보면 NSLayoutConstraint 으로
레이아웃을 설정하던 부분이 훨씬 읽기 쉽고 간결해진 것을 알 수 있다.
let box = UIView()
let container = UIView()
container.addSubview(box)
... 생략
box.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
box.widthAnchor.constraint(equalToConstant: 200),
box.heightAnchor.constraint(equalToConstant: 200),
box.centerXAnchor.constraint(equalTo: container.centerXAnchor),
box.centerYAnchor.constraint(equalTo: container.centerYAnchor),
])
위의 공식문서를 들어가 문법을 살펴봐도 많지 않고 간단한 것을 확인할 수 있다.
translatesAutoresizingMaskIntoConstraints 처리도 자동으로 해주기도 한다. 스냅킷을 활용하면 오토
레이아웃으로 작업할 때 보다 훨씬 생산성 있게 뷰를 생산할 수 있을 것이다. 아래의 표처럼, 1:1로 대응되는
내용들이 많다.
ViewAttribute
| ViewAttribute | NSLayoutAttribute |
|---|---|
| view.snp.left | NSLayoutConstraint.Attribute.left |
| view.snp.right | NSLayoutConstraint.Attribute.right |
| view.snp.top | NSLayoutConstraint.Attribute.top |
| view.snp.bottom | NSLayoutConstraint.Attribute.bottom |
| view.snp.leading | NSLayoutConstraint.Attribute.leading |
| view.snp.trailing | NSLayoutConstraint.Attribute.trailing |
| view.snp.width | NSLayoutConstraint.Attribute.width |
| view.snp.height | NSLayoutConstraint.Attribute.height |
| view.snp.centerX | NSLayoutConstraint.Attribute.centerX |
| view.snp.centerY | NSLayoutConstraint.Attribute.centerY |
| view.snp.lastBaseline | NSLayoutConstraint.Attribute.lastBaseline |
왜 Snapkit을 선택하지 않았나?
사용하기 편하게 만들어진 패키지는 맞지만, 코드가 간결해지는 장점 이외에 특별히 오토 레이아웃과의 차별점을 느끼지 못했다. (읽기 쉬운 오토 레이아웃?) 오토 레이아웃 식의 작성방식에 익숙치 않기도 하고, 오토 레이아웃과는 다른 방식으로 뷰를 그릴 수 없을까 생각이 많이 들었던 것 같다.
아무래도 스유 방식에 많이 익숙해졌기 때문인 듯.. 스유도 처음 접했을 때는, css와 다른 방식에 익숙치 않긴
했지만, 직관적인 함수형 덕분에 쉽고 빠르게 적응했던 것 같다. 리액트스럽기도하고.. 그러던 중 알게된게
PinLayout 과 FlexLayout 이다.
PinLayout

핀레이아웃은 "No Auto layout constraints attached" 라고 자신들을 소개하고 있는데, 사용 구문을 살펴보면 그 말 그대로 오토 레이아웃 방식과 다른 모습을 확인할 수 있다. 원리 및 철학을 살펴보면, 수동 레이아웃으로 작동하며, 그렇기에 빠르고, 오토 레이아웃과도 함께 사용할 수 있다고 설명되어 있다. 속도가 빠르다고 하는데.. 솔직히 유의미한 지표일지는 잘 모르겠다. 속도 때문에 스택 뷰나 오토 레이아웃을 안쓸 상황이 얼마나 되려나? 그보다는 개발자가 데이터 처리 및 레이아웃 갱신처리를 똑바로 하는게 더 효과적이지 않을까하는 개인적인 생각.
여튼 사용 구문을 보면 오토 레이아웃과는 상당히 다른 방식으로 뷰를 그리는 것을 볼 수 있다. 스냅킷처럼 뷰의 익스텐션의 구문이 추가되었고, 메서드 체이닝 방식으로 뷰를 그리는 것을 확인할 수 있다.

viewA.pin.top(10).bottom(10).left(10).right(10)
view.pin.top(20).bottom(20) // The view has a top margin and a bottom margin of 20 pixels
view.pin.top().left() // The view is pinned directly on its parent top and left edge
view.pin.all() // The view fill completely its parent (horizontally and vertically)
view.pin.all(pin.safeArea) // The view fill completely its parent safeArea
view.pin.top(25%).hCenter() // The view is centered horizontally with a top margin of 25%
view.pin.left(12).vCenter() // The view is centered vertically
view.pin.start(20).end(20) // Support right-to-left languages.
view.pin.horizontally(20) // The view is filling its parent width with a left and right margin.
view.pin.top().horizontally() // The view is pinned at the top edge of its parent and fill it horizontally.
위의 사용예를 보면 직관적으로 뷰가 어떻게 그려질 지 예측할 수 있다.(예측은 늘 빗나가) 또한 %로 슈퍼 뷰를
기준으로 한 계산된 비율도 사용할 수 있다. 위처럼 간단한 사용예 말고도, 위 구문들을 조합해 더 간단하게
사용할 수 있도록 만들어 놓은 체이닝 메서드도 많아 필요에 따라 활용할 수도 있다. 다만. 너무 다양해서 다 쓸
일은 없을 듯.
해당 패키지는 css의 특히 absolute 에 영향을 받았다고 하는데, 그 말처럼 css 문법을 안다면 좀 더 쉽게
적용할 수 있을 것으로 보인다.
주의점으로 수동 레이아웃이라는 점인데, 뷰의 변화를 자동으로 갱신해주지 않고, 사용자가 직접 뷰를 갱신해 줘야한다.
override func layoutSubviews() {
super.layoutSubviews()
let padding: CGFloat = 10
logo.pin.top(pin.safeArea).left(pin.safeArea).width(100).aspectRatio().margin(padding)
segmented.pin.after(of: logo, aligned: .top).right(pin.safeArea).marginHorizontal(padding)
textLabel.pin.below(of: segmented, aligned: .left).width(of: segmented).pinEdges().marginTop(10).sizeToFit(.width)
separatorView.pin.below(of: [logo, textLabel], aligned: .left).right(to: segmented.edge.right).marginTop(10)
}
그렇기 때문인지 기본적으로 뷰의 레이아웃 설정이 layoutSubviews 시점[2]에 작성하는 것을 확인할 수 있다.
스냅킷과 비교해보면, 핀레이아웃은 좀 더 선언형에 가까운 스타일로 뷰를 그릴 수 있게 해준다. 그렇지만, 오토 레이아웃과는 완전히 다른 방식으로 뷰를 그리기 때문에, 오토 레이아웃에 익숙하다면 사용하기 귀찮을 것 같다. 뷰 그리는 취향에 따라 선호하는게 많이 달라질 듯. 개인적으로 플렉스레이아웃이 없었다면 핀레이아웃만으로 사용하진 않았을 것 같다.
핀레이아웃이 css 방식과 유사한 구문 등을 제시하고 있긴하지만, 수동 레이아웃 갱신이고, 결국 iOS 방식으로 뷰를 그리기 때문에, 제대로 사용하려면, 핀레이아웃에 적합한 사용방법을 익혀야 하는데, 많은 기능을 제공하고 있어, 시간이 오래 걸리기 때문이다. 물론 다른 패키지들도 익혀야 하는건 마찬가지긴 하지만, 오히려 스냅킷이 순정에 가까워 쉽게 접근할 수 있을 것 같았다.
실제로 핀레이아웃을 사용하면서, 플렉스레이아웃도 마찬가지로 생각대로 원하는 뷰를 그리려고 할 때 css를 생각해 작성했더라도, 그렇게 동작하지 않는 경우가 상당했다. css구문스러운 다른 방식의 레이아웃 작성법이라는점을 염두해야야 한다. (특히 마진과 패딩이 생각대로 안돼..) 다행히 해당 문서들은 각 예제별로 샘플들을 잘 제공하고 있어서, 원하는 동작에 대한 가이드와 샘플을 제공하고 있다.
샘플들을 살펴보면 해당 패키지를 사용하는 패턴을 쉽게 파악할 수 있는데, 크게 살펴보면 뷰와 뷰 컨트롤러를 분리하는 점과, 수동으로 뷰를 갱신 시킨다는 점이다.
class MyViewController: UIViewController {
private var mainView: MyView {
return self.view as! MyView
}
...init 생략
override viewDidLoad() {
super.viewDidLoad()
// 바인딩
mainView.view1.rx.. {
mainView.viewAnimation()
}
}
override func loadView() {
view = MyView()
}
}
class MyView: UIView {
var view1 = UIView()
private var view2 = UIView()
init() {
// 뷰 스타일링 및 바인딩
}
override func layoutSubviews() {
super.layoutSubviews()
view1.pin.top().horizontally()
view2.pin.below(to:view1).horizontally().bottom()
viewAnimation() // 호출시키면 여기서 실행됨
}
func viewAnimation() {
// 대충 뷰 레이아웃을 바꾸는 코드
}
}
위 코드들을 살펴보면, 뷰 컨트롤러와 뷰를 분리한 한쌍으로 두고, loadView 시점에 뷰 컨트롤러의 루트 뷰로
설정한다. 그 후 바인딩 처리 등을 viewDidLoad 에서 처리하게 되면 뷰 컨트롤러에서 뷰를 조작할 수 있다.
뷰에 접근할 수 있도록 mainView 라는 변수를 둔 점이 재밌는 듯.
또한 뷰의 변화가 생기면 새롭게 그려줘야 하기 때문에, layoutSubviews 시점에서 뷰 변화시 뷰 계산을 시키는
점도 눈여겨 봐야할 점이다.
핀레이아웃은 오토 레이아웃과 다르고 좀 더 필자에게 익숙한 구문을 제공하고 있긴 했지만 앞서 말했듯 핀레이아웃만 있었다면 사용하진 않았을 것 같다.
FlexLayout

플렉스레이아웃은 살펴보고 시험해보면서, 바로 써야겠다고 마음먹게 된 레이아웃 패키지다. 솔직히 말해서, 필자는 css에 훨씬 익숙했고, SwiftUI로 레이아웃을 작성해왔다 보니, 이와 유사하게 뷰를 그릴 수 있다는 점이 크게 매력적이었다.
플렉스레이아웃은 그 이름처럼 css의 flexbox를 연상시킨다. 구문도 유사하며, 동작도 유사하다. 그리고 핀레이아웃과 구문도 유사해서 굉장히 직관적으로 뷰를 그릴 수 있다.
rootFlexContainer.flex.direction(.column).padding(12).define { (flex) in
// Row container
flex.addItem().direction(.row).define { (flex) in
flex.addItem(imageView).width(100).aspectRatio(of: imageView)
// Column container
flex.addItem().direction(.column).paddingLeft(12).grow(1).define { (flex) in
flex.addItem(segmentedControl).marginBottom(12).grow(1)
flex.addItem(label)
}
}
flex.addItem().height(1).marginTop(12).backgroundColor(.lightGray)
flex.addItem(bottomLabel).marginTop(12)
}
위 코드가 플렉스레이아웃을 활용해 뷰를 그린 예시인데, 몇 가지 다른 점은 있지만 flexbox 및 css에 익숙하다면, 쉽게 뷰 구조를 연상할 수 있다. 물론.. 여러 환경에 따라 항상 생각대로 그려지진 않지만..
참고로 플렉스레이아웃은 yoga 라는 flexbox 레이아웃 엔진을 랩핑했다고 한다. 동작에 대해 자세히 알고자 한다면 살펴보면 좋을 듯하다.
플렉스레이아웃도 핀레이아웃과 마찬가지로, 뷰 갱신시 처리해야할 조건들이 몇 있지만, 그럼에도 불구하고 flexbox 스타일로 뷰를 그릴 수 있다는 것 만으로도 굉장히 큰 이점으로 다가왔다. 특히 피그마와도 통합할 수 있지 않을까? 하는 기대감이 들기도..! 또한 간단한 레이아웃 시 layer에 대한 고민을 안하게 만들어주는 점도 한 몫했다.
// Create a gray column container and add a black horizontal line separator
flex.addItem().backgroundColor(.gray).define { (flex) in
flex.addItem().height(1).backgroundColor(.black)
}
// Set rounded corner
flex.addItem().cornerRadius(12) // Layer로 작업이 필요한 라인처리
// Set border
flex.addItem().border(1, .black) // Layer로 작업이 필요한 라인처리
핀레이아웃과 마찬가지로, 관련된 메서드의 양이 상당하므로 모든 메서드를 다 사용하긴 어려울 것 같지만, flexbox를 겪어봤다면, 전반적인 뷰 작업은 어렵지 않게 진행할 수 있다.
주의할 점으로 뷰 갱신을 위해 처리해야할 작업들이 있는데, 레이아웃 변경 시 재배치를 알리는 markDirty() 와
같은 메서드들이 있다는 점이다. 애니메이션과 같은 처리 시 setNeedsLayout() 나 layoutIfNeeded() 와 같은
기본적인 뷰 갱신 관련 코드들도 잘 활용해야 한다.[3]
// 1) Update UILabel's text
label.text = "I love FlexLayout"
// 2) Mark the UILabel as dirty
label.flex.markDirty()
// 3) Then force a relayout of the flex container.
rootFlexContainer.flex.layout()
OR
setNeedsLayout()
플렉스레이아웃도 핀레이아웃과 마찬가지로 다양한 샘플들을 제공하고 있고, 여기에도 일반적인 사용패턴을 확인할 수 있다.
class MyView: UIView {
// 1. rootView를 생성한다.
private rootView = UIView()
var view1 = UIView()
private var view2 = UIView()
init() {
// 뷰 스타일링 및 바인딩
// 2. rootView에 때려박는다.
rootView.flex.define { flex in
flex.addItem(view1) // flex 스타일로 스타일링
flex.addItem(view2)
}
}
override func layoutSubviews() {
super.layoutSubviews()
// 3. 핀레이아웃으로 고정한다.
rootView.pin.all(pin.safeArea)
// 4. 플렉스레이아웃을 실행한다.
rootView.flex.layout()
}
func viewAnimation() {
// 대충 뷰 레이아웃을 바꾸는 코드
// 변경 시 알림
view1.flex.markDirty()
setNeedsLayout()
}
}
핀레이아웃과 함께 사용할 수 있기 때문에 뷰를 그리는 상황에 맞춰 조합해서 쓰이고, rootView를 처음에 고정시키고 하위 뷰들을 flex로 처리하는 것이 일반적인 듯하다. 플렉스레이아웃으로 뷰를 그려보니, 아직 익숙치 않아 생각대로 잘 안 그려지는 경우도 있긴 하지만, 훨씬 직관적이고 빠르게 그릴 수 있었다.
맺음말
뷰를 그리는 도구로 플렉스레이아웃을 선택한 것에 대해 작성해 보았다. 뷰를 그리는 방법은 너무나도 다양하다. 관련 도구도 다양하고, 스유도.. 그 중 하나다. 어떤 것을 선호하는지는 성향과 상황에 따라 달라질 것이다. 맨날 새롭고 더 좋은게 나타나기도 하고(불안정한).. 당장 사이드 프로젝트를 진행할 때 편의를 위해 플렉스레이아웃을 골랐긴 하지만, 스냅킷이든 순정 오토 레이아웃이든 필요하다면 배워서 써야지..
필요에 의해 배우는 건 그때 하기로 하고.. 우선 툴을 선택했으니, 이를 잘 활용해서 사이드 프로젝트 시 뷰를 그려갈 것 같다. UIKit 컴포넌트화에도 활용해봐도 좋을 듯하고 이번 사이드로 테스트를 많이 해봐야겠다. 우선 초반 지금까지는 신경써야 할 점들은 있지만 꽤나 맘에 든다 !
[1] 많이 사용되는 레이아웃 패키지 리스트
[2] 관련 링크들
[3] FlexLayout 사용 시 팁