Cannot Add Sections to Table View with Rx Swift and MVVM - mvvm

I 'm practicing SwiftRx with MVVM and TableView where i wrote down simple program to download data from remote api https://jsonplaceholder.typicode.com/photos .Now i want to group rows with data from api i-e albumId but stuck in it.Please help me
i tried with seperate datasource but i am completely new and have not much understanding.
viewModel.photoCells.bind(to: self.tableView.rx.items) {
tableView, index, element in
let indexPath = IndexPath(item: index, section: 0)
switch element {
case .normal(let viewModel):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCell", for: indexPath) as? PhotosViewCell
else {
return UITableViewCell()
}
cell.viewModel = viewModel
return cell
case .error(let message):
let cell = UITableViewCell()
cell.isUserInteractionEnabled = false
cell.textLabel?.text = message
return cell
case .empty:
let cell = UITableViewCell()
cell.isUserInteractionEnabled = false
cell.textLabel?.text = "No data available"
return cell
}
}.disposed(by: disposeBag)

For this sort of work, your best bet is to use the RxDataSources SDK. Read the documentation on it and find some tutorials too. If you have any more specific questions about it. Post them here or on the RxSwift slack channel.

Related

core data fetching int not displaying on tableview cell

I am writing swift code with the goal of displaying a increasing number on every tableview cell. Right now the int is not being display. So the first tableview cell should say 1 and the 2nd should say 2. You can see in the gif below what is going along with the tableview cell and nothing is appearing in them when the button is clicked. The func below is when the button is clicked.
var pageNumber = 1
var itemName : [Player] = []
func enterData() {
theScores.reloadData()
let appDeldeaget = UIApplication.shared.delegate as! AppDelegate
let context = appDeldeaget.persistentContainer.viewContext
// Simpler way to create a new Core Data object
let theTitle = Player(context: context)
// Simpler way to set the position attribute
theTitle.positon = Int64(pageNumber)
print(pageNumber)
// pageNumber must be of type Int64, otherwise use Int64(pageNumber)
do {
try context.save()
itemName.append(theTitle)
pageNumber += 1
} catch {
// handle errors
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let title = itemName[indexPath.row]
let cell = theScores.dequeueReusableCell(withIdentifier: "MyCell", for : indexPath)
cell.selectionStyle = .default
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
cell.textLabel?.text = "\(text)"
cell.textLabel?.textAlignment = .center
cell.layoutMargins = UIEdgeInsets.zero
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
return cell
}
Here's why it doesn't work. You have this:
let attr5 = title.value(forKey: "positon") as? String
let text = [" Item :", attr5].compactMap { $0 }.reduce("", +)
This is a really complicated way to try and do this, and it doesn't work as written. The problem is that the value of position is an Int64 and you need a string. But using as? like that doesn't turn it into a string. When that line of code runs, Swift says, can I just make this into a string? But it can't. So the as? String is nil, and your table cells don't include the number because the conversion failed.
A better way would be something like
if let position = title.value(forKey: "positon") {
cell.textLabel?.text = "Item : \(positon))"
}
But that's only if you really want to use value(forKey:) for some reason. You probably don't need that because normally Xcode creates a subclass of NSManagedObject for each entity with named properties. So even better would be
cell.textLabel?.text = "Item: \(title.position)"
These both work because string interpolation knows how to convert an integer to a string.
You probably should call .reloadData() after context.save()

How to add an extra static cell to a Diffable Data Source?

With reference to this question, I would like to do something similar. There were two ways I tried to replicate the answer for my diffable data source for my collection view.
The first way was to create another diffable data source but for the same collection view. The actual item cells would be configured when the indexpath.row is less than the count of my array while when it is equals to the count, then it will display my static cell.
//configure cells: categoryCollectionViewDataSource is the actual datasource for the items that I want to display.
categoryCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, MistakeCategory>(collectionView: categoryCollectionView){
(collectionView, indexPath, mistakeCategory) in
if indexPath.row < (subjectSelected?.MistakeCategories.count)! {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "CategoryCollectionViewCell", for: indexPath) as! CategoryCollectionViewCell
cell.setProperty(SubjectCategory: nil, MistakeCategory: self.subjectSelected?.MistakeCategories[indexPath.row])
return cell
} else {
return nil
}
}
//configure cells: staticCollectionViewDataSource is the data source for the cell that I want to display no matter what. it is displayed at the last row of the indexpath.
staticCellCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: categoryCollectionView){
(collectionView, indexPath, mistakeCategory) in
if indexPath.row == subjectSelected?.MistakeCategories.count {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "StaticCreateNewCategoryCollectionViewCell", for: indexPath) as! StaticCreateNewCategoryCollectionViewCell
return cell
} else {
return nil
}
}
And here is where I update my diffable data source which I run on my viewDidLoad after configuring my cells:
internal func updateCategoryCollectionView() {
var snapshot = NSDiffableDataSourceSnapshot<Int, MistakeCategory>()
snapshot.appendSections([0])
snapshot.appendItems(Array(subjectSelected!.MistakeCategories))
categoryCollectionViewDataSource.apply(snapshot) //error: 'attempt to insert section 0 but there are only 0 sections after the update'
var staticSnapshot = NSDiffableDataSourceSnapshot<Int,Int>()
staticSnapshot.appendSections([0])
staticSnapshot.appendItems([0])
staticCellCollectionViewDataSource.apply(staticSnapshot)
}
This results in the error 'attempt to insert section 0 but there are only 0 sections after the update'. I have tried to implement the functions of UICollectionViewDataSource but ran into the problem of how I can merge these two type of data sources.
As such, I am at a lost to what I can do to create a static custom cell at the end of my row.
I had the same problem with diffable data sources. Instead of creating a different data source and snapshot, I solved it by appending a default [AnyHashable] data to my snapshot where the static cell was wanted. In your case it would be after you append your usual MistakeCategories data.
To do this you need to initialize your data source as
categoryCollectionViewDataSource = UICollectionViewDiffableDataSource<Int, AnyHashable>(collectionView: categoryCollectionView){
(collectionView, indexPath, dataItem) in
and when using the dataItem object you need to cast
if let mistakeCategory = dataItem as? MistakeCategory {
// return your default cell here
}
and after the MistakeCategory cells
if indexPath.row == subjectSelected?.MistakeCategories.count {
let cell = self.categoryCollectionView.dequeueReusableCell(withReuseIdentifier: "StaticCreateNewCategoryCollectionViewCell", for: indexPath) as! StaticCreateNewCategoryCollectionViewCell
return cell
}
And finally when you apply your snapshot, you also need to use it as
var snapshot = NSDiffableDataSourceSnapshot<Int, AnyHashable>()
snapshot.appendSections([0])
snapshot.appendItems(Array(subjectSelected!.MistakeCategories))
//append your static value here
snapshot.appendItems([AnyHashable(0)])
categoryCollectionViewDataSource.apply(snapshot)
Disclaimer: This is an extremely hard-coded solution and I am looking for more elegant methods to do this, but I couldn't find any yet.
DiffableDataSource accepts enums for item and/or section. This allows you to easily have as many cell and section types as you need - just define them in an enum, register cells for them, and then point datasource to the right cell based on the enum value passed in.
The example below only has two types of items and one section, but you can use the same idea for different section types as well.
//Define as many types of items as you need
enum CollectionViewItem: Hashable {
case actualItem(ActualItem.ID) //You must to have a unique associated value if you want the datasource to be able to differentiate items. In this case I'm just using the item ID.
case staticButtonItem
}
//Register cells for each type of item
actualItemCellRegistration = UICollectionView.CellRegistration<YourDesiredCellTypeHere, CollectionViewItem> { (cell, indexPath, item) in
//Register cell
}
staticButtonCellRegistration = UICollectionView.CellRegistration<YourDesiredCellTypeHere, CollectionViewItem> { (cell, indexPath, item) in
//Register cell
}
//Point the datasource at the right registration based on the enum case
dataSource = UICollectionViewDiffableDataSource<Int, CollectionViewItem>(collectionView: collectionView){ (collectionView, indexPath, item) in
switch item {
case .actualItem:
return collectionView.dequeueConfiguredReusableCell(using: actualItemCellRegistration, for: indexPath, item: item)
case .staticButtonItem:
return collectionView.dequeueConfiguredReusableCell(using: staticButtonCellRegistration, for: indexPath, item: item)
}
}
//Make your snapshot with your defined items
func makeNewSnapshot(with data: [YourDataType]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, CollectionViewItem>()
//Add your single section
snapshot.appendSections([0])
//Add your static cell
snapshot.appendItems(CollectionViewItem.staticButtonItem, toSection: 0)
//Add your actual items
let itemsForView = data.map {CollectionViewItem.actualItem($0.ID)}
snapshot.appendItems(itemsForView, toSection: 0)
dataSource.apply(snapshot)
}

How to give Image from another thread?

I have a code for getting user pic:
if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
let data = try? Data(contentsOf: URL) {
cell.userPic.image = UIImage(data: data)
}
When I'm using it, tableView lagging at scrolling.
Please help me to put this code in another thread.
Here is a good sample provided by Apple, that you can adapt for your needs:
Prefetching collection view data
Basic idea is to create AsyncFetcher for your images and put image creation code to separate operation.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else {
fatalError("Expected `\(Cell.self)` type for reuseIdentifier \(Cell.reuseIdentifier). Check the configuration in Main.storyboard.")
}
let model = models[indexPath.row]
let id = model.id
cell.representedId = id
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: id) {
// The data has already been fetched and cached; use it to configure the cell.
cell.configure(with: fetchedData)
} else {
// There is no data available; clear the cell until we've fetched data.
cell.configure(with: nil)
// Ask the `asyncFetcher` to fetch data for the specified identifier.
asyncFetcher.fetchAsync(id) { fetchedData in
DispatchQueue.main.async {
/*
The `asyncFetcher` has fetched data for the identifier. Before
updating the cell, check if it has been recycled by the
collection view to represent other data.
*/
guard cell.representedId == id else { return }
// Configure the cell with the fetched image.
cell.configure(with: fetchedData)
}
}
}
return cell
}
But in your case you should use Table View prefetching
I can confirm that this approach works and (when done right) results smooth scrolling and good UX

