[iOS] UITableViewCell내 UIButton 액션이 작동하지 않을 때 (feat. tag/delegate/closure)
👨🏻‍💻iOS 공부/iOS & Swift

[iOS] UITableViewCell내 UIButton 액션이 작동하지 않을 때 (feat. tag/delegate/closure)

728x90
반응형

아무렇지 않게 tableView의 CustomCell에 UIButton을 넣어주고 addTarget을 하여 액션을 넣어주고 있었다. 

.

.

응??🧐

.

.

당연히 cell에 넣어주고 action을 주면 작동을 하리라 생각했으나 되지 않았다..!

바로 검색 시작...

 

정말 다양한 답변들을 찾아볼 수 있었다. 하지만 모든 방법이 문제를 해결해주지는 못했고...😇 여러 가지 방법을 찾아본 결과...

결론을 먼저 이야기하자면 총 세 가지 방법으로 해결을 할 수 있다.

 

1. cellForRowAt에서 Button의 tag를 이용하는 방법

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Contents_UserInfoCell",for: indexPath) as! Contents_UserInfoCell

    cell.mannerInfo.tag = indexPath.row
    cell.mannerInfo.addTarget(self, action: #selector(mannerClicked), for: .touchUpInside)
            
    return cell
}

 

mannerInfo라는 버튼의 tag 값에 indexPath.row를 주어 해결하는 방식이다. 

cell내 구현은 기본적으로 같게하고 tableView의 cellForRowAt에서 tag를 부여하는 방식!

 

설명이 다른 부분에 보다 적은 이유는... 이 방법은 별로 추천하고 싶지 않는 방법이기 때문이다!

indexPath의 row를 이용하고 있는 만큼 cell의 추가/삭제와 같은 변화에 매우 민감하기에 데이터가 복잡해지거나 변경되는 이슈가 있을 때 제대로 작동하지 않을 수 있다. 

 

이런 경우를 방지하기 위해선? 아래의 두 가지 케이스를 고려해서 사용해줘야 한다!!

 

2. DelegatePattern을 이용하는 방법 

 

별표 다섯 개 치고 시작하자! ⭐️⭐️⭐️⭐️⭐️ 

 

delegate 패턴은 많이 들어봤을 것이다. 단어 뜻 그대로 "위임"을 하는 것을 의미한다. 

이러한 delegate 패턴을 사용하기 위해서틑 프로토콜(규약)을 만들고 채택해야 한다. 

 

위임자가 프로토콜을 만들고 피위임자가 해당 프로토콜을 채택하고 그 규약을 따라야 한다. 

그리고 피위임자는 해당 규약 내에서 자신이 기능을 수행하게 되는데, 이 때 기능을 수정하여 사용하면 된다. 

말이 좀 어려운데.. 우선 프로세스를 본 후 코드로 보면서 이해해보자. (UIButton을 컨트롤 하는 것을 예시로!)

 

1) cell에 Protocol을 만들어준다.
2) cell내에 위임자(delegate)를 생성해준다. 
3) cell내에 위임할 action을 만들고, button에 action을 적용해준다.
4) 1번에서 만든 프로토콜을 채택하고 규약들을 지키기 위해 함수들을 구현해준다. (여기서 구현한대로 실행됨!)
5) tableView의 cellForRowAt으로 돌아와 2)번에서 만들었던 delegate를 위임해준다. 

 

자 이제 코드로 봐보자!

// 1) cell에 Protocol을 만들어준다. 
// 위치 : TableViewCell

// 범용성을 위해 class가 아닌 AnyObject로 선언해준다. 
protocol ContentsMainTextDelegate: AnyObject {
    // 위임해줄 기능
    func categoryButtonTapped()
}

cell내에 프로토콜을 선언해주고 

 

// 2) cell내에 위임자(delegate)를 생성해준다. 
// 위치 : TableViewCell

class Contents_MainTextCell: UITableViewCell {
    ....
    
    var cellDelegate: ContentsMainTextDelegate?
    
    ....
}

위임자를 만들어준다!

 

// 3) cell내에 위임할 action을 만들고, button에 action을 적용해준다.
// 위치 - TableViewCell

