Clone App/Twitter

[Twitter Clone] Continue implementing the profile view

밤새는 탐험가89 2024. 5. 27. 11:05

🟨 구현 화면

 

 

🟨 구현 순서

  • 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