RxSwift: Observing UITextFields from TableViewCell inside a child view controller - ios

I have a tableView and a childController in a parent viewController, the tableView in the ParentViewController can have from 1 - 4 cells, each cell contains a UITextField.
The ChildController also have a TableView, that list results(autocomplete) based on what is inputted in any of the TextField in the ParentViewController tableView cell.
I want the childController to always listen to any of the UITextField and show the result on the tablView. This is what I have currently
private var query = Variable<String>("")
var queryDriver: Driver<String> {
return query.asDriver()
}
TableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView
.dequeueReusableCell(
withIdentifier: "StopCell", for: indexPath) as? StopCell else {
fatalError("Cannot dequeue StopCell")
}
cell.delegate = self
cell.locationTextField.rx.text.map {$0 ?? ""}
.bind(to: query)
.disposed(by: disposeBag)
cell.locationTextField.rx.controlEvent(.editingDidEnd)
.asDriver(onErrorJustReturn: ())
.drive(onNext: { [unowned self] in
cell.locationTextField.resignFirstResponder()
})
.disposed(by: disposeBag)
return cell
}
Add child controller
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let noteVC = NoteVc()
addChildController(viewController: noteVC)
}
NoteVC
class NoteVc: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(tableView)
viewModel = SearchLocationViewModel(query: <#T##SharedSequence<DriverSharingStrategy, String>#>)
}
ViewModel
class SearchLocationViewModel {
let disposeBag = DisposeBag()
// MARK: - Properties
var querying: Driver<Bool> { return _querying.asDriver() }
var locations: Driver<[Location]> { return _locations.asDriver() }
// MARK: -
var hasLocations: Bool { return numberOfLocations > 0 }
var numberOfLocations: Int { return _locations.value.count }
// MARK: -
private let _querying = BehaviorRelay<Bool>(value: false)
private let _locations = BehaviorRelay<[Location]>(value: [])
// MARK: -
private let disposeBag = DisposeBag()
// MARK: - Initializtion
init(query: Driver<String>) {
Behave.shared.queryDriver
.throttle(0.5)
.distinctUntilChanged()
.drive(onNext: { [weak self] (addressString) in
self?.geocode(addressString: addressString)
})
.disposed(by: disposeBag)
}
Like it is implemented in the Uber app, users can add up to three destinations, the yellow rectangle box in the image below is my ChildViewController

Here's the simplest I could think of. I made viewModel a global constant but you might want to get more elaborate with it.:
class ViewModel {
let inputs: [AnyObserver<String>]
let outputs: [Observable<String>]
init(count: Int) {
let subjects = (0..<count).map { _ in BehaviorSubject<String>(value: "") }
inputs = subjects.map { $0.asObserver() }
outputs = subjects.map { $0.asObservable() }
}
}
class ParentViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let bag = self.bag
Observable.just(viewModel.inputs)
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { _, element, cell in
let textField = cell.viewWithTag(99) as! UITextField
textField.rx.text.orEmpty
.bind(to: element)
.disposed(by: bag)
}
.disposed(by: bag)
}
let bag = DisposeBag()
}
class ChildViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let bag = self.bag
Observable.just(viewModel.outputs)
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { _, element, cell in
element
.bind(to: cell.textLabel!.rx.text)
.disposed(by: bag)
}
.disposed(by: bag)
}
let bag = DisposeBag()
}

Related

How to disable table view scroll in UITableViewCell class rather than in the UITableView view class?

Consider you are having the table view with a separate cell class that will be registered for table view later.
Now, we know how to disable the table view scroll using the table view instance, like in the below line.
tableView.isScrollEnabled = true/false
But what if I require to show some coach marks on the cell class, And I need to lock the table view scroll until that coach marks disappear using cell rather than table view. Because for a cell class table view instance is inaccessible since cell is within table view, not the table view within cell.
I've achieved this by using Notifications and Observers. But Please let me know if this can be achieved in any other way.
Is your target supporting iOS 13+? If so you can use Combine SDK. It will give you the same principle of notification and observers.
You will need a viewModel conforms to ObservableObject, then you will use #Published property wrapper, lets us easily construct types that emit signals whenever some of their properties were changed.
ViewModel.swift
enum ViewState {
case loading
case loaded
case showCoachMark
case hideCoachMark
}
class ViewModel: ObservableObject {
#Published var state: ViewState = .loading
.....
}
ViewController.swift
import Combine
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let viewModel = ViewModel()
private var cancellable: AnyCancellable?
override func viewDidLoad(){
cancellable = viewModel.$state.sink { value in
// call tableview.isScrollEnabled = true/false
// Please push this back to the main thread if the state has
// been fetched via a request call
}
}
}
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() {}
}

