Danny의 iOS 컨닝페이퍼
article thumbnail

목차

  1. FlexLayout, PinLayout을 사용해 보자!
  2. FlexLayout 모든 메서드들을 알아보자!
  3. PinLayout 메서드를 알아보자! (+ ScrollView)!

메서드들도 많고 CSS를 사용해 보지 않아서 그런가 생소한 단어들도 보이네요 ㅠ.ㅠ

뭔가 느낌은 알 것 같은데, 그래도 일단은 한 번씩은 써봐야 이해되겠죠?!

 

자, 이전 글 의 예제로 시작해 봅시다.

import UIKit
import FlexLayout
import PinLayout

class ViewController: UIViewController {
    
    private let containerView = UIView()
    
    private let label1: UILabel = {
        let label = UILabel()
        label.backgroundColor = .systemOrange
        label.text = "FlexLayout"
        return label
    }()
    
    private let label2: UILabel = {
        let label = UILabel()
        label.backgroundColor = .systemBlue
        label.text = "PinLayout"
        return label
    }()
    
    private let label3: UILabel = {
        let label = UILabel()
        label.backgroundColor = .systemBrown
        label.text = "공부하기"
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(containerView)
        
        containerView.flex
            .define {
                $0.addItem(label1)
                $0.addItem(label2)
                $0.addItem(label3)
            }
        
    }
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        containerView.pin.all(view.pin.safeArea)  // == (view.safeAreaInsets)
        containerView.flex.layout()
    }
}

 

결과

 

 

FlexLayout의 메서드

생성

.addItem(:UIView)

ContainerView의 하위 뷰에 추가합니다.

 

 

.addItem()

ContainerView 내부에 새로운 UIView(Flexbox)를 생성합니다. 손쉽게 새로운 컨테이너(Flexbox) 및 아이템을 추가할 수 있습니다.

 

설명을 위해 UIStackView라고 가정을 해보면, StackView 내부에 새로운 StackView를 만들어 주는 것과 비슷한 원리일 겁니다.

// containerView를 수직 방향(cloumn)으로 생성
containerView.flex
    .direction(.column)
    .define { flex in
    	// 위의 containerView 내부에 새로운 containerView를 생성 == addItem(),
        // center 방향으로 정렬 및 수평 방향(row)으로 label1, label2를 추가
        flex.addItem().direction(.row).justifyContent(.center).define {
            $0.addItem(label1)
            $0.addItem(label2)
        }
        
        // 다시 containerView 내부에 새로운 containerView를 생성 == addItem(),
        // center 방향으로 정렬 및 수평 방향(row)으로 label3를 추가
        flex.addItem().direction(.row).justifyContent(.center).define {
            $0.addItem(label3)
        }
    }

 

 

.layout(mode:)

ContainerView 내부의 자식들을 레이아웃합니다. 레이아웃 메서드는 마지막에 꼭 사용해줘야 합니다.

  • fitContainer - 기본 값
  • adjustHeight (height는 자식의 높이까지 조정되고, width는 ContainerView 레이아웃 크기)
  • adjustWidth (width는 자식의 너비까지 조정되고, height는  ContainerView 레이아웃 크기)

일단, 확인을 위해서 ContainerView에 색을 입혀주고 X-Center 위치하게 만들어 줬습니다.

containerView.flex
    .backgroundColor(.systemGray)
    .alignItems(.center)
    .direction(.column)
    .define {
        $0.addItem(label1)
        $0.addItem(label2)
        $0.addItem(label3)
    }


override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    containerView.pin.all(view.pin.safeArea)
    containerView.flex.layout(mode: .adjustHeight)  // ✋
}

 

 

.direction()

UIStackView의 axis와 비슷하게 사용됩니다. 축을 따라서 아이템이 생성될 방향을 설정합니다.

 

⭐️ 생성 방향을 알기 위해, 위의 그림은 꼭 기억해 주세요! (main-axis, cross-axis) ⭐️

 

간단히 column은 수직 방향, row는 수평 방향을 따라서 아이템이 생성된다고 생각하면 쉽습니다.

  • column (열) - 기본 값
  • columnReverse (열 반전) 
  • row (행)
  • rowReverse (행 반전)

코드는 아래와 같이 flex 다음 define 이전에 정의해 줍니다.

containerView.flex
    .direction(.column)  // ✋
    .define {
        $0.addItem(label1)
        $0.addItem(label2)
        $0.addItem(label3)
    }

 

