https://github.com/layoutBox/PinLayout 을 참고했습니다.
PinLayout
Luc Dion이라는 개발자가 만든 프레임워크이다. layoutBox라는 Swift를 사용한 layout 오픈 소스 organization 관리자인 이분은 PinLayout뿐만 아니라 FlexLayout을 만든 사람이기도 하다.
PinLayout은 기본적으로 AutoLayout을 사용하지 않고 레이아웃을 구성할 수 있도록 도와주는 프레임워크이다. AutoLayout을 사용하지 않음으로서 렌더링 성능이 훨씬 빠르다고 한다(8~12배). PinLayout은 수동으로 view를 layout하는데 때문에 Manual Layout과 속도가 비슷하거나 더 빠르다.
SnapKit이 AutoLayout을 좀 더 직관적으로 잡을 수 있도록 도와준 프레임워크라면,
PinLayout은 ManualLayout을 직관적으로 잡을 수 있게 도와주는 프레임워크이다.
(그렇다고 PinLayout을 사용하면 무조건 AutoLayout을 사용하면 안되는 것은 아니고 부분적으로도 적용할 수 있다.)
유의사항
PinLayout은 auto layout constrainst를 사용하지 않는다, 수동적으로 view들의 layout을 잡는 프레임워크이다.
이러한 이유때문에 우리는 디바이스의 회전을 포함한 layout의 업데이트를 UIView.layoutSubviews() 혹은 UIViewController.viewDidLayoutSubViews()에서 작성해주어야한다.
PinLayout 의 철칙
- Manual layouting (auto layout에 의존하지 않는다).
- PinLayout은 가능한 한 간단하고 빠른 상태로 존재한다. 앞서 말했듯, manual layout만큼 빠르다. performance results below.
- Full control: You're in the middle of the layout process, no magic black box(음 내가 다 관리할 수 있다 정도..?)
- 한번에 하나의 뷰를 layout한다. 코드와 디버깅을 간단하게 만들어준다.
- 간결한 구문을 가짐으로써 대부분의 뷰를 한 줄로 layout할 수 있다.
Edges Layout
빠르게 위의 레이아웃을 잡기위한 코드부터 보자면
viewA.pin.top(10).bottom(10).left(10).right(10) // 방법 1
view.pin.all(10) // 방법 2
이런식으로 사용한다. snp 대신 pin을 쓰는 방식이라고 생각하면 된다.
조금 다른 점이 있다면 snapkit의 경우에는 superView와 safeArea 모두 명시를 해주어야 했지만 pinLayout은 좀 다르다.
default가 superView이다.
따라서 기본적으로 top, bottom, left, right가 superview의 edge를 기준으로 한다.
그래서 safeArea만 따로 처리해주면 된다.
edge(top, left, right, bottom)의 layout을 잡는 방법에는 4가지가 있다.
1. top(_ offset: CGFloat), 2. top()
일반적인 사용. 띄우고 싶은 offset을 넣는다. 아예 안넣으면 0으로 인지
2. top(_ offset: Percent)
%로 넣을 수 있는건데..음 잘 쓰일지는 의문이다.
3. top(_ margin: UIEdgeInsets)
UIEdgeInsets을 사용하기 때문에 safeArea Layout을 잡을 때 유용하다.
testView.pin.top(self.view.pin.safeArea.top)
center의 layout을 잡기 위한 속성은 다음과 같다.
1. vCenter(vertical)
(_offset: CGFloat), (_offset: Percent), ()
// left, right를 superView에 맞추고
// y축 중앙(vertical)에
// 높이 100 크기의 뷰를 정렬한다.
label.pin.left().right().vCenter().height(100)
2. hCenter(horizontal)
(_offset: CGFloat), (_offset: Percent), ()
// top, bottom을 superView에
// x축(horizontal) 중앙에 맞추고
// 너비를 100을 준다
label.pin().top().bottom().hCenter().width(100)
이 외에도 다양한 메서드들이 존재한다. 메서드 이름들이 직관적이라서 크게 헷갈리는 건 없는 것 같다.
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.
Layout multiple edges relative to superview
pinLayout은 topLeft, topCenter 등 좀 더 직관적인 네이밍으로 코드를 간결하게 줄여준다.
(_ offset: CGFloat)
예를 들어, 우측 상단으로 뷰를 붙이고 싶다면 이렇게 코드를 작성해야한다.
viewA.pin.top().right().size(100)
하지만 pinLayout에서 제공하는 키워드를 이용하면 좀 더 간결한 코드 작성이 가능해진다.
viewA.pin.topRight().size(100)
하지만 얘는 percent나 UIEdgeInset 적용은 불가능하다.
얘네도 있음! 비슷한 친구들~
↔️ << 요 표시는 AutoLayout의 leading, trailing과 비슷한 개념이 적용된다는 뜻인데
우리나라는 글을 읽는 방향이 left to right라서 topStart와 topLeft가 똑같지만
글을 읽는 방향이 right to left인 나라에서는 topStart == topRight가 된다.
- topStart(_ offset: CGFloat) / topStart() ↔️
- topEnd(_ offset: CGFloat) / topEnd() ↔️
- bottomStart(_ offset: CGFloat) / bottomStart() ↔️
- bottomEnd(_ offset: CGFloat) / bottomEnd() ↔️
- centerStart(_ offset: CGFloat) / centerStart() ↔️
- centerEnd(_ offset: CGFloat) / centerEnd() ↔️
Relative Edges layout
pinLayout은 다른 뷰들과 상대적인 포지션을 잡는 메서드들이 존재한다. 그래서 pinLayout을 사용하면 view가 다른 view 옆에 위치해야하거나 위에 위치해야 할 때 보다 편리하게 하나의 키워드로 작성이 가능하다.
snapkit을 사용했다면 하나씩 잡아줘야해서 코드가 길어졌을 것이다.
Methods: 이런 메소드가 존재한다.
- above(of: UIView) / above(of: [UIView])
- below(of: UIView) / below(of: [UIView])
- before(of: UIView) / before(of: [UIView]) ↔️
- after(of: UIView) / after(of: [UIView]) ↔️
- left(of: UIView) / left(of: [UIView])
- right(of: UIView) / right(of: [UIView])
[Example 1]
oneView.pin.topLeft().size(200)
twoView.pin.after(of: self.oneView).size(100)
threeView.pin.after(of: self.oneView).below(of: self.twoView).size(50)
이런식으로 작성하는 느낌이다!
[Example 2]
또 다른 예시로, C라는 뷰가 오른쪽처럼 되려면 아래의 세가지 스타일의 방법이 있다.
세번째 줄의 코드에 horizontallyBetween에 주목하자 저렇게 쓰면 코드 길이가 또 한번 줄어든다
같은 방식으로 verticallyBetween도 있다.
horizontallyBetween(:UIView, and: UIView, aligned: VerticalAlign)
verticallyBetween(:UIView, and: UIView, aligned: HorizontalAlign)
viewC.pin.top().right(of: viewA).left(of: viewB).margin(10)
viewC.pin.top().left(to: viewA.edge.right).right(to: viewB.edge.left). margin(10)
viewC.pin.horizontallyBetween(viewA, and: viewB, aligned: .top).marginHorizontal(10)
여기에 추가적으로 alignment를 곁들여서 좀 더 쉽게 레이아웃을 잡을 수도 있다.
- HorizontalAlignment(.left / .center / .right / .start ↔️/ .end ↔️)
- VerticalAlignment(.top / .center / .bottom)
Methods:
- above(of: UIView, aligned: HorizontalAlignment)
above(of: [UIView], aligned: HorizontalAlignment) - below(of: UIView, aligned: HorizontalAlignment)
below(of: [UIView], aligned: HorizontalAlignment) - before(of: UIView, aligned: HorizontalAlignment)↔️
before(of: [UIView], aligned: HorizontalAlignment)↔️ - after(of: UIView, aligned: HorizontalAlignment)↔️
after(of: [UIView], aligned: HorizontalAlignment)↔️ - left(of: UIView, aligned: VerticalAlignment)
left(of: [UIView], aligned: HorizontalAlignment) - right(of: UIView, aligned: VerticalAlignment)
right(of: [UIView], aligned: HorizontalAlignment)
[Example 3]
oneView.pin.topLeft().size(200)
twoView.pin.after(of: self.oneView, aligned: .bottom).size(100)
threeView.pin.below(of: self.twoView, aligned: .right).size(50)
Edge
"지금 뷰를 다른 뷰의 오른쪽에 맞추고싶어, 왼쪽에 맞추고싶어"
Edges(top, vCenter, bottom, left, hCenter, right, start, end)를 이용해서 레이아웃 잡기
Methods:
- top(to edge: ViewEdge)
- vCenter(to edge: ViewEdge)
- bottom(to edge: ViewEdge)
- left(to: edge: ViewEdge)
- hCenter(to: edge: ViewEdge)
- right(to: edge: ViewEdge)
- start(to: edge: ViewEdge)↔️
- end(to: edge: ViewEdge)↔️
[Example 1]
aView.pin
.top(to: bView.edge.top)
.hCenter(to: bView.edge.hCenter)
.marginTop(10)
Anchors
"지금 뷰를 다른 뷰의 오른쪽위, 왼쪽아래에 맞추고싶어"
Methods:
- topLeft(to anchor: Anchor)
- topCenter(to anchor: Anchor)
- topRight(to anchor: Anchor)
- topStart(to anchor: Anchor)↔️
- topEnd(to anchor: Anchor)↔️
- centerLeft(to anchor: Anchor)
- center(to anchor: Anchor)
- centerRight(to anchor: Anchor)
- centerStart(to anchor: Anchor)↔️
- centerEnd(to anchor: Anchor)↔️
- bottomLeft(to anchor: Anchor)
- bottomCenter(to anchor: Anchor)
- bottomRight(to anchor: Anchor)
- bottomStart(to anchor: Anchor)↔️
- bottomEnd(to anchor: Anchor)
[Example 1]
viewB.pin.center(to: viewA.anchor.topRight)
Width, Height
"너비, 높이를 지정하고 싶어"
Basic
- width(:CGFloat) / width(:Percent) / width(of: UIView)
- height(:CGFloat) / height(:Percent) / height(of: UIView)
- size(:CGSize) / size(:Percent) / size(_ sideLength: CGFloat) / size(of: UIView)
[Example]
view.pin.width(100)
view.pin.width(50%)
view.pin.width(of: view1)
Max/Min
- minWidth(:CGFloat) / minWidth(:Percent)
- maxWidth(:CGFloat) / maxWidth(:Percent)
- minHeight(:CGFloat) / minHeight(:Percent)
- maxHeight(:CGFloat) / maxHeight(:Percent)
[Example]
viewA.pin.top(20).hCenter().width(100%).maxWidth(200)
justify()라는 키워드를 사용해도 된다.
- justify(_ : HorizontalAlign)
- align(_ : VerticalAlign)
viewA.pin.top(20).horizontally().maxWidth(200).justify(.center)
Adjusting Size
"내용에 맞게 size를 유동적으로 조절하고싶어"
- sizeToFit()
- sizeToFit(: FitType)
- Parameter fitType
- .width
- .height
- .widthFlexible
- .heightFlexible
- Parameter fitType
[Example]
// Adjust the view's size based on the result of `UIView.sizeToFit()` and center it.
view.pin.center().sizeToFit()
// Adjust the view's size based on a width of 100 pixels.
// The resulting width will always match the pinned property `width(100)`.
view.pin.width(100).sizeToFit(.width)
// Adjust the view's size based on view's current width.
// The resulting width will always match the view's original width.
// The resulting height will never be bigger than the specified `maxHeight`.
view.pin.sizeToFit(.width).maxHeight(100)
// Adjust the view's size based on 100% of the superview's height.
// The resulting height will always match the pinned property `height(100%)`.
view.pin.height(100%).sizeToFit(.height)
// Adjust the view's size based on view's current height.
// The resulting width will always match the view's original height.
view.pin.sizeToFit(.height)
// Since `.widthFlexible` has been specified, its possible that the resulting
// width will be smaller or bigger than 100 pixels, based of the label's sizeThatFits()
// method result.
label.pin.width(100).sizeToFit(.widthFlexible)
[Example 2]
label.pin.after(of: image, aligned: .top).right().marginHorizontal(10).sizeToFit(.width)
Aspect Ratio
"너비나 높이만 지정하고 비율로 사이즈를 잡고싶어"
- aspectRatio(_ ratio: CGFloat)
- aspectRatio(of view: UIView)
- aspectRatio():
UIImageView에서 사용했을 때에만 image의 dimension을 사용하여 비율을 잡아준다. 다른 타입의 view들에서 이 메서드는 영향이 없다.
[Example]
imageView.pin.top().hCenter().width(50%).aspectRatio()
safeArea, readable and layout margins
UIKit은 3 종류의 area/guide가 존재하는데 PinLayout은 이걸 다음과 같이 표현한다.
- UIView.pin.safeArea: UIKit's UIView.safeAreaInsets / UIView.safeAreaLayoutGuide.
- UIView.pin.readableMargins: UIKit's UIView.readableContentGuide.
- UIView.pin.layoutMargins: UIKit's UIView.layoutMargins / UIView.layoutMarginsGuide.
UIView's transforms
지금까지는 .pin이라는 키워드를 사용했지만 .pinFrame이라는 키워드도 존재합니다.
아까 글의 서두에서 유의사항을 다룰 때 PinLayout은 수동으로 레이아웃을 잡기 때문에 회전과 같은 상황에서도 직접 처리를 해주어야 한다는 것을 암시했어요.
.pinFrame은 뷰의 변환이 일어나는 상황(UIView.transform, scailing, rotation(회전))에서 사용됩니다.
[pin]
변화가 일어나지 않는 뷰의 사이즈와 위치를 잡는다. pin을 사용하면 변화가 일어나기 전에 레이아웃이 적용된다. 따라서 만약 자체적인 레이아웃의 수정 없이 transform을 사용하여 애니메이션을 주고 싶은 상황에서는 유용하게 사용될 수 있다.
[pinFrame]
변화가 일어난 뷰의 사이즈와 위치를 잡는다. 따라서 변화가 일어난 뒤에 레이아웃을 잡는다.
[Example]
이런 뷰가 있다고 할 때, 이 뷰를 가운데로 정렬하고 뷰의 회전을 시킬 것이다.
.pin을 사용하면?
view.transform = .init(rotationAngle: CGFloat.pi / 2)
view.pin.center().width(100).height(50)
뷰의 레이아웃이 잡히고 나서 transform이 이루어지기 때문에 처음 잡았던 레이아웃이 유지된채로 회전이 일어난다.
.pinFrame을 사용하면?
view.transform = .init(rotationAngle: CGFloat.pi / 2)
view.pinFrame.center().width(100).height(50)
transform이 일어난 뒤에 뷰의 레이아웃이 잡히기 때문에 회전에 따라서 뷰의 레이아웃도 변경된다.
같은 맥락에서
aView.transform = .init(scaleX: 1.5, y: 1.5)
aView.pin.width(100).height(50)
bView.pin.below(of: aView, aligned: .left)
viewB가 변환 전인 A의 below에 레이아웃되었따.
aView.transform = .init(scaleX: 1.5, y: 1.5)
aView.pin.width(100).height(50)
bView.pinFrame.below(of: aView, aligned: .left)
viewB가 변환 이후의 A의 below에 레이아웃되었다.
Warning
PinLayout은 가능하지 않은 pin rule이 적용될 수 없거나 유효하지 않는 경우에 대해서 콘솔에 warning메시지를 띄워준다.
warning을 끌 수도 있는데 기본적으로는 나오도록 설정이 되어있다.
PinLayout Guide
가이드도 따로 있다.
항상 같은 순서로 작성해야한다. 코드를 항상 같은 형식으로 유지함으로써 좀 더 이해하기 쉽게 하기위한 목적이다.
아래의 순서가 추천된다.
view.pin.[EDGE|ANCHOR|RELATIVE].[WIDTH|HEIGHT|SIZE].[pinEdges()].[MARGINS].[sizeToFit()]
로직도 이 순서대로 적용이 된다. pinEdges()가 margin 전에 적용되고 sizeToFit()이 그 이후에 적용된다.
EDGE도 아래의 순서를 지켜주면 좋다.
TOP, BOTTOM, LEFT, RIGHT
PinLayout에 대해 전반적으로 훑어보는 시간을 가져보았다.
SnapKit을 더 많은 사람들이 사용하고 있지만 PinLayout만이 가진 장점들이 존재하기 때문에 충분히 고려할 만한 선택지일 것 같다.
좀 더 사용해보아야 단점도 보이겠지? 일단은 오늘은 여까지!