[UIKit] UITableView: Stretchy TableView Header

Junyoung Park·2022년 12월 16일
0

UIKit

목록 보기
121/142
post-thumbnail
post-custom-banner

Stretchy TableView Header in App (Swift 5, Xcode 12, iOS 2020) - iOS Development

UITableView: Stretchy TableView Header

구현 목표

  • stretchy 테이블 뷰 헤더 구현

구현 태스크

  • 테이블뷰 타이틀 헤더 뷰 구현
  • 이미지 뷰 오토 레이아웃 구현
  • 스크롤 오프셋에 따른 타이틀 헤더 이미지 조정 로직

핵심 코드

private func applyConstraints() {
        let viewConstraints = [
            widthAnchor.constraint(equalTo: containerView.widthAnchor),
            centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            heightAnchor.constraint(equalTo: containerView.heightAnchor)
        ]
        NSLayoutConstraint.activate(viewConstraints)
        
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
        containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor)
        containerViewHeightConstraint?.isActive = true
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        imageViewBottomConstraint?.isActive = true
        imageViewHeightConstraint = imageView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        imageViewHeightConstraint?.isActive = true
    }
  • 현재 스트레치 헤더 뷰는 containverView를 서브 뷰로 삽입한 뒤 해당 containerViewimageView를 삽입한 구조
  • 컨테이너 뷰의 넓이는 이미지 뷰의 넓이와 동일
  • 이미지 뷰 오토 레이아웃 또한 컨테이너 뷰와 bottomAnchor, heightAnchor 동일
func scrollViewDidScroll(scrollView: UIScrollView) {
        containerViewHeightConstraint?.constant = scrollView.contentInset.top
        let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
        containerView.clipsToBounds = offsetY <= 0
        imageViewBottomConstraint?.constant = offsetY >= 0 ? 0 : -offsetY / 2
        imageViewHeightConstraint?.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
        
    }
  • 테이블 뷰는 스크롤 뷰를 상속하기 때문에 파라미터로 받아들이는 스크롤 뷰는 곧 테이블 뷰
  • 테이블 뷰가 스크롤될 때 scrollView.contentOffset.y 값을 통해 위/아래 스크롤을 감지 가능: 아래로 계속해서 스크롤할 경우 양수 값, 위로 올리면서 테이블 뷰를 내려갈 수록 음수 값의 offsetY
  • clipsToBounds는 이러한 offsetY가 0 이하일 때, 즉 테이블 뷰를 위로 올리는 순간 true가 되면서 컨테이너 뷰가 주어진 바운드에 맞춰 클립
  • 테이블 뷰를 끝까지 내릴 때에는 컨테이너 뷰가 커지고, 올릴 때에는 작아져야 함을 주의

소스 코드

import UIKit

final class StretchyTableViewHeaderView: UIView {
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.clipsToBounds = true
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    private var imageViewHeightConstraint: NSLayoutConstraint?
    private var imageViewBottomConstraint: NSLayoutConstraint?
    private let containerView: UIView = {
        let view = UIView()
        return view
    }()
    private var containerViewHeightConstraint: NSLayoutConstraint?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setUI() {
        addSubview(containerView)
        containerView.addSubview(imageView)
        applyConstraints()
        guard let url = URL(string: "https://cdn.vox-cdn.com/thumbor/2DRQBNSQ4tc2Sga4VL8kxew7dzE=/0x178:1199x778/fit-in/1200x600/cdn.vox-cdn.com/uploads/chorus_asset/file/13449801/DsI6yX_VYAAwXLz.jpg") else { return }
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard
                let data = data,
                let response = response as? HTTPURLResponse,
                response.statusCode >= 200 && response.statusCode < 400,
                error == nil else { return }
            DispatchQueue.main.async { [weak self] in
                self?.imageView.image = UIImage(data: data)
            }
        }
        .resume()
        
    }
    
    private func applyConstraints() {
        let viewConstraints = [
            widthAnchor.constraint(equalTo: containerView.widthAnchor),
            centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            heightAnchor.constraint(equalTo: containerView.heightAnchor)
        ]
        NSLayoutConstraint.activate(viewConstraints)
        
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
        containerViewHeightConstraint = containerView.heightAnchor.constraint(equalTo: heightAnchor)
        containerViewHeightConstraint?.isActive = true
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        imageViewBottomConstraint?.isActive = true
        imageViewHeightConstraint = imageView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        imageViewHeightConstraint?.isActive = true
    }
    
    func scrollViewDidScroll(scrollView: UIScrollView) {
        containerViewHeightConstraint?.constant = scrollView.contentInset.top
        let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
        containerView.clipsToBounds = offsetY <= 0
        imageViewBottomConstraint?.constant = offsetY >= 0 ? 0 : -offsetY / 2
        imageViewHeightConstraint?.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
    }
}
  • offsetY를 구한 뒤, 해당 값의 양/음 조건에 따라 현재 컨테이너 뷰가 작아져야 할지, 커져야 할지 결정
import UIKit

class StretchyHeaderTableViewController: UIViewController {
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
        return tableView
    }()
    private let models = [
        "pikachu",
        "charizard",
        "bulbasaur",
        "squirtle"
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
        tableView.tableHeaderView?.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width)
        print("CurrentWidth: \(view.frame.size.width)")
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
        let appearance = UINavigationBarAppearance()
        appearance.largeTitleTextAttributes = [.foregroundColor : UIColor.white]
        navigationItem.standardAppearance = appearance
        tableView.tableHeaderView = StretchyTableViewHeaderView()
    }
}

extension StretchyHeaderTableViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard let header = tableView.tableHeaderView as? StretchyTableViewHeaderView else { return }
        header.scrollViewDidScroll(scrollView: tableView)
    }
}

extension StretchyHeaderTableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return models.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
        cell.textLabel?.text = models[indexPath.row].uppercased()
        return cell
    }
}
  • 테이블 뷰 델리게이트 함수는 기본적으로 스크롤 뷰의 델리게이트 함수를 상속

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글