iOS: Custom Multiple Selections in Table View

hyobยท2020๋…„ 5์›” 16์ผ
2
post-thumbnail

๐Ÿ’ก๋ฌด์—‡์„ ํ• ๊ฑฐ๋ƒ

๊ธฐ๋ณธ ์•ฑ์˜ ๋ฉ€ํ‹ฐ ์…€๋ ‰์…˜UI

UITableView ๋Š” ์ด๋ฏธ ์…€๋ ‰์…˜ ๋ชจ๋“œ์—์„œ ๋ฉ€ํ‹ฐ ์…€๋ ‰์…˜์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ฒดํฌ๋ฐ•์Šค(์„œํด)๊ฐ€ ์™ผ์ชฝํŽธ์— ์žˆ๊ณ  ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ถ™์ด๋Š” ์˜ต์…˜์€ ๋”ฐ๋กœ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

UITableViewCell ์— editingAccessoryView ๋ผ๋Š” ์ธ์Šคํ„ด์Šค ํ”„๋กœํผํ‹ฐ๊ฐ€ ์žˆ์–ด์„œ ์จ๋ดค์Šต๋‹ˆ๋‹ค๋งŒ, editingAccessoryType ์— ๋”ฐ๋ผ ์ •ํ•ด์ง„ ์•„์ด์ฝ˜์„ ๋ณด์—ฌ์ค„ ๋ฟ ์›ํ•˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์„ ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์™ผ์ชฝ์— ์…€๋ ‰์…˜ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์—†์•จ ์ˆ˜ ์—†๋Š”๊ฒƒ ๊ฐ™๋”๋ผ๊ตฌ์š”.. ๐Ÿ˜‚๊ทธ์ง€๊ฐ™์€ ์• ํ”Œใ…‹

ํ˜น์‹œ ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ๊ณ„์‹œ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค yeye๐Ÿ‘€

๋ณ„๊ฑฐ ์•„๋‹Œ๋ฐ iOS์—์„œ ์ œ๊ณตํ•˜๋Š”๊ฒŒ ์—†์–ด์„œ ๊ท€์ฐฎ์•˜์ง€๋งŒ, ๋””์ž์ด๋„ˆ์˜ ๋””์ž์ธ์ปจ์…‰์„ ์ง€์ผœ๋“œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์ง์ ‘ ์ปค์Šคํ…€ํ–ˆ์Šต๋‹ˆ๋‹ค.
(์นดํ†ก์— ์žˆ๋Š” UI์—ฌ์„œ ์ €๋„ ํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค ์‹ถ์—ˆ๊ตฌ์š”๐ŸŒ)

๐Ÿƒ๐Ÿปโ€โ™‚๏ธ์š”๊ตฌ์‚ฌํ•ญ

  1. ์—๋””ํŒ…๋ชจ๋“œ์—์„œ ๋ฉ€ํ‹ฐ์…€๋ ‰ํŒ…์œผ๋กœ ์ฑ„ํŒ…๋ฐฉ ๋‚˜๊ฐ€๊ธฐ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.
  2. ์ฑ„ํŒ…๋ฆฌ์ŠคํŠธ/๋‚ด ์ฑ„ํŒ… ์ด๋™์‹œ ์—๋””ํŒ… ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™” ํ•œ๋‹ค.(์ฑ„ํŒ… ํƒญ ๋‚ด๋ถ€์— ํŽ˜์ด์ ธ๋ทฐ๊ฐ€ ์žˆ๊ณ  ๊ทธ ์•ˆ์— 2๊ฐœ์˜ ํŽ˜์ด์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.)

๊ฐœ๋ฐœ ๊ณ ์ˆ˜์ธ ์—ฌ๋Ÿฌ๋ถ„๋“ค์—๊ฒŒ๋Š” ์•„์ฃผ ์‰ฝ์ฃ ?
์ €๋Š” ์กด๋ชป์— Abstraction ์ค‘๋…์ž, HDD๋ผ ๋˜ ์ด์ƒํ•œ์ง“ ๋งŽ์ด ํ–ˆ์Šต๋‹ˆ๋‹ค๐Ÿ’ฉ

๐Ÿ‘€๋ณธ๋ก 

๋ทฐ ๋ ˆ์ด์–ด

๋จผ์ € ์ž์†Œ์„ค๋‹ท์ปด ์•ฑ์˜ ์ฑ„ํŒ… ๋ชฉ๋ก ๋ทฐ ๋ ˆ์ด์–ด๋ฅผ ๊ฐ„๋žตํžˆ ์†Œ๊ฐœํ•ด์•ผ ํ• ๊ฒƒ ๊ฐ™๋„ค์š”


UITabBarController - ChatMainVC(RED) - PagerView - ChatTableVC(BLUE), MyChatTableVC(BLUE)
๋Œ€์ถฉ ์ด๋ ‡์Šต๋‹ˆ๋‹ค.

PagerView ๋Š” XLPagerTabStrip ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค ใ…‹ ์•ˆ๋“œ ์• ํ”Œ ํ˜ผ์ข…๐Ÿ˜ˆ

