본문 바로가기

iOS

Cell안의 UIButton 이벤트 처리 (+WebKit View)

728x90

Cell안의 버튼에 대한 이벤트 처리를 어떻게 할 수 있을까? 

 

여러 가지 방법이 있을 수 있지만, 이번 정리에서는 3가지 방법을 살펴보자.

  • Tag
  • Delegate
  • Closure 

 

UIButton의 tag를 이용하는 방법

(준비중)

 

 

DelegatePattern를 이용하는 방법

개인적으로 그동안 많이 사용한 방식이다.

 

델리게이트 패턴은 말 그대로 위임하는 방식을 말한다.

이러한 델리게이트 패턴을 사용하기 위해서는 프로토콜을 만들고 채택해야 한다.

 

위임자가 프로토콜을 만들고, 피위임자가 해당 프로토콜을 채택한 후 규약을 따르는 방식을 코드를 작성하면 된다.

피위임자는 해당 규약 내에서 기능을 수행할 수 있는 이 때, 기능을 알맞게 수정해서 사용하면 된다.

 

동작 과정을 요약하면 다음과 같다.

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

 

코드로 보면 아래와 같다.

 

1. cell에 프로토콜을 만들어준다.

cell 내에 프로토콜을 선언한다. 

protocol TrendMediaCollectionViewCellDelegate: MediaViewController {
    // 위임할 기능을 적는다.
    func touchUpClipButton()
}

 

2. cell 내에 위임자를 생성한다.

weak var delegate: TrendMediaCollectionViewCellDelegate?

 

3. cell 내에 위임할 action을 만들고 button에 action을 적용한다.

@IBAction func touchUpClipButton(_ sender: UIButton) {ㅋ
    delegate?.touchUpClipButton()
}

 

4. 1번에서 만든 프로토콜을 채택하고 규약을 지키기 위한 함수들을 구현한다.

// MARK: - Custom Protocol

extension MediaViewController: TrendMediaCollectionViewCellDelegate {
    func touchUpClipButton() {
        // 구현하고 싶은 기능 작성 
    }
}

 

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

extension MediaViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return mediaList.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendMediaCollectionViewCell.reuseIdentifier, for: indexPath) as? TrendMediaCollectionViewCell else { return UICollectionViewCell() }
        cell.setData(mediaList[indexPath.item])
        cell.delegate = self
        return cell
    }
}

 

이렇게 하면 cell 안의 버튼이 문제 없이 작동하는 것을 확인할 수 있다. 

 

Swift Closure를 이용하는 방법

클로저에 대한 자세한 문법적인 내용은 .. 다음 글 .. (|| 다다음글일수도 ..) 에서 다루도록 하고 .. 일단 기능 구현을 해보자.

 

동작 과정을 요약하면 다음과 같다.

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

 

1. cell에 클로저 프로퍼티를 생성한다.

var clipButtonAction : (() -> ())?

 

2. Button에 추가해 줄 action을 cell에 생성한다.

3. cell에서의 이벤트와 action을 연결한다.

@IBAction func touchUpClipButton(_ sender: UIButton) {
    clipButtonAction?()
}

위와 같이 버튼과 그에 필요한 액션을 추가한다.

 

4. tableView로 돌아와 cellForRowAt 부분에 기능을 구현한다.

// MARK: - UICollectionView Protocol

extension MediaViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return mediaList.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendMediaCollectionViewCell.reuseIdentifier, for: indexPath) as? TrendMediaCollectionViewCell else { return UICollectionViewCell() }
        cell.setData(mediaList[indexPath.item])
        
        // Closure를 이용한 버튼 이벤트 처리
        cell.clipButtonAction = { [unowned self] in
            self.mediaId = mediaList[indexPath.item].id
        }
        
        return cell
    }
}

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

 

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

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

 

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

 

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

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

 

델리게이트 VS 클로저

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

 


이대로 끝내기 아쉬우니까 (누구마음대로?) .. 버튼 클릭했을 때 웹뷰를 띄우는 것으로 마무리를 해보자.

 

