apple/iOS, UIKit, Documentation

iOS Snapkit 나만의 정리 모음

lgvv 2021. 8. 25. 13:21

iOS Snapkit 나만의 정리 모음

 

내가 스냅킷 공부하면서 정리하고자 작성한 자료

추후에 사용하면서 점차 업데이트하기

 

히스토리

  • (init) 2021. 8. 25. 13:21 : 최초 포스팅 (목차 5까지)
  • (update) 2022. 1. 14. 21:04 : (목차 6번 추가)
  • (update) 2022. 2 .18 20:52 : (목차 7~ 10 추가, LayoutTraining.zip 코드 추가)
  • (update) 2022. 9 .22 19:00 : (코드 스타일에 대한 첨언)
  • (update) 2022. 9. 25 00:11: (FlexLayout, PinLayout추가) 및 포스팅 코드 스타일 변경
    • 예제 파일을 업데이트 하려고 했으나, 너무 해야할 일들이 많아서 포스팅에 나온 코드의 일부를 정리
  • (update) 2023. 6. 17 14:04: 블로그에 나온 코드 일부를 컨벤션을 적용한 스타일로 수정
  • (update) 2024. 11. 18 02:13: 코드 및 포스팅 스타일 전반적으로 개선

 

목차

  • 1. translatesAutoresizingMaskIntoConstraints = false를 쓰지 않아도 되는 이유
  • 2. offset 과 inset의 차이점
  • 3. snp.left와 snp.leading의 차이 / snp.right와 snp.trailing의 차이
  • 4. Constraint를 updateConstraints로 조정해보기
  • 5. Constraint를 remakeConstraints로 조정해보기 (feat. addTarget)
  • 6. StackView와 divideby를 활용한 snapKit구성(코드 라인 줄이기?)
  • 7. setContentCompressionResistancePriority 알아보기.
  • 8. setContentHuggingPriority 알아보기
  • 9. SnapKit + animation 효과주기
  • 10. remakeConstraints와 updateConstraints 추가 정리
  • 11. FlexLayout + PinLayout

 

1. translatesAutoresizingMaskIntoConstraints = false를 쓰지 않아도 되는 이유

  • 일반적으로 코드 기반으로 오토레이아웃을 사용한다면 해당 옵션을 반드시 작성해주어야 함.
  • 하지만 SnapKit을 사용한다면 작성하지 않아도 된다.
  • 오픈소스 내부에서 prepare() 메소드에서 처리해주고 있음.

Pods/Pods/SnapKit/LayoutConstraintItem 에 정의되어 있다.

 

 

2. offset 과 inset의 차이점

Offset

  • 정의: 특정 뷰의 경계 외부에서 간격을 설정하는 데 사용
  • 설명: offset은 뷰가 기준점(Anchor)에서 떨어져야 하는 거리(간격)를 나타냄. 기준점의 바깥쪽으로 이동
  • 예시:
    • offset(10): 기준점에서 아래(positive 방향)로 10포인트 떨어짐.
    • offset(-10): 기준점에서 위(negative 방향)로 10포인트 떨어짐.
        box.snp.makeConstraints {
            $0.top.leading.equalToSuperview().offset(50)
            $0.bottom.trailing.equalToSuperview().offset(-50)
        }

 

 

Inset

  • 정의: 특정 뷰의 경계 내부에서 간격을 설정하는 데 사용
  • 설명: inset은 기준점의 내부에 있는 간격을 나타냄. 주로 뷰의 내부 여백(padding)을 지정할 때 사용
  • 예시:
    • inset(10): superview의 내부 여백이 10포인트.
    • inset(UIEdgeInsets(top: 10, left: 15, bottom: 20, right: 25)): 상, 좌, 하, 우의 여백을 각각 지정.
box.snp.makeConstraints { 
   $0.edges.inset(50)
}

 

 

offset 과 inset 예제 코드의 결과는 같음.

 

 

3. snp.left와 snp.leading의 차이 / snp.right와 snp.trailing의 차이

 

아래의 표처럼 ViewAttribute는 NSLayoutAttribute와 매칭됨

http://snapkit.io/docs/

 

그럼 NSLayoutAttribute의 left와 leading, right와 trailing이 왜 각각 존재하는 이유

 

  • leading, trailing으로 설정하면 right-to-left 순서로 읽는 지역에서 화면이 거꾸로(flip되어서) 표시
  • 하지만 left, right로 설정하면 왼쪽 오른쪽 고정.
  • 왼쪽, 오른쪽은 모든 지역에서 항상 똑같은 위치이지만, leading, trailing은 각 지역마다 다르게 받아들임.

https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/SupportingRight-To-LeftLanguages/SupportingRight-To-LeftLanguages.html

 

 

 

4. Constraint를 updateConstraints로 조정해보기

