We are facing issues while integrating coredata with our chat application. Please help us to resolve the issue. We tried to figure out each issue individually but sometimes it gets fixed and then shows up randomly. We are tring to fix it from last 1 week.
Our setup stack
We are using sockets library to for real time chatting. To persist the data we are using core-data. Our application is supporting iOS 8 and above so we can't use PersistenceContainer so to workoround this we are using BNRCoreDataStack [url: https://github.com/bignerdranch/CoreDataStack] which is similiar to what PersistenceContainer does.
Also to display chat we are using IGListKit and we have created viewModels to avoid sending mutable coredata objects to IGLIstkit as IGListkit works fine with immutable model. Also we have used this setup to create our own viewModels [url: https://github.com/Instagram/IGListKit/blob/master/Guides/Working%20with%20Core%20Data.md]
issues we are facing
1] Constraint validation failure
2] FetchResult controller crash issue
crash-log:
2018-07-19 21:41:36.515153+0530 Toppr Doubts[62803:2359707] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3698.54.4/UITableView.m:2012
2018-07-19 21:41:36.517093+0530 Toppr Doubts[62803:2359707] [error] error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (60) must be equal to the number of rows contained in that section before the update (50), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (60) must be equal to the number of rows contained in that section before the update (50), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
3] Illegal attempt to establish a relationship 'lastMessage' between objects in different contexts
Below is our CoreDataModel, we are not using any abstract entity for Message as we came to know this could cause lot of performance issues.
Session Entity
We are creating object in fromJSON method and setup is same for rest of the entities. Also I am sharing LastMessage Entity to show the way we are integrating relationships, again same for others
public class Session: NSManagedObject {
class func fromJSON(_ json: JSON, moc: NSManagedObjectContext) -> Session? {
if let entityDescription = NSEntityDescription.entity(forEntityName: "Session", in: moc) {
// Object creation
let object = Session(entity: entityDescription, insertInto: moc)
object.internalIdentifier = json[kSessionIdKey].int64Value
if let date = json[kSessionStartedOnKey].dateTime as NSDate? {
object.startedOn = date
}
if let date = json[kSessionEndedOnKey].dateTime as NSDate? {
object.endedOn = date
}
object.statusType = SessionStatus(rawValue: json[kSessionStatusKey].stringValue).map { $0.rawValue } ?? SessionStatus.none.rawValue
object.stateType = SessionState(rawValue: json[kSessionStateKey].stringValue).map { $0.rawValue } ?? SessionState.none.rawValue
if let ratingDict = json[kSessionRatingKey].dictionaryObject {
if let rating = ratingDict["student"] as? Int {
object.rating = rating
}
}
object.subjectID = json[kSessionSubjectKey]["id"].intValue
// Subjects are already stored need to fetch and assign
let subjectFetchRequest: NSFetchRequest<Subject> = Subject.fetchRequest()
subjectFetchRequest.predicate = NSPredicate(format: "internalIdentifier == %d", Int64(json[kSessionSubjectKey]["id"].intValue))
do {
if let subject = try moc.fetch(subjectFetchRequest).first {
object.subject = subject
}
} catch let erroe as NSError {
Logger.log.error(error)
}
// Student Object initialisation
if json[kSessionStudentKey] != JSON.null {
if let student = Student.fromJSON(json[kSessionStudentKey], moc: moc) {
object.student = student
}
}
// Tutor Object initialisation
if json[kSessionTutorKey] != JSON.null {
if let tutor = Tutor.fromJSON(json[kSessionTutorKey], moc: moc) {
object.tutor = tutor
}
}
// LastMessage Object initialisation
if json[kSessionLastMessageKey] != JSON.null {
if let lastMessage = LastMessage.fromJSON(json[kSessionLastMessageKey], moc: moc) {
object.lastMessage = lastMessage
} else {
return nil
}
}
return object
}
return nil
}
}
LastMessage
public class LastMessage: NSManagedObject, Message {
class func fromJSON(_ json: JSON, moc: NSManagedObjectContext) -> LastMessage? {
if let entityDescription = NSEntityDescription.entity(forEntityName: "LastMessage", in: moc) {
let object = LastMessage(entity: entityDescription, insertInto: moc)
object.id = json[kMessageIdKey].intValue
object.body = json[kBodyKey].stringValue
object.type = MessageType(rawValue: json[kTypeKey].stringValue) ?? MessageType.none
object.doubtTag = json[kDoubtTagKey].stringValue
if json[kAttachmentKey] != JSON.null {
if let attachment = Attachment.fromJSON(json[kAttachmentKey], moc: moc) {
object.attachment = attachment
}
}
if let date = json[kSentOnKey].dateTime as NSDate? {
object.sentOn = date
}
if json[kSentByKey] != JSON.null {
if let sentBy = SentBy.fromJSON(json[kSentByKey], moc: moc) {
object.sentBy = sentBy
}
}
object.deliveryState = DeliveryState(rawValue: json[kDeliveryStateKey].stringValue) ?? DeliveryState.none
object.sessionId = json[kSessionIdKey].intValue
return object
}
return nil
}
}
Get User State
Helps us fetch data for subject and live chat.
static func getUserState(completion:#escaping (_ success: Bool) -> Void) {
SocketManager.sharedInstance.send(eventName: .userState) { (response) in
guard !response.isError() else { return completion(false) }
// Coredatastack
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return }
let wmoc = coreDataStack.newChildContext()
// Save Subject and Live Sessions
let subjects = response.result["subjects"].arrayValue.flatMap({ Subject.fromJSON($0, moc: wmoc) })
let sessions = response.result["live_sessions"].arrayValue.flatMap({ Session.fromJSON($0, moc: wmoc) })
if sessions.isNotEmpty || subjects.isNotEmpty {
CoreDataStack.batchUpdate(moc: wmoc, completion: {
NotificationCenter.default.post(name: NSNotification.Name("didUpdateUserState"), object: nil)
completion(true)
})
}
completion(false)
}
}
Get Previous Session
Helps us fetch data for inactive chats. We are getting problem in this while storing sessions
static func getPreviousSessions(isLoadingMore: Bool, completion: #escaping (_ success: Bool,_ isMore: Bool)->Void) {
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return }
let wmoc = coreDataStack.newChildContext()
var sessionID = 0
// TODO: - Need to implement last sessionID from CoreData
if isLoadingMore {
// Get Sessions with Status == closed order by sessionID asc
let archiveSession = Session.filterArchivedSessions(moc: wmoc)
let sortedArchiveSession = archiveSession?.sorted(by: { $0.0.id < $0.1.id })
// Get last sessionID
if let lastSessionID = sortedArchiveSession?.first?.id {
sessionID = lastSessionID
}
}
let request: [String: Any] = [ "last_session_id": sessionID ]
SocketManager.sharedInstance.send(request, eventName: .getPreviousSessions) { (response) in
if response.result.isEmpty {
completion(false, false)
} else {
let sessions = response.result["sessions"].arrayValue.flatMap({ Session.fromJSON($0, moc: wmoc) })
if sessions.isNotEmpty {
CoreDataStack.batchUpdate(moc: wmoc)
} else {
for session in sessions { wmoc.delete(session) }
}
if let isMore = response.result["is_more_server"].bool {
completion(true, isMore)
}
}
}
}
In the above image every session is suppose to have one last message and one subject. But as you can see, there are no last messages and some session have subjects as null
CoreDataStack+Extension
To Save data directly to Store
extension CoreDataStack {
// This method will add or update a CoreData's object.
static func batchUpdate(moc: NSManagedObjectContext? = nil, completion: (()-> Void)? = nil) {
guard let moc = moc, moc.hasChanges else { return }
if #available(iOS 10.0, *) {
moc.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
}
do {
try moc.performAndWaitOrThrow {
try moc.saveContextToStoreAndWait()
DispatchQueue.main.async {
completion?()
}
}
} catch {
print("Error creating initial data: \(error)")
}
}
}
NSFetchedResultsController Setup
lazy var archievedSessionFRC: NSFetchedResultsController<Session> = {
guard let coreDataStack = (UIApplication.shared.delegate as! AppDelegate).coreDataStack else { return NSFetchedResultsController() }
// Create Fetch Request
let fetchRequest: NSFetchRequest<Session> = Session.fetchRequest()
// Configure Fetch Request
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "internalIdentifier", ascending: false)]
fetchRequest.predicate = NSPredicate(format: "statusType = %#", "closed")
let archievedSessionFRC = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.mainQueueContext,
sectionNameKeyPath: nil,
cacheName: nil)
archievedSessionFRC.delegate = self
return archievedSessionFRC
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
do {
try archievedSessionFRC.performFetch()
if let sessions = archievedSessionFRC.fetchedObjects {
self.previousSessions = sessions
}
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension HomeVC: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let indexPath = newIndexPath {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
case .delete:
if let indexPath = indexPath {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
case .move:
if let indexPath = indexPath , let newIndexPath = newIndexPath {
self.tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .update:
if let indexPath = indexPath {
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
}
Thanks in Advance
Related
In my swift code below the code saves an item in core data. The goal is to overwrite that item. I am getting a runtime error at
CoreDataHandler.changeName(user: fetchUser!\[indexNumber\], jessica: "jo")
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
I don't know how to wrap in the index number. The goal is it to print judo then jo
import UIKit;import CoreData
class ViewController: UIViewController {
var fetchUser: [UserInfo]? = nil
var indexNumber : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
CoreDataHandler.saveObject2( name: "judo")
getText(textNo: indexNumber)
saveTheItem()
}
#objc func saveTheItem(){
CoreDataHandler.changeName(user: fetchUser![indexNumber], jessica: "jo")
}
func getText(textNo:Int) {
// first check the array bounds
let info = helpText.shareInstance.fetchText()
if info.count > textNo {
if let imageData = info[textNo].name
{
print(imageData)
} else {
// no data
print("data is empty Textss")
}
} else {
// image number is greater than array bounds
print("you are asking out of bounds")
}
}
}
class CoreDataHandler : NSManagedObject {
class func saveObject2( name: String) -> Bool {
let context = getContext()
let entity = NSEntityDescription.entity(forEntityName: "UserInfo", in: context)
let managedObject = NSManagedObject(entity: entity!, insertInto: context)
managedObject.setValue(name, forKey: "name")
do{
try context.save()
return true
}
catch {
return false
}
}
private class func getContext() -> NSManagedObjectContext{
let appD = UIApplication.shared.delegate as! AppDelegate
return appD.persistentContainer.viewContext
}
class func changeName(user: UserInfo,jessica : String) -> Bool
{
let context = getContext()
user.name = jessica
print(jessica)
do{
try context.save()
return true
}
catch{
return false
}
}
}
class helpText: UIViewController{
private class func getContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
static let shareInstance = helpText()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveName(data: String) {
let imageInstance = UserInfo(context: context)
imageInstance.name = data
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
func fetchText() -> [UserInfo] {
var fetchingImage = [UserInfo]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "UserInfo")
do {
fetchingImage = try context.fetch(fetchRequest) as! [UserInfo]
} catch {
print("Error while fetching the image")
}
return fetchingImage
}
}
No offense but your code is a mess.
And there is a big misunderstanding. Core Data records are unordered, there is no index. To update a record you have to fetch it by a known attribute, in your example by name, update it and save it back.
This is a simple method to do that. It searches for a record with the given name. If there is one, update the attribute with newName and save the record.
The code assumes that there is a NSManagedObject subclass UserInfo with implemented class method fetchRequest.
func changeName(_ name: String, to newName: String) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request : NSFetchRequest<UserInfo> = UserInfo.fetchRequest()
request.predicate = NSPredicate(format: "name == %#", name)
do {
let records = try context.fetch(request)
guard let foundRecord = records.first else { return }
foundRecord.name = newName
try context.save()
} catch {
print(error)
}
}
Regarding your confusing code:
Create CoreDataHandler as singleton (and it must not be a subclass of NSManagedObject). Move the Core Data related code from AppDelegate and the methods to read and write in this class.
I am using NSTableViewDiffableDataSource. When I do a multiple delete, I see the wrong insert-animation after (flicker and moving from top or bottom). How fix it?
// I am using UUID instead of NSManagedObjectID because when I create a new NSManagedObject it first has a temporary objectID.
func configureDataSource() {
let dataSource : NSTableViewDiffableDataSource<String, UUID> = .init(tableView: tableView) { table, column, index, objectID in
let request = NSFetchRequest<Task>()
request.entity = Task.entity()
request.predicate = NSPredicate(format: "id = %#", argumentArray: [objectID])
guard let task = try? self.viewContext.fetch(request).first as? Task else {
return NSView()
}
let cell = self.create(viewFor: column, task: task)
return cell
}
dataSource.defaultRowAnimation = .effectGap
dataSource.sectionHeaderViewProvider = nil
self.dataSource = dataSource
}
func storeDidReloadContent() {
var snapshot = NSDiffableDataSourceSnapshot<String, UUID>()
snapshot.appendSections([""])
snapshot.appendItems(store.objects.compactMap{ $0.id }, toSection: "")
dataSource.apply(snapshot, animatingDifferences: false)
}
func storeDidChangeContent(with snapshot: NSDiffableDataSourceSnapshotReference) {
var newSnapshot = NSDiffableDataSourceSnapshot<String, UUID>()
newSnapshot.appendSections([""])
newSnapshot.appendItems(store.objects.compactMap{ $0.id }, toSection: "")
dataSource.apply(newSnapshot, animatingDifferences: true)
}
// class ObjectFactory
// Batch operation
public func delete(objects: [T]) {
let objectIDs = objects.compactMap{ $0.objectID }
CoreDataStorage.shared.performBackground { privateContext in
objectIDs.forEach{
let object = privateContext.object(with: $0)
privateContext.delete(object)
}
try? privateContext.save()
}
}
PS: Store class (var store) incapsulate all works with NSFetchedResultController.
ObjectFactory class incapsulate all works with NSManagedObjects.
NSFetchedResultController works with only main NSManagedObjectContext.
Batch operation in NSTableView is pain :(
I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.
My swift code below when loaded places 3 items in the core data entity named "UserName". When the user enters a number into textfield enterT I want the label labelName to display it. So when the user enters 1 the label should display jessica biel because Jesical Biel is the first name entered. Someone stated the suggestion below to solve this problem. I dont know exactly how to do this.I have added a gif below.
Convert the entered number to Int. If this succeeds pass the integer to joke and fetch the record matching the idx attribute.
https://github.com/redrock34/index-fetch
import UIKit
import CoreData
class ViewController: UIViewController,UITextFieldDelegate {
#IBOutlet var labelName : UILabel!
#IBOutlet var enterT : UITextField!
lazy var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
openDatabse()
fetchData()
enterT.delegate = self
}
func textFieldDidEndEditing(_ textField: UITextField) {
guard let index = Int(textField.text!) else {
// display an alert about invalid text
return
}
joke(at: index - 1)
}
func joke(at index : Int) {
let fetchRequest = NSFetchRequest<Users>(entityName: "Users")
fetchRequest.predicate = NSPredicate(format: "idx == %d", Int32(index))
do {
if let user = try context.fetch(fetchRequest).first {
labelName.text = user.username
}
} catch {
print("Could not fetch \(error) ")
}
}
func openDatabse()
{
let names = ["kim kardashian", "jessica biel", "Hailey Rienhart"]
for i in 0..<names.count {
let newUser = Users(context: context)
newUser.username = names[i]
newUser.idx = Int32(i + 1)
}
print("Storing Data..")
do {
try context.save()
} catch {
print("Storing data Failed", error)
}
}
func fetchData()
{
print("Fetching Data..")
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Users")
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
let userName = data.value(forKey: "username") as! String
print("User Name is : "+userName)
}
} catch {
print("Fetching data Failed")
}
}}
Of course you have to assign values to the idx attribute and you have to assign the result of the fetch to the label.
First replace
let appDelegate = UIApplication.shared.delegate as! AppDelegate //Singlton instanc
var context:NSManagedObjectContext!
with
lazy var context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
Then replace both openDatabse and saveData with
func openDatabse()
{
let names = ["kim kardashian", "jessica biel", "Hailey Rienhart"]
for i in 0..<names.count {
let newUser = Users(context: context)
newUser.name = names[i]
newUser.idx = Int32(i + 1)
}
print("Storing Data..")
do {
try context.save()
} catch {
print("Storing data Failed", error)
}
}
Finally add a line in joke to display the value
func joke(at index : Int) {
let fetchRequest = NSFetchRequest<Users>(entityName: "Users")
fetchRequest.predicate = NSPredicate(format: "idx == %d", Int32(index))
do {
if let user = try context.fetch(fetchRequest).first {
labelName.text = user.username
}
} catch {
print("Could not fetch \(error) ")
}
}
It creates the records and assigns the proper indexes. Then entering a number in the text field should work.
But – once again – on each launch of the app the 3 records are inserted again with the same names and indexes. Be aware of that!
Im working with NSURLSession. I have an array with restaurants and i'm requesting the dishes for every restaurant in the array to the api. The dataTask works,i'm just having a real hard time trying to call a method only when the all dataTasks are finished.
self.findAllDishesOfRestaurants(self.restaurantsNearMe) { (result) -> Void in
if result.count != 0 {
self.updateDataSourceAndReloadTableView(result, term: "protein")
} else {
print("not ready yet")
}
}
the self.updateDataSourceAndREloadTableView never gets called, regardless of my completion block. Here is my findAllDishesOfRestaurants function
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
self.allDishes.addObjectsFromArray(dishesArray as [AnyObject])
print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray)
})
completion(result:allDishesArray)
}
}
}
And here is my the function where i perform the dataTasks.
func getDishesByRestaurantName(restaurant:Resturant, completion:(result:NSArray) ->Void) {
var restaurantNameFormatted = String()
if let name = restaurant.name {
for charachter in name.characters {
var newString = String()
var sameCharacter:Character!
if charachter == " " {
newString = "%20"
restaurantNameFormatted = restaurantNameFormatted + newString
} else {
sameCharacter = charachter
restaurantNameFormatted.append(sameCharacter)
}
// print(restaurantNameFormatted)
}
}
var urlString:String!
//not to myself, when using string with format, we need to igone all the % marks arent ours to replace with a string, otherwise they will be expecting to be replaced by a value
urlString = String(format:"https://api.nutritionix.com/v1_1/search/%#?results=0%%3A20&cal_min=0&cal_max=50000&fields=*&appId=XXXXXXXXXappKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",restaurantNameFormatted)
let URL = NSURL(string:urlString)
let restaurantDishesArray = NSMutableArray()
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(URL!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
do {
let anyObjectFromResponse:AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
if let asNSDictionary = anyObjectFromResponse as? NSDictionary {
let hitsArray = asNSDictionary.valueForKey("hits") as? [AnyObject]
for newDictionary in hitsArray! as! [NSDictionary]{
let fieldsDictionary = newDictionary.valueForKey("fields") as? NSDictionary
let newDish = Dish.init(dictionary:fieldsDictionary!, restaurant: restaurant)
restaurantDishesArray.addObject(newDish)
}
}
completion(result:restaurantDishesArray)
} catch let error as NSError {
print("failed to connec to api")
print(error.localizedDescription)
}
}
dataTask.resume()
}
Like i said before, I need to wait until the fun findAllDishesOfRestaurants is done. I tried writing my completion blocks but I'm not sure I'm doing it right. Any help is greatly appreciated. Thank
The problem is that you are calling the completion method in findAllDishesOfRestaurants before al tasks are complete. In fact, you are calling it once for each restaurant in the list, which is probably not what you want.
My recommendation would be for you to look into NSOperationQueue for two reasons:
It will let you limit the number of concurrent requests to the server, so your server does not get flooded with requests.
It will let you easily control when all operations are complete.
However, if you are looking for a quick fix, what you need is to use GCD groups dispatch_group_create, dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify as follows.
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let group = dispatch_group_create() // Create GCD group
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
dispatch_group_enter(group) // Enter group for this restaurant
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
// self.allDishes.addObjectsFromArray(dishesArray as [AnyObject]) <-- do not do this
// print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray) <-- No need for this, remove
dispatch_group_leave(group) // Leave group, marking this restaurant as complete
})
// completion(result:allDishesArray) <-- Do not call here either
}
}
// Wait for all groups to complete
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(result:allDishesArray)
}
}