본문으로 건너뛰기

PinLayout, FlexLayout 사용하여 뷰 그리기(3)

머리말

플렉스 레이아웃을 사용하는 방법들을 간략하게 소개한다.

이미지 설명

GitHub - layoutBox/FlexLayout

플렉스레이아웃은 요가 플렉스 박스를 스위프트로 구현한 레이아웃 패키지다.

요가는 CSS 플렉스 박스를 멀티플랫폼으로 구현한 것이고 리액트 네이티브의 엔진이기도 하다.

  • 플렉스박스는 핀레이아웃과 함께 사용할 수 있도록 설계되었다.
  • 두 패키지 모두 레이아웃박스라는 레포에 소속되어 있다.
  • 많은 레이아웃의 정밀한 레이아웃 및 설정이 필요하지 않다면 플렉스레이아웃이 권장된다.
  • 복잡한 레이아웃 및 애니메이션은 핀레이아웃이 권장된다.

설치

설치 방법은 아래 세 가지를 지원한다.

  • CocoaPods
  • Carthage
  • Swift Package Manager

간편한 Swift Package Manager 를 사용하였다.

  • 패키지에 직접추가하기
.package(url: "https://github.com/layoutBox/FlexLayout.git", from: "1.3.18")

  • xcode UI로 추가하기

필자는 설치 시 핀레이아웃 패키지도 함께 설치해 사용하길 권장한다.

CSS 구문과의 비교

플렉스레이아웃은 CSS의 FlexBox 의 레이아웃 배치 방식을 사용하며 동일한 이름의 구문을 제공한다.

FlexLayout NameCSS NameReact Native Name
directionflex-directionflexDirection
wrapflex-wrapflexWrap
growflex-growflexGrow
shrinkflex-shrinkflexShrink
basisflex-basisflexBasis
startflex-startflexStart
endflex-endflexEnd

프로퍼티도 대부분 동일하지만 약간의 차이가 있기도 하다.

PropertyFlexLayout default valueCSS default valueReact Native default value
directioncolumnrowcolumn
justifyContentstartstartstart
alignItemsstretchstretchstretch
alignSelfautoautoauto
alignContentstartstretchstart
grow000
shrink010
basis0auto0
wrapnoWrapnowrapnoWrap

플렉스 레이아웃에만 있는 구문들도 있다.

  • FlexLayout additions:
    • addItem() → 플렉스의 하위 뷰 추가
    • define() → 플렉스 박스 선언 및 하위 플렉스 클로저 반환
    • layout() → 플렉스 레이아웃 배치 실행
    • isIncludedInLayout() → UIView가 플렉스박스 레이아웃에 포함되는지 동적으로 제어
    • markDirty() → 레이아웃 변화 예고
    • intrinsicSize → 고유 사이즈 반환
    • sizeThatFits() → 특정 사이즈일 때 사이즈 반환

사용방법

뷰 컨트롤러와 뷰 분리

뷰 작성 코드의 효율적인 관리를 위해 뷰 컨트롤러와 뷰를 분리하는 것이 기본적인 패턴이다. MVC 패턴을 적용한다면 뷰 컨트롤러는 상태 변화를 감지할 경우 mainView 에 전달하여 뷰를 갱신한다.

class ViewController: UIViewController {
// 1. MyView를 반환하는 계산 프로퍼티
private var mainView: MyView {
view as! MyView
}

override func viewDidLoad() {
super.viewDidLoad()
// 3. 뷰를 컨트롤한다.
mainView.backgroundColor = .red
}

// 2. 뷰가 불러온 시점에 뷰 생성 후 뷰 컨트롤러의 뷰로 할당
override func loadView() {
view = MyView()
}
}

