✅ 이 포스팅은 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: 블로그에 나온 코드 일부를 컨벤션을 적용한 스타일로 수정
예제파일
✅ 코드스타일
스냅킷을 공부할 때 변수나 함수 등 스타일 가이드를 숙지하지 못했습니다.
아래 스타일 가이드를 따라 주세요. 여기서 설명하는 것처럼 변수 및 함수 등 코드 작성하시면 안됩니다.
https://github.com/StyleShare/swift-style-guide
✅ 목차
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.
12.
✅ 시작하기에 앞서...
//
// ViewController11.swift
// SnapKit_practice
//
// Created by Hamlit Jason on 2021/08/25.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
final class ViewController: UIViewController {
private lazy var box: UIView() = {
$0.backgroundColor = .green
return $0
}(UIView())
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - UI
private func configureUI() {
view.addSubview(box)
// ... 레이아웃 잡아주기
// 본 포스팅에서는 func layout() 함수를 정의하여 표현
}
}
기본적인 코드 세팅
✅ 1. translatesAutoresizingMaskIntoConstraints = false를 쓰지 않아도 되는 이유
- 코드로 레이아웃을 잡아줄 때에는 위의 코드를 작성해주어야 하나, snapkit을 사용한다면 작성하지 않아도 된다.
그 이유는 아래 코드와 같이 프로토콜로 정의되어 있기 때문에 스냅킷을 사용하면 알아서 적용된다.
✅ 2. offset 과 inset의 차이점
1️⃣ offset을 사용했을 때
box.snp.makeConstraints {
$0.top.left.equalToSuperview().offset(50)
$0.bottom.right.equalToSuperview().offset(-50)
}
이렇게 주면 된다.
🔸 offset의 공식
-> 현재 뷰 constraint = 슈퍼뷰 constraint + offset 값
따라서 bottom과 right는 마이너스 부호를 갖게 된다.
2️⃣ inset을 사용했을 때
box.snp.makeConstraints {
$0.top.left.right.bottom.equalToSuperview().inset(50)
}
이렇게 주면 되는데, 결과는 offset과 현재 결과는 똑같이 나온다.
inset의 경우에는 UIEdgeInsets을 주었다고 생각하면 된다.
box.snp.makeConstraints {
$0.top.left.bottom.right.equalToSuperview()
.inset(UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50))
}
inset의 경우에는 이렇게 들어간다. 좌표값이 bottom과 right가 음수 값이 아니라 양수 값임을 확인하자.
box.snp.makeConstraints {
$0.edges.equalToSuperview().inset(50)
}
상하좌우가 같은 값이라면 이렇게 작성해서 더 깔끔하게 만들 수도 있다.
✅ 3. snp.left와 snp.leading의 차이 / snp.right와 snp.trailing의 차이
아래의 표처럼 ViewAttribute는 NSLayoutAttribute와 매칭됩니다.
그럼 NSLayoutAttribute의 left와 leading, right와 trailing이 왜 각각 존재하는지 알아봐야겠죠?!
leading, trailing으로 설정하면 right-to-left 순서로 읽는 지역에서 화면이 거꾸로(flip되어서) 표시된다고 합니다.
하지만 left, right로 설정하면 안그럽니다.
(왼쪽, 오른쪽은 모든 지역에서 항상 똑같은 위치이지만(??) leading, trailing은 각 지역마다 다르게 받아들인다!!
그래서 leading, trailing으로 하면 오른쪽으로 왼쪽으로 읽는 지역에서 flip된 UI가 나온다!! 라고 생각하면 될 것 같아요)
✅ 4. Constraint를 updateConstraints로 조정해보기
snapkit을 사용하지 않으면 아래 사진처럼 reference를 들고 있을 수 있다.
그리고 topSpace.constant = 100 해서 constant를 업데이트 해주고!
스냅킷도 가능하다. 그럼 어떻게 해야할까?
우선 updateConstraints 코드를 통해 변경해 보았다
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)
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()
}
}
아래에 결과를 보면 찾아볼 수 있다.
여기서 조금 어려웠던 부분이 remake를 사용할 때 bottom을 주지 않으면 레이아웃 값이 제대로 들어가지 않았다는 점이다.
또한 애니메이션을 주는 것이 알았지만 어려웠다.
remake는 이미 컨텐츠가 들어와있는 조건에서 나를 뻬고 다시 넣는 개념이라 레이아웃이 생략되면 기존에 있던 요소들에 의하여 나의 공간 자체가 사라지는 문제가 발생할 수 있다.
update의 경우에는 나의 레이아웃을 유지한 상태해서 수정해주는 느낌이라서 다른 친구들이 나의 영역을 채우는 과정이 없다.
=> 여기 개념은 물론 내 추측이라 아닐 수도 있음.
✅ 6. StackView와 divideby를 활용한 snapKit구성(코드 라인 줄이기?)
import Foundation
import UIKit
import RxCocoa
import RxSwift
import SnapKit
import Then
class ChallengeViewController: UIViewController {
//MARK: UI Properties
// 위의 코드는 자동으로 연결하는 코드
private lazy var stackViewAuto = UIStackView().then {
$0.distribution = .fillEqually
}
private lazy var connectBtnAuto = UIButton().then {
$0.setTitle("connectBtnAuto", for: .normal)
$0.backgroundColor = .magenta
}
private lazy var writeBtnAuto = UIButton().then {
$0.setTitle("writeBtnAuto", for: .normal)
$0.backgroundColor = .purple
}
private lazy var disconnectBtnAuto = UIButton().then {
$0.setTitle("disconnectBtnAuto", for: .normal)
$0.backgroundColor = .systemPink
}
// 아래 코드는 수동으로 연결하는 코드
private lazy var stackViewFirst = UIStackView().then {
$0.distribution = .fillEqually
}
private lazy var connectBtn = UIButton().then {
$0.setTitle("connectBtn", for: .normal)
$0.backgroundColor = .blue
}
private lazy var writeBtn = UIButton().then {
$0.setTitle("writeBtn", for: .normal)
$0.backgroundColor = .green
}
private lazy var disconnectBtn = UIButton().then {
$0.setTitle("disconnectBtn", for: .normal)
$0.backgroundColor = .red
}
private lazy var tableView = UITableView().then {
$0.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
$0.backgroundColor = .brown
}
override func viewDidLoad() {
super.viewDidLoad()
[stackViewFirst, tableView, stackViewAuto].forEach { view.addSubview($0) }
[connectBtn, writeBtn, disconnectBtn].forEach { stackViewFirst.addArrangedSubview($0) }
[connectBtnAuto, writeBtnAuto, disconnectBtnAuto].forEach { stackViewAuto.addArrangedSubview($0) }
setConstraints()
}
func setConstraints() {
stackViewFirst.snp.makeConstraints {
$0.top.left.right.equalToSuperview()
$0.bottom.equalTo(view.snp.centerY).dividedBy(2)
}
stackViewAuto.snp.makeConstraints {
$0.top.equalTo(stackViewFirst.snp.bottom)
$0.left.right.equalToSuperview()
$0.bottom.equalTo(view.snp.centerY)
}
tableView.snp.makeConstraints {
$0.top.equalTo(view.snp.centerY)
$0.left.bottom.right.equalToSuperview()
}
}
}
위와 같이 구성하면 코드 라인을 획기적으로 줄일 수 있어.
✅ 7. setContentCompressionResistancePriority 알아보기.
-> setContentCompressionResistancePriority 내 압축 저항 우선순위가 이 정도이니 나보다 낮은 애들이 줄어들어!
아래는 코드
let spacing: CGFloat = 16.0
commonTitle.setContentCompressionResistancePriority(.init(rawValue: 751), for: .horizontal)
commonTitle.snp.makeConstraints {
$0.top.leading.equalTo(safeAreaLayoutGuide).inset(spacing)
$0.trailing.equalTo(commonAuthor.snp.leading)
$0.width.equalTo(150)
}
commonAuthor.setContentCompressionResistancePriority(.init(rawValue: 750), for: .horizontal)
commonAuthor.snp.makeConstraints {
$0.top.trailing.equalTo(safeAreaLayoutGuide).inset(spacing)
$0.leading.equalTo(commonTitle.snp.trailing)
$0.width.equalTo(1000)
}
width를 보면 Author가 1000이라서 전부 다 차지해야 하지만, 압축저항이 commonTitle이 더 높기에 150아래로는 줄어들지가 않는다.
그렇다면 둘의 우선순위를 바꾸면 어떻게 될까?
아래 그림을 보다시피 같은 결과가 나타난다
여기서 이제 하나의 개념이 추가되는데, width를 equalTo로 잡고 있는데, 이게 priority가 더 높아서 저 상황에서는 commonTitle의 width가 보장받게 된다.
그렇다면 commonTitle의 width의 우선순위를 낮춰보자
우선순위를 이렇게 조정할 수 있다.
무엇보다 줄어드는 상황에서 적용되는 것이다.
✅ 8. setContentHuggingPriority 알아보기
얘는 누군가 늘어나야 하는 상황에서!
아래 코드를 보자
bookTitle.setContentHuggingPriority(.init(rawValue: 751), for: .horizontal)
bookTitle.snp.makeConstraints {
$0.top.equalTo(commonTitle.snp.bottom).offset(spacing)
$0.leading.equalTo(safeAreaLayoutGuide).inset(spacing)
$0.trailing.equalTo(bookAuthor.snp.leading)
$0.width.equalTo(100)
}
bookAuthor.setContentHuggingPriority(.init(rawValue: 750), for: .horizontal)
bookAuthor.snp.makeConstraints {
$0.top.equalTo(commonAuthor.snp.bottom).offset(spacing)
$0.trailing.equalTo(safeAreaLayoutGuide).inset(spacing)
$0.leading.equalTo(bookTitle.snp.trailing)
$0.width.equalTo(100)
}
얘는 둘다 equal(100)을 주었지만, 서로의 양끝이 서로를 물고 있는 상황이라 누군가는 늘어나야 한다. 그럼 누가 늘어날까?
바로 Hugging이 낮은애가 늘어난다. 얘 같은 경우에는 Hugging 우선 순위가 높으면 내 크기를 유지할테니 너가 와서 안아줘 이런 느낌으로 받아들이면 쉽다.
✅ 9. SnapKit + animation 효과주기
스냅킷으로 저 빨간색 뷰의 크기를 커졌다가 작아졌다를 부드럽게 처리해 보았다.
아래 코드처럼 작성해 두었다.
layoutView.animatingBtn.rx.tap
.scan(ButtonState.cancel) { lastState, _ in
switch lastState {
case .excute: return .cancel
case .cancel: return .excute
}
}
.bind { [weak self] in
self?.didTapAnimatingBtn($0)
UIView.animate(
withDuration: 1.0) {
self?.layoutView.layoutIfNeeded()
}
}
.disposed(by: bag)
/*
코드에 대한 첨언
작성일: 2022년 09월 25일
이 예제를 작성할 당시에는 RxSwift의 이해가 부족했다.
버튼의 상태를 판단하기 위해서 저렇게 작성했는데, 애초에 Relay를 사용하는게 더 좋다.
*/
✅ 10. remakeConstraints와 updateConstraints 추가 정리
remakeConstraints -> 새로운 레이아웃을 적용해야 할 경우
updateConstraints -> 기존에 작성된 레이아웃에서 값을 변경해야 할 경우
make -> remake, make -> update를 각각 실행한다고 가정하였을때,
remake는 레이아웃을 다시 잡아주는 과정이라 새로운 레이아웃을 줄 수 있고 전부 잡아주면 되지만 update는 make에 들어있는 것들을 기준으로 바꿀 수만 있다 그렇지 않으면 fatal error~ 가 나타난 것임
// make (1)
greenView.snp.makeConstraints {
$0.top.equalTo(yellowView.snp.bottom)
$0.bottom.equalTo(safeAreaLayoutGuide.snp.bottom)
$0.leading.trailing.equalTo(safeAreaLayoutGuide)
$0.height.greaterThanOrEqualTo(500).priority(300) // 500보다 크거나 같다.
}
// remake (1)-1
greenView.snp.remakeConstraints {
$0.top.greaterThanOrEqualTo(yellowView.snp.bottom)
$0.bottom.greaterThanOrEqualTo(safeAreaLayoutGuide.snp.bottom)
$0.leading.trailing.greaterThanOrEqualTo(safeAreaLayoutGuide)
$0.height.greaterThanOrEqualTo(500).priority(300) // 500보다 크거나 같다.
}
// update (1)-2
greenView.snp.makeConstraints {
$0.top.equalTo(100)
$0.height.greaterThanOrEqualTo(30).priority(300) // 500보다 크거나 같다.
}
✅ FlexLayout + PinLayout
- 이 부분은 추가로 학습하시면 좋습니다.
https://rldd.tistory.com/478?category=952720
'apple > iOS' 카테고리의 다른 글
Showing All Messages Undefined symbol: __swift_FORCE_LOAD_$_XCTestSwiftSupport (0) | 2022.02.18 |
---|---|
[iOS] TTGTagCollectionView에 대해서 알아보자. (0) | 2022.02.17 |
iOS Snapkit 10 | CollectionView 코드로 구성하는 법 03 (0) | 2021.08.23 |
iOS Snapkit 09 | CollectionView 코드로 구성하는 법 02 (0) | 2021.08.22 |
iOS Snapkit 08 | CollectionView 코드로 구성하는 법 01 (0) | 2021.08.22 |