Collapsible TableView Header

hyob·2020년 5월 9일
2

💡무슨 글이냐

iOS 개발에 항상 등장하는 TableView의 SectionView에 관한 글입니다.
자소설닷컴 앱의 채팅서비스를 개선하면서 채팅목록 tableView를 바꾸고 있습니다.
Section Header를 누를때 마다 섹션이 접혔다 펴지는것을 swifty 하게 만들어 보려구요.

솔직히 개쉬운건데 뭣하러 글을 쓰냐 라고하면 할말 없습니다. 예예

👀시작 하기 전에

저는 학생때 잠깐 배운 c 이후로 컴파일 언어(객체, 프로토콜 등)를 제대로 다뤄본게 swift가 처음입니다.
그 전까지는 루비, 자바스크립트, html/css 위주로 개발했었고 자소설닷컴 iOS 앱 또한 swift스럽지 않게(혹은 루비스럽게) 만들어져 있어
☠️소개하는 코드 수준이 떨어질 수 있습니다.

사실 욕 안 먹으려는 핑계고 개발을 개못합니다.
그래도 노오력하고 있습니다 😂
swift(iOS) 포스팅도 많이 할 계획이니까 구경 와주세요ㅋㅋ

🤬어떻게 만들었는데

프로토콜

혹은 자바의 인터페이스입니다.

루비는 이런거 없이 대충 만들어도 찰떡같이 잘 돌아가던데.. 개불편하네?

라고 생각하던 시절이 있었지만, 쓰다보니 좋은 것 같습니다..
아직도 추상화 중독에서 벗어나지 못하고 까불었어요.

실제 헤더 뷰를 위한 CollapsibleTableViewHeader 클래스를 만들고
CollapsibleTableViewHeaderDelegate 프로토콜을 만들어 구현했습니다.

protocol CollapsibleTableViewHeaderDelegate {
    func toggleSection(_ header: CollapsibleTableViewHeader, section: Int)
}

코드에 예쁘게 색깔 넣는거 아시는 분 댓글로 알려주세요..

CollapsibleTableViewHeader 는 위의 델리게이트 프로토콜을 채택하는 친구를 통해 섹션을 접었다 펼겁니다.
UITableViewDelegate를 채택해서 사용하는 것 처럼요!

프로토콜을 사용하는 이유?

필요한 속성이나 함수들을 미리 약속 해 두고, 그 내부 로직은 프로토콜을 채택한 클래스들이 각자 입맛에 맞게 정의한다.

이것 때문인 듯 합니다.
미리 약속해 두지 않을 경우, 해당 함수나 속성이 없어 에러가 발생 할 수도 있고
레일즈에서 자주 만나는 Ruby error: undefined method for nil:NilClassㅋㅋ

고민

toggleSection, toggleSectionCollapsed.. 좋은 이름 알려주실분? 섹션 접었다 폈다 해주는 함수 입니다..

yeye..
toggleSection() 함수의 세부 로직은 CollapsibleTableViewHeaderDelegate를 채택할 친구들에게 맡겨두고, CollapsibleTableViewHeader 를 만들어 보겠습니다.

CollapsibleTableViewHeader

📱완성된 테이블뷰 입니다. 섹션헤더를 봐주세요.
기본적으로 필요한것은 섹션 헤더의 제목과 오른쪽 arrow이미지 입니다.
그리고 헤더 부분을 탭하면 해당 섹션이 접혔다가 열렸다 해야합니다.

//
//  CollapsibleTableViewHeader.swift
//  jasoseol
//
//  Created by 류효광 on 20/04/2020.
//  Copyright © 2020 anchoreer. All rights reserved.
//

import UIKit
import SnapKit
import Then

protocol CollapsibleTableViewHeaderDelegate {
    func toggleSection(_ header: CollapsibleTableViewHeader, section: Int)
}

class CollapsibleTableViewHeader: UITableViewHeaderFooterView {
    static let reuseIdentifier = "CollapsibleHeader"
    
    var delegate: CollapsibleTableViewHeaderDelegate?
    
    var section: Int = 0
    
    let titleLabel = UILabel().then {
        $0.textColor = UIColor("#777")
        $0.font = UIFont.systemFont(ofSize: 12.0, weight: .regular)
    }

    let arrowImageView = UIImageView().then {
        $0.tintColor = UIColor("#999")
    }

    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)

        contentView.backgroundColor = UIColor("#f0f0f0")
        contentView.addSubview(titleLabel)
        titleLabel.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.left.equalToSuperview().inset(10)
        }

        contentView.addSubview(arrowImageView)
        arrowImageView.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(4)
            make.right.equalToSuperview().inset(10)
        }

        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(CollapsibleTableViewHeader.tapHeader(_:))))
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc func tapHeader(_ gestureRecognizer: UITapGestureRecognizer) {
        guard let cell = gestureRecognizer.view as? CollapsibleTableViewHeader else { return }

        delegate?.toggleSection(self, section: cell.section)
    }

    func setCollapsed(_ collapsed: Bool) {
        arrowImageView.image = UIImage(named: collapsed ? "ic_keyboard_arrow_down" : "ic_keyboard_arrow_up")
    }
}

