본문 바로가기

EXPERIENCE/iOS

[Xcode/iOS] Swift TableView 섹션이 있는 확장셀(Expandable Cell) 만들기 (with. xib)

728x90
728x90
SMALL

 

 

 

 

아주 유용하게 쓰이는 TableView!

이래저래 필요한 부분을 생각하다보니 눌렀을 때 아래로 관련 행이 하나 더 추가되는 확장 가능한 Expandable Cell이 만들고 싶어졌다!

그런데 아무리 뒤져바도 섹션(Section)헤더를 쓰지 않고 한 섹션 당 한 행이 존재하는 구현 방법들이 나올뿐.. 

한 섹션 당 여러 행이 존재하여 헤더를 사용하는 경우가 나오지 않아 직접 만들어 보았다.

 

 

 

 

 

 

 

Github
 

GitHub - sohay19/ExpandableCell: Expandable Cell with Multi-Row Section Available

Expandable Cell with Multi-Row Section Available. Contribute to sohay19/ExpandableCell development by creating an account on GitHub.

github.com

 

 

 

 

 

결과화면

원하는 이름 셀을 클릭하면 아래 확장 셀이 열리면서 핸드폰 번호를 확인 할 수 있다

 

 

 

 

데이터 구조

 

  • UserData: 이름, 분류, 핸드폰 번호를 저장하는 구조체
struct UserData {
    var name:String
    var category:Category
    var phone:String
}

 

 

  • Category: 가족, 친구, 동료 등 관계를 규정하는 분류 체계
enum Category:String {
    case Family = "Family"
    case Friend = "Friend"
    case Coworker = "Coworker"
}

 

 

 

 

기본 세팅

프로젝트 생성 후 TableView를 생성하고 Auto Layout을 잡아준 뒤,
설정에서 Style을 Groped로 지정한다. (원하는 스타일로 설정해도 무방)

 

 

 

 

Xib를 이용한 셀 만들기

 

  • Xib, Swift 파일 생성



 

1. 파일 추가 버튼 선택
2. Cocoa Touch Class 클릭
3. 세부 설정
Class:
원하는 클래스명
Subclass of: UITableViewCell 설정
Language: Swift
4. Next 클릭
5. 같은 이름의 Storyboard, Swift 파일이 1개씩 생성됨

 

  • Cell Storyboard

Default View
Expanded View

1. 일반 셀, 확장 셀 2가지를 구현해야함
2. View를 두 개 추가하여 Cell 전체로 레이아웃을 설정한다
3. 각 뷰를 원하는 대로 설정한다
4. 추후 isHidden을 이용하여 View를 변경한다

 

  • 코드 구현
class ExpandableCell: UITableViewCell {
    // 각각의 셀 뷰
    @IBOutlet weak var defaultView: UIView!
    @IBOutlet weak var ExpandedView: UIView!
    //
    @IBOutlet weak var labelName: UILabel!
    @IBOutlet weak var btnArrow: UIButton!
    @IBOutlet weak var labelPhone: UILabel!
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        initUI()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        // Configure the view for the selected state
    }
    
    func initUI() {
        // 폰트 및 색상 설정
        labelName.font = UIFont.systemFont(ofSize: 15)
        labelName.textColor = .black
        labelPhone.font = UIFont.systemFont(ofSize: 15)
        labelPhone.textColor = .black
        // 화살표 이미지 및 기타 설정
        btnArrow.setImage(UIImage(systemName: "chevron.down"), for: .normal)
        btnArrow.tintColor = .black
        btnArrow.contentMode = .center
        btnArrow.isUserInteractionEnabled = false
    }
    
    func inputCell(isExpand:Bool, name:String, phone:String) {
        // 셀 종류에 따른 컨트롤
        controllCell(isExpand: isExpand)
        // 텍스트 입력
        labelName.text = name
        labelPhone.text = phone
    }
    
    private func controllCell(isExpand:Bool) {
        defaultView.isHidden = isExpand
        ExpandedView.isHidden = !isExpand
    }
}
- awakFromNib() : Cell이 생성될 때 시작됨
- setSelected() :  Cell이 클릭되었을 때 실행됨
- initUI() : 셀이 생성될 때 기본적으로 UI 세팅을 진행한다
- inputCell() : 셀에 값을 전달하여 설정할 때 사용

 

 

 

 

구현하기

 

  • 기본 세팅
class ViewController: UIViewController {
    // TableView
    @IBOutlet weak var phoneTable: UITableView!
    