์ƒ๋‹จ ์šฐ์ธก์— โ€ขโ€ขโ€ข๋ฒ„ํŠผ(๋”๋ณด๊ธฐ) ๋ณด์ด์‹œ์ฃ ?
๋„ค ์ €๊ฒƒ์„ ๋ˆ„๋ฅด๋ฉด ์•ก์…˜์‹œํŠธ๊ฐ€ ์˜ฌ๋ผ์™€์„œ ํŽธ์ง‘๋ชจ๋“œ๋กœ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ChatMainVC ๋‚ด๋ถ€์˜ ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด, ํŽ˜์ด์ ธ๋ทฐ์˜ ํ•˜์œ„์— ์žˆ๋Š” ํ˜„์žฌ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ(ChatTableVC, MyChatTableVC)์˜ ํ…Œ์ด๋ธ” ๋ทฐ๊ฐ€ ๋ฉ€ํ‹ฐ์…€๋ ‰ํŒ… ๋ชจ๋“œ๋กœ ๋ฐ”๋€Œ์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ํ˜„์žฌ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋ฐ”๊ฟ€๋•Œ ๋งˆ๋‹ค, ํŽธ์ง‘์ƒํƒœ๋Š” ์ดˆ๊ธฐํ™” ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ท.

1. ํ…Œ์ด๋ธ” ๋ทฐ์˜ ๋ฉ€ํ‹ฐ์…€๋ ‰ํŒ… ์ปค์Šคํ…€
2. isEditing state ๊ด€๋ฆฌ

ํฌ๊ฒŒ ๋‘๊ฐ€์ง€๋กœ ๋‚˜๋ˆ  ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํ˜น์‹œ โ€ขโ€ขโ€ข๋ฒ„ํŠผ ์˜ ๋ช…์นญ์„ ์•„์‹œ๋‚˜์š”? ์ €๋Š” moreHorizonButton์ด๋ผ๊ณ  ๊ทธ๋ƒฅ ํ–ˆ์Šต๋‹ˆ๋‹ค;;

Multiple Selection

  1. isEditing
    UIViewController์—๋Š” isEditing ํ”„๋กœํผํ‹ฐ๊ฐ€ ์ด๋ฏธ ์žˆ์Šต๋‹ˆ๋‹ค.
    ์ €๋Š” ์ด๊ฑธ๋กœ ์—๋””ํŒ… ๋ชจ๋“œ state ๋ฅผ ๊ด€๋ฆฌ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  2. TableViewCell
    ChatTableVC์— ์ด์šฉ๋˜๋Š” ChatCell์— ๋ฏธ๋ฆฌ ์ฒดํฌ๋ฐ•์Šค๋“ค์„ ๋งŒ๋“ค์–ด ๋†“๊ณ , isEditing ์ƒํƒœ์— ๋”ฐ๋ผ constraints๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ๊ณ , isHidden๊ฐ’์„ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let chatGroupVM = vm.chatGroupVMs[indexPath.section]
    let chatCellVM = chatGroupVM.chatCellVMs[indexPath.row]
    let cell = chatCellVM.cellInstance(tableView, indexPath: indexPath)

    guard let chatCell = cell as? ChatCellRepresentable else { return cell }
    if chatGroupVM.isRemovable {
        chatCell.toggleSelectionImageView(isEditing: isEditing)
        chatCell.setSelected(isSelected: chatCellVM.isSelected)
    } else {
        chatCell.toggleSelectionImageView(isEditing: false)
        chatCell.setSelected(isSelected: chatCellVM.isSelected)
    }
    return chatCell
}

ChatTableVC.swift

import UIKit
import SnapKit

class ChatCell: UITableViewCell, ChatCellRepresentable {
    @IBOutlet weak var imgLogo: UIImageView!
    @IBOutlet weak var lbTitle: UILabel!
    @IBOutlet weak var lbNowChatting: UILabel!
    var selectionImageView = UIImageView().then {
        $0.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
        $0.isHidden = true
    }

    private let unselectedImg = UIImage(named: "Round_Check โ€“ 1")
    private let selectedImg = UIImage(named: "Round_Check_Select โ€“ 1")

    override func awakeFromNib() {
        super.awakeFromNib()

        addSubview(selectionImageView)
        selectionImageView.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.trailing.equalToSuperview().inset(14.0)
        }
    }

    func toggleSelectionImageView(isEditing: Bool) {
        selectionImageView.isHidden = !isEditing
        lbNowChatting.snp.remakeConstraints { make in
            make.width.equalTo(45.0)
            make.height.equalTo(21.0)
            make.centerY.equalToSuperview()
            make.trailing.equalToSuperview().inset(isEditing ? 45.0 : 15.0)
        }
    }

    func setSelected(isSelected: Bool) {
        selectionImageView.image = isSelected ? selectedImg : unselectedImg
        self.isSelected = isSelected
    }

    func setup(vm: ChatCellVMRepresentable) {
        if let imageUrl = vm.chat.imageUrl, let url = URL(string: imageUrl) {
            imgLogo.kf.setImage(with: url)
        }
        lbTitle.text = vm.title
        lbNowChatting.isHidden = !vm.chat.isNowChatting
        selectionImageView.image = !vm.isSelected ? selectedImg : unselectedImg
    }
}