스냅킷을 사용하지 않고 오토레이아웃을 처리한다면

  • 아래 사진처럼 레이아웃 프로퍼티를 선언하고
  • 값을 변경한 후
  • isActive를 다시 수행해줌.

updateConstraints를 사용할 경우에는 초기에 makeConstraints에서 잡혀있는 값들만 수정 가능.

  • 이건 오토레이아웃도 동일한 원리원칙
  • 만약 make에서 없는 제약조건을 변경할 경우 크래시 발생

 

Constraint를 reference로 들고 있는 모습

 

 

SnapKit을 활용하면 더 간결하게 처리할 수 있음.

        box.snp.makeConstraints {
            $0.edges.equalToSuperview().inset(100)
        }
        
        button.snp.makeConstraints {
            $0.top.equalTo(box.snp.bottom).offset(10)
            $0.centerX.equalToSuperview()
        }
        
        button.rx.tap
            .withUnretained(self)
            .bind { owner, event in 
                owner.box.snp.updateConstraints {
                    $0.top.left.right.equalToSuperview()
                }
            }
            
 // MARK: - 위 코드는 RxSwift 6.5 이상을 사용한 예제
 // 아래 코드와 의미는 동일
         button.rx.tap
            .bind { [weak self] event in
                self?.box.snp.updateConstraints {
                    $0.top.left.right.equalToSuperview()
                }
            }

 

 

5. Constraint를 remakeConstraints로 조정해보기 (feat. addTarget)

remake는 말그대로 make 된 값을 지우고 다시 그려내는 과정.

  • remake 내부를 보면 뷰 계층에서 remove도 함께 수행함
  • remove 때문에 애니메이션 및 다른 UI에 영향을 줄 수 있음.