class MyView: UIView {
init() {
super.init(frame: .zero)
print("Hello FlexLayout")
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}


루트 뷰 생성 및 뷰 배치하기

플렉스레이아웃은 스택뷰처럼 부모 뷰를 생성하고, 그 하위에 자식 뷰들을 추가하는 방식으로 뷰를 그린다.

class MyView: UIView {
// 1. 최상위 부모가 될 루트 뷰를 생성한다.
private let rootView = UIView()

init() {
super.init(frame: .zero)
// 2. 인스턴스 생성 시 뷰를 설정하고, 스타일링 한다.
rootView.flex.width(100).height(100).backgroundColor(.blue)
// 3. 뷰를 추가한다.
addSubview(rootView)
}

override func layoutSubviews() {
super.layoutSubviews()
// 4. 서브 뷰가 레이아웃 되는 시점에 레이아웃을 적용한다.
rootView.flex.layout(mode: .adjustHeight)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

핀 레이아웃을 사용해 여러 개의 뷰를 작성하지 않고, 단일 플렉스 레이아웃만 사용할 경우 아래와 같은 사용이 권장 된다.

override func layoutSubviews() {
super.layoutSubviews()
// 5. pin 레이아웃과 함께 사용하면 쉽게 뷰 전체를 채울 수 있다.
rootView.pin.all(pin.safeArea)
rootView.flex.layout()
}

플렉스 박스 레이아웃이 적용된 뷰의 변화는 오토 레이아웃으로 변동되지 않는다. 혼용시 주의 필요

override func viewDidLoad() {
super.viewDidLoad()
mainView.backgroundColor = .red

// 오토 레이아웃 코드 적용 안됨.
NSLayoutConstraint.activate([
mainView.rootView.topAnchor.constraint(equalTo: view.topAnchor),
mainView.rootView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mainView.rootView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mainView.rootView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

// 코드 적용 됨.
mainView.rootView.flex.width(200)
}

자식 플렉스박스 뷰 추가하기

define 과 addItem 을 조합해 자식 플랙스 박스를 생성할 수 있다.


let rootView = UIView()

init() {
super.init(frame: .zero)
// 1. define 선언
rootView.flex.define { flex in
// 2. addItem() 을 통해 자식 플렉스박스 생성
flex.addItem().width(50).height(50).backgroundColor(.brown)
}
.width(100).height(100).backgroundColor(.blue)

addSubview(rootView)
}

플랙스 박스 추적을 위해 미리 뷰를 생성하고 하위 뷰에 추가할 수도 있다.

let rootView = UIView()
// 1. 뷰 생성
private let myButton: UIButton = {
let button = UIButton()
button.setTitle("버튼", for: .normal)

return button
}()

init() {
super.init(frame: .zero)
rootView.flex.define { flex in
flex.addItem()
.width(50).height(50).backgroundColor(.brown)
flex.addItem()
.width(200).height(200).backgroundColor(.yellow)
// 2. addItem에 View 전달
flex.addItem(myButton)
.width(200).height(64).backgroundColor(.green)
// 하위 플랙스 박스 뷰의 최대 사이즈가 상위 플랙스 박스의 intrinsicSize 가 된다.
print(rootView.flex.intrinsicSize) // (200.0, 314.0)
}
.backgroundColor(.blue)

addSubview(rootView)
}

클로저이므로 다양한 문법도 사용 가능

  • 파라미터 축약 가능
// 축약 구문 사용
rootView.flex.define { $0.addItem(myButton).width(200).height(50) }
.backgroundColor(.blue)

  • 클로저 내 반복문도 가능
rootView.flex.define { flex in
// 반복문 사용
for index in 0...10 {
flex.addItem().width(CGFloat(index)).height(50).backgroundColor(.red)
.marginBottom(4)
}
flex.addItem(myButton)
.width(200).height(64).backgroundColor(.green)
print(rootView.flex.intrinsicSize)
}
.backgroundColor(.blue)

뷰 레이아웃 및 스타일하기

  // flex 레이아웃 사용 // Y축 정렬 중앙, X축 정렬 중앙
rootView.flex.justifyContent(.center).alignItems(.center).define {
flex in
for index in 0...10 {
flex.addItem().width(CGFloat(index)).height(50).backgroundColor(.red)
.marginBottom(4)
}
flex.addItem().width(100).height(50).backgroundColor(.yellow)
flex.addItem(myButton)
.width(200).height(64).backgroundColor(.green)
}
.backgroundColor(.blue)


rootView.flex.justifyContent(.spaceBetween).alignItems(.end).define {
flex in
// 하위 플랙스박스 추가
flex.addItem().direction(.row).gap(4).define { flex in
for index in 0...10 {
flex.addItem()
.width(CGFloat(index)).height(50)
.backgroundColor(.red).marginBottom(4)
}
}
flex.addItem().width(100).height(50).backgroundColor(.yellow)
flex.addItem(myButton)
.width(200).height(64).backgroundColor(.green)
}
.backgroundColor(.blue)

스타일링 시 주의점으로 %가 있다. width, height 등 값 사용 시 %를 사용해 특정 비율을 사용할 수 있는데, 이 비율이 연속될 경우, CSS와 달리 균등하게 분배되는 것이 아닌 동일한 값이 적용된다.

%값과 상수값과의 +- 와 같은 연산도 불가능 그러므로 고정 값을 가져온 후 계산할 수 있게 하자.

override func layoutSubviews() {
super.layoutSubviews()
print(rootView.frame) // (0.0, 0.0, 0.0, 0.0)
rootView.pin.all(pin.safeArea) // --- 루트 뷰의 프레임은 핀 레이아웃 이후 계산된다.
print(rootView.frame) // (0.0, 0.0, 393.0, 759.0)
print(myButton.frame) // (0.0, 0.0, 0.0, 0.0)
rootView.flex.layout() // --- 루트 뷰 하위 뷰 프레임은 레이아웃 이후 계산된다.
print(myButton.frame) // (193.0, 695.0, 200.0, 64.0)

}

스크롤 뷰 설정하기

스크롤 뷰의 컨텐츠 사이즈를 조정해 줘야한다. 스크롤 뷰를 감싸는 컨테이너의 사이즈와 동일하게 하기를 권장

override func layoutSubviews() {
super.layoutSubviews()

// 1) Layout the contentView & rootFlexContainer using PinLayout
contentView.pin.top().bottom().left().right()
rootFlexContainer.pin.top().left().right()

// 2) Let the flexbox container layout itself and adjust the height
rootFlexContainer.flex.layout(mode: .adjustHeight)

// 3) Adjust the scrollview contentSize
contentView.contentSize = rootFlexContainer.frame.size
}

뷰 변경 적용하기

레이아웃이 변경될 때 플렉스 레이아웃은 수동 갱신이 불가능하므로, 직접 뷰를 갱신 시켜줘야 한다.

lazy var action = UIAction { _ in
self.myButton.flex.backgroundColor(.purple) // 바로 적용된다.
self.myButton.flex.width(100) // 바로 적용되지 않는다.
}

lazy var action = UIAction { _ in
self.myButton.flex.backgroundColor(.purple) // 바로 적용된다.
self.myButton.flex.width(100) // 바로 적용된다.

self.setNeedsLayout() // 추가

}


lazy var action = UIAction { _ in
self.myButton.flex.backgroundColor(.purple)
self.myButton.flex.width(100)

self.myButton.flex.markDirty() // 최적화를 위해 추가해주자.
self.setNeedsLayout()

}

뷰 애니메이션

애니메이션을 통해 레이아웃 변경시 layoutInfNeeded() 작성해 줘야한다.

lazy var action = UIAction { _ in
self.myButton.flex.backgroundColor(.purple)
self.myButton.flex.markDirty()
self.setNeedsLayout()

// 애니메이션 추가
UIView.animate(withDuration: 0.3) {
self.myButton.flex.width(100)
self.layoutIfNeeded()
}
}

맺음말

지금까지 플렉스레이아웃을 사용한 뷰 그리는 방법에 대해 알아봤다. 플렉스레이아웃은 선언형으로 직관적이고 쉽게 뷰를 그릴 수 있도록 도와준다. 장점만 갖는 것은 아니다. 오토 레이아웃에 익숙하다면, 다른 방식의 뷰 그리는 방식이 불편할 수 있다. 또한 뷰의 변화가 있다면 수동으로 갱신할 필요가 있다. 이러한 장 · 단점을 잘 인지해서 뷰를 그릴 때 활용하도록 하자.


참고 자료