제 생각엔 Reverse 들어가는 것들은 완전히 뒤집히는 구조라서, 자주 사용하진 않을 것 같네요.

 

 

정렬

.justifyContent()

이건, 현재 방향 축을 따라 정렬을 해주는 메서드입니다. (수평, 수직 정렬)

  • start - 기본값
  • end
  • center
  • spaceBetween (처음과 끝점에 item을 배치되며, 나머지는 고르게 분포)
  • spaceAround (축을 따라서 아이템을 고르게 분포)
  • spaceEvenly (축을 따라서 아이템을 고르게 분포 + 컨테이너의 양 끝까지 포함)

👉 즉, 수직 방향(column) 일 때는 Y축이 main-axis 이므로 Y을 따라서 정렬,

수평 방향(row) 일 때는 X축이 main-axis 이므로 X축을 따라서 정렬

 

⭐️ 참고, '축을 따라서' '축을 기준으로' 는 완전히 다른 말입니다! ⭐️

containerView.flex
    .direction(.row)
    .justifyContent(.center)  // ✋
    .define {
        $0.addItem(label1)
        $0.addItem(label2)
        $0.addItem(label3)
    }

 

 

.alignItems()

기본 값(stretch)을 제외하고 뷰의 본질적인 사이즈(intrinsicContentSize)로 변형이 됩니다.

 

alignItmes는 justifyContent와는 반대 방향의 축(교차 방향)을 따라서 정렬됩니다.

(예를 들어, 아이템이 수직 방향으로 나아간다면, 수평 방향을 따라서 정렬함)

  • stretch - 기본 값
  • start
  • end
  • center
  • baseline

솔직히 글만으로는 헷갈리는데요. 그럼 그림과 비교해 보는 게 답이죠!

(stretch는 늘려주기 때문에 제외하고 비교해 봅시다)

 

👉 즉, 수직 방향(column) 일 때는 Y축이 main-axis 이므로 X축을 따라서 정렬,

수평 방향(row) 일 때는 X축이 main-axis 이므로 Y축을 따라서 정렬

containerView.flex
    .direction(.row)
    .alignItmes(.center)  // ✋
    .define {
        $0.addItem(label1)
        $0.addItem(label2)
        $0.addItem(label3)
    }

 

 

👉 justifyContent, alignItems은 한 축의 방향에서만 정렬됩니다.

예를 들어, 수직 방향(Column)에서 justifyContent(.center)를 해줬다고 가정해 봅시다. 그렇다면 Y축에서만 Center로 정렬되게 됩니다. X축에서는 Center로 정렬되지 않아요! 그러므로 X-Y축을 모두 Center로 정렬시켜 주기 위해선, justifyContent, alignItems 두 메서드를 동시에 사용해줘야 합니다.

containerView.flex
    .direction(.column)
    .justifyContent(.center)  // ✋
    .alignItems(.center)      // ✋
    .define {
        $0.addItem(label1)
        $0.addItem(label2)
        $0.addItem(label3)
    }

 

 

.alignSelf()

이건 alignItems를 재정의하여, ContainerView의 item(자식)을 교차 방향으로 정렬하는 방식입니다.

간단히, 'ContainerView 내부의 item에 대한 정렬'이라고 생각해도 될 것 같습니다.

containerView.flex
    .direction(.column)
    .alignItems(.center)
    .define {
        $0.addItem(label1).alignSelf(.start)  // ✋
        $0.addItem(label2)
        $0.addItem(label3).alignSelf(.end)  // ✋
    }

 

 

wrap()

한 줄로 쌓일지 여러 줄로 쌓일지 선택하는 메서드입니다.

(ㅁ축에 따라서 줄이 쌓이는 방향이 달라집니다)

  • noWrap - 기본값
  • wrap
  • wrapReverse

설명을 조금 더 추가해 보자면, 만약 item이 쌓여 ContainerView의 영역보다 커지게 된다면, 다음줄로 이동해서 다시 item이 쌓이는 형식입니다.

// 방향 (Column)
containerView.flex
    .direction(.column)
    .wrap(.wrap)  // ✋
    .define {
        $0.addItem(label1).height(400)
        $0.addItem(label2).height(300)
        $0.addItem(label3).height(200)
        $0.addItem(label4).height(100)
    }
// 방향 (Row)
containerView.flex
    .direction(.row)
    .wrap(.wrap)  // ✋
    .define {
        $0.addItem(label1).width(400)
        $0.addItem(label2).width(300)
        $0.addItem(label3).width(200)
        $0.addItem(label4).width(100)
    }

 

 