class Contents_MainTextCell: UITableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        // 여기서 버튼에 액션 추가 
        self.categoryButton.addTarget(self, action: #selector(categoryClicked), for: .touchUpInside)
    }
    
    // 버튼 
    let categoryButton: UIButton = {
        let bt = UIButton()
        bt.setTitle("기타 중고물품", for: .normal)
        bt.setTitleColor(UIColor(named: CustomColor.reply.rawValue), for: .normal)
        bt.titleLabel?.font = UIFont(name: "Helvetica", size: 12)
        return bt
    }()
    
    // 위임해줄 기능을 미리 구현해두어 버튼에 액션 추가
    @objc func categoryClicked() {
        cellDelegate?.categoryButtonTapped()
    }    

}

스토리보드가 아닌 코드로 구현하다 보면 addTarget을 어느 부분에 해줘야할지 고민이 될 수 있다. 

 

실제로 Button을 생성하는 시점에 addTarget하게 되면 delegate 패턴이 작동하지 않는다. 

// 이렇게 버튼 생성시에 액션을 추가해주면 작동하지 않는다!
// 위치 - TableViewCell

let categoryButton: UIButton = {
    let bt = UIButton()
    bt.setTitle("기타 중고물품", for: .normal)
    bt.setTitleColor(UIColor(named: CustomColor.reply.rawValue), for: .normal)
    bt.titleLabel?.font = UIFont(name: "Helvetica", size: 12)
    bt.addTarget(self, action: #selector(categoryClicked), for: .touchUpInside)
    return bt
}()

그렇기에 꼭 `init` 부분에 addTarget을 해줘야만한다. (따로 함수로 빼서 구현하는 것도 마찬가지로 괜찮다)

 

자 이제 cell에서 해줄 역할은 끝이고 이제 TableView가 있는 ViewController로 이동해보자. 

 

// 4) 1번에서 만든 프로토콜을 채택하고 규약들을 지키기 위해 함수들을 구현해준다. (여기서 구현한대로 실행됨!)
// 위치 - ViewController

extension ContentsViewController: ContentsMainTextDelegate {
    func categoryButtonTapped() {
        // --ToDo--
        print("사용하고 싶은 기능들 추가")
    }
}

프로토콜을 채택해주고, 기능을 추가해주면 된다.

 

// 5) tableView의 cellForRowAt으로 돌아와 2)번에서 만들었던 delegate를 위임해준다. 
// 위치 - ViewController

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    ....
    let cell = tableView.dequeueReusableCell(withIdentifier: "Contents_MainTextCell",for: indexPath) as! Contents_MainTextCell
    // !!위임!!
    cell.cellDelegate = self
            
    return cell
    ....
}

 

이렇게 해주면 Cell안의 버튼이 문제없이 작동된다!

 

3. Swift Closure를 이용하는 방법

마지막으로 Closure 타입을 이용하는 방법을 알아보자! 

바로 프로세스 먼저 봐보자!

 

1) Cell에 Closure Property를 생성해준다. 
2) Button에 추가해 줄 action을 Cell에 생성해준다. 
3) init에 addTarget 추가 (delegate 때 처럼 똑같이!)
4) TableView로 돌아와 cellForRowAt 부분에 기능을 구현해준다. 

 

delegate과 비교했을 때 간소화되어 보인다. 

프로토콜을 채택할 필요도 없고, 규약을 지킬 필요도 없고...! (하지만 서로 장단점이 있을 것 같다. 이건 다음에!)

 

아무튼 다시 돌아와서 코드를 봐보자. 

 

// 1) Cell에 Closure Property를 생성해준다. 
// 위치 : TableViewCell

class Contents_ReportCell: UITableViewCell {
    // Input과 return이 없다는 의미
    // Closure optional로 만들기 위해 ()로 덮었씌움
    var reportButtonAction : (() -> ())?
}

클로저 프로퍼티를 하나 추가해준다. 

 

// 2) Button에 추가해 줄 action을 Cell에 생성해준다. 
// 3) init에 addTarget 추가 (delegate 때 처럼 똑같이!)
// 위치 : TableViewCell

