본문 바로가기
iOS📱/Swift

[ Swift ] Expandable UITableView 만들기

by 텅빈비니 2023. 5. 23.
반응형

안녕하세요🐶

빈지식 채우기의 비니🙋🏻‍♂️입니다.

 

오늘의 포스팅은 펼침이 가능한 UITableView 만들기입니다.

흔히들 Expandable UITableView 라고도 불리지요~

실무에서 해당 기능을 사용을 하게 되었고~ 정리를 해보았습니다.

 

Full Code 는 맨 하단에 Github 링크 올려놨으니 참고 부탁드립니다 ㅎㅎ

 

1. 프로젝트 구조

프로젝트 구조는 다음과 같습니다.

프로젝트 구조

  • 초록색 : Dummy Data를 위한 데이터 소스
  • 빨간색 : 화면 출력을 위한 ViewController 소스

 

2. 소스

간단한 프로젝트 구조를 알아보았으니 작성된 소스에 대해 알아보도록 하겠습니다.

 

2-1. StoryBoard

StoryBoard

  • 간단히 TableView만 생성 후 ViewController에 연결합니다.

 

2-2. Model

class Comment { // 클릭이 가능한 Header에 들어갈 데이터
    var commentId: Int
    var commentText: String
    var replies: [Reply]

    init(commentId: Int, commentText: String, replies: [Reply]) {
        self.commentId = commentId  // 순서를 위한 ID
        self.commentText = commentText  // Title Text
        self.replies = replies  // Reply 배열 참조
    }
}

class Reply {   // 펼침이 되는 Cell에 들어갈 데이터
    var replyId: Int
    var replyText: String

    init(replyId: Int, replyText: String) {
        self.replyId = replyId  // 순서를 위한 ID
        self.replyText = replyText  // Title Text
    }
}
  • TableView에 들어갈 데이터 구조를 작성합니다.

 

2-3. ViewController

2-3-1. 뷰 설정 및 Dummy Data 생성

class TableViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!  // 스토리보드 TableView 연결
    var comments: [Comment] = [Comment]()   // Dummy Data
    
    var hiddenSections = Set<Int>() // 숨겨진 Section을 저장할 변수 선언
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        initView()  // 기초적인 View 설정
    }
    
    func initView() {
        // TableView Delegate 및 DataSource 연결
        tableView.delegate = self
        tableView.dataSource = self
        // 테이블뷰 라인 없애기
        tableView.separatorStyle = .none
        
        comments = getDummyComments(with: 3)    // Dummy Data 생성
    }
    
    func getDummyComments(with count: Int) -> [Comment] {

        var comments = [Comment]()
        for i in 1...count {
            comments.append(Comment(commentId: i,
                                    commentText: "Comment \(i)",
                                    replies: getDummyReplies(with: i)))
        }
        return comments

    }

    func getDummyReplies(with count: Int) -> [Reply] {
        var replies = [Reply]()
        for i in 1...count {
            replies.append(Reply(replyId: i, replyText: "Reply \(i)"))
        }
        return replies
    }

여기서 잠깐! 왜 숨겨진 Section 정보를 Set(집합)에 저장할까?

  1. 어떤 Section이 있다는 정보는 순서 중요 X
  2. Set중복되는 요소가 없기 때문에 그로 인한 실수 방지
  3. Hashable 프로토콜을 채택한 타입을 사용, 빠르게 탐색 가능

 

2-3-2. TableView DataSource와 Delegate 메서드 구현

extension TableViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return comments.count   // Section 개수 설정
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if self.hiddenSections.contains(section) {
            return 0    // 섹션이 hidden이므로 행을 노출 X.
        }
        
        return comments[section].replies.count  // 가진 데이터의 개수만큼 노출.
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()    // TableView Cell 생성
        // Dummy Data 삽입
        cell.textLabel?.text = (comments[indexPath.section].replies[indexPath.row]).replyText
        
        return cell
    }
}

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerText = UILabel()  // Section 에 사용될 UILabel 생성
        headerText.textColor = .lightGray
        headerText.adjustsFontSizeToFitWidth = true
        headerText.textAlignment = .left
        headerText.text = comments[section].commentText // Dummy Data의 Comment 데이터 삽입
        headerText.tag = section    // 숨김 처리를 위한 Tag 설정
        
        // Section 선택 시 Tap 이벤트 설정
        let tap = UITapGestureRecognizer(target: self,
                                         action: #selector(self.hideSection(sender:)))
        headerText.isUserInteractionEnabled = true
        headerText.addGestureRecognizer(tap)
        
        return headerText
    }
}
  • DataSourceDelegate 함수 선언 및 작업
  • Section 부분 UILabel로 설정
@objc
    private func hideSection(sender: UITapGestureRecognizer) {
        // section의 tag 정보를 가져와서 어느 섹션인지 구분한다.
        let section = sender.view!.tag
        
        // 특정 섹션에 속한 행들의 IndexPath들을 리턴하는 메서드
        func indexPathsForSection() -> [IndexPath] {
            var indexPaths = [IndexPath]()
            
            for row in 0..<comments[section].replies.count {
                indexPaths.append(IndexPath(row: row,
                                            section: section))
            }
            
            return indexPaths
        }
        
        // 가져온 section이 원래 감춰져 있었다면
        if self.hiddenSections.contains(section) {
            // section을 다시 노출시킨다.
            self.hiddenSections.remove(section)
            self.tableView.insertRows(at: indexPathsForSection(), with: .fade)
            
            self.tableView.scrollToRow(at: IndexPath(row: comments[section].replies.count - 1,
                                section: section),
                                       at: UITableView.ScrollPosition.bottom, animated: true)
        } else {
            // section이 원래 노출되어 있었다면 행들을 감춘다.
            self.hiddenSections.insert(section)
            self.tableView.deleteRows(at: indexPathsForSection(), with: .fade)
        }
    }
  • Section 클릭 시 호출 될 hideSection 구현

 

3. 실행

  • 위와 같은 작업이 이루어 지면 다음과 같은 실행 결과를 얻을 수 있다.

 

4. 전체 소스

 

GitHub - sangbeani/Expandable-UITableView

Contribute to sangbeani/Expandable-UITableView development by creating an account on GitHub.

github.com

 

이상으로 Expandable UITableView 만들기 포스팅을 마치겠습니다.
틀린 부분이나 궁금한 사항은 댓글 남겨주세요~

 


참고

반응형