Reloading NSTableView after downloading data - swift

I'm writing an app that will help me update my database on parse and I'm trying to load objects into an nstableview.
For whatever reason, I'm returning nil when I make the call in my viewcontroller so I moved the data into my appdelegate to get the objects.
let storyBoard = NSStoryboard(name: "Main", bundle: nil)
let myVC = storyBoard.instantiateControllerWithIdentifier("teacher") as! ViewController
var teachers = [Teacher]()
let query = PFQuery(className: "TeacherList")
query.findObjectsInBackgroundWithBlock {
objects, error in
if let objects = objects {
for object in objects {
let teacher = Teacher()
teacher.name = object["name"] as! String
teacher.email = object["email"] as! String
teacher.subjectsTaught = object["subjectsTaught"] as! [String: String]
teacher.category = object["category"] as! String
teacher.uniqueID = object.objectId!
teachers.append(teacher)
}
}
print(teachers)
myVC.teacherList = teachers
}
As you see, I pass this along to my VC. So I understand that I need to reload the data as viewDidLoad() will be called before the data has been downloaded. I've tried putting tableView.reloadData() in didSet for teacherList and just in case that's set before the viewloads, I even throw it in viewdidload.
var teacherList: [Teacher]? {
didSet {
print("got set")
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.setDelegate(self)
tableView.setDataSource(self)
tableView.reloadData()
}
However, no matter what, my tableview is nil for anything that happens after viewdidload. I've also tried optional channing in my getSet.
I should also say that I'm brand new to OSX programming as I've done most of my programming for iOS.
I can put the query in my VC but whenever I run the query, I return nil.

PFQuery works asynchronously, the data is returned much later - in terms of computer speed – after viewDidLoad exits.
Reload the table view in the block right after the array has been populated on the main thread.
var teachers = [Teacher]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.setDelegate(self)
tableView.setDataSource(self)
let query = PFQuery(className: "TeacherList")
query.findObjectsInBackgroundWithBlock { [unowned self] (objects, error) in
if let objects = objects {
for object in objects {
let teacher = Teacher()
teacher.name = object["name"] as! String
teacher.email = object["email"] as! String
teacher.subjectsTaught = object["subjectsTaught"] as! [String: String]
teacher.category = object["category"] as! String
teacher.uniqueID = object.objectId!
self.teachers.append(teacher)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
print(self.teachers)
}
}

Related

UITableViewDiffableDataSource cell provider fails to fetch newly added managed object, with temporary id, when reusing cells

The sample app has a table view that is powered by UITableViewDiffableDataSource that gets data from NSFetchedResultsController. You can add letters of the alphabet to the table view by pressing the plus button. To implement data source, I used this article. The issue is that when I add new item to Core Data NSFetchedResultsController feeds temporary IDs to the cell provider. And when I scroll down and cell provider has to reuse cells, it fails to fetch managed object with temporary ID. It does not, however, happen when the item is added to the area of the table view that is on the screen.
lazy var fetchedResultsController: NSFetchedResultsController<Item> = {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
let sort = NSSortDescriptor(key: #keyPath(Item.name), ascending: true)
fetchRequest.sortDescriptors = [sort]
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: moc,
sectionNameKeyPath: nil,
cacheName: nil
)
controller.delegate = self
return controller
}()
func configureDiffableDataSource() {
let diffableDataSource = UITableViewDiffableDataSource<Int, NSManagedObjectID>(tableView: tableView) { (tableView, indexPath, objectID) -> UITableViewCell? in
guard let object = try? self.moc.existingObject(with: objectID) as? Item else {
// Crash happens here.
fatalError("Managed object should be available.")
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell_id", for: indexPath)
cell.textLabel?.text = object.name
return cell
}
self.diffableDataSource = diffableDataSource
tableView.dataSource = diffableDataSource
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
guard let dataSource = tableView?.dataSource as? UITableViewDiffableDataSource<Int, NSManagedObjectID> else {
assertionFailure("The data source has not implemented snapshot support while it should.")
return
}
var snapshot = snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
let currentSnapshot = dataSource.snapshot() as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>
let reloadIdentifiers: [NSManagedObjectID] = snapshot.itemIdentifiers.compactMap { itemIdentifier in
guard let currentIndex = currentSnapshot.indexOfItem(itemIdentifier), let index = snapshot.indexOfItem(itemIdentifier), index == currentIndex else {
return nil
}
guard let existingObject = try? controller.managedObjectContext.existingObject(with: itemIdentifier), existingObject.isUpdated else { return nil }
return itemIdentifier
}
snapshot.reloadItems(reloadIdentifiers)
let shouldAnimate = tableView?.numberOfSections != 0
dataSource.apply(snapshot as NSDiffableDataSourceSnapshot<Int, NSManagedObjectID>, animatingDifferences: shouldAnimate)
}
Adding try! fetchedResultsController.performFetch() right after saving to Core Data fixes the issue, however, it’s a brute-force solution, which causes double call to controller(_:didChangeContentWith:) delegate method and, sometimes, double animation. Fetching should happen automatically in this case. I wonder, why cell provider fails to fetch data and how to fix this in an efficient way.
#objc func handleAdd() {
// Add item to Core Data.
let context = moc
let entity = Item.entity()
let item = Item(entity: entity, insertInto: context)
item.name = "\(letters[counter])" // Adds letters of the alphabet.
counter += 1
try! context.save()
// Manually fetching right after saving doesn’t seem efficient.
try! fetchedResultsController.performFetch()
}

How save model after pass as param ? or get as function ? core data

Problem 1.
I have a tableViewController
var stocksResults: [Any] = []
override func viewDidLoad() {
super.viewDidLoad()
loadList()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toTrade" {
let indexPath = self.stockTableView.indexPathForSelectedRow
let vc = segue.destination as! StockTradeController
vc.stockResults = stocksResults[indexPath!.row] as AnyObject
}
}
func loadList() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Stocks")
fetchRequest.returnsObjectsAsFaults = false
do {stocksResults = try context.fetch(fetchRequest)}
catch {print("Error")}
stockTableView.reloadData()
}
code is simple, i show data from CoreData entity Stocks and when iIclick on row i will pass model to another viewController as
vc.stockResults = stocksResults[indexPath!.row] as AnyObject
in other view i can modify this model
(stockResults as AnyObject).setValue(345, forKey: "own")
And the question is how can i save model after modification ?
Problem 2
I created class
class UserAccount {
func getData() -> Array<Any> {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Account")
fetchRequest.returnsObjectsAsFaults = false
do {
let accountUser = try context.fetch(fetchRequest)
return accountUser
}
catch {print("Error")}
return []
}
}
i can get and change model in any VC
(UserAccount().getData()[0] as AnyObject).setValue(433, forKey: "cash")
I can run this function from any VC and the same question how can i save changes ? Should i pass context in both cases some how ? or what is the best way ?
If you have the standard implementation of the Core Data stack in AppDelegate (it seems so) you can save the context from everywhere with
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.saveContext()
There are many bad practices in your code.
Declare the data source array with the specific entity type
var stocksResults: [Stocks] = []
This avoids the ugly type cast to AnyObject
In the second view controller you are dealing with a single object so declare
var stock : Stocks!
and assign
vc.stock = stocksResults[indexPath!.row]
By the way it's recommended to name also an entity in singular form (Stock)
In loadList() specify the fetch request and print the real error
func loadList() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<Stocks>(entityName: "Stocks")
fetchRequest.returnsObjectsAsFaults = false
do {
stocksResults = try context.fetch(fetchRequest)
} catch {print(error)}
stockTableView.reloadData()
}
Rather than KVC use dot notation
stock.own = 345
In UserAccount do the same: Take advantage of the generic fetch request and return the Entity
func getData() -> Array<Account> { ...

How to execute two completion blocks in a single function and pass the data of the completion block to next view controller?

This is my database structure:
I'm using a function with closure, performing two completion blocks and store the data in two separate arrays. Once I get the data I want to pass the data to next view controller into different variables, but instead I'm getting same value for both arrays.
#IBAction func GoToAnswerPage(_ sender: Any) {
self.getData(refe:JoinCodeTextField.text!) { (array) in
self.performSegue(withIdentifier:"JoinToAnswerPage",sender:array)
}
}
func getData(refe: String, completion: #escaping (([Any]) -> ())) {
var questionArray = [Any]()
var answerArray = [Any]()
let ref = Database.database().reference(fromURL: "https://pollapp-30419.firebaseio.com/").child("Questions/\(refe)/")
ref.child("Question_And_Options").observeSingleEvent(of: .value,with: { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot, let value = rest.value{
questionArray.append(value)
}
completion(questionArray)
})
ref.child("Answer_Key").observeSingleEvent(of: .value,with: { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot, let value = rest.value{
answerArray.append(value)
}
completion(answerArray)
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let joinViewController = segue.destination as? JoinAnswerViewController
else {
return
}
joinViewController.answers = sender as! [String]
joinViewController.options = sender as! [String]
}
On the next view controller.
var options = [Any]()
var answers = [Any]()
This is the output I'm getting:
answers-["Test Q-1", "Test A-1", "Test A-2"]
questions-["Test Q-1", "Test A-1", "Test A-2"]
answers-["Test A-1"]
questions-["Test A-1"]
Instead I should get:
questions-["Test Q-1", "Test A-1", "Test A-2"]
answers-["Test A-1"]
Your completion handler will be called twice, once for "answers" and once for "questions". They could come in either order, so you should pass an additional type in the completion to know which you have received. Use a [String : [Any]] dictionary to collect the two arrays, and call self.performSegue(withIdentifier:sender:) when you've received both arrays and stored them in the dictionary arrays.
In prepare(for:sender:) unpack the sender dictionary and assign the values:
#IBAction func GoToAnswerPage(_ sender: Any) {
var arrays = [String : [Any]]()
self.getData(refe: JoinCodeTextField.text!) { (array, type) in
arrays[type] = array
if arrays.count == 2 {
self.performSegue(withIdentifier:"JoinToAnswerPage",sender: arrays)
}
}
}
func getData(refe: String, completion: #escaping (([Any], String) -> ())) {
var questionArray = [Any]()
var answerArray = [Any]()
let ref = Database.database().reference(fromURL: "https://pollapp-30419.firebaseio.com/").child("Questions/\(refe)/")
ref.child("Question_And_Options").observeSingleEvent(of: .value,with: { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot, let value = rest.value{
questionArray.append(value)
}
completion(questionArray, "question")
})
ref.child("Answer_Key").observeSingleEvent(of: .value,with: { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot, let value = rest.value{
answerArray.append(value)
}
completion(answerArray, "answer")
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let joinViewController = segue.destination as? JoinAnswerViewController
else {
return
}
guard let arrays = sender as? [String : [Any]],
let answers = arrays["answer"] as? [String],
let questions = arrays["question"] as? [String]
else { return }
joinViewController.answers = answers
joinViewController.options = questions
}
Note: When the user presses a button, they should get an immediate response. Since you are loading the data from the network, there may be a delay making the user wonder if anything is happening. It would be better to pass JoinCodeTextField.text! to JoinAnswerViewController and let it load the question/answer data. JoinAnswerViewController could display a UIActivityIndicatorView (spinner) while the data is loading to let the user know the data is coming. Once you have both arrays, you can set up the JoinAnswerViewController.

fetched json data Dictionary or array is empty in viewdidload or tableview datasource?

I want to add strings from an array of dictionary from backend.
but it's always empty outside the fetch function
//fetch data
func fetchFaqs(){
let manager = APIManager()
manager.parsingGet(url: BaseURL.faqs) { (JSON, Status) in
if Status {
let dict = JSON.dictionaryObject
let data = dict!["data"] as! [[String:Any]]
self.faqs = data as! [[String : String]]
}
}
}
//Viewdidload
class FaqViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
var faqs = [[String:String]]()
var questions = NSMutableArray()
var answers = NSMutableArray()
#IBOutlet var faqsTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
fetchFaqs()
self.faqsTableView.reloadData()
print(faqs)
// faqsTableView.rowHeight = UITableView.automaticDimension
// faqsTableView.estimatedRowHeight = 600
}
}
Reload the tableview inside the api call closure in Main thread
func fetchFaqs(){
let manager = APIManager()
manager.parsingGet(url: BaseURL.faqs) { (JSON, Status) in
if Status {
let dict = JSON.dictionaryObject
let data = dict!["data"] as! [[String:Any]]
self.faqs = data as! [[String : String]]
DispatchQueue.main.async {
self.faqsTableView.reloadData()
}
}
}
}

Core Data Update in swift while selecting any row in list table view not working?

I have got a scenario in which I have 2 VC which are -
VC1 - To enter detail & save the data.
VC2 - To display the datas in a table view.
Now I want that whenever I select any particular row I would update my Database at the particular row. For that I am passing the selected managed object at that particular row for which I use the following code.
VC2 class name - ViewController
Code to fetch request -
func fetchData()
{
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appdelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Person")
do
{
people = try managedContext.executeFetchRequest(fetchRequest) as! [NSManagedObject]
print(people)
print("FETCHING DATA")
}
catch let error as NSError
{
print("could not fetch \(error), \(error.userInfo)")
}
}
Code to pass the data in selected row -
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if segue.identifier == "segueupdate"
{
let update = segue.destinationViewController as! EnterDetailViewController
// var managedobject = NSManagedObject()
let indexpath = self.tableview.indexPathForSelectedRow
let row = indexpath?.row
let managedobject = people[row!]
update.managedobjectt = managedobject
}
}
VC2 class name - EnterDetailViewController
class EnterDetailViewController: UIViewController {
#IBOutlet weak var nametextfield: UITextField!
var managedobjectt = NSManagedObject()
override func viewDidLoad() {
super.viewDidLoad()
if let s = managedobjectt.valueForKey("name") as? String
{
nametextfield.text = s //here I show the user the existing name value
}
}
Now in my save function I do -
#IBAction func savedata(sender: AnyObject)
{
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedcontext = appdelegate.managedObjectContext
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedcontext)
let person = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedcontext)
person.setValue(self.nametextfield.text, forKey: "name")
do
{
try managedcontext.save()
print("SAVED")
}
catch let error as NSError
{
print("could not save \(error), \(error.userInfo)")
}
self.navigationController?.popToRootViewControllerAnimated(true)
}
Here I want the compiler to check the received managed object and update any changes to the database which I am unable to do because SWIFT doesn't accept a managedobject type as condition but I was able to achieve update of database by this concept in ObjC.
And another problem is when I try to compile I get an error as -
failed to call designated initializer on NSManagedObject class
'NSManagedObject'
in the prepereforsegue() method. So how to solve the problem and perform update.
This line is wrong because you're trying to create an invalid managed object instance:
var managedobjectt = NSManagedObject()
It should be
var managedobjectt : NSManagedObject?
And when you update you aren't changing the current item if it exists, you're just always creating a new instance. You should
if let person = self.managedobjectt {
// update (if anything other than below)
} else {
// create new (insert and set any default values)
}
person.setValue(self.nametextfield.text, forKey: "name")
// save