ChatCell.swift

์ •๋ฆฌํ•˜๊ธฐ ๊ท€์ฐฎ์•„์„œ ๊ทธ๋ƒฅ ๋‹ค ๊ณต๊ฐœํ•ฉ๋‹ˆ๋‹ค๐Ÿ’ฉ

  1. isSelected
    didselectRowAt ํ•จ์ˆ˜์— isEditing ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๊ณ  ๋กœ์ง์„ ๋ถ„๊ธฐ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
    cell์— ๋Œ€์‘ํ•˜๋Š” vm์˜ isSelected์†์„ฑ์„ ๋ณ€๊ฒฝํ•˜๊ณ , cell์˜ ๋ทฐ๋„ ๋ณ€๊ฒฝ ์‹œ์ผœ์ค๋‹ˆ๋‹ค.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let chatVM = vm.chatGroupVMs[indexPath.section].chatCellVMs[indexPath.row]
    
    if isEditing {
        chatVM.isSelected.toggle()
        guard let cell = tableView.cellForRow(at: indexPath) as? ChatCellRepresentable else { return }
        cell.setSelected(isSelected: chatVM.isSelected)
    } else {
        self.openChat(by: chatVM.chat)
        tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.none)
    }
}

ChatTableVC.swift

isEditing state

state๋ฅผ ๋ฐ”๊พธ๋Š” ui๋Š” ChatTableVC๊ฐ€ ์•„๋‹ˆ๋ผ ๊ทธ ์ƒ์œ„ ๋ทฐ ์ž…๋‹ˆ๋‹ค.
ํ•˜๋‚˜์˜ ๋ฒ„ํŠผ์œผ๋กœ ํ•˜์œ„ 2๊ฐœ์˜ ํ…Œ์ด๋ธ”๋ทฐ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋ผ์„œ ์ผ๋ฐ˜์ ์ด์ง„ ์•Š์€๊ฒƒ ๊ฐ™์•„์š” ๐Ÿ˜ˆ

์—ฌ๊ธฐ ๊นŒ์ง€ ์“ฐ๋‹ค๊ฐ€ ํž˜์ด ๋น ์ ธ์„œ ๋‚˜๋จธ์ง€๋Š” ๋‹ค์Œ ํฌ์ŠคํŒ…์— ์ด์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค...

๐Ÿ‘€๋์œผ๋กœ

iOS๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ์„๋•Œ ์ €ํฌ ์•ฑ์€ MVC ํŒจํ„ด์œผ๋กœ ๋งŒ๋“ค์–ด์ ธ ์žˆ์—ˆ๊ณ , ๊ทธ ์กฐ์ฐจ๋„ ์ „ํ˜€ swiftyํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
์ œ๊ฐ€ ์กฐ๋ฅด๊ณ  ์กธ๋ผ ์†Œ์ค‘ํ•œ ๋ฆฌํŒฉํ† ๋ง ์‹œ๊ฐ„์„ ์–ป์–ด๋‚ด์–ด MVC -> MVVM ์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์—ˆ๋Š”๋ฐ, ์ด๋ฒˆ์— ReactorKit๊นŒ์ง€ ์ง‘์–ด ๋„ฃ์–ด ๋ดค์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ง€๊ธˆ์€ MVC, MVVM, ReactorKit ์„ธ๊ฐ€์ง€๊ฐ€ ์„ž์—ฌ ์žˆ์๋‹ˆ๋‹ค.. ์ด์ƒํ•ด ๋ณด์ด๋”๋ผ๋„ ์ดํ•ดํ•ด ์ฃผ์„ธ์š”๐Ÿ™๐Ÿป

ํฌ์ŠคํŒ…ํ•˜๊ณ  ์‹ถ์€ ๋‚ด์šฉ์€ ๊ณ„์† ์Œ“์—ฌ๊ฐ€๋Š”๋ฐ ๊ธ€์“ฐ๋Š”๊ฒŒ ์ฐธ ์‰ฝ์ง€ ์•Š๋„ค์š”๐Ÿƒ๐Ÿปโ€โ™‚๏ธ๐Ÿคฌ

์ผ๋‹จ ์ƒ๊ฐํ•˜๊ณ  ์žˆ๋Š”๊ฒŒ

  1. ReactorKit์„ ์ด์šฉํ•œ isEditing state ๊ด€๋ฆฌ(๊ฐœ์‰ฌ์›€)
  2. Apple Login(iOS, Web)

๋‘๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
์ž˜ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?๐Ÿ™‰

profile
์•ต์ปค๋ฆฌ์–ด์—์„œ ์ž์†Œ์„ค๋‹ท์ปด์„ ๊ฐœ๋ฐœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€