remake 내부

 

    var button: UIButton = {
        $0.backgroundColor = .brown
        $0.setTitle("button", for: .normal)
        return $0
    }(UIButton())
    
    func layout() {
        box.snp.makeConstraints {
            $0.edges.equalToSuperview().inset(100)
        }
        button.snp.makeConstraints{
            $0.top.equalTo(box.snp.bottom).offset(10)
            $0.centerX.equalToSuperview()
        }
        button.addTarget(self,action: #selector(self.didTappedAction(_:)), for: .touchUpInside)
    }
    
    @objc private func didTappedAction(_ sender : UIButton) {
    	print("didTapButton")
        self.box.snp.remakeConstraints {
            $0.top.left.right.bottom.equalTo(0)    
        }
        
        self.button.snp.remakeConstraints {
            $0.center.equalToSuperview()
        }
        
        UIView.animate(withDuration: 1.0) {
            self.view.layoutIfNeeded()
        }
    }

 

 

6. StackView와 divideby를 활용해 SnapKit 구성하기

기본적으로 StackView를 많이 활용하는데, SnapKit을 활용해도 좋음.

import Then
import UIKit
import RxCocoa
import RxSwift
import SnapKit

final class ViewController: UIViewController {
    
    init() {
        super.init(nibName: nil, bundle: nil)
        
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        configureUI()
    }
    
    //MARK: UIComponents
    
    private let stackViewAuto = UIStackView().then {
        $0.distribution = .fillEqually
    }
    
    private let connectBtnAuto = UIButton().then {
        $0.setTitle("connectBtnAuto", for: .normal)
        $0.backgroundColor = .magenta
    }
    
    private let writeBtnAuto = UIButton().then {
        $0.setTitle("writeBtnAuto", for: .normal)
        $0.backgroundColor = .purple
    }
    
    private let disconnectBtnAuto = UIButton().then {
        $0.setTitle("disconnectBtnAuto", for: .normal)
        $0.backgroundColor = .systemPink
    }
    
    private let stackViewFirst = UIStackView().then {
        $0.distribution = .fillEqually
    }
    
    private let connectBtn = UIButton().then {
        $0.setTitle("connectBtn", for: .normal)
        $0.backgroundColor = .blue
    }
    
    private let writeBtn = UIButton().then {
        $0.setTitle("writeBtn", for: .normal)
        $0.backgroundColor = .green
    }
    
    private let disconnectBtn = UIButton().then {
        $0.setTitle("disconnectBtn", for: .normal)
        $0.backgroundColor = .red
    }
    
    private let tableView = UITableView().then {
        $0.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        $0.backgroundColor = .brown
    }
    
    private func configureUI() {
        view.addSubview(stackViewFirst)
        stackViewFirst.snp.makeConstraints {
            $0.top.left.right.equalToSuperview()
            $0.bottom.equalTo(view.snp.centerY).dividedBy(2)
        }
        
        view.addSubview(stackViewAuto)
        stackViewAuto.snp.makeConstraints {
            $0.top.equalTo(stackViewFirst.snp.bottom)
            $0.left.right.equalToSuperview()
            $0.bottom.equalTo(view.snp.centerY)
        }
        
        view.addSubview(tableView)
        tableView.snp.makeConstraints {
            $0.top.equalTo(view.snp.centerY)
            $0.left.bottom.right.equalToSuperview()
        }
        
        [connectBtn, writeBtn, disconnectBtn].forEach { stackViewFirst.addArrangedSubview($0) }
        [connectBtnAuto, writeBtnAuto, disconnectBtnAuto].forEach { stackViewAuto.addArrangedSubview($0) }
    }
}

 

 

위의 코드의 결과

 

 

7. setContentCompressionResistancePriority 알아보기.

setContentCompressionResistancePriority 

  • 압축에 대한 저항성이 높을수록 다른 것과 비교했을때 내가 줄어들지 않을 수 있음.

 

해당 UI

 

title.setContentCompressionResistancePriority(.init(rawValue: 750), for: .horizontal)

title.snp.makeConstraints {
    $0.top.leading.equalTo(safeAreaLayoutGuide).inset(16)
    $0.trailing.equalTo(author.snp.leading)
    $0.width.equalTo(150)
}
author.setContentCompressionResistancePriority(.init(rawValue: 250), for: .horizontal)
author.snp.makeConstraints {
    $0.top.trailing.equalTo(safeAreaLayoutGuide).inset(16)
    $0.leading.equalTo(title.snp.trailing)
    $0.width.equalTo(1000)
}

 

예제에서는 author width가 1000으로 설정되어 있음.

  • author와 title이 서로 레이아웃이 걸려있음.
  • author가 width가 1000이라서 전부 차지해야 할 것 같은데, title의 압축저항이 더 높아서 title이 width 150이 보장됨

 

그렇다면 둘의 우선순위를 바꾸면 어떻게 될까?

그림과 같은 결과 도출

 

 

 

title의 압축저항을 낮추면 어떻게 될까?

 

  • 당연하겠지만 author가 모든 width를 차지하게 됨.
  • 둘의 레이아웃 충돌 상황에서 title이 저항도가 낮아서 author가 다 가짐

변경된 코드로 나타난 UI

무엇보다 줄어드는 상황에서 적용되는 것이다.

 

 

 

8. setContentHuggingPriority 알아보기

이건 누군가 늘어나야 하는 상황에서의 우선도를 처리함.

  • 애는 둘다 100을 주어서 화면을 둘다 전부 차지할 수 없음
  • 하지만 leading, trailing을 서로를 잡고 있어서, 빈 영역에 대해서 누군가 늘어나야 하는 상황
  • hugging 우선순위가 낮은게 늘어남
    • 우선순위가 높으면 "너가 나한테 와서 안아줘"로 해석 가능
title.setContentHuggingPriority(.init(rawValue: 750), for: .horizontal)

title.snp.makeConstraints {
    $0.top.equalToSuperView()
    $0.leading.equalToSuperView()
    $0.trailing.equalTo(bookAuthor.snp.leading)
    $0.width.equalTo(100)
}
author.setContentHuggingPriority(.init(rawValue: 250), for: .horizontal)
author.snp.makeConstraints {
    $0.top.equalToSuperView()
    $0.trailing.equalToSuperView()
    $0.leading.equalTo(title.snp.trailing)
    $0.width.equalTo(100)
}

 

 

UI 예시

 

 

9. SnapKit으로 애니메이션 효과처리

스냅킷을 사용해 애니메이션 효과를 처리하고자 함.

  • 원하는 레이아웃으로 updateConstraint를 한 후 layoutIfNeed 호출

 

button.rx.tap
    .bind(with: self) { this, controlEvent in
        UIView.animate(
            withDuration: 1.0
        ) {
            this.layoutIfNeeded()
        }
     }.disposed(by: bag)

 

 

 

10. remakeConstraints와 updateConstraints 추가 정리

 

개발하다가 레이아웃 업데이트 하는 지점에서 지속적으로 크래시 만남.

  • remakeConstraints -> 새로운 레이아웃을 적용해야 할 경우
    • 애는 remove부터 새로 잡음.
    • remove하는 것에서 발생하는 사이드 및 레이아웃 렌더링 비용 증가할 수도 있음.
  • updateConstraints -> 기존에 작성된 레이아웃에서 값을 변경해야 할 경우
    • 얘는 기존에 make로 잡힌 제약이어야 함!

 

FlexLayout + PinLayout

 

요즘은 Flex + Pin이 대세인 것 같음.

성능도 더 좋다고 하니 추가적으로 학습해보기

 

 

https://rldd.tistory.com/478?category=952720

 

[iOS] UICollectionView에 대해서 알아보기 4편 (Rx + FlexLayout + PinLayout)

UICollectionView에 대해서 알아보기 4편 (Rx + FlexLayout + PinLayout) 이번에는 RxSwfit + MVVM + FlexLayout + PinLayout을 사용해서 구성해보자. FlexLayout 및 PinLayout을 학습하기 위해서 구글링..

rldd.tistory.com