alignContent()

justifyContent와 같은 방식으로 정렬을 하는 메서드입니다. 대신 두 줄 이상 쌓여야 적용이 가능합니다.

 

간단히 사용해 보니 addItem()으로 두 줄을 만들었을 땐 적용이 안되네요... wrap메서드를 통해 두 줄 이상이 만들어졌을 때 동작하였습니다.

  • start - 기본값
  • end
  • center
  • stretch
  • spaceBetween, spaceAround (이유를 모르겠지만, 생각한 데로 정렬이 나오지 않음)
// 방향 (Column)
containerView.flex
    .direction(.column)
    .wrap(.wrap)
    .alignContent(.center)  // ✋
    .define {
        $0.addItem(label1).height(400)
        $0.addItem(label2).height(300)
        $0.addItem(label3).height(200)
        $0.addItem(label4).height(100)
    }
// 방향 (Row)
containerView.flex
    .direction(.row)
    .wrap(.wrap)
    .alignContent(.center)  // ✋
    .define {
        $0.addItem(label1).width(400)
        $0.addItem(label2).width(300)
        $0.addItem(label3).width(200)
        $0.addItem(label4).width(100)
    }

 

 

ContainerView 내부 item 관련 메서드

grow()

item의 확장과 관련이 있습니다.

ContainerView에서 남은 여백에 따라, item을 설정한 비율로 확장시킵니다.

파리미터에 배율을 입력(0 < grow-value), 기본값은 0입니다. ContainerView 보다 커지지 않음

 

코드를 살펴보면, 각 Label에 grow 메서드가 사용되었습니다.

  • Label1은 기존 사이즈를 유지
  • Label2는 회색 영역(남은 여백)의 1배 크기로 확장
  • Label3는 회색 영역(남은 여백)의 2배 크기로 확장
containerView.flex
    .direction(.column)
    .define {
        $0.addItem(label1)
        $0.addItem(label2).grow(1)
        $0.addItem(label3).grow(2)
    }

 

사진을 잘라서 비교해 본 결과, 완벽한 1 : 2 비율은 아니더라고요?! 대략적인 크기로 확장되는 것 같습니다.

 

 

shrink()

item의 축소와 관련 있습니다. item이 ContainerView의 영역보다 커졌을 때,

그 벗어난 영역에서 크기에 대해서, 설정한 비율로 item을 축소시킵니다.

파리미터에 배율을 입력(0 < shrink-value), 기본값은 0입니다. ContainerView 보다 작아지지 않음

 

코드를 살펴보면, 일단 여기서 ContainerView는 self.view 영역입니다.

일단 item이 ContainerView의 영역을 벗어나기 위해 각각의 Label의 height를 200, 400, 600을 줬습니다.

 

shrink(1)로 설정했기 때문에, 전체적인 비율은 1:1:1을 유지하면서 ContainerView 내부로 축소됩니다.

containerView.flex
    .direction(.column)
    .define {
        $0.addItem(label1).height(200)
            .shrink(1)
        $0.addItem(label2).height(400)
            .shrink(1)
        $0.addItem(label3).height(600)
            .shrink(1)
    }

 

비율(0:1:3)로 만들어 준다면, Label1은 기존 높이(200)를 갖고, Label2와 Label3만 1:3 비율로 축소하게 됩니다.

containerView.flex
    .direction(.column)
    .define {
        $0.addItem(label1).height(200)
            .shrink(0)
        $0.addItem(label2).height(400)
            .shrink(1)
        $0.addItem(label3).height(600)
            .shrink(3)
    }

 

머리가 안 좋은 건가? 2시간 동안 계산 해봤는데, 값이 계속 틀려요 ㅠ.ㅠ 도저히 모르겠어요...

그래서 계산식은 패스하겠습니다.

 

 

basis()

grow, shrink로 메서드로 인해 공간이 분배되기 이전에, item의 크기를 초기화합니다.

nil을 지정하면 자동으로 설정됩니다. 즉, 길이가 정해지지 않을 경우 Content에 따라 결정됩니다.

 

 

isIncludedInLayout()

특정 UIView를 레이아웃에 포함 여부를 묻는 메서드입니다.

 

음... 레이아웃에서 포함을 시키지 않는다면 화면에 나타나지 않을 텐데, 왜 쓰는 걸까?

  • 특정 조건에 따라 UI의 일부를 동적으로 숨기거나 나타내야 할 때 (데이터 호출 성공 등..)
  • 디바이스의 화면 방향에 따라 특정 UI요소를 숨기거나 나타내야 할 때 (가로 방향, 세로 방향)
  • 사용자와 상호작용에 의해 특정 UI 요소를 숨겨야 하거나 나타야 할 때 (버튼)