완성된 코드 전체를 복붙했습니다. 지금 보니까 리팩토링을 더 하고싶네요 🤫

주목할 부분은 CollapsibleTableViewHeaderDelegate 인스턴스를 가지고 있다는것.
그리고 탭했을때, 델리게이트를 통해 toggleSection을 실행하는것 입니다.

ChatTableViewController

그 다음은 위에서 만든 클래스로 인스턴스를 만들어 활용하는 부분입니다.

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: CollapsibleTableViewHeader.reuseIdentifier) as? CollapsibleTableViewHeader ?? CollapsibleTableViewHeader(reuseIdentifier: CollapsibleTableViewHeader.reuseIdentifier)

    header.titleLabel.text = vm.chatGroupVMs[section].name
    header.setCollapsed(vm.chatGroupVMs[section].isCollapsed)
    header.section = section
    header.delegate = self

    return header
}

테이블뷰 델리게이트의 viewForHeaderInSection 함수

각 섹션의 헤더 뷰를 만들어주는 부분이죠.
중요한 부분은
header.delegate = self 요 부분입니다.
헤더의 탭 제스쳐를 감지하는것은 CollapsibleTableViewHeader 에서 구현되어있고, 탭 제스쳐가 일어났을때 실제 동작은 CollapsibleTableViewHeaderDelegate를 채택하는 ChatTableVC에 만들어 줘야합니다.

header.setup() 같은 함수를 만들어줄걸 그랬네요😱

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if vm.chatGroupVMs[section].isCollapsed {
        return 0
    } else {
        return vm.chatGroupVMs[section].chatCellVMs.count
    }
}

더불에 섹션의 row수를 결정해 줍니다

extension ChatTableVC: CollapsibleTableViewHeaderDelegate {
    func toggleSection(_ header: CollapsibleTableViewHeader, section: Int) {
        let chatGroup = vm.chatGroupVMs[section].chatGroup
        chatGroup.isCollapsed.toggle()
        header.setCollapsed(chatGroup.isCollapsed)
        chatTableView.reloadSections([section], with: .automatic)

        guard let userId = User().user_id, userId == chatGroup.userId else { return }
        ChatGroupRequest.patch(chatGroup: chatGroup)
    }
}

CollapsibleTableViewHeaderDelegate 를 채택하여 약속을 지키는 부분.. 약속을 잘 지킵시다🤬

맨 처음 프로토콜을 만들때 toggleSection함수를 가지고 있어야 했죠?
실제로 섹션을 접고 펼치는 로직을 여기에 작성해 줍니다.

CollapsibleTableViewHeadersection: Int 프로퍼티를 가지고 있던 이유는 탭된 해당 섹션을 리로드 하기 위해서 입니다 ㅎㅎ
지난 포스팅에서 만든 엔드포인트 한개를 여기서 사용하네요👍🏻

프로토콜로 toggleSection을 약속했기 때문에, 만약에 다른 TableViewController에서도 CollapsibleTableViewHeader를 사용한다면, 델리게이트 프로토콜을 채택하고 toggleSection을 다르게 구성할 수 있겠죠?
toggleSection은 무조건 존재 해야 컴파일이 되구요!

마무리💩

뭔가 열심히 써보려 했는데 또 결국 코드만 갖다 붙이고 급하게 끝낸 느낌입니다.
💩을 덜 닦고 나온듯한..
예예.. 개못하죠?
이렇게 프로토콜을 잘 정의하면, 잘 추상화 해서 잘 정의하면,, 굉장히,, 재사용이 그럴듯하게,, 잘 된다,,, 예예
그렇게 믿고 만들었습니다 하하
아직 재사용 한 곳은 없지만 저희 앱의 채용공고 탭에 쓸 수 있을것 같네요 ㅋ

다음 포스팅은 위에 보이는 채팅 테이블 뷰의 에디팅 -> 멀티 셀렉팅 커스텀 이야기 입니다.
멀티셀렉팅 커스텀 이거 약간 불편했는데, 불편했던 만큼 다른분들이 보시기에 좀 쓸모있는 포스팅이 아닐까 기대합니다💡👀

profile
앵커리어에서 자소설닷컴을 개발하고 있습니다.

1개의 댓글

comment-user-thumbnail
2020년 5월 9일

‘’’java
public interface LearnUpKr {
void pushLike(String id);
void pushSubscribe(String id);
}
‘’’

이런 식으로 백틱 옆에 언어 적어주면
코드 블럭에 색 들어가여 ㅎㅎ

답글 달기