먼저 스토리보드에 ViewController를 하나 만들어주고 그 위에 WebKit View를 올린다.

 

WebKit View를 선택

위의 이미지를 통해서 알 수 있는 것처럼 WebView는 이제 deprecated 되었기 때문에 WebKit View를 사용해야 한다.

 

레이아웃도 적절하게 잡아준다.

여기까지 UI에 대한 작업을 했다면, 이제 서버 통신을 하고 데이터를 전달해서 웹 뷰를 띄워보도록 하자.

 

동작 과정은 다음과 같다.

1. TMDB API를 통해서 트렌드 미디어 정보를 GET 한 다음,
2. 응답값 중에서 movie_id를 통해 Video API에 연결해서 key 값을 GET 한다.
3. 그리고 그 키 값을 바탕으로 URL을 만들어서 웹 뷰에 띄운다.

 

앞선 과정에서 1번까지는 했다.

여기서 버튼을 눌렀을 때 movie_id값을 바탕으로 video_key 값을 가져오면 된다.

// Closure를 이용한 버튼 이벤트 처리
cell.clipButtonAction = { [unowned self] in
    self.mediaId = mediaList[indexPath.item].id
    
    var videoKey: String = ""
    // VideoAPI를 통해 서버 연결
}

 

APIManger를 만들어서 서버통신 코드를 분리할 수 있다. 

class MediaVideoAPIManger {
    static let shared = MediaVideoAPIManger()
    
    private init() { }
    
    typealias completionHandler = (String) -> ()
    
    private var keyList: [String] = []
    private var key: String = ""
    
    func fetchVideo(movieId: Int, completionHandler: @escaping completionHandler) {
        let url = URLConstant.BaseURL + URLConstant.MovieURL + "/\(movieId)/videos?api_key=\(APIKey.APIKey)&language=en-US"
        
        AF.request(url, method: .get).validate(statusCode: 200...500).responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                // 여러 키 값들이 있는 배열에서 
                self.keyList = json["results"].arrayValue.map { $0["key"].stringValue }
                // 0번째 값으로 웹 뷰를 띄울 수 있으므로 0번째 값만 가져온다.
                self.key = self.keyList[0]
                
                completionHandler(self.key)
                
            case .failure(let error):
                print(error)
            }
        }
    }
}

 

그리고 필요한 시점에서 서버통신을 하면 된다.

 // Closure를 이용한 버튼 이벤트 처리
 cell.clipButtonAction = { [unowned self] in
     self.mediaId = mediaList[indexPath.item].id
     
     var videoKey: String = ""
     MediaVideoAPIManger.shared.fetchVideo(movieId: self.mediaId) { key in
         videoKey = key
         
         guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: VideoWebViewController.reuseIdentifier) as? VideoWebViewController else { return }
         viewController.videoKey = videoKey
         self.navigationController?.pushViewController(viewController, animated: true)
     }
 }

 

이 때, 웹 뷰를 띄울 ViewController에서는 키 값을 받을 변수를 가지고 있어야 한다.

var videoKey = ""

 

그리고 이 비디오 키 값을 바탕으로 URL을 만들어서 웹 뷰를 띄우면 된다.

import UIKit
import WebKit

final class VideoWebViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
        
        var videoKey = ""
        
        override func viewDidLoad() {
            super.viewDidLoad()
            openTrailerURL(key: videoKey)
        }
        
        func openTrailerURL(key: String) {
            guard let url = URL(string: "https://www.youtube.com/watch?v=\(key)") else { return }
                    
            let request = URLRequest(url: url)
            webView.load(request)
        }
}

 

'iOS' 카테고리의 다른 글

required init VS override init  (0) 2022.08.10
Kakao 다음(Daum) 검색 API 구현 (feat. Expandable Cell)  (0) 2022.08.08
TMDB - GET / Pagenation  (0) 2022.08.08
APIManager  (1) 2022.08.08
Pagenation  (0) 2022.08.05