A derivation of UIView cannot be casted to UIView on iOS version less than 13

I've got a class derived from UIView called ContentListView that goes like this:
import UIKit
import RxSwift
import RxRelay
import RxCocoa
import SwinjectStoryboard
class ContentListView: UIView {
#IBInspectable var listName: String = ""
#IBInspectable var headerHeight: CGFloat = 0
#IBInspectable var footerHeight: CGFloat = 0
#IBOutlet weak var tableView: UITableView!
let viewDidLoad = PublishRelay<Void>()
let viewDidAppear = PublishRelay<Void>()
let reloadData = PublishRelay<Void>()
let manualLoadData = PublishRelay<[ContentCellType]>()
var initialContents: [ContentCellType]?
private(set) lazy var selectedContent = selectedContentRelay.asSignal()
private let disposeBag = DisposeBag()
private let cellTypes = BehaviorRelay<[ContentCellType]>(value: [])
private let didSelectIndexRelay = PublishRelay<Int>()
private let selectedContentRelay = PublishRelay<ContentCellType>()
private let contentNotFoundReuseId = R.reuseIdentifier.contentNotFoundErrorCell.identifier
private let contentNotMatchReuseId = R.reuseIdentifier.contentNotMatchErrorCell.identifier
private let myContentReuseId = R.reuseIdentifier.myContentTableViewCell.identifier
private let associatedPracticeReuseId = R.reuseIdentifier.associatedPracticeTableViewCell.identifier
private let associatedPracticeContentReuseId = R.reuseIdentifier.associatedPracticeContentTableViewCell.identifier
override init(frame: CGRect) {
super.init(frame: frame)
instantiateView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
instantiateView()
}
private func instantiateView() {
guard let nib = R.nib.contentListView(owner: self) else { return }
addSubview(nib, method: .fill)
}
override func awakeFromNib() {
super.awakeFromNib()
setupTableView()
setupViewModel()
}
private func setupTableView() {
setupTableViewLayouts()
registerCells()
setupTableViewEvents()
}
private func setupViewModel() {
let viewModel = createViewModel()
viewModel.contents
.drive(cellTypes)
.disposed(by: self.disposeBag)
viewModel.selectedContent
.emit(to: selectedContentRelay)
.disposed(by: disposeBag)
viewDidLoad.asSignal()
.emit(to: viewModel.viewDidLoad)
.disposed(by: disposeBag)
viewDidAppear.asSignal()
.emit(to: viewModel.viewDidAppear)
.disposed(by: disposeBag)
reloadData.asSignal()
.emit(to: viewModel.reloadData)
.disposed(by: disposeBag)
let loadInitialContents = Observable.just(initialContents).compactMap { $0 }
Observable.merge(loadInitialContents,
manualLoadData.asObservable())
.bind(to: viewModel.manualLoadData)
.disposed(by: disposeBag)
didSelectIndexRelay
.bind(to: viewModel.didSelectIndex)
.disposed(by: disposeBag)
}
private func createViewModel() -> ContentListViewModel {
if let viewModel = SwinjectStoryboard.defaultContainer.resolve(ContentListViewModel.self, name: listName) {
return viewModel
} else {
let viewModel = SwinjectStoryboard.defaultContainer.resolve(ContentListViewModel.self,
name: "NoDataProvider")!
return viewModel
}
}
private func setupTableViewLayouts() {
tableView.backgroundColor = R.color.grey91()
tableView.separatorStyle = .none
}
private func registerCells() {
tableView.register(UINib(resource: R.nib.contentNotFoundTableViewCell),
forCellReuseIdentifier: contentNotFoundReuseId)
tableView.register(UINib(resource: R.nib.contentNotMatchTableViewCell),
forCellReuseIdentifier: contentNotMatchReuseId)
tableView.register(UINib(resource: R.nib.myContentTableViewCell),
forCellReuseIdentifier: myContentReuseId)
tableView.register(UINib(resource: R.nib.associatedPracticeTableViewCell),
forCellReuseIdentifier: associatedPracticeReuseId)
tableView.register(UINib(resource: R.nib.associatedPracticeContentTableViewCell),
forCellReuseIdentifier: associatedPracticeContentReuseId)
}
private func setupTableViewEvents() {
tableView.rx.setDelegate(self).disposed(by: disposeBag)
cellTypes.asDriver()
.drive(tableView.rx.items) { [weak self] tableView, _, element in
return self?.createCell(tableView: tableView, element: element) ?? UITableViewCell()
}
.disposed(by: disposeBag)
cellTypes.accept([.notFound])
}
private func createCell(tableView: UITableView, element: ContentCellType) -> UITableViewCell? {
switch element {
case .notFound: return tableView.dequeueReusableCell(withIdentifier: contentNotFoundReuseId)
case .notMatch: return tableView.dequeueReusableCell(withIdentifier: contentNotMatchReuseId)
case .content(data: _): return nil
case .myContent(let data):
let cell = tableView.dequeueReusableCell(withIdentifier: myContentReuseId) as? MyContentTableViewCell
cell?.setup(with: data)
return cell
case .practice(let data):
let cell = tableView.dequeueReusableCell(withIdentifier: associatedPracticeReuseId)
as? AssociatedPracticeTableViewCell
cell?.setup(with: data)
return cell
case .provider(let data):
let cell = tableView.dequeueReusableCell(withIdentifier: associatedPracticeContentReuseId)
as? AssociatedPracticeContentTableViewCell
cell?.setup(with: data)
return cell
}
}
}
extension ContentListView: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let type = cellTypes.value[indexPath.row]
switch type {
case .notFound, .notMatch: return 320
case .myContent: return 440
case .practice: return 76
case .provider: return 412
default: return 0
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return headerHeight
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return footerHeight
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
didSelectIndexRelay.accept(indexPath.row)
}
}
It is used in the view controller like this:
import UIKit
import RxSwift
import RxCocoa
class ContentsViewController: UIViewController, HideNavigationBarToggling {
#IBOutlet var contentButtonViews: [ContentsButtonView]!
#IBOutlet var contentListViews: [ContentListView]!
private let disposeBag = DisposeBag()
private var selectedPracticeName: String?
private var selectedParam: MyContentViewParam?
override func viewDidLoad() {
super.viewDidLoad()
hideListViews() //<<<<<<<<<<<<<< CRASH!
contentsButtonController.setup(with: contentButtonViews)
contentsButtonController.activeSelectionIndex
.drive(onNext: { [weak self] in
self?.hideListViews()
self?.contentListViews[$0].isHidden = false
})
.disposed(by: disposeBag)
contentListViews.forEach {
$0.selectedContent
.emit(onNext: { [weak self] in self?.onSelected(with: $0) })
.disposed(by: disposeBag)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
contentListViews.forEach { $0.viewDidAppear.accept(()) }
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let providerVC = segue.destination as? AssociatedPracticeContentsViewController {
providerVC.title = selectedPracticeName
} else if let destinationNavigation = segue.destination as? KolibreeNavigationController,
let bottomVC = destinationNavigation.visibleViewController as? BottomMessageViewController {
let messageSegue = segue as? SwiftMessagesBottomTabSegue
messageSegue?.interactiveHide = false
bottomVC.titleString = selectedParam?.title ?? ""
bottomVC.setup = { [weak self] bottomMessage in
if let pdfReader = bottomMessage as? PDFReaderMessageView,
let param = self?.selectedParam {
pdfReader.load(param: param)
}
}
}
}
private func hideListViews() {
contentListViews.forEach {
$0.isHidden = true
}
}
private func onSelected(with cellType: ContentCellType) {
switch cellType {
case .myContent(let param): openContent(for: param)
case .practice(let param): showAssociatedPracticeContents(for: param)
default: return
}
}
private func openContent(for param: MyContentViewParam) {
switch param.type {
case .book:
selectedParam = param
performSegue(withIdentifier: R.segue.contentsViewController.openPdfReaderSegue.identifier, sender: nil)
case .video, .audio:
let avContentPlayerVC = AVContentPlayerViewController()
present(avContentPlayerVC, animated: true) {
avContentPlayerVC.load(param: param)
}
default: return
}
}
private func showAssociatedPracticeContents(for param: AssociatedPracticeViewParam) {
SelectedAssociatedPracticeStorageAdapter().store(param.practiceId)
selectedPracticeName = param.practiceName
performSegue(withIdentifier: R.segue.contentsViewController.showAssociatedPracticeContents.identifier,
sender: nil)
}
}
But when I tried to run it on iOS 11 and 12 simulators, it crashed. Although it worked on iOS 13 and 14. It crashed with this error:
Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ContentListView but found UIView: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1001.0.82.4/swift/stdlib/public/core/ArrayBuffer.swift, line 346
2021-09-22 13:24:27.624568+0700 Kolibree[16970:513272] Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ContentListView but found UIView: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1001.0.82.4/swift/stdlib/public/core/ArrayBuffer.swift, line 346
The contentListViews in the storyboard themselves are ContentListView so the error seems weird. How do I solve this? It has been days and I'm stuck at this. :(
Thanks in advance.
EDIT:
I've tried just using a singular ContentListView in the storyboard and deleted the other. And then I changed the outlet to:
#IBOutlet weak var myContentListView: ContentListView!
It produced another error:
2021-09-23 13:59:05.669493+0700 Kolibree[14267:377067] Unknown class _TtC8Kolibree15ContentListView in Interface Builder file.
And actually, when I scrolled the error messages, the same error message above was also there.
Also tried to do this instead:
#IBOutlet weak var myContentUIView: UIView!
private var myContentListView: ContentListView!
override func viewDidLoad() {
super.viewDidLoad()
myContentListView = myContentUIView as! ContentListView
....
}
And it also produced the error above with other ones:
Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).
2021-09-23 15:29:12.151228+0700 Kolibree[15518:434665] Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).
Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).
I've tried all the answers in Unknown class in interface builder
But nothing worked so far.
After distancing myself from the bug and decided to do another feature story for a few days, I have found the cause of the bug. It was because I used PublishRelays and BehaviorRelays in the UIViewControllers and UIViews. They worked fine anywhere else, just not in UIKit clases. Observables, Signals, Drivers, Completeables, Singles, and Maybes can also work fine in UIViewControllers and UIViews. When I removed all relays in all the crashing UIViewControllers and UIViews and change them to use delegates instead, the crash doesn't appear anymore.

