🟨 구현 화면

🟨 구현 순서
- profile view의 상태표시줄 (시간, 배터리 표시하는 부분)에 대한 설정한다.
- table의 header를 safearea까지 늘리고, 스크롤에 따라 상태 표시줄을 지정해준다.
- profile의 각 카테고리 버튼을 열거형으로 설정한다.
🟨 ProfileViewController.swift 코드 구현
- viewDidLoad() 메서드 내에 코드 구현
navigationController?.navigationBar.isHidden = true
profileTableView.contentInsetAdjustmentBehavior = .never
- 위의 코드를 구현하면 메인화면이 SafeArea까지 넘어 간다.
- 근데 이렇게 하면 HomeViewController → ProfileView → HomeViewController로 이동하게 되면
- 처음에는 네비게이션 부분이 있다가 없다가 다시 HomeViewController로 돌아와도 네비게이션 부분이 없는 문제 발생한다.
- 위의 코드를 구현하면 아래 이미지처럼 나온다.
- 좌측: 코드 적용 전
- 우측: 코드 적용 후


🟨 HomeViewController.swift 코드 구현
- 위에서 발생한 네비게이션바가 보이지 않는 문제를 해결하기 위해 아래 코드를 구현한다.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = false
}
🟨 ProfileViewController.swift 코드 구현
- 아래 이미지에서 시간을 표시하는 부분을 보면 화면을 내리면 시간 표시줄이 화면을 가리는 문제가 있다.
- 좌측: 코드 적용 전
- 우측: 코드 적용 후


- 위의 문제를 해결하기 위해 상태표시줄을 별도의 변수로 생성하고, 조건에 따른 UI 설정을 한다.
class ProfileViewController: UIViewController {
// 상태표시줄을 숨길지 말지 정할 변수
private var isStausBarHidden: Bool = true
// 상태표시줄 설정
private let statusBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBackground
view.layer.opacity = 0
return view
}()
...
// 상태표시줄 제약 조건
private func configureConstraints() {
...
let statusBarConstraints = [
statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
statusBar.topAnchor.constraint(equalTo: view.topAnchor),
statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
statusBar.heightAnchor.constraint(equalToConstant: view.bounds.height > 800 ? 40 : 20)
]
...
NSLayoutConstraint.activate(statusBarConstraints)
}
extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
...
// 스크롤할 때 상태창 설정 함수
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yPosition = scrollView.contentOffset.y
if yPosition > 100 && isStausBarHidden {
isStausBarHidden = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear) { [weak self] in
self?.statusBar.layer.opacity = 1
} completion: { _ in }
} else if yPosition < 0 && !isStausBarHidden {
isStausBarHidden = true
UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear) { [weak self] in
self?.statusBar.layer.opacity = 0
} completion: { _ in }
}
}
}
🟨 ProfileViewController.swift 최종 코드
import UIKit
class ProfileViewController: UIViewController {
private var isStausBarHidden: Bool = true
// 상태표시줄 설정
private let statusBar: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBackground
view.layer.opacity = 0
return view
}()
private let profileTableView: UITableView = {
let tableView = UITableView()
tableView.register(TweetTableViewCell.self, forCellReuseIdentifier: TweetTableViewCell.identifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
navigationItem.title = "Profile"
view.addSubview(profileTableView)
view.addSubview(statusBar)
profileTableView.delegate = self
profileTableView.dataSource = self
profileTableView.contentInsetAdjustmentBehavior = .never
// 네비게이션 부분 숨기기 -> HomeView에는 네비게이션 부분이 있다가 -> 프로필에 가면 없어졌다가 -> 다시 돌아오면 HomeView에도 없어져있다.
navigationController?.navigationBar.isHidden = true
configureConstraints()
let headerView = ProfileTableViewHeader(frame: CGRect(x: 0, y: 0, width: profileTableView.frame.width, height: 380))
profileTableView.tableHeaderView = headerView
}
private func configureConstraints() {
let profileTableViewConstraints = [
profileTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
profileTableView.topAnchor.constraint(equalTo: view.topAnchor),
profileTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
profileTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]
let statusBarConstraints = [
statusBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
statusBar.topAnchor.constraint(equalTo: view.topAnchor),
statusBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
statusBar.heightAnchor.constraint(equalToConstant: view.bounds.height > 800 ? 40 : 20)
]
NSLayoutConstraint.activate(profileTableViewConstraints)
NSLayoutConstraint.activate(statusBarConstraints)
}
}
extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TweetTableViewCell.identifier, for: indexPath) as? TweetTableViewCell else { return TweetTableViewCell() }
return cell
}
// 스크롤할 때 상태창 설정 함수
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yPosition = scrollView.contentOffset.y
if yPosition > 100 && isStausBarHidden {
isStausBarHidden = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear) { [weak self] in
self?.statusBar.layer.opacity = 1
} completion: { _ in }
} else if yPosition < 0 && !isStausBarHidden {
isStausBarHidden = true
UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear) { [weak self] in
self?.statusBar.layer.opacity = 0
} completion: { _ in }
}
}
}
🟨 ProfileTableViewHeader.swift 코드 구현
- profile 부분에 각 카테고리별 버튼을 생성한다.
- 생성된 버튼을 누르면 각각의 함수가 실행된다.

