Glitch
I am using CoreData with a NSFetchResultController to have data displayed in a UITableView. I have one problem: the UITableView changes the contentOffSet.y when a new row is inserted/moved/deleted. When the user have scrolled to, for e.g. the middle, the UITableView bounces when a new row is inserted.
Reproduction project
This github link to a project which contains the minimum code to reproduce this behavior: https://github.com/Jasperav/FetchResultControllerGlitch (the code is down below as well)
This is showing the glitch. I am standing in the middle of my UITableView and I am constantly seeing new rows being inserted, regardless of the current contentOffSet.y.:
Similar questions
How to prevent from scrolling UITableView up when NSFetchedResultsController add new record?
not relevant since I explicitly set a rowHeight and
estimatedRowHeight.
Error: UITableView jump to top with UITableViewAutomaticDimension
tried this before the endUpdates without luck
UITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded
Same as first link, I have set the rowHeight and
estimatedRowHeight.
Concerns
I also tried switch to performBatchUpdates instead of begin/endUpdates, that didn't worked out also.
The UITableView just shouldn't move when inserting/deleting/moving rows when those rows aren't visible to the user. I expect something like this just should work out of the box.
Final goal
This is what I eventually want (just a replication of the chat screen of WhatsApp):
When the user is completely scrolled to the top (for WhatsApp this is the bottom) where the new rows are being inserted, the UITableView should animate the new inserted row and change the current contentOffSet.y.
When the user isn't completely scrolled to the top (or bottom, depending where the new rows are being inserted) the cells the user is seeing should not bounce around when a new row is inserted. This is really bad for the user experience of the application.
It should work for dynamic height cells.
I also see this behavior when moving/deleting cells. Is there any easy fix for all glitches here?
If a UICollectionView would be a better fit, that would be fine to.
Use case
I am trying to replicate the WhatsApp chat screen. I am not sure if they use NSFetchResultController, but besides that, the final goal is to provide them the exact user experience. So inserting, moving, deleting and updating cells should be done the way WhatsApp is doing it. So for a working example: go to WhatsApp, for a not-working example: download the project.
Copy paste code
Code (ViewController.swift):
import CoreData
import UIKit
class ViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let tableView = MyTableView()
let resultController = ViewController.createResultController()
override func viewDidLoad() {
super.viewDidLoad()
// Initial cells
for i in 0...40 {
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = randomString(length: i + 1)
x.date = Date()
x.height = Float.random(in: 50...100)
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = self.randomString(length: Int.random(in: 10...50))
x.date = Date()
x.height = Float.random(in: 50...100)
}
resultController.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .update:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.textLabel?.text = resultController.object(at: indexPath).something
return cell
}
private static func createResultController() -> NSFetchedResultsController<SomeEntity> {
let fetchRequest: NSFetchRequest<SomeEntity> = SomeEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataContext.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0...length-1).map{ _ in letters.randomElement()! })
}
}
class MyTableView: UITableView {
init() {
super.init(frame: .zero, style: .plain)
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyTableViewCell: UITableViewCell {
}
class CoreDataContext {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FetchViewControllerGlitch")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
return container
}()
}
let lastScrollOffset = tableView.contentOffset;
tableView.beginUpdates();
tableView.insertRows(at: [newIndexPath!], with: .automatic);
tableView.endUpdates();
tableView.layer.removeAllAnimations();
tableView.setContentOffset(lastScrollOffset, animated: false);
Do the best you can establishing estimated heights for all of your table cell types. Even if heights are somewhat dynamic this helps the UITableView.
Save your scroll position and after updating your tableView and making a call to endUpdates() reset the content offset.
You can also check this tutorial
I've managed to achieve this.
stop applying update if user scrolls down table view, by removing resultController.delegate
restart applying if user is back to top of table view, by setting resultController.delegate again
sync diff between disabled time
Drawback is disabling fetch also disables updates or deletion of existing rows.
Those change will be applied after fetch restarts.
I've also tried to adjust contentOffset on controller(_:didChange:at:for:newIndexPath:) but it didn't work at all.
Code follows.
import CoreData
import UIKit
class ViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate {
let tableView = MyTableView()
let resultController = ViewController.createResultController()
var needsSync = false
override func viewDidLoad() {
super.viewDidLoad()
// Initial cells
for i in 0...40 {
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = randomString(length: i + 1)
x.date = Date()
x.height = Float.random(in: 50...100)
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = self.randomString(length: Int.random(in: 10...50))
x.date = Date()
x.height = Float.random(in: 50...100)
}
resultController.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let threshold = CGFloat(100)
if scrollView.contentOffset.y > threshold && resultController.delegate != nil {
resultController.delegate = nil
}
if scrollView.contentOffset.y <= threshold && resultController.delegate == nil {
resultController.delegate = self
needsSync = true
try! resultController.performFetch()
}
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .update:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if needsSync {
tableView.reloadData()
}
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if needsSync {
needsSync = false
}
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.textLabel?.text = resultController.object(at: indexPath).something
return cell
}
private static func createResultController() -> NSFetchedResultsController<SomeEntity> {
let fetchRequest: NSFetchRequest<SomeEntity> = SomeEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataContext.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0...length-1).map{ _ in letters.randomElement()! })
}
}
class MyTableView: UITableView {
init() {
super.init(frame: .zero, style: .plain)
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyTableViewCell: UITableViewCell {
}
class CoreDataContext {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FetchViewControllerGlitch")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
return container
}()
}
Do this
tableView.bounces = false
And it will work
Step 1: Define what you mean by "not move". For humans it is very clear that it is jumping. But the computer sees that the contentOffset is staying the same. So let us be very precise and define that the first cell that has a visible top should stay exactly where it after the change. All the other cells can move around, but this is our anchor.
var somethingIdOfAnchorPoint:String?
var offsetAnchorPoint:CGFloat?
func findHighestCellThatStartsInFrame() -> UITableViewCell? {
var anchorCell:UITableViewCell?
for cell in self.tableView.visibleCells {
let topIsInFrame = cell.frame.origin.y >= self.tableView.contentOffset.y
if topIsInFrame {
if let currentlySelected = anchorCell{
let isHigerUpInView = cell.frame.origin.y < currentlySelected.frame.origin.y
if isHigerUpInView {
anchorCell = cell
}
}else{
anchorCell = cell
}
}
}
return anchorCell
}
func setAnchorPoint() {
self.somethingIdOfAnchorPoint = nil;
self.offsetAnchorPoint = nil;
if let cell = self.findHighestCellThatStartsInFrame() {
self.offsetAnchorPoint = cell.frame.origin.y - self.tableView.contentOffset.y
if let indexPath = self.tableView.indexPath(for: cell) {
self.somethingIdOfAnchorPoint = resultController.object(at: indexPath).something
}
}
}
When we call setAnchorPoint we find and remember which entity (not indexPath because that may change shortly) is near the top and exactly how far from the top it is.
Next lets call setAnchorPoint right before changes happen:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.setAnchorPoint()
tableView.beginUpdates()
}
And after the changes are done we scroll back to where we are suppose to be without any animation:
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
self.tableView.layoutSubviews()
self.scrollToAnchorPoint()
}
func scrollToAnchorPoint() {
if let somethingId = somethingIdOfAnchorPoint, let offset = offsetAnchorPoint {
if let item = resultController.fetchedObjects?.first(where: { $0.something == somethingId }),
let indexPath = resultController.indexPath(forObject: item) {
let rect = self.tableView.rectForRow(at: indexPath)
let contentOffset = rect.origin.y - offset
self.tableView.setContentOffset(CGPoint.init(x: 0, y: contentOffset), animated: false)
}
}
}
And that is it! This will not do what you when when the view is completely scrolled to the top, but I trust that you can handle that case yourself.
you can try this its a edit on above pooja's answer, I've faced issue like yours the UIView.performWithoutAnimation removes the issue for me.Hope it helps.
UIView.performWithoutAnimation {
let lastScrollOffset = tableView.contentOffset;
tableView.beginUpdates();
tableView.insertRows(at: [newIndexPath!], with: .automatic);
tableView.endUpdates();
tableView.setContentOffset(lastScrollOffset, animated: false);
}
EDIT
you can also try the above but instead of insert rows you can use reload data on tableview but before that append the data fetched to you datasource, and set the last contentoffeset inside the block.
The table view is a complex beast. It behaves differently depending on its configuration. The table view adjusts the content offset when inserting, updating, deleting and moving rows. If the table view is used within a table view controller the scrollview delegate method scrollViewDidScroll(_:) is called.
The solution is to revoke the content offset adjustment there. However, this is against the intent of the table view and therefore needs to be done several times until viewDidLayoutSubviews() is called. So the solution is not optimal, but it works with dynamic height cells, section headers, section footers and should match your goals.
For the solution I have rebuilt your code. Your ViewController is no longer based on UIViewController but on UITableViewController. The essential part of the solution is the treatment and use of the property fixUpdateContentOffset.
import CoreData
import UIKit
class ViewController: UITableViewController, NSFetchedResultsControllerDelegate {
let resultController = ViewController.createResultController()
private var fixUpdateContentOffset: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
// Initial cells
for i in 0...40 {
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = randomString(length: i + 1)
x.date = Date()
x.height = Float.random(in: 50...100)
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = self.randomString(length: Int.random(in: 10...50))
x.date = Date()
x.height = Float.random(in: 50...100)
}
resultController.delegate = self
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixUpdateContentOffset = nil
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
fixUpdateContentOffset = nil
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let fixUpdateContentOffset = fixUpdateContentOffset,
tableView.contentOffset.y.rounded(.toNearestOrAwayFromZero) != fixUpdateContentOffset.y.rounded(.toNearestOrAwayFromZero) {
tableView.contentOffset = fixUpdateContentOffset
}
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .update:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
fixUpdateContentOffset = tableView.contentOffset
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
fixUpdateContentOffset = tableView.contentOffset
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = resultController.object(at: indexPath).something
return cell
}
private static func createResultController() -> NSFetchedResultsController<SomeEntity> {
let fetchRequest: NSFetchRequest<SomeEntity> = SomeEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataContext.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0...length-1).map{ _ in letters.randomElement()! })
}
}
class CoreDataContext {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FetchViewControllerGlitch")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
return container
}()
}
It's a typical issue for messages lists like Telegram, WhatsApp, or other messengers.
If you use UICollectionView, you might consider using StableCollectionViewLayout
It uses the UICollectionViewLayout subclass to solve this issue.
UICollectionViewLayout has some methods that can help:
override open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
// there is possible to calculate a content offset the difference
// with the help layout attributes for each updated item or only visible items
self.offset = calculate(...)
}
override open func finalizeCollectionViewUpdates() {
super.finalizeCollectionViewUpdates()
self.offset = nil
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
// there is necessary to add difference to/instead proposedContentOffset
if let offset = self.offset {
return offset
}
return proposedContentOffset
}
This solution has a lot of underwater rocks, but it is quite isolated. Everything magic will happen inside CollectionViewLayout.
It will allow using just insert/reload/delete UICollectionView methods.
Related
This class should be triggering a swipe or slide action into the table view cells, from right to left and that will allow me to delete based on the method tableView(_:commit:forRowAt:), but it is not being triggered and I have no idea why.
Code Class:
import UIKit
import CoreData
class ViewController: UIViewController {
// MARK: - Core Data Properties
var dataController: DataController!
var fetchedResultsController: NSFetchedResultsController<Garment>!
// MARK: - Properties & Variables
let garmentCellID = "GarmentCellID"
let segmentedControl = UISegmentedControl(items: ["Alpha", "Creation Time"])
let tableView = UITableView(frame: .zero, style: .plain)
// MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.isUserInteractionEnabled = true
//First Setting up the NavBar with it's features
settingUpNavBar()
setupSegmentedControl()
setupTableView()
setUpFetchedResultsController()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setUpFetchedResultsController()
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: false)
tableView.reloadRows(at: [indexPath], with: .fade)
}
}
//Automatic TableView height fit to its content
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Initialize the tableView with automatic fit for its height based on its content
tableView.frame = CGRect(x: tableView.frame.origin.x, y: tableView.frame.origin.y, width: tableView.frame.size.width, height: tableView.contentSize.height)
//Update the table with the actual height needed
tableView.reloadData()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
fetchedResultsController = nil
}
fileprivate func settingUpNavBar() {
let pluNavBarBtn = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(handleAdd))
navigationItem.setRightBarButton(pluNavBarBtn, animated: true)
navigationItem.title = "List"
}
fileprivate func setupSegmentedControl() {
//Second Set up the second view, done.
//Third: setup the segment
segmentedControl.addTarget(self, action: #selector(handleSegmentChange), for: .valueChanged)
view.addSubview(segmentedControl)
segmentedControl.setUpAnchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, topPadding: 100, leftPadding: 10, bottomPadding: 0, rightPadding: 10, width: 0, height: 40)
}
// MARK: - TableView set up
fileprivate func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.alwaysBounceVertical = true
tableView.register(GarmentCell.self, forCellReuseIdentifier: garmentCellID)
view.addSubview(tableView)
tableView.setUpAnchor(top: segmentedControl.bottomAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, topPadding: 10, leftPadding: 0, bottomPadding: 0, rightPadding: 0, width: 0, height: 0)
}
fileprivate func setUpFetchedResultsController() {
//Reusing code to use the fetchedResultsController
//NSFetchRequest is a generic type that's why <> and we specify Garment so it will work with a specific managed object subclass. After the = it will return a fetch request initialized with that entity.
let fetchRquest: NSFetchRequest<Garment> = Garment.fetchRequest()
//Sort rule: newest to oldest
//Using a descriptor is like a SQL: ORDER by
let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRquest.sortDescriptors = [sortDescriptor]
//End of reused code
//Starting the initialzer of resultsFetchedController
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRquest, managedObjectContext: dataController.viewContext, sectionNameKeyPath: nil, cacheName: "cach")
fetchedResultsController.delegate = self
do {
//This throws so we wrap it into a do catch block
try fetchedResultsController.performFetch()
} catch {
fatalError("The fetch could not be performed: \(error.localizedDescription)")
}
}
// MARK: - Selectors
#objc func handleSegmentChange() {
print("segment")
//Data persistance calls
}
#objc func handleAdd() {
print("Plus btn")
let addVC = AddController()
let nav2 = UINavigationController(rootViewController: addVC)
navigationController?.present(nav2, animated: true, completion: nil)
}
/// Deletes the garment at the specified index path
func deleteGarment(at indexPath: IndexPath) {
let garmentToDelete = fetchedResultsController.object(at: indexPath)
dataController.viewContext.delete(garmentToDelete)
try? dataController.viewContext.save()
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedResultsController.sections?[section].numberOfObjects ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let aGarment = fetchedResultsController.object(at: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: garmentCellID, for: indexPath) as! GarmentCell
cell.textLabel?.text = aGarment.name
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 1
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
deleteGarment(at: indexPath)
}
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableView.setEditing(editing, animated: animated)
}
}
extension ViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
break
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
break
case .update:
tableView.reloadRows(at: [indexPath!], with: .fade)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
#unknown default:
fatalError()
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert: tableView.insertSections(indexSet, with: .fade)
case .delete: tableView.deleteSections(indexSet, with: .fade)
case .update, .move:
fatalError("Invalid change type in controller(_:didChange:atSectionIndex:for:). Only .insert or .delete should be possible.")
#unknown default:
fatalError()
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
Credit to #DonMag: feel free to post it and I'll make it as the answer.
Removed the whole method ViewDidLayoutSubview
//Automatic TableView height fit to its content
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Initialize the tableView with automatic fit for its height based on its content
tableView.frame = CGRect(x: tableView.frame.origin.x, y: tableView.frame.origin.y, width: tableView.frame.size.width, height: tableView.contentSize.height)
//Update the table with the actual height needed
tableView.reloadData()
}
I just needed to remove the tableView.reloadData() and it will work like a charm.
My application has two tab bars. The first one presents a list of games added on view controller and save them on the core data database. Switching on the second tab/view reads from the database and presents it inside a table view. I implemented the NSFetchedResultsControllerDelegatewith a fetch method. But whenever I add or insert an item to the context on the first tab and switch to second tab, FRC delegate methods are not getting called. But when i implement the same methods on the first tab I can see them being call when I made a change to the database.
import UIKit
import CoreData
class AllWLeagueController : UITableViewController {
var fetchRequestController : NSFetchedResultsController<GameMo>!
var arrayOfGamesModel : [[GameMo]]? = []
var gameMo: GameMo?
var gamesMo: [GameMo] = []
override func viewDidLoad() {
validation(object: arrayOfGamesModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchRequest()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfGamesModel?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let weekL = arrayOfGamesModel?[indexPath.row] {
if let cell = tableView.dequeueReusableCell(withIdentifier: "WL") as? AllWLeaguesTableViewCell {
let winCounts = WLManager.winCountMethod(from: weekL)
let lossCounts = WLManager.lossCountMethod(from:weekL)
cell.setOulet(win: winCounts, loss: lossCounts, rankName: rankString)
cellLayer(with: cell)
return cell
}
}
}
extension AllWLeagueController: NSFetchedResultsControllerDelegate {
func fetchRequest () {
let fetchRequest = NSFetchRequest<GameMo>(entityName: "Game")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "win", ascending: true)]
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate){
let context = appDelegate.persistentContainer.viewContext
// fetch result controller
fetchRequestController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchRequestController.delegate = self
do{
try fetchRequestController.performFetch()
if let fetchedObjects = fetchRequestController.fetchedObjects {
gamesMo = fetchedObjects
print("Fetech Request Activated")
print(gamesMo)
}
}catch{
fatalError("Failed to fetch entities: \(error)")
}
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView beginupdates")
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
print("insert")
tableView.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
print("delete")
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath {
print("update")
tableView.reloadRows(at: [indexPath], with: .fade)
}
default:
tableView.reloadData()
}
if let fetchedObjects = controller.fetchedObjects {
gamesMo = fetchedObjects as! [GameMo]
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView endupdates")
tableView.endUpdates()
}
}
It looks like your fetchedResultsController is updating gamesMo, but your tableView is looking at arrayOfGamesModel. But arrayOfGamesModel is never updated.
You can either change your tableView methods to look at gamesMo, or change your fetchedResultsController to update arrayOfGamesModel.
In my app I have a ViewController with a collectionView and a tableView.
My CollectionView shows the dates and the tableView shows the data in that date.
I need to create a date before create the other datas, so when I type in the date it shows up a alert and them we go to other view to Create a new data.
The problem is that when I save a new data and I use a popViewController to go back my previous view my tableView does not reload data.
I can see the saved data when I back the view again and so I return to the view
I've tried to use the myTableView.reloadData() in ViewWillAppear, but it didn't work.
How can I reload myTableView as soon as return the view?
Here is a link to the app video.
Here is the app working
Here is my IBAction to save data:
#IBAction func btSaveNewMeasures(_ sender: UIBarButtonItem) {
let allMeasures = NSEntityDescription.entity(forEntityName: "AllMeasures", in: context)
for textField in tfMeasuresValues {
let newMeasure = NSManagedObject(entity: allMeasures!, insertInto: context)
if let valueTxt = textField.text {
let value = convertNumber.convertToDouble(valueTxt)
if value != 0 {
switch textField.tag {
case 1: ///Body Measures
//Peso
let type = MeasuresType.bodyMeasures.rawValue
let index = NSNumber(value: textField.tag).int64Value
self.saveNewMessure(newMeasure, type: type, title: "Peso", value: value, index: index)
case 2:
//Altura
let type = MeasuresType.bodyMeasures.rawValue
let index = NSNumber(value: textField.tag).int64Value
self.saveNewMessure(newMeasure, type: type, title: "Altura", value: value, index: index)
case 3:
//%Gordura
let type = MeasuresType.bodyMeasures.rawValue
let index = NSNumber(value: textField.tag).int64Value
self.saveNewMessure(newMeasure, type: type, title: "% Gordura", value: value, index: index)
default:
break
}
}
}
}
do {
try context.save()
guard let navigation = navigationController else {return}
navigation.popViewController(animated: true)
self.delegate?.didFinishUpdates(finished: true)
} catch let error {
print("Error Saving Measures Values: \(error)")
}
}
}
This is my cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MeasureCell.identifier, for: indexPath) as! MeasureCell
let measures = fetchedRCAllMeasuresByDay.object(at: indexPath)
cell.setupMeasureCell(measure: measures)
return cell
}
I've checked if myTableView.reloadData() has been called in viewWillApperar and I confirmed that.
Here are my methods setupTableView and setupCollectionView in my class MeasureController:
private func setupTableView() {
myTableView.backgroundColor = Theme.background
myTableView.separatorStyle = .none
myTableView.tableFooterView = UIView()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.rowHeight = UITableView.automaticDimension
myTableView.estimatedRowHeight = 80
myTableView.register(MeasureCell.nib, forCellReuseIdentifier: MeasureCell.identifier)
}
private func setupCollectionView() {
myCollectionView.backgroundColor = Theme.background
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.register(UINib(nibName: "DateCollectionCell", bundle: nil), forCellWithReuseIdentifier: cellCollectionViewID)
//Set Collection View Cell Aligment
let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .right, verticalAlignment: .top)
alignedFlowLayout.scrollDirection = .horizontal
myCollectionView.collectionViewLayout = alignedFlowLayout
}
==============================================
This is my NSFetechedResultsControllerDelegate fixed:
extension MeasureController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
myTableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
case .delete:
myTableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
case .move:
break
case .update:
myTableView.reloadSections(IndexSet(integer: sectionIndex), with: .fade)
#unknown default:
fatalError()
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
myTableView.reloadData()
myCollectionView.reloadData()
}
//Allow to show full section name in header.
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String) -> String? {
return sectionName
}
}
And here is the link for the video with the new app behaviour, after fixed the NSFRCD.
New App Behaviour
I found that cellForRowAt is not called after new data is added as soon as old data is deleted. Although numberOfSections has been called, the value is zero. But if you go back to the previous view and re-enter MeasureView, numberOfSections will have a correct value, in other words, numberOfSections will be nonzero.
I've tried to use delegates and callbacks to reload myTableView but I didn't success.
I have a strange problem where my cells in the tableview are not consistent. Sometimes they will show as a blank cell and other times they will load with the correct data. See the GIF below.
Notice the blank cell in section 1 changes each time.
I also have this problem when adding new cells to the tableview, but closing and reopening the app always fixes it. It just doesn't load correctly when getting added... but sometimes it does load correctly. See GIF Below.
I've been recommended to use the reloadData(), but that doesn't seem to help anything at all. I'm hoping someone will see this that will know what to do.
See Code Below
Table View Controller: (Swift)
import UIKit
import CoreData
class ListItemsTVC: UITableViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController = NSFetchedResultsController()
//var sequeItem: createItem?
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
frc = getFCR()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to perform inital fetch")
}
self.tableView.rowHeight = 62
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = frc.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
override func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 28
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = frc.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
let item = frc.objectAtIndexPath(indexPath) as! Items
cell.separatorInset = UIEdgeInsets(top: 0, left: 78, bottom: 0, right: 0)
cell.itemName.text = item.name
cell.itemSection.text = item.section
cell.itemQty.text = "Qty: \(item.qty!)"
cell.itemSize.text = item.size
cell.itemPrice.text = floatToCurrency(Float(item.price!))
//cell.itemImage.image = UIImage(data: item.image!)
cell.itemID.text = String(item.id!)
return cell
}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .Destructive, title: "Delete") { (action, indexPath) in
let item = self.frc.objectAtIndexPath(indexPath) as! Items
let id = item.id!
let request = self.fetchRequest()
let pred = NSPredicate(format: "%K == %#", "id",id)
request.predicate = pred
var fetchResults = [AnyObject]()
do {
fetchResults = try self.moc.executeFetchRequest(request)
} catch {
fatalError("Fetching Data to Delete Failed")
}
self.moc.deleteObject(fetchResults[0] as! NSManagedObject)
fetchResults.removeAtIndex(0)
do {
try self.moc.save()
} catch {
fatalError("Failed to Save after Delete")
}
}
return [delete]
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break
case NSFetchedResultsChangeType.Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break
/*case NSFetchedResultsChangeType.Update:
tableView.reloadSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Automatic)
break*/
default:
print("Default in didChangeSection was called")
break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Automatic)
break
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
break
default:
print("Default in didChangeObject was called")
break
}
}
// MARK: - Custom Functions
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Items")
let sortDesc1 = NSSortDescriptor(key: "section", ascending: true)
let sortDesc2 = NSSortDescriptor(key: "isChecked", ascending: true)
let sortDesc3 = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDesc1, sortDesc2, sortDesc3]
return fetchRequest
}
func getFCR() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "section" , cacheName: nil)
return frc
}
func floatToCurrency(flt: Float) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
return String(formatter.stringFromNumber(flt)!)
}
}
Add Button View Controller: (Swift)
import UIKit
import CoreData
class AddItemListVC: UIViewController, NSFetchedResultsControllerDelegate {
// MARK: - Constants and Variables
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var sendItem: Items?
// MARK: - App loading Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets and Actions
#IBAction func addItem(sender: AnyObject) {
let entityDesc = NSEntityDescription.entityForName("Items", inManagedObjectContext: moc)
let item = Items(entity: entityDesc!, insertIntoManagedObjectContext: moc)
if (NSUserDefaults.standardUserDefaults().objectForKey("nextItemID") == nil) {
NSUserDefaults.standardUserDefaults().setObject(1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
}
let id = NSUserDefaults.standardUserDefaults().integerForKey("nextItemID")
item.id = id
switch id {
case 1..<10:
item.name = "Item ID 00\(id)"
case 10..<100:
item.name = "Item ID 0\(id)"
default:
item.name = "Item ID \(id)"
}
item.brand = "Brand \(id)"
item.qty = 1
item.price = 0
item.size = "Size \(id)"
let sec: Int = Int(arc4random_uniform(UInt32(4 - 1))) + 1
item.section = "Section \(sec)"
item.isChecked = false
do {
try moc.save()
NSUserDefaults.standardUserDefaults().setObject(id + 1, forKey: "nextItemID")
NSUserDefaults.standardUserDefaults().synchronize()
} catch {
fatalError("New item save failed")
}
navigationController!.popViewControllerAnimated(true)
}
}
#Jason Brady, I have just downloaded your code.
There is no problem with you core data, array or table view.
When i run an app in iPhone 5 / iPhone 6 / iPhone 6 Plus with 8.1 it is working fine, none of cell or add button is getting hidden.
But with same devices with 9.2 there is a problem.
Solutions
(1) Custom cell with dequeueReusableCellWithIdentifier
let cell : ListItemsTVCell! = tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath) as! ListItemsTVCell
(2) DidselectedRowAtIndex - Here you will get information at cell selection, So data is going perfectly.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Select")
let myCell = tableView.cellForRowAtIndexPath(indexPath) as! ListItemsTVCell
print(myCell.itemName.text)
}
(3) Problem is with AutoLayout, when i disabled it, all label went to -X positions, when i have aligned it properly using auto resizing, it is now working fine. ( See attached screen shot )
So you need to check with AutoLayout, why it is giving problem in iOS 9 and newer.
Refer link
I hope you can figure out and resolve further.
All the best.
Download
Instead of using
tableView.dequeueReusableCellWithIdentifier("listContentCell", forIndexPath: indexPath)
try using
tableView.dequeueReusableCellWithIdentifier("listContentCell")
I've read a few other questions on this that the problem related to the size of the CollectionView. I've tried adjusting the size as recommended in those answers, but none of them were working. I'm using a NSFetchedResultsController which convolutes it for me as well (I'm wondering if it has something to do with it not firing).
Anyways, the real problem is I have no content appearing for my UICollectionView. There are no errors at runtime, simply a blank screen. I'm working in Swift (obviously).
Here is my code from the ViewController:
import UIKit
import CoreData
private let reuseIdentifier = "Family"
private var selectedFirstName:String = "Blank"
private var selectedLastName:String = "Blank"
private var selectedNumber:String = "Blank"
private var selectedEmail:String = "Blank"
class FamilyViewController: UIViewController, UICollectionViewDataSource {
var coreDataStack: CoreDataStack!
var fetchedResultsController: NSFetchedResultsController!
#IBOutlet var familyCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
//1
let fetchRequest = NSFetchRequest(entityName: "Family")
let firstNameSort =
NSSortDescriptor(key: "firstName", ascending: true)
fetchRequest.sortDescriptors = [firstNameSort]
//2
self.coreDataStack = CoreDataStack()
fetchedResultsController =
NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.context,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = CollectionViewFetchedResultsControllerDelegate(collectionView: familyCollectionView)
//3
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
func configureCell(cell: FamilyCCell, indexPath: NSIndexPath) { let family = fetchedResultsController.objectAtIndexPath(indexPath) as! Family
cell.firstNameLabel.text = family.firstName
print("configureCell ran")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
print("numberOfSectionsInCollectionView ran")
return fetchedResultsController.sections!.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
let sectionInfo = fetchedResultsController.sections![section]
print("numberOfItemsInSection ran")
return sectionInfo.numberOfObjects
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! FamilyCCell
print("cellForItemAtIndexPath ran")
configureCell(cell, indexPath: indexPath)
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "showDetail") {
let detailVC = segue.destinationViewController as! ContactViewController
print("prepareForSegue ran")
detailVC.detailFirstName = selectedFirstName
detailVC.detailLastName = selectedLastName
detailVC.detailNumber = selectedNumber
detailVC.detailEmail = selectedEmail
}
}
}
extension FamilyViewController: UICollectionViewDelegate {
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print("didSelectItemAtIndexPath ran")
collectionView.delegate = self
let family = fetchedResultsController.objectAtIndexPath(indexPath) as! Family
selectedFirstName = family.firstName!
selectedLastName = family.lastName!
selectedNumber = family.phone!
selectedEmail = family.email!
coreDataStack.saveContext()
}
}
class CollectionViewFetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: Properties
private let collectionView: UICollectionView
private var blockOperations: [NSBlockOperation] = []
// MARK: Init
init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
// MARK: Deinit
deinit {
blockOperations.forEach { $0.cancel() }
blockOperations.removeAll(keepCapacity: false)
}
// MARK: NSFetchedResultsControllerDelegate
func controllerWillChangeContent(controller: NSFetchedResultsController) {
blockOperations.removeAll(keepCapacity: false)
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.insertItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Update:
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadItemsAtIndexPaths([newIndexPath]) }
blockOperations.append(op)
case .Move:
guard let indexPath = indexPath else { return }
guard let newIndexPath = newIndexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.moveItemAtIndexPath(indexPath, toIndexPath: newIndexPath) }
blockOperations.append(op)
case .Delete:
guard let indexPath = indexPath else { return }
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteItemsAtIndexPaths([indexPath]) }
blockOperations.append(op)
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
let op = NSBlockOperation { [weak self] in self?.collectionView.insertSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Update:
let op = NSBlockOperation { [weak self] in self?.collectionView.reloadSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
case .Delete:
let op = NSBlockOperation { [weak self] in self?.collectionView.deleteSections(NSIndexSet(index: sectionIndex)) }
blockOperations.append(op)
default: break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView.performBatchUpdates({
self.blockOperations.forEach { $0.start() }
}, completion: { finished in
self.blockOperations.removeAll(keepCapacity: false)
})
}
}
I have print statements confirming that the cellForItemAtIndexPath is not running. Any ideas? I realize this is quite specific and I gave a crap ton of code, just not quite sure where the error could be coming from. Thanks for any help in advance.
Make sure...you Confirm UICollectionViewDelegate protocol methods.
set collectionview.delegate = self
collectionview.datasource = self
in the collection view datasource for the methods numberOfSectionsInCollectionView return 1 and in collectionViewNumberOfCellsForSection return the number of cells you want to display
to clarify on tableview or a collection view a "section" is a group of related things where as a cell (or row for tableview) are the actual things you want to display - when you do not override the amount of cells required it will return 0 for all sections
(ill check the exact method signatures in the morning and edit if they are slightly off)
Write it:
class FamilyViewController: UIViewController, UICollectionViewDataSource,UICollectionViewDelegate{
and write these two lines in your viewDidLoad method:
familyCollectionView.delegate = self
familyCollectionView.datasource = self