UITableViewCell의 ImageView 클릭이벤트 처리
- Cell에서 ImageView의 탭제스처 감지 후 옵저버블로 방출
- ViewController에서는 옵저버블 구독 후, ViewModel로 전달
- ViewModel은 이벤트를 받아 처리하고, 필요한 데이터 업데이트 수행
방법 1. ViewController에서 바로 ViewModel로 전달
Cell
import UIKit
import RxSwift
import RxCocoa
class MyTableViewCell: UITableViewCell {
@IBOutlet weak var myImageView: UIImageView!
private var disposeBag = DisposeBag()
private var indexPath: IndexPath?
private var imageViewTapSubject = PublishSubject<IndexPath>()
var imageViewTap: Observable<IndexPath> {
return imageViewTapSubject.asObservable()
}
override func awakeFromNib() {
super.awakeFromNib()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageViewTapped))
myImageView.addGestureRecognizer(tapGesture)
myImageView.isUserInteractionEnabled = true
}
func configure(with item: Item, indexPath: IndexPath) {
self.indexPath = indexPath
// Cell의 데이터를 설정하는 로직
}
@objc private func imageViewTapped() {
guard let indexPath = indexPath else {
return
}
imageViewTapSubject.onNext(indexPath)
}
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
}
ViewController
import UIKit
import RxSwift
class MyViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var disposeBag = DisposeBag()
private var viewModel: MyViewModel!
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
bindViewModel()
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: "MyTableViewCell", bundle: nil), forCellReuseIdentifier: "MyTableViewCell")
}
private func bindViewModel() {
viewModel = MyViewModel()
viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: "MyTableViewCell", cellType: MyTableViewCell.self)) { [weak self] row, item, cell in
guard let indexPath = self?.tableView.indexPath(for: cell) else {
return
}
cell.configure(with: item, indexPath: indexPath)
cell.imageViewTap
.bind(to: self!.viewModel.imageViewTap)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
viewModel.handleImageViewTap
.subscribe(onNext: { [weak self] indexPath in
self?.handleImageViewTap(indexPath: indexPath)
})
.disposed(by: disposeBag)
viewModel.fetchData()
}
private func handleImageViewTap(indexPath: IndexPath) {
// 해당 IndexPath에 대한 처리 로직 구현
}
}
extension MyViewController: UITableViewDelegate {
// TableView의 delegate 메서드 구현
}
ViewModel
import RxSwift
class MyViewModel {
private let disposeBag = DisposeBag()
private let itemsSubject = BehaviorSubject<[Item]>(value: [])
private let imageViewTapSubject = PublishSubject<IndexPath>()
var items: Observable<[Item]> {
return itemsSubject.asObservable()
}
var handleImageViewTap: Observable<IndexPath> {
return imageViewTapSubject.asObservable()
}
func fetchData() {
// 데이터를 가져오는 비즈니스 로직
// itemsSubject.onNext(updatedItems) 호출하여 데이터 업데이트
}
// TableView의 didSelectRow 메서드를 호출하여 ImageView 클릭 이벤트를 전달받음
func didSelectRow(at indexPath: IndexPath) {
imageViewTapSubject.onNext(indexPath)
}
}
방법 2. ViewController의 Subject로 전달 후 ViewModel로 전달( InOut 패턴 적용에 용이하며 UIUpdate같은 다른 요소에도 활용하기 용이.)
import UIKit
import RxCocoa
import RxKeyboard
import RxSwift
import SnapKit
import Then
final class ChatViewController: UIViewController {
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()
).then {
// ...생략
}
private let disposeBag = DisposeBag()
private let viewModel: ViewModel
//// ⭐️⭐️⭐️⭐️
private let selectedUser = PublishSubject<User>()
private let menuSelected = PublishSubject<Menu>()
//// ⭐️⭐️⭐️⭐️
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lifecycles
override func viewDidLoad() {
super.viewDidLoad()
bind()
layout()
configure()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = false
}
// MARK: - ViewController Methods
private func bind() {
let input = ViewModel.Input(
// ... 중간 생략
//// ⭐️⭐️⭐️⭐️
selectedUser: selectedUser
.throttle(.seconds(1), scheduler: MainScheduler.asyncInstance),
menuSelected: menuSelected.asObservable()
//// ⭐️⭐️⭐️⭐️
)
let output = viewModel.transform(input: input)
bindCollection(output: output)
}
// MARK: - Binds
func bindCollection(output: ViewModel.Output) {
output.messages
.drive(collectionView.rx.items) { [weak self] collectionView, index, chat in
guard let self = self,
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: Cell.identifier,
for: IndexPath(row: index, section: 0)) as? ChatCell else {
return UICollectionViewCell()
}
cell.layoutChat(chat: chat)
//// ⭐️⭐️⭐️⭐️
cell.profileImageButton.rx.tap
.compactMap { chat.user }
.bind(to: self.selectedUser)
.disposed(by: self.disposeBag)
cell.menuSelected
.bind(to: self.menuSelected)
.disposed(by: self.disposeBag)
//// ⭐️⭐️⭐️⭐️
return cell
}
.disposed(by: disposeBag)
}
extension ChatViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
}
}
저는 방법 2를 선호하는 편이긴 합니다.. 혹시 더 좋은 방법이 있다면 알려주세요 !