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.
Related
I use the MVVM architecture and after setting everything up there is no cell in the TableView and breakpoints in the cell file will not get hit.
In my code:
controller:
private lazy var tableView: BaseTableView = {
let tableView = BaseTableView()
tableView.register(MyCell.self, forCellReuseIdentifier: String(describing: MyCell.self))
tableView.rowHeight = 148
tableView.estimatedRowHeight = 148
self.view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.top.bottom.left.right.equalToSuperview()
}
return tableView
}()
let dataSource = RxTableViewSectionedReloadDataSource<MytSection>(configureCell: {(datasource, tableView, indexPath, model) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MyCell.self), for: indexPath) as! MyCell
cell.model = model
return cell
})
viewModel.output
.items
.map { [MySection(header: "", items: $0)] }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
The breakpoint in the cell file was not triggered. I think the cell was not called, but my delegate and datasource are both bound properly.
I'd like to know why.
notice that the below code works as expected. So the question is, what are you doing differently? Maybe that snp.makeConstraints isn't working the way you think it is. Maybe your viewModel.output.items isn't emitting any next events. Maybe your MySection is implemented incorrectly...
class MyViewController : UIViewController {
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: view.bounds)
tableView.register(MyCell.self, forCellReuseIdentifier: String(describing: MyCell.self))
tableView.rowHeight = 148
tableView.estimatedRowHeight = 148
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(tableView)
return tableView
}()
let dataSource = RxTableViewSectionedReloadDataSource<MySection>(configureCell: {(datasource, tableView, indexPath, model) -> UITableViewCell in
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MyCell.self), for: indexPath) as! MyCell
cell.model = model
return cell
})
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
Observable.just(["hello", "world"])
.map { [MySection(model: "", items: $0)] }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
}
typealias MySection = SectionModel<String, String>
final class MyCell: UITableViewCell {
var model: String = "" {
didSet {
textLabel!.text = model
}
}
}
I have a view with an embedded UITableViewController that is filled with custom cells. When I hit the save button I would like the getProjectName() within the UITableViewController to return the projectNameTF data within in the custom cell. Currently when I try to get the cell within the getProjectName() it returns a nil cell.
Main View:
class NewProjectView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func saveBtn(_ sender: UIButton) {
print("Save button hit")
print(NewProjectTableViewController().getProjectName())
}
}
Embedded TableViewController
import UIKit
struct cellType{
var mainTitle = String()
var numOfChildCells = Int()
var opened = Bool()
}
class NewProjectTableViewController: UITableViewController {
var tableViewData = [cellType]()
var customCellData = [UITableViewCell]()
var projectNameTFR = UITextField()
// Counts the number of cells and displays them
override func numberOfSections(in tableView: UITableView) -> Int {
return tableViewData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If the parent cell is opened display the number of cells inside it
if tableViewData[section].opened == true {
return tableViewData[section].numOfChildCells + 1
}
else {
return 1
}
}
//
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
self.tableView.separatorStyle = .none
// Do this for the header cell
if indexPath.row == 0{
guard let cell = tableView.dequeueReusableCell(withIdentifier: "HeaderCell") as? HeaderCell else {return UITableViewCell()}
cell.backgroundColor = .clear
cell.setUpCell(title: tableViewData[indexPath.section].mainTitle)
// If cell should be opened, display correct open image
if tableViewData[indexPath.section].opened{
cell.openCell()
}
// else display closed image
else{
cell.closeCell()
}
return cell
// else it is a child cell
}else {
switch tableViewData[indexPath.section].mainTitle{
// Load Project info cell
case "Project Information":
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ProjectNameCell") as? ProjectNameCell else {return UITableViewCell()}
projectNameTFR = cell.projectNameTF
return cell
case "Client Information":
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ClientInfoCell") as? ClientInfoCell else {return UITableViewCell()}
return cell
default:
print("defaulted cell")
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else {return UITableViewCell()}
return cell
}
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableViewData[indexPath.section].opened == true{
tableViewData[indexPath.section].opened = false
let sections = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(sections, with: .none)
let headerCell = tableView.cellForRow(at: indexPath) as! HeaderCell
}
else{
tableViewData[indexPath.section].opened = true
let sections = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(sections, with: .none)
let headerCell = tableView.cellForRow(at: indexPath) as! HeaderCell
}
}
override func viewDidLoad() {
super.viewDidLoad()
//self.definesPresentationContext = true
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = UITableView.automaticDimension
// Do any additional setup after loading the view.
print("add new client screen loaded")
registerTableViewCells()
// Create the cells
tableViewData = [cellType(mainTitle: "Project Information", numOfChildCells: 1, opened: true ),
cellType(mainTitle: "Client Information", numOfChildCells: 1, opened: false )]
}
override func viewWillAppear(_ animated: Bool) {
// Add a background view to the table view
let backgroundImage = UIImage(named: "App Background.png")
let imageView = UIImageView(image: backgroundImage)
self.tableView.backgroundView = imageView
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
private func registerTableViewCells(){
let ClientInfoCell = UINib(nibName: "ClientInfoCell", bundle: nil)
self.tableView.register(ClientInfoCell, forCellReuseIdentifier: "ClientInfoCell")
let ProjectNameCell = UINib(nibName: "ProjectNameCell", bundle: nil)
self.tableView.register(ProjectNameCell, forCellReuseIdentifier: "ProjectNameCell")
let HeaderCell = UINib(nibName: "HeaderCell", bundle: nil)
self.tableView.register(HeaderCell, forCellReuseIdentifier: "HeaderCell")
let SaveCell = UINib(nibName: "SaveCell", bundle: nil)
self.tableView.register(SaveCell, forCellReuseIdentifier: "SaveCell")
}
func getProjectName() -> String{
let indexPath = NSIndexPath(row: 0, section: 0)
let cell = tableView?.cellForRow(at: indexPath as IndexPath) as? ProjectNameCell
print(type(of: cell))
if(cell==nil){
print("cell is nil")
}
return "I returned this test string"
}
}
Custom Cell I am trying to reach
import UIKit
class ProjectNameCell: UITableViewCell {
#IBOutlet weak var projectNameTF: UITextField!
var projectName = String()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
backgroundColor = .clear
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Well sir you are getting cell on index path 0 section number and 0th row
let indexPath = NSIndexPath(row: 0, section: 0)
on that index you have HeaderCell instead of ProjectNameCell thats why you are getting nil
this line can't cast your HeaderCell to ProjectNameCell
let cell = tableView?.cellForRow(at: indexPath as IndexPath) as? ProjectNameCell
I'm setting up a tableView with the new UITableViewDiffabledatasource and an NSFetchedResultController.
Insertion and deletion are correctly being handle out of the box. But when an item is updated (as in one of it's property is updated) and the cell displaying that item should be updated, it does not get updated.
How can I got about making sure the UITableViewDiffabledatasource sees that change and fires a refresh of the cell?
Adding the code I'm using:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
}
func makeDataSource() -> UITableViewDiffableDataSource<String, NSManagedObjectID> {
let reuseIdentifier = String(describing: RegisterTableCell.self)
let dataSource = UITableViewDiffableDataSource<String, NSManagedObjectID>(
tableView: tableView,
cellProvider: { tableView, indexPath, objectID in
let cartItem = try! self.container.viewContext.existingObject(with: objectID) as! CartItem
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
) as! RegisterTableCell
cell.count.text = String(format: "%#", cartItem.count ?? "0")
cell.label.text = cartItem.name
cell.price.text = self.priceFormatter.string(from: NSNumber(value: cartItem.totalPrice()))
return cell
}
)
dataSource.defaultRowAnimation = .left
return dataSource
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
updateUI()
let diffableSnapshot = snapshot as NSDiffableDataSourceSnapshot<String,NSManagedObjectID>
dataSource.apply(diffableSnapshot, animatingDifferences: true, completion: nil)
}
My model:
enum NHAnswerType:Int
{
case NHAnswerCheckboxButton = 1
case NHAnswerRadioButton = 2
case NHAnswerSmileyButton = 3
case NHAnswerStarRatingButton = 4
case NHAnswerTextButton = 5
}
class NH_QuestionListModel: NSObject {
var dataListArray33:[NH_OptionsModel] = []
var id:Int!
var question:String!
var buttontype:String!
var options:[String]?
var v:String?
var answerType:NHAnswerType?
var optionsModelArray:[NH_OptionsModel] = []
init(dictionary :JSONDictionary) {
guard let question = dictionary["question"] as? String,
let typebutton = dictionary["button_type"] as? String,
let id = dictionary["id"] as? Int
else {
return
}
// (myString as NSString).integerValue
self.answerType = NHAnswerType(rawValue: Int(typebutton)!)
print(self.answerType?.rawValue)
if let options = dictionary["options"] as? [String]{
print(options)
print(options)
for values in options{
print(values)
let optionmodel = NH_OptionsModel(values: values)
self.optionsModelArray.append(optionmodel)
}
}
self.buttontype = typebutton
self.question = question
self.id = id
}
}
In viewcontroller:
override func viewDidLoad() {
super.viewDidLoad()
self.tableview?.register(NH_SmileyCell.nib, forCellReuseIdentifier: NH_SmileyCell.identifier)
self.tableview?.register(NH_StarRatingCell.nib, forCellReuseIdentifier: NH_StarRatingCell.identifier)
self.tableview?.register(NH_CheckBoxCell.nib, forCellReuseIdentifier: NH_CheckBoxCell.identifier)
self.tableview?.register(NH_RadioTypeCell.nib, forCellReuseIdentifier: NH_RadioTypeCell.identifier)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = questionViewModel.titleForHeaderInSection(atsection: indexPath.row)
// print(model.answerType?.hashValue)
print(model.answerType)
print(model.answerType?.rawValue)
switch model.answerType {
case .NHAnswerRadioButton?:
if let cell = tableview.dequeueReusableCell(withIdentifier: NH_RadioTypeCell.identifier, for: indexPath) as? NH_RadioTypeCell {
cell.textLabel?.text = "dfdsfgs"
// cell.item = item
return cell
}
case .NHAnswerCheckboxButton?:
if let cell = tableView.dequeueReusableCell(withIdentifier: NH_CheckBoxCell.identifier, for: indexPath) as? NH_CheckBoxCell {
cell.textLabel?.text = "dfdsfgs"
// cell.item = item
return cell
}
case .NHAnswerSmileyButton?:
if let cell = tableView.dequeueReusableCell(withIdentifier: NH_SmileyCell.identifier, for: indexPath) as? NH_SmileyCell {
cell.textLabel?.text = "dfdsfgs"
// cell.item = item
return cell
}
case .NHAnswerStarRatingButton?:
if let cell = tableView.dequeueReusableCell(withIdentifier: NH_StarRatingCell.identifier, for: indexPath) as? NH_StarRatingCell {
cell.textLabel?.text = "dfdsfgs"
// cell.item = item
return cell
}
// case .NHAnswerTextButton?:
// if let cell = tableView.dequeueReusableCell(withIdentifier: NH_TextCell.identifier, for: indexPath) as? NH_TextCell{
// // cell.item = item
// return cell
// }
default:
return UITableViewCell()
}
And in the radiotypecell:
class NH_RadioTypeCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel?
var item: NH_QuestionViewModelItem? {
didSet {
guard let item = item as? NHQuestionViewModelQuestionNameItem else {
return
}
questionLabel?.text = item.question
}
}
static var nib:UINib {
return UINib(nibName: identifier, bundle: nil)
}
static var identifier: String {
return String(describing: self)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
This always produces the following error message:
NH_CheckBoxCell - must register a nib or a class for the identifier or
connect a prototype cell in a storyboard'
How can I fix the problem?
What changes should I do in while registration of the cell in the tableview in swift?
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()
}