    // 전화번호부 데이터
    let dataList:[UserData] = [
        UserData(name: "김하나", category: .Friend, phone: "010-1111-1111"),
        UserData(name: "이두울", category: .Family, phone: "010-2222-2222"),
        UserData(name: "박세엣", category: .Coworker, phone: "010-3333-3333"),
        UserData(name: "윤네엣", category: .Friend, phone: "010-4444-4444"),
        UserData(name: "김다섯", category: .Family, phone: "010-5555-5555"),
        UserData(name: "이여섯", category: .Friend, phone: "010-6666-6666"),
        UserData(name: "박일곱", category: .Family, phone: "010-7777-7777"),
        UserData(name: "김여덟", category: .Friend, phone: "010-8888-8888"),
        UserData(name: "이아홉", category: .Coworker, phone: "010-9999-9999"),
        UserData(name: "박여얼", category: .Friend, phone: "010-0000-0000")
    ]
    // 카테고리 데이터
    let categoryList:[Category] = [
        .Family,
        .Friend,
        .Coworker
    ]
    // 열린 셀 정보
    var openedIndex:IndexPath?
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // TableView 설정
        phoneTable.dataSource = self
        phoneTable.delegate = self
        // Custom Cell 등록
        registerCell()
        // TableView UI 세팅
        phoneTable.separatorInsetReference = .fromCellEdges
        phoneTable.separatorInset = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18)
        phoneTable.sectionHeaderTopPadding = 18
    }
    
    private func registerCell() {
        // Nib 등록
        phoneTable.register(UINib(nibName: "ExpandableCell", bundle: nil), forCellReuseIdentifier: "ExpandableCell")
    }
}
- dataList, categoryList : 테이블 관련 데이터
- openedIndex : 유저가 클릭하여 열린 셀이 어떤 셀인지에 대한 데이터를 저장
- registerCell() : 이전에 만든 Cell의 등록을 위한 작업

 

 

 

  • 설명

 

 

 

 

  • Delegate &DataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    // 섹션 수
    func numberOfSections(in: UITableView) -> Int {
        return categoryList.count
    }
    // 섹션 당 행 수
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let category = categoryList[section]
        // 해당 카테고리에 부합하는 행의 수
        let count = dataList.filter({$0.category == category}).count
        // 열린 셀이 있을 경우
        if let openedIndex = openedIndex {
            // 열린 셀의 섹션에 해당하는 경우엔 행을 하나 더 추가한다
            return openedIndex.section == section ? count + 1 : count
        }
        return count
    }
    // 셀 정보
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell", for: indexPath) as? ExpandableCell else {
            return UITableViewCell()
        }
        // 셀 설정을 위한 인덱스
        var index = indexPath
        var isExpand = false
        // 셀 상태 체크
        if let openedIndex = openedIndex { // 열려 있는 셀이 존재
            if index.section == openedIndex.section && index.row > openedIndex.row {
                if index.row == openedIndex.row + 1 {
                    isExpand = true
                }
                index = IndexPath(row: index.row-1, section: index.section)
            }
        }
        let category = categoryList[index.section]
        let item = dataList.filter({ $0.category == category })[index.row]
        cell.inputCell(isExpand: isExpand, name: item.name, phone: item.phone)
        return cell
    }
    // 행 선택 시 이벤트
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //선택 된 셀을 열린 인덱스로 지정
        openedIndex = openedIndex == nil ? indexPath : nil
        tableView.reloadData()
    }
    //MARK: - Size
    // 행 높이
    func tableView(_ tableView: UITableView, heightForRowAt: IndexPath) -> CGFloat {
        return 45
    }
    // 헤더 높이
    func tableView(_ tableView: UITableView, heightForHeaderInSection: Int) -> CGFloat {
        return 60
    }
    //푸터 높이
    func tableView(_ tableView: UITableView, heightForFooterInSection: Int) -> CGFloat {
        return 18
    }
    
    //MARK: - Custom
    // 커스텀 헤더
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 60))
        headerView.backgroundColor = .lightGray.withAlphaComponent(0.1)
        let labelHeader = UILabel(frame: CGRect(x: 18, y: 15, width: 300, height: 45))
        let category = categoryList[section]
        labelHeader.text = category.rawValue
        labelHeader.font = UIFont.boldSystemFont(ofSize: 18)
        labelHeader.textColor = .black
        headerView.addSubview(labelHeader)
        return headerView
    }
    //커스텀 푸터
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        let footerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 18))
        let lineView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 9))
        footerView.addSubview(lineView)
        lineView.backgroundColor = .lightGray.withAlphaComponent(0.1)
        return footerView
    }
}
- 카테고리 수 대로 섹션을 생성한다
- 카테고리에 해당하는 데이터들을 하위 행으로 추가한다
- 셀을 클릭하는 경우 해당 셀을 openedIndex로 지정한다
- 셀이 닫힌 경우에는 따로 처리할 일이 없으나, 셀이 열려 있을 경우 열린 셀과 [같은] 섹션이며, [이후] 행일 경우 행의 숫자를 -1하여 진행한다.
- (셀이 열려 있을 경우) 열린 셀과 [같은] 섹션이며, [바로 다음] 행일 경우 해당 셀은 확장된 셀(Expanded Cell)이다

 

 

 

 

 

TableViewCell 이외에도 Xib를 이용하면 다양한 UI를 편하게 사용할 수 있는데,

추후 그 부분에 대한 포스팅도 진행해보아야겠다.

 

 

 

 

 

 

 

728x90
728x90
LIST