class Contents_ReportCell: UITableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        // 마찬가지로 init시에 addTarget 추가
        self.reportButton.addTarget(self, action: #selector(reportClicked), for: .touchUpInside)
    }
    
    // 버튼
    let reportButton: UIButton = {
        let bt = UIButton()
        bt.setTitle("이 게시글 신고하기", for: .normal)
        bt.titleLabel?.textColor = UIColor(named: CustomColor.text.rawValue)
        bt.titleLabel?.font = UIFont(name: "Helvetica-Bold", size: 14)        
        return bt
    }()
    
    // 버튼 클릭시 실행
    @objc func reportClicked() {
        print("신고해!!")
        reportButtonAction?()
    }
}

버튼과 그에 필요한 액션들을 추가해준다. 

 

// 4) TableView로 돌아와 cellForRowAt 부분에 기능을 구현해준다. 
// 위치 - ViewController

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Contents_ReportCell",for: indexPath) as! Contents_ReportCell
    cell.reportButtonAction = { [unowned self] in
        // 기능 구현 위치
        print("신고")
    }

}

cellForRowAt에 클로저를 구현해준다. 

 

여기서 `unowned` 로 선언해준 것을 볼 수 있는데 이는 Cycle을 막기 위함이다. 

어떤 싸이클이냐 하면... 

 

지금의 구조는 ViewController > TableView > TableViewCell > reportButtonAction 순으로 되어 있다. 여기서 그냥 self로만 구현하게 되면 reportButtonAction이 ViewController를 보유하게 되어 무한 싸이클이 돌게 되는 것이다. 

 

그래서 weak나 unowned를 써줘야 한다.

(TableViewCell의 버튼을 클릭할 때, 아직 ViewController가 여전히 메모리에 있음을 알 수 있어 unowned를 사용해준다. 

 

클로저 방법의 경우 간단해보이지만 각 셀에서 기능을 구현하고자 할 때, 셀이 각각의 클로저들을 저장하기 위해 메모리에 할당되어야 하는 단점이 하나 있다. 

 

이에 delegate패턴과 closure 패턴을 적절하게 섞어서 사용하거나, 상황에 맞게 판단해서 사용하면 좋을 듯 하다. 


시도했으나.. 해결방법이 아니었던 것들에 대해 이야기해보자 한다...😇

 

1. TableView에 isUserInteractionEnabled 값을 true로 주어라!

가장 그럴듯 해보였으나... 작동하지 않았다. 

cell 내의 작동과는 거리가 멀어보였다. 그리고 기본적으로 true로 되어있어 인터랙션이 이루어질 수 있도로 되어있는 것으로 알고 있다!

 

2. TableView에 allowSelection 값을 false로 주어라!

cell내 구성요소가 아니라 cell 전체가 클릭된다고 생각되어 나오게된 발상 같다... 

마찬가지로 didSelectRowAt을 제거하라는 방식도 비슷하지만... 둘 다 해결 방법은 아님!

 

3. TableView에 delayContentTouches 값을 false로 주어라!

터치시 딜레이를 관할하는 부분이라 유관할 수 있다고 생각했지만 애초에 "delay"이기 때문에... delay였으면 기다렸을 때 버튼이 작동했어야 하는데.. 이 또한 해결 방법은 아니었다!

 

결론적으로 delegate를 활용하거나 button의 tag를 이용하는 방식 중 하나로 택 1하여 사용해야할 듯 싶다. 

이후에 데이터를 넘겨주거나, 관리할 일이 있을 걸 대비하자면... delegate 패턴으로 구현하는 것이 조금 더 합리적일 듯 하다. 

 

끄읕

 

refer : https://fluffy.es/handling-button-tap-inside-uitableviewcell-without-using-tag/

 

Handling button tap inside UITableView Cell without using tag

Table of contents : Why not to use tag The Delegate way The Closure way Notes Say you have a button inside every cell in a tableview like this : You want the app to show an alert with message "Subscribed to [youtuber name]" when user tap on the subscribe

fluffy.es

 

728x90
반응형