Swift 3: TPKeyboardAvoidingScrollView not working properly on one UITextField in UITableViewCell

I'm having this problem where I have three cells, all created by the same function, but for some reason the third one has issues with scrolling (last name cell).
I'm using TPKeyboardAvoidingTableView, which is usually the greatest thing ever imagined, but for some reason it does not like this cell. Here is the code:
guard let cell = tableView.dequeueReusableCell(withIdentifier: SignUp_Constants.DetailsCellIdentifier, for: indexPath) as? DetailsCell else { return UITableViewCell() }
cell.isUserInteractionEnabled = true
cell.detailsInputTextField.removeTarget(nil, action: nil, for: .allEvents)
cell.detailsInputTextField.addTarget(cell, action: #selector(DetailsCell.textFieldDidChange(_:)), for: .editingChanged)
cell.detailsInputTextField.autocapitalizationType = .none
cell.detailsInputTextField.spellCheckingType = .default
cell.detailsInputTextField.isSecureTextEntry = false
cell.detailsInputTextField.isUserInteractionEnabled = true
cell.detailsInputTextField.keyboardType = .default
cell.delegate = self
var title = SignUp_Constants.getTitle(forCellType: currentSectionView, atRow: indexPath.row)
cell.setTextFieldText(toValue: signUpDictionary[title] as? String ?? "")
cell.setTitleLabel(to: title)
return cell
Every other cell that is created by the same code in the ENTIRE project works fine.
Anything helps and thanks in advance!
After a lot of hassle and banging my head on the table, I decided to throw in tableView.scrollToRow(row: x, atIndexPath: indexPath) when the user clicked on the textfield, and the error went away! I've used this pod many times and never had to use that function with it, so if anyone has any other solutions it would be great to hear them!

swift UICollectionView showing empty sometimes bug even header

I have a UIcollectionView that has a header and Footer and sometimes a weird bug just shows the screen empty. It is strange because even on the View Debugger it shows as empty but the Header is a static image that is on the app not something is getting from API and that does not show either.
Also the console does not give any errors methods. I try going to another view and forcing a reloadData() but still does not show anything. Any way I can debug this better or make sure it does not happen?
This is how the view debugger looks like:
You can see the header and footer empty reusable views:
Edit:
This is how the supplementary views are being created using RxDataSources
dataSource.supplementaryViewFactory = { (dataSource, collectionView, kind, indexPath) in
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CellReuseId.contentHeaderCollectionView, for: indexPath ) as! ContentHeaderView
headerView.imageView.image = ImageAssets.contentBanner
let tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(self.showListOfEvents(_:)))
headerView.addGestureRecognizer(tapGestureRecognizer)
return headerView
case UICollectionElementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CellReuseId.contentFooterCollectionView, for: indexPath) as! ContentFooterView
footerView.setUpObjects()
//setUp Appropiate label or animation
let showFooter: Bool = !(self.centerActivityIndicator?.isAnimating ?? false)
footerView.setUpAppropiateDependingOnResults(isThereMoreResults: self.isThereMoreResults, showFooter: showFooter)
return footerView
default: break
}
//return default
return UICollectionReusableView()
}
And this is the code that gets the models for the API and binds them to the collectionView
let results = Observable.of(paginationObserver, offlineObserver).merge()
//calls method that calls DB with the appropiate data
.flatMapLatest { [unowned self] parametersChanged -> Driver<LecturesStateResults> in
//since this is being called again, we make sure to clean out "old cache data" on view model
self.videoObject.lecturesResults.value.removeAll(keepingCapacity: true)
return self.setupLectures().asDriver(onErrorJustReturn: LecturesStateResults.empty)
}
results
//Bind the result observable to the UIcollectionView
//UiCollection view only wants an array not an Observable
.map {
if !$0.results.isEmpty { self.centerActivityIndicator?.stopAnimating()}
return [SectionModel(model: "", items: $0.results)]
}
.bind(to: lectureViewSquare.rx.items(dataSource: dataSource))
.addDisposableTo(disposeBag)
lectureViewSquare.rx.setDelegate(self)
.addDisposableTo(disposeBag)
I had the same problem with the same ui rendering, but without using RxSwift.
The code that fixed the issue was
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});