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)
}
Related
I am testing iOS15 and some new functionalities of UIKit. I've encountered some issues, not sure how to solve them. I did not change that code. This is just a piece of code that worked perfectly with the iOS 14, now after updating my target, it throws an error.
Xcode crashes the moment when my custom header for the UICollectionView of type UICollectionElementKindSectionHeader is being returned for the dataSource. Here is my code:
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Follower>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, followers) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FollowerCell.reuseId, for: indexPath) as! FollowerCell
cell.set(on: followers)
return cell
})
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: FollowersCollectionHeaderView.reuseId,
for: indexPath) as! FollowersCollectionHeaderView
header.set(with: self.user)
return header
}
}
The log says:
the view returned from
-collectionView:viewForSupplementaryElementOfKind:atIndexPath: does not match the element kind it is being used for. When asked for a view
of element kind 'FollowersCollectionHeaderView' the data source
dequeued a view registered for the element kind
'UICollectionElementKindSectionHeader'.
I did cast UICollectionElementKindSectionHeader to FollowersCollectionHeaderView, therefore I am not sure what is the issue here.
I've watched WWDC21 what's new in UIKit but haven't seen any mentioning of any change for that particular code.
Any suggestions, what to fix in that code?
Here is the partial solution that I came up with. Apple recommends using object's ID as a reference for the collectionView cells.
enum Section { case main }
var dataSource: UICollectionViewDiffableDataSource<Section, Follower.ID>!
// MARK: - Collection View configurations
fileprivate lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createCompositionalLayout())
collectionView.delegate = self
collectionView.backgroundColor = .systemBackground
collectionView.register(FollowersCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: FollowersCollectionHeaderView.reuseId)
view.addSubview(collectionView)
return collectionView
}()
fileprivate lazy var snapshot: NSDiffableDataSourceSnapshot<Section, Follower.ID> = {
var snapshot = NSDiffableDataSourceSnapshot<Section, Follower.ID>()
snapshot.appendSections([.main])
let itemIdentifiers = followers.map { $0.id }
snapshot.appendItems(itemIdentifiers, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
return snapshot
}()
fileprivate func updateData(with followers: [Follower]) {
snapshot = NSDiffableDataSourceSnapshot<Section, Follower.ID>()
snapshot.appendSections([.main])
let itemIdentifiers = followers.map { $0.id }
snapshot.appendItems(itemIdentifiers, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
}
fileprivate func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<FollowerCell, Follower.ID> { [weak self]
cell, indexPath, followerID in
guard let self = self else { return }
let followerArray = self.followers.filter { $0.id == followerID }
if let follower = followerArray.first {
cell.set(on: follower)
}
}
dataSource = UICollectionViewDiffableDataSource<Section, Follower.ID>(collectionView: collectionView) {
collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: itemIdentifier)
}
let headerRegistration = createSectionHeaderRegistration()
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
}
fileprivate func createSectionHeaderRegistration() -> UICollectionView.SupplementaryRegistration<FollowersCollectionHeaderView> {
return UICollectionView.SupplementaryRegistration<FollowersCollectionHeaderView>(
elementKind: FollowersCollectionHeaderView.reuseId) { [weak self] supplementaryView, elementKind, indexPath in
guard let self = self else { return }
supplementaryView.set(with: self.user)
}
}
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.
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 am fetching from core data and printing it on label. The problem what is being printed on the label has a lot of run off stuff. As you can see in the photo below. In each collection view cell I want it to print 1 element of the array. So if the array has [vanessa,taylor,bastista]. Collection view cell should print vanessa.
var people: [Jessica] = []
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = newCollection.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomeCell
cell.backgroundColor = .white
cell.layer.cornerRadius = 5
cell.layer.shadowOpacity = 3
cell.textLabel.text = people[indexPath.row].playName
return cell
}
MORE METHODS
override func viewWillAppear(_ animated: Bool) {
fetchData()
}
func fetchData() {
do {
items = try context.fetch(Jessica.fetchRequest())
DispatchQueue.main.async {
self.newCollection.reloadData()
}
} catch {
print("Couldn't Fetch Data")
}
}
link to Core Data Pic
pic of collection view cell
var people: [People] = [] //Any suitable variable
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = newCollection.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomeCell
cell.backgroundColor = .white
cell.layer.cornerRadius = 5
cell.layer.shadowOpacity = 3
cell.textLabel.text = people[indexPath.row].name //retrieve the name from your array
return cell
}
func fetchData() {
do {
people = fetchPeople()
DispatchQueue.main.async {
self.newCollection.reloadData()
}
} catch {
print("Couldn't Fetch Data")
}
}
func fetchPeople() -> [People] {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return [] }
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PeopleEntity")
let items = try! context.fetch(fetchRequest)
var detail: [People] = []
for data in items as! [NSManagedObject] {
var p = People(name: (data.value(forKey: "name") as? String) ?? "N/A") //check for nil
detail.append(p)
}
return detail
}
I have been trying to display data from Firebase and display in the tableView. The data is fetched properly but is not displayed. Apart from the code mentioned below, I have already added the dataSource and Delegate for the table and the identifier for the cell.
This is the HomeTableViewController
import UIKit
import FirebaseDatabase
import Firebase
class HomeTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
print(artists.count)
return artists.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HomeTableViewCell
print("Here")
let artist = artists[indexPath.row]
cell.artist = artist
return cell
}
let artistRef = Database.database().reference().child("Fund")
var artists = [ArtistList]()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
artistRef.observe(.value, with: { (snapshot) in
self.artists.removeAll()
for child in snapshot.children {
let childSnapshot = child as! DataSnapshot
let artist = ArtistList(snapshot: childSnapshot)
print(artist)
self.artists.insert(artist, at: 0)
}
self.tableView.reloadData()
})
}
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = false
self.navigationItem.hidesBackButton = true
title = "Artcall"
self.tableView.estimatedRowHeight = 290.0
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.reloadData()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
}
}
The main code for my HomeTableViewCell is
var artist: ArtistList!{
didSet
{
let dateString:String = String(format: "%#", artist.date as CVarArg)
labelDate.text = dateString
labelName.text = artist.name
labelLocation.text = artist.city
}
}
And the code for my model, ArtistList is here
class ArtistList
{
var name: String = ""
var city: String = ""
var date: Int64 = 0
let ref: DatabaseReference!
init(snapshot: DataSnapshot) {
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
name = value["artist_name"] as! String
city = value["perf_location"] as! String
date = value["target_date"] as! Int64
}
}
}
UI updates need to occur on the main thread if they are being called within asynchronous functions, so try putting the tableview reload on the main queue like this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
artistRef.observe(.value, with: { (snapshot) in
self.artists.removeAll()
for child in snapshot.children {
let childSnapshot = child as! DataSnapshot
let artist = ArtistList(snapshot: childSnapshot)
print(artist)
self.artists.insert(artist, at: 0)
}
//Print the artists array to verify it is definitely populated
print(self.artists)
//Reload tableview on the main queue
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}