Here is simple trick that can answer your question:
class YourTableViewCell: UITableViewCell {
weak var tableView: UITableView? // declare a weak reference to your tableView that contains this cell
func disableScroll() {
tableView?.isScrollEnabled = false
}
func enableScroll() {
tableView?.isScrollEnabled = true
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: YourTableViewCell = ...
cell.tableView = tableView // assign this tableView to weak ref of tableView in YourTableViewCell
}
More isolation and loose-coupling:
class YourTableViewCell: UITableViewCell {
weak var onScrollEnabledChange: ((Bool) -> Void)?
func disableScroll() {
onScrollEnabledChange?(false)
}
func enableScroll() {
onScrollEnabledChange?(true)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: YourTableViewCell = ...
cell.onScrollEnabledChange = { isEnabled in
// update above tableView's isScrollEnabled
tableView.isScrollEnabled = isEnabled
}
}
For a real solution:
In your BookViewModel.swift
import Foundation
import Combine
struct BookItemModel {
let name: String
var isDisplayingMark: Bool = true
}
enum DataLoadingState: Int {
case new, loading, finish
}
struct BookViewModelInput {
let loadData = PassthroughSubject<Void, Never>()
}
struct BookViewModelOutput {
let dataLoadingState = PassthroughSubject<DataLoadingState,Never>()
}
protocol IBookViewModel {
var input: BookViewModelInput { get }
var output: BookViewModelOutput { get }
func binding()
func clickCoachMark(index: Int)
func getItemCount() -> Int
func getItemAt(index: Int) -> BookItemModel
}
class BookViewModel: IBookViewModel {
private let _input = BookViewModelInput()
var input: BookViewModelInput { return self._input }
private let _output = BookViewModelOutput()
var output: BookViewModelOutput { return self._output }
private var cancellable = Set<AnyCancellable>()
private lazy var defaultQueue: DispatchQueue = {
let id = UUID().uuidString
let queue = DispatchQueue(label: "BookViewModel.\(id)", attributes: .concurrent)
return queue
}()
private var _books: [BookItemModel] = []
// MARK: - Function implementation
func binding() {
self._input.loadData
.receive(on: self.defaultQueue)
.sink {[unowned self] in
// this is event triggered from UI
// your screen own this BookViewModel, same life-cycle, so you can refer to self with unowned
// call your async data fetching
self.fetchData()
}.store(in: &self.cancellable)
}
func clickCoachMark(index: Int) {
self._books[index].isDisplayingMark = false
self._output.dataLoadingState.send(.finish) // trigger reloadData() again
}
// MARK: - Output handling
func getItemCount() -> Int {
return self._books.count
}
func getItemAt(index: Int) -> BookItemModel {
return self._books[index]
}
// MARK: - Input handling
private func fetchData() {
self._output.dataLoadingState.send(.loading)
// trigger block after 1 sec from now
self.defaultQueue.asyncAfter(deadline: DispatchTime.now() + 1) { [weak self] in
// Update data first
self?._books = Array(1...5).map({ BookItemModel(name: "\($0)") })
// then trigger new state
self?._output.dataLoadingState.send(.finish)
}
}
}
In your BookViewController.swift
import UIKit
import Combine
// I mostly name it as ABCScreen: UIViewController
class YourViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var cancellable = Set<AnyCancellable>()
var viewModel: IBookViewModel!
override func viewDidLoad() {
super.viewDidLoad()
self.setupListView()
// you can make a base class that trigger binding() on initialization then make BookViewModel become its subclass
self.viewModel.binding()
self.binding()
self.viewModel.input.loadData.send() // Void data type here, you can pass nothing as argument.
}
private func setupListView() {
self.tableView.register(UINib(nibName: "BookItemTableCell", bundle: nil), forCellReuseIdentifier: "BookItemTableCell")
self.tableView.dataSource = self
self.tableView.delegate = self
}
// here we are binding for output
private func binding() {
self.viewModel.output.dataLoadingState
.sink {[weak self] newState in
guard let _self = self else { return } // unwrapping optional self
// alway use weak self to refer to self in block that is triggered from viewModel
// because it can be async execution, different life-cycle with self (your screen)
// Perform some big data updating here
// This block is triggered from viewModel's defaultQueue
// it is not mainQueue to update UI
// Then switch to main queue to update UI,
DispatchQueue.main.async {
_self.tableView.isScrollEnabled = true // reset on each reloadData()
_self.tableView.reloadData() // refresh to update UI by new data
switch newState {
case .finish: _self.[hideLoadingIndicator]
case .loading: _self.[showLoadingIndicator]
case .new: break // do nothing, new is just initial-value
}
}
}.store(in: &self.cancellable)
}
}
extension YourViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.getItemCount()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookItemTableCell", for: indexPath) as! BookItemTableCell
let item = self.viewModel.getItemAt(index: indexPath.row)
cell.setupUI(with: item)
cell.onClickCoachMark = { [unowned self] in
self.viewModel.clickCoachMark(index: indexPath.row)
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let item = self.viewModel.getItemAt(index: indexPath.row)
// if there is atleast one book need to show coachMark -> disable scrolling
if item.isDisplayingMark {
tableView.isScrollEnabled = false
}
}
}
extension YourViewController {
// You will init YourViewController like below:
// let screen = YourViewController.build()
// navigationController.push(screen)
static func build() -> YourViewController {
let view = YourViewController()
view.viewModel = BookViewModel() // inject by setter
return view
}
}
In your BookItemTableCell.swift
import UIKit
class BookItemTableCell: UITableViewCell {
var onClickCoachMark: (() -> Void)?
override func awakeFromNib() {
super.awakeFromNib()
}
// Button click handler
func clickOnCoachMark() {
self.hideCoachMark()
self.onClickCoachMark?()
}
func setupUI(with item: BookItemModel) {
if item.isDisplayingMark {
self.showCoachMark()
} else {
self.hideCoachMark()
}
}
private func hideCoachMark() {}
private func showCoachMark() {}
}