더 있을 것 같은데, 지금은 생각이 안 나네요.

 

일단 진짜 간단히 예제를 만들어 봤습니다.

var isOn: Bool = true

override func viewDidLoad() {
    super.viewDidLoad()
        
    containerView.flex
        .direction(.row)
        .define {
            $0.addItem(label1)
            $0.addItem(label2)
            $0.addItem(label3)
            $0.addItem(button)
        }
}


@objc func tappedButton() {
    isOn.toggle()
    
    label2.flex.isIncludedInLayout(isOn)  // ✋
    containerView.flex.layout()  // or view.setNeedsLayout()
}

 

 

display()

만약, value를 none으로 설정하면 item을 레이아웃에 포함시키지 않고 숨깁니다.

 

음, isIncludedInLayout와 똑같은 기능 아닌가? 싶은데 이상하네요...

 

 

markDirty()

ContainerView의 item의 크기가 변할 때, 레이아웃을 변경시켜 줍니다.

구동 방식 - markDirty를 적용한 item이 변경이 일어나는 시점에 ContainerView 내부 전체를 다시 계산시켜 줍니다.

 

UILabel로 예를 들자면, 기본적으로 레이아웃이 잡혀있다면 텍스트가 변해도 설정한 레이아웃은 변경되지 않을 겁니다. 만약, 텍스트의 글자수에 따라 동적으로 레이아웃을 업데이트시켜주고 싶을 땐, markDirty()를 사용할 수 있습니다.

 

버튼을 눌렸을 때, 길어진 텍스트만큼 레이아웃을 업데이트시키는 예제

containerView.flex
    .justifyContent(.center)
    .define { flex in
        flex.addItem().justifyContent(.center).direction(.row).define {
            $0.addItem(label1)
            $0.addItem(label2)
            $0.addItem(label3)
        }
        flex.addItem().define {
            $0.addItem(button)
        }
    }
}


@objc func tappedButton() {
    label1.flex.markDirty()  // ✋ 위치는 상관없어 보입니다. layoutSubviews에서도 동작하네요.

    label1.text = "길어진다 길어진다 길어진다!"
    
    containerView.flex.layout() // or view.setNeedsLayout()
}

 

왼쪽 - MarkDirty 적용 안 함, 오른쪽 - MarkDirty 적용함

 

 

sizeThatFits()

지정된 사이즈로 CGSize를 리턴합니다.

let layoutSize = viewA.flex.sizeThatFits(size: CGSize(width: 200, 
                                                      height: CGFloat.greatestFiniteMagnitude))

 

 

intrinsicSize

item이 갖고 있는 본질적인 크기를 나타냅니다. 대게 아이템의 내용, 텍스트, 이미지 등에 의해 결정됩니다.

let intrinsicSize = label.flex.intrinsicSize

 

 

Margins & Padding

Margins - 특정 가장 가까운 형제 또는 부모로부터 가져야 하는 offset을 지정합니다.

Padding - ContainerView 가장자리에서 자식(itme)이 가져야 하는 offset을 지정합니다.

view.flex.margin(20)
view.flex.marginTop(20%).marginLeft(20%)
view.flex.marginHorizontal(20)
view.flex.margin(safeAreaInsets)
view.flex.margin(10, 12, 0, 12)

view.flex.padding(20)
view.flex.paddingTop(20%).paddingLeft(20%)
view.flex.paddingBottom(20)
view.flex.paddingHorizontal(20)
view.flex.padding(10, 12, 0, 12)

 

 

기타

// position
view.flex.position(.absolute).top(10).right(10).width(100).height(50)
view.flex.position(.absolute).left(20%).right(20%)
// size
view.flex.width(100)	
view.flex.width(50%)	
view.flex.height(200)

view.flex.size(250)
// maxWidth, height
view.flex.maxWidth(200)
view.flex.maxWidth(50%)
view.flex.width(of: view1).maxWidth(250)

view.flex.maxHeight(100)
view.flex.height(of: view1).maxHeight(30%)
// aspectRatio
imageView.flex.aspectRatio(16/9)
// 배경, 라운드, 테두리
flex.addItem().backgroundColor(.gray).define { (flex) in
    flex.addItem().height(1).backgroundColor(.black)
}

flex.addItem().cornerRadius(12)

flex.addItem().border(1, .black)

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!