본문 바로가기

iOS/힙한 UI 따라 만들기

iOS 힙한 UI 따라 만들기 Ep.01 "에이블리, 배민" 헤더가 고정되는 테이블 뷰 만들기👋

일상 생활을 하면서 정말 많이 쓰이는 힙한 앱들이 많은데 그 앱에서 보여주는 UI에 대해서 당연하게 사용만 해보고 구현 방법에 대해서는 깊게 생각해본적이 별로 없는 것 같습니다..😭
힙한 서비스, UI/UX를 구현하고싶지만 지금의 실력으로는 구현하는데 생각보다 어려울 것 같아보여서 힙한 서비스들에서 보여지는 UI들을 따라 만들어보자!!는 생각이 들었습니다.
그래서 지금부터라도 서비스들을 분석하면서 하나씩 따라만들어볼 예정입니다.


헤더가 고정되는 테이블 뷰?

에이블리 제품 상세화면

에이블리나 배민에서 옷 상세화면, 가게 상세화면으로 들어가면 스크롤을 내릴수록 상단 네비게이션 바가 나타나게됩니다. 그리고 탭이 나타나면 상단 네비게이션에 탭이 걸리게되는 그런 뷰를 보게되었어요! 별거 아닌 것 같은데 트랜디해 보이기는 한데 막상 만들려고 생각해보니 어려울 것 같아서 한번 따라해보려합니다.


완성 버전(?)

비슷하게 따라해서 만들어본 결과입니다. 하지만 밑에 포스트에서는 이 화면을 간단한 버전으로 만들어보려합니다.

디자인이 있을때는 이렇게 이쁘지만

 

디자인을 빼니까 이렇게 단촐해지네요...ㅠㅠ


기본 테이블 뷰 사용

기본적으로 테이블 뷰를 사용해서 헤더를 사용하게 되면 자연스럽게 위 사진처럼 헤더가 네비게이션 바에 걸리게 되서 손쉽게 만들 수 있게 됩니다. 하지만 저희가 만들어야 하는 화면은.. 네비게이션바가 투명이였다가 스크롤을 내릴수록 네비게이션바가 나타나야 할 뿐더러, 테이블 뷰의 레이아웃이 safe area까지 포함되어야 하는 상황입니다 ㅠㅠ
이렇게 되면 테이블 뷰의 헤더가 원하는 위치에서 걸리지 않기 때문에 헤더가 원하는 위치에 걸리도록 저희가 직접 구현해줘야 합니다.


구현 방법

기본 테이블뷰의 헤더뷰가 위에서 고정되는 현상을 응용할겁니다.
단순하게 설명을 하면 사용자가 헤더가 고정되야하는 지점까지 스크롤 했을 때, tableview의 contentInset을 설정해서 상단 네비게이션 바에 걸려지도록 설정할겁니다. 반대로 헤더가 고정되지 않아도 되는 지점에서 스크롤을 할때에는 contentInset을 해지해줍니다.


뷰 그리기

일단 뷰를 먼저 그려봅시다. 여기에서는 스토리보드를 사용하지 않고, 코드로 뷰를 그립니다.
navigationView는 완성 화면 상단에 보여지는 까만 반투명한 뷰, tableView는 그대로 뒤에서 스크롤 되는 뷰입니다.

// CustomTableView.swift
import UIKit

class CustomTableView: UIView {
  
  let navigationView: UIView = {
    let view = UIView()
    
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = .black
    view.alpha = 0.5
    return view
  }()
  
  let tableView: UITableView =  {
    let tableView = UITableView()
    
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.tableFooterView = UIView()
    tableView.backgroundColor = .clear
    tableView.estimatedSectionHeaderHeight = 50
    tableView.rowHeight = 200
    tableView.contentInsetAdjustmentBehavior = .never
    tableView.sectionHeaderHeight = UITableView.automaticDimension
    return tableView
  }()
  
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    self.backgroundColor = .white
    self.addSubview(self.tableView)
    self.addSubview(self.navigationView)
    
    self.navigationView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
    self.navigationView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    self.navigationView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
    self.navigationView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 50).isActive = true
    
    self.tableView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
    self.tableView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
    self.tableView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
    self.tableView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
  }
}

 


뷰 컨트롤러 구현

// ViewController.swift
import UIKit

class ViewController: UIViewController {

  lazy var customTableView = CustomTableView(frame: self.view.frame)
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.view = customTableView
    self.customTableView.tableView.delegate = self
    self.customTableView.tableView.dataSource = self
    self.customTableView.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TextCell")
  }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
  
  func numberOfSections(in tableView: UITableView) -> Int {
    return 2
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if section == 0 {
      return 1
    } else {
      return 4
    }
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "TextCell") else {
      return UITableViewCell()
    }
    cell.backgroundColor = .white
    cell.textLabel?.text = "Section: \(indexPath.section), Row: \(indexPath.row)"
    cell.textLabel?.textColor = .black
    return cell
  }
  
  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    if section == 0 {
      return UIView(frame: .zero)
    } else {
      let headerView = UIView()
      headerView.backgroundColor = .red
      headerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 50)
      
      let titleLabel = UILabel()
      titleLabel.textColor = .white
      titleLabel.text = "Section1 헤더 뷰"
      titleLabel.frame = CGRect(x: 0, y: 0, width: headerView.frame.width, height: headerView.frame.height)
      headerView.addSubview(titleLabel)
      
      return headerView
    }
  }  
}

간단하게 UITableViewDelegate와 UITableViewDataSource를 채택해서 테이블뷰를 구현합니다.
테이블 뷰의 스크롤에 따라서 헤더가 고정되는 위치가 변경되기 위해서 func scrollViewDidScroll(_ scrollView: UIScrollView)를 채택해서 구현합니다.

// ViewController.swift
import UIKit

class ViewController: UIViewController {
  ...
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {

  ...
  
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offset = 200 - self.customTableView.navigationView.frame.height

    if scrollView.contentOffset.y > offset {
      scrollView.contentInset = UIEdgeInsets(top: self.customTableView.navigationView.frame.height, left: 0, bottom: 0, right: 0)
    } else {
      scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
  }
}

헤더가 고정되어야하는 스크롤 위치인 offset을 먼저 계산해보면 첫번째 cell의 높이(200) - navigationView의 높이가 됩니다.
해당 위치까지 스크롤이 되기 전까지는 tableView의 contentInset을 0으로 설정하고 offset 이상만큼 스크롤되었을 경우에는 navigationView의 높이만큼 contentInset을 설정해주면 헤더의 고정되는 위치가 navigationView의 바로 아래로 변경됩니다!