RxSwift DisposeBag and UITableViewCell

I have a UITableViewCell, and it contain a UICollectionView.
I put data for collection view in dataSource's tableview.
class HomeTableViewCell: UITableViewCell {
var disposeBag = DisposeBag()
#IBOutlet weak var collectionItems: UICollectionView!
var listItem: PublishSubject<HomeResultModel> = PublishSubject<HomeResultModel>()
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfMenuData>(configureCell: {(_, _, _, _) in
fatalError()
})
override func awakeFromNib() {
super.awakeFromNib()
setup()
setupBinding()
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}
func setup() {
collectionItems.register(UINib(nibName: MenuCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: MenuCollectionViewCell.identifier)
collectionItems.register(UINib(nibName: RecipeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: RecipeCollectionViewCell.identifier)
collectionItems.rx.setDelegate(self).disposed(by: disposeBag)
collectionItems.rx.modelAndIndexSelected(HomeListModel.self).subscribe { (model, index) in
if let recipesID = model.recipesID {
tlog(tag: self.TAG, sLog: "recipesID : \(recipesID)")
self.recipeIdSelected.onNext("\(recipesID)")
}
}.disposed(by: disposeBag)
dataSource.configureCell = { (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in
let cellType = dataSource.sectionModels.first?.type ?? .recipeCell
if cellType == .menuCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MenuCollectionViewCell.identifier, for: indexPath) as! MenuCollectionViewCell
cell.showInfo(item)
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecipeCollectionViewCell.identifier, for: indexPath) as! RecipeCollectionViewCell
cell.showInfo(item.recipesName ?? "", imageLink: item.imageThumb ?? "")
return cell
}
}
}
func setupBinding() {
listItem.map { model -> [SectionOfMenuData] in
let modelID = try JSONEncoder().encode(model.id)
let modelResultID = String.init(data: modelID, encoding: .utf8)!
return [SectionOfMenuData(type: modelResultID == "0" ? .menuCell : .recipeCell, id: modelResultID, items: model.list)]
}.bind(to: collectionItems.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
I pass data for tableviewcell with code:
let dataSource = RxTableViewSectionedReloadDataSource<SectionOfHome> { dataSource, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as! HomeTableViewCell
cell.listItem.onNext(item)
return cell
}
First show it ok, but when scroll tableview, I reset DisposeBag in:
func prepareForReuse{
disposeBag = DisposeBag()
}
and so tableview show blank.
How wrong was I in this regard?
Your subscription to listItems is getting disposed when the cell is reused and is never recreated. You should run the one-time tasks in awakeFromNib and move individual cell specific bindings to a function that you can call after resetting the disposeBag in prepareForReuse.

RxSwift: button click action from awakeFromNib to UIViewController

I am Rx-Swift beginner. I am trying to get UIButton action which is present inside UITableViewCell.
Attempt:
I have tried with UITableViewCell's instance that I can get from UIViewController. Thats working fine.
Need:
How can I transfer that clicked tag value from awakeFromNib() to UIViewController ?
Code:
// UITableViewCell
class TripInfoCell: UITableViewCell {
#IBOutlet weak var btnMore: UIButton!
var cellbag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
cellbag = DisposeBag()
}
override func awakeFromNib() {
super.awakeFromNib()
btnMore.rx.tap.asObservable()
.subscribe { _ in
print("Tapping_ ", self.tag)
}.disposed(by: cellbag)
}
}
// UIViewController
override func viewDidLoad() {
super.viewDidLoad()
Observable.of(visitsModel).bind(to: tblView.rx.items(cellIdentifier: "cell", cellType: TripInfoCell.self)) { (row, element, cell) in
cell.tag = row
}
.disposed(by: disposeBag)
}
Attempt:
Here, UIButton action I have used inside UIViewController. So I can transfer data.
Observable.of(visitsModel).bind(to: tblView.rx.items(cellIdentifier: "cell", cellType: TripInfoCell.self)) { (row, element, cell) in
cell.tag = row
cell.btnMore.rx.tap.asObservable()
.subscribe { _ in
print("Inside_Tapping_ ", cell.tag)
}.disposed(by: cell.cellbag)
}

can we pass data from table cell to table view where both are xib files?

I want to pass table cell data (xib file) to table view (also a xib file). I have tried passing the data using the following piece of code but did not get an appropriate result.
PropertyCell.swift
import UIKit
class PropertyCell: UITableViewCell {
#IBOutlet weak var propertyCodeLbl: UILabel!
#IBOutlet weak var addressLbl: UILabel!
}
I have attached the screenshot for PropertyCell below -
PropertyCell.xib
PropertyCell.xib file
PropertyTableVC.swift
import UIKit
import Alamofire
class PropertyTableVC: UITableViewController {
#IBOutlet var propertyTabel: UITableView!
let URL_Landlord_Property_List = "http://127.0.0.1/source/api/LandlordPropertyList.php"
var count: Int = 0
var landlordPropertyArray: [PropertyList]? = []
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
propertyTabel.dataSource = self
propertyTabel.delegate = self
let nibName = UINib(nibName: "PropertyCell", bundle:nil)
self.propertyTabel.register(nibName, forCellReuseIdentifier: "Cell")
}
func fetchData(){
let urlRequest = URLRequest(url: URL(string: URL_Landlord_Property_List)!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if error != nil{
print(error!)
return
}
print(data!)
self.landlordPropertyArray = [PropertyList]()
self.count = (self.landlordPropertyArray?.count)!
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let datafromjson = json["landlords_property_list"] as? [[String: AnyObject]] {
print(datafromjson)
for data in datafromjson{
var property = PropertyList()
if let id = data["ID"] as? Int,let code = data["Code"] as? String, let address1 = data["Address"] as? String
{
property.id = id
property.code = code
property.address1 = address1
}
self.landlordPropertyArray?.append(property)
}
print(self.landlordPropertyArray)
}
DispatchQueue.main.async {
self.propertyTabel.reloadData()
}
}catch let error {
print(error)
}
}
task.resume()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (landlordPropertyArray?.count)!
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Configure the cell...
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PropertyCell
cell.propertyCodeLbl.text = self.landlordPropertyArray?[indexPath.item].code
cell.addressLbl.text = self.landlordPropertyArray?[indexPath.item].address1
return cell
}
}
Attached the screenshot for Property Table below -
PropertyTableVC.xib
PropertyTableVC.xib file
Your TableViewCell :
import UIKit
protocol yourProtocolName { //add protocol here
func getDataFromTableViewCellToViewController (sender : self) //* as you need to pass the table view cell, so pass it as self
}
class PropertyCell: UITableViewCell {
#IBOutlet weak var propertyCodeLbl: UILabel!
#IBOutlet weak var addressLbl: UILabel!
var delegate : yourProtocolName? //set a delegate
override func awakeFromNib() {
super.awakeFromNib()
if delegate != nil {
delegate.getDataFromTableViewCellToViewController(sender :self) *
}
}
}
And your ViewController :
import UIKit
import Alamofire
class PropertyTableVC: UITableViewController,yourProtocolName { //conform to the protocol you created in tableViewCell
#IBOutlet var propertyTabel: UITableView!
let URL_Landlord_Property_List = "http://127.0.0.1/source/api/LandlordPropertyList.php"
var count: Int = 0
var landlordPropertyArray: [PropertyList]? = []
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
propertyTabel.dataSource = self
propertyTabel.delegate = self
let nibName = UINib(nibName: "PropertyCell", bundle:nil)
self.propertyTabel.register(nibName, forCellReuseIdentifier: "Cell")
}
func fetchData(){
let urlRequest = URLRequest(url: URL(string: URL_Landlord_Property_List)!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if error != nil{
print(error!)
return
}
print(data!)
self.landlordPropertyArray = [PropertyList]()
self.count = (self.landlordPropertyArray?.count)!
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let datafromjson = json["landlords_property_list"] as? [[String: AnyObject]] {
print(datafromjson)
for data in datafromjson{
var property = PropertyList()
if let id = data["ID"] as? Int,let code = data["Code"] as? String, let address1 = data["Address"] as? String
{
property.id = id
property.code = code
property.address1 = address1
}
self.landlordPropertyArray?.append(property)
}
print(self.landlordPropertyArray)
}
DispatchQueue.main.async {
self.propertyTabel.reloadData()
}
}catch let error {
print(error)
}
}
task.resume()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (landlordPropertyArray?.count)!
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Configure the cell...
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PropertyCell
cell.propertyCodeLbl.text = self.landlordPropertyArray?[indexPath.item].code
cell.addressLbl.text = self.landlordPropertyArray?[indexPath.item].address1
return cell
}
func getDataFromTableViewCellToViewController(sender :UITableViewCell) {
//make a callback here
}
}
(*) Marked fields are updated code
Call fetchData() function after tableview delegate and datasource assigning
propertyTabel.dataSource = self
propertyTabel.delegate = self
Updated answer is
Create Cell Class like this
import UIKit
class YourTableViewCell: UITableViewCell {
#IBOutlet weak var profileImageView: UIImageView!
#IBOutlet weak var userNameLabel: UILabel!
#IBOutlet weak var timeDateLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.tableViewBackgroundColor()
self.selectionStyle = .none
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
class func cellForTableView(tableView: UITableView, atIndexPath indexPath: IndexPath) -> YourTableViewCell {
let kYourTableViewCell = "YourTableViewCellIdentifier"
tableView.register(UINib(nibName:"RRLoadQuestionsTableViewCell", bundle: Bundle.main), forCellReuseIdentifier: kYourTableViewCell)
let cell = tableView.dequeueReusableCell(withIdentifier: kYourTableViewCell, for: indexPath) as! YourTableViewCell
return cell
}
}
Then create UIViewController Class and place UITableView on it and link with outlet
class YourViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [LoadYourData]()
// MARK: - Init & Deinit
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: "YourViewController", bundle: Bundle.main)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
setupViewControllerUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
// MARK: - UIViewController Helper Methods
func setupViewControllerUI() {
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
tableView.delegate = self
tableView.dataSource = self
loadData()
}
func loadData() {
// Write here your API and reload tableview once you get response
}
// MARK: - UITableView Data Source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = YourTableViewCell.cellForTableView(tableView: tableView, atIndexPath: indexPath)
// assign here cell.name etc from dataSource
return cell
}

Resources