function not called in escaping closure - ios

I start learning MVVM and I decided to refactor my code with MVVM.
In ViewModel I have function which fetch director value:
func loadDirector(id: Int, completed: #escaping () -> ()) {
homeRepository.getDirector(id: id){ (creditReponse) -> (Void) in
creditReponse?.crew.forEach({ singleValue in
if singleValue.knownForDepartment == .directing {
self.directorName = singleValue.name
}
})
}
}
Function getDirector is from Repository where I am fetching data from URL.
func getDirector(id:Int, _ completed: #escaping (CreditsResponse?) -> (Void)) {
movieServiceAPI.fetchData(from: NetworkData.directorUrl(id: id).value, by: completed)
}
View part where I am calling loadDirector :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let movie = homeViewModel.movieList[indexPath.row]
homeViewModel.loadDirector(id: movie.id, completed: {
self.changeVC(movie: movie, director: (self.homeViewModel.directorName), groups: self.homeViewModel.setupGenres(groups: movie.genreIds), movieIndex: indexPath.row)
})
}
Problem is when I click on cell in my tableview, function changeVC is not called (it doesnt show any error, function is just not called). loadDirector function firstly only had id parameter and then problem was that directorName wasnt updated on time (second cell click had first cell click director value). For that reason I added escaping closure so I can change my VC when director value is updated, but I am having problem with that. Where am I making mistake?

Related

saving API response to coreData, still shows nil when accessing

