iOS

iOS) PinLayout

snowe 2022. 2. 2. 12:30
https://github.com/layoutBox/PinLayout 을 참고했습니다.

 

PinLayout

Luc Dion이라는 개발자가 만든 프레임워크이다. layoutBox라는 Swift를 사용한 layout 오픈 소스 organization 관리자인 이분은 PinLayout뿐만 아니라 FlexLayout을 만든 사람이기도 하다.

 

 

PinLayout은 기본적으로 AutoLayout을 사용하지 않고 레이아웃을 구성할 수 있도록 도와주는 프레임워크이다. AutoLayout을 사용하지 않음으로서 렌더링 성능이 훨씬 빠르다고 한다(8~12배). PinLayout은 수동으로 view를 layout하는데 때문에 Manual Layout과 속도가 비슷하거나 더 빠르다.

https://github.com/layoutBox/PinLayout/blob/master/docs/Benchmark.md

 

SnapKitAutoLayout을 좀 더 직관적으로 잡을 수 있도록 도와준 프레임워크라면,

PinLayoutManualLayout을 직관적으로 잡을 수 있게 도와주는 프레임워크이다.

(그렇다고 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

Superview에게 모든 edge를 10만큼 떨어뜨려서 레이아웃 잡기

빠르게 위의 레이아웃을 잡기위한 코드부터 보자면

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

[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은 이걸 다음과 같이 표현한다.

  1. UIView.pin.safeArea: UIKit's UIView.safeAreaInsets / UIView.safeAreaLayoutGuide.
  2. UIView.pin.readableMargins: UIKit's UIView.readableContentGuide.
  3. UIView.pin.layoutMargins: UIKit's UIView.layoutMargins / UIView.layoutMarginsGuide.

ipad에서 나타낸 영역과 가이드들

 


 

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만이 가진 장점들이 존재하기 때문에 충분히 고려할 만한 선택지일 것 같다.

 

좀 더 사용해보아야 단점도 보이겠지? 일단은 오늘은 여까지!