import UIKit
class ProfileTableViewHeader: UIView {
// 5.각 버튼에 맞는 리턴값을 보여준다.
private enum SectionTabs: String {
case tweets = "Tweets"
case tweetAndReplies = "Tweets & Replies"
case media = "Media"
case likes = "Likes"
var index: Int {
switch self {
case .tweets:
return 0
case .tweetAndReplies:
return 1
case .media:
return 2
case .likes:
return 3
}
}
}
// 6. 각 버튼을 누르면 리턴된 값을 갖고 있을 변수를 생성한다.
// 변수의 값이 변화가 생기면 didSet을 통해 감지한다.
private var selectedTab: Int = 0 {
didSet {
print(selectedTab)
}
}
// 1. 버튼을 생성한다.
private var tabs: [UIButton] = ["Tweets", "Tweets & Replies", "Media", "Likes"]
.map { buttonTitle in
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
button.tintColor = .label
button.setTitle(buttonTitle, for: .normal)
return button
}
// 2. 생성된 버트을 StackView로 묶는다.
// 여기서 lazy로 선언한 이유는 버튼을 먼저 생성 후 스택뷰에 담아야 하기 때문이다.
private lazy var sectionStack: UIStackView = {
let stackView = UIStackView(arrangedSubviews: tabs)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .equalSpacing
stackView.axis = .horizontal
stackView.alignment = .center
return stackView
}()
...
override init(frame: CGRect) {
super.init(frame: frame)
...
// 3. view에 sectionStack를 담는다.
addSubview(sectionStack)
configureConstraints()
configureStackButton()
}
// 4.각 버튼에 액션을 할당한다.
private func configureStackButton() {
for (_, button) in sectionStack.arrangedSubviews.enumerated() {
guard let button = button as? UIButton else { return }
button.addTarget(self, action: #selector(didTapTab(_: )), for: .touchUpInside)
}
}
// 7. 각 버튼을 누르면 나오는 액션을 구현한다.
// 각 버튼을 누르면 해당 버튼의 이름을 갖고 와서
// switch 문을 통해 열거형의 case에 맞게 selectedTabd에 할당한다.
@objc private func didTapTab(_ sender: UIButton) {
guard let label = sender.titleLabel?.text else { return }
switch label {
case SectionTabs.tweets.rawValue:
selectedTab = 0
case SectionTabs.tweetAndReplies.rawValue:
selectedTab = 1
case SectionTabs.media.rawValue:
selectedTab = 2
case SectionTabs.likes.rawValue:
selectedTab = 3
default:
selectedTab = 0
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureConstraints() {
...
let sectionStackConstraints = [
sectionStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 25),
sectionStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -25),
sectionStack.topAnchor.constraint(equalTo: followingCountLabel.bottomAnchor, constant: 3),
sectionStack.heightAnchor.constraint(equalToConstant: 35)
]
...
NSLayoutConstraint.activate(sectionStackConstraints)
}
}
🟨 TIL
- contentInsetAdjustedmentBehavior
- safeArea를 고려하여 자동으로 contentOffset을 조절해주는 프로퍼티
- 4가지 케이스가 있다.

- 1) automatic: scrollView의 safe area를 고려하여 scrollView의 contentOffset을 자동으로 조정
- (디폴트 값)
- 스크롤 뷰의 contentOffset.y값이 상단의 safeArea를 고려하여, -값으로 시작
- contentOffset 크기의 기준은 컨텐츠가 줄어드는 쪽으로 스크롤 되는 경우 음수
scrollView.contentInsetAdjustmentBehavior = .automatic

- 2) scrollableAxes: 스크롤 방향으로만 safeArea inset 적용
- scroll 방향이 vertical이므로, top, bottom의 safe area만 고려된 형태 (아이폰을 회전하면 leading, trailing에 inset 고려가 안되어 쭉 늘어난 상태)
scrollView.contentInsetAdjustmentBehavior = .scrollableAxes

- 3). never: safeArea inset 고려 x
scrollView.contentInsetAdjustmentBehavior = .never

- 4). always: 항상 safeArea를 고려하여 contentOffset 적용
scrollView.contentInsetAdjustmentBehavior = .always


아이폰을 회전시켜도 적용
- button 설정할 때 동일한 UI일 경우에는 아래와 같이 코드를 작성한다.
// 1. 버튼을 생성한다.
private var tabs: [UIButton] = ["Tweets", "Tweets & Replies", "Media", "Likes"]
.map { buttonTitle in
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
button.tintColor = .label
button.setTitle(buttonTitle, for: .normal)
return button
}
// 2. 생성된 버트을 StackView로 묶는다.
// 여기서 lazy로 선언한 이유는 버튼을 먼저 생성 후 스택뷰에 담아야 하기 때문이다.
private lazy var sectionStack: UIStackView = {
let stackView = UIStackView(arrangedSubviews: tabs)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .equalSpacing
stackView.axis = .horizontal
stackView.alignment = .center
return stackView
}()
https://youtu.be/fh6qqo02Lqg?si=t-uBL23UPaGvyhKU
'Clone App > Twitter' 카테고리의 다른 글
[Twitter Clone] Add firebase - part1 (0) | 2024.05.28 |
---|---|
[Twitter Clone] Add Indicator button in profile view (0) | 2024.05.27 |
[Twitter Clone] Add logo, Design ProfileView (0) | 2024.05.26 |
[Twitter Clone] Add tweet actions (0) | 2024.05.24 |
[Twitter Clone] Add a custom cell (0) | 2024.05.24 |