func getGenreKeys(complition: #escaping (_ genre : GenreListModel?) -> ())
{
let genreUrl = URL(string: "\(baseUrl)\(genreListUrl)\(apiKey)")!
urlSessionManager(url: genreUrl,toUseDataType: GenreListModel.self) { json in
//json will contain genreList Object , which can be used to get keys
switch json
{
case .success(let genreListData) :
complition(genreListData)
CoreData.shared.saveGenreList(json: genreListData)
case .failure(let error) :
print(error)
}
}
}
this above is the api completion code
func saveGenreList(json: GenreListModel){
let context = persistentContainer.viewContext
let genreList = GenreList(context: context)
json.genres?.forEach({ Genres in
genreList.name = Genres.name
do{
try context.save()
}
catch{
print("error in saving")
}
})
}
this is what i did to save data after completion of api fetch.
var coreGenre : GenreList?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.name?.count ?? 0
this above code is the part of VC that requires to get the coreGenre.name to give the count but it is nill
but when i try to access from viewController by creating a variable of the core data entity class , it returns nill
What's wrong?
I'll base my answer on code there. In your GitHub account where you shared that code.
First issue:
You have:
var genrelist : GenreListModel? {
didSet{
//after getting data a table needs to reload and ui elements needs to be used in main thread only
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
var coreGenre : [GenreList]?
override func viewWillAppear(_ animated: Bool) {
...
ApiManager.shared.getGenreKeys { genre in
self.genrelist = genre
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
genrelist?.genres?[indexPath.row].id
...
cell.genreLabel.text = genrelist?.genres?[indexPath.row].name
...
}
First issue:
coreGenre IS NEVER SET! genreList is set, but not coreGenre. So don't expect it to be something else that nil!
You didn't write coreGenre = something
So tableView(_:numberOfRowsInSection:) will always return 0.
Make your choice. Either genreList or coreGenre:
ApiManager.shared.getGenreKeys { genreList in
self.coreGenre = genreList.genres
}
&
var coreGenre: [GenreList] {
didSet { DispatchQueue.main.async { self.tableView.reloadData() } }
}
And remove var genreList
Or, remove coreGenre.
That should fix the UITableView, and that's where your question is misleading: There is no CoreData here. In the completion of getGenreKeys(), you return the value. CoreData saves the value in the method, but that's all.
It's:
API -> Parse Value -> Save in CoreData
-> Completion -> Reload TableView to Display
Now in your CoreData stack, you have a method saveGenreList(json:). Assuming it works, you save it but never retrieve it.
There is no NSFetchRequest in your code. Look how to execute fetch in CoreData.
Just stop thinking about CoreData, to understand the issue:
let retrieveValueFromAPI = "I got it"
let savedValue = retrieveValueFromAPI // (1)
return retrieveValueFromAPI
In (1), you'll have an unused variable warning from Xcode. It's exactly the same issue. Here its a "RAM value" instead of a disk value, but that's the same logic. You don't use what you've saved.
You need to continue working and debug. And debug is not only fixing the issue. There are a few steps:
Reproduce the issue
Find its origin/understand what could be the issue
Fix the issue
Finding its origin, and why, are the steps you were missing, either by misconcept, or not enough step by step research in your code.

Removing value from UITableView causes crash

I have a UITableView and when I click on a cell I update a value in firebase to be true. I also implemented the ability to delete the UITableViewCell. The problem is that if the cell has been clicked on (updated) and then deleted the app crashes. When I have a look in firebase the value that has been updated has not been deleted (everything else has been), this only happens if the cell has been updated. This is what I got
// remove task
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let taskToDelete = groupTask[indexPath.row]
if editingStyle == UITableViewCellEditingStyle.delete {
groupTask.remove(at: indexPath.row)
DataService.instance.REF_GROUPS.child(group!.key).child("task").child(taskToDelete.id).removeValue(completionBlock: { (error, refer) in
if error != nil {
} else {
}
})
self.tableView.reloadData()
}
}
// update task value
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? GroupTaskCell else { return }
if cell.isSelected == true {
let selected = groupTask[indexPath.row]
DataService.instance.REF_GROUPS.observe(.value) { (snapShot) in
DataService.instance.updateTaskStatus(desiredGroup: self.group!, selected: true, atIndexpath: indexPath.row, childPath: selected.id, handler: { (complete) in
self.tableView.reloadData()
})
}
} else {
}
self.tableView.reloadData()
}
// update task status
func updateTaskStatus(desiredGroup: Group, selected: Bool, atIndexpath: Int, childPath: String, handler: #escaping (_ taskArray: [Task]) -> ()) {
REF_GROUPS.child(desiredGroup.key).child("task").child(childPath).updateChildValues(["selected": selected])
}
EDIT!
Could not cast value of type 'NSNull' (0x1b5915f18) to 'NSString' (0x1b5921ad8).
2018-01-01 17:38:53.431901+0200 WireUp[587:103199] Could not cast value of type 'NSNull' (0x1b5915f18) to 'NSString' (0x1b5921ad8).
(lldb)
What firebase looks like before I delete the task:
And after:
As you can see it is not removing the value that has been updated and that is what's causing the crash. I appreciate all help.
func addTask(withTask task: String, andPeople people: String, forUID uid: String, withGroupKey groupKey: String?, selectedStatus selected: Bool, sendComplete: #escaping (_ taskCreated: Bool ) -> ()) {
if groupKey != nil {
REF_GROUPS.child(groupKey!).child("task").childByAutoId().updateChildValues(["taskToDo": task, "peopleToDoTask": people, "senderId": uid, "selected": selected])
sendComplete(true)
} else {
}
}
func updateTaskStatus(desiredGroup: Group, selected: Bool, childPath: String, handler: #escaping (_ taskArray: [Task]) -> ()) {
REF_GROUPS.child(desiredGroup.key).child("task").child(childPath).updateChildValues(["selected": selected])
}
func configureTaskCell(taskToDo task: String, nameForPerson name: String, isSelected: Bool ) {
self.task.text = task
self.name.text = name
if isSelected {
self.chechImg.isHidden = false
} else {
self.chechImg.isHidden = true
}
}
Instead of using self.tableView.reloadData() from within the table view delegate functions try updating and deleting cells using the beginUpdate() and endUpdate() methods of UITableView. You won't crash and you won't be reloading the whole table. In Obj-C, it looks like this for removing and updating a cell:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
[tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects: indexPath, nil] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
It seems as though the code you've provided isn't the issue. It's being deleted correctly but as you delete it, it's also updating again with just the selected: true. Somewhere in your code, it'll be trying to fetch the rest of the data, including the selected: true BUT it's not there; resulting in the crash. You may want to do some debugging to see where you're actually updating this selected value to your database and get rid of it

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Catch event in view from another class

I have async task with request where i fetching products every 3 seconds in class Item.
class Item: NSManagedObject {
var is_fetching:Bool = false;
func fetchProducts(q: String) {
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
self.is_fetching = true;
//some code
if ((response as! NSHTTPURLResponse).statusCode == 202) {
sleep(3)
self.fetchProducts(q)
return
}
if ((response as! NSHTTPURLResponse).statusCode == 200) {
self.is_fetching = false;
}
})
task.resume()
}
}
And i have UITableViewController where i show data from response. How do i update my cells when status code is 200:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:
indexPath) as! CartTableViewCell
if item.is_fetching {
cell.fetchIndicator.startAnimating();
} else {
cell.fetchIndicator.stopAnimating();
cell.fetchIndicator.hidden = true;
}
}
You can do it in few ways.
NSNotificationCenter (simplest).
You can post notifications, that will trigger your controller's methods. Looks like this:
// if response.code == 200
NSNotificationCenter.defaultCenter().postNotificationName("kSomeConstatnString", object: nil)
...
// in viewDidLoad of your controller:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateTable", object: nil)
// you also need implement updateTable() func inside your controller
// or if you need just update table
NSNotificationCenter.defaultCenter().addObserver(self.tableView, selector: "reloadData", object: nil)
// do not forget to delete observer (for instance in -deinit method)
NSNotificationCenter.defaultCenter().removeObserver(self)
// or tableView. also you can specify for which selector, if you use couple of them.
Delegate pattern.
You can describe your protocol, make your controller implement this protocol and save it as instance in your model object. Then just call methods from delegate. Details here.
Block callbacks.
Create block for action and call it from your model. For example:
// inside controller
model.refreshCallback = { Void in
self.tableView.reloadData() // or whatever
}
// inside model
var refreshCallback: (() -> Void)?
...
// if result.code == 200
if let callback = refreshCallback {
callback()
}
Use one of the UITableView’s reload functions, perhaps:
func reloadRowsAtIndexPaths(_ indexPaths: [NSIndexPath],
withRowAnimation animation: UITableViewRowAnimation)
This will cause it to ask again for the cell in question. Make sure you do this on the main thread.

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

Resources