I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we don't get indexing error
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
self.refresher.endRefreshing()
}
It appears you are removing everything from users and then trying to access it in getPost.findObjectsInBackgroundWithBlock.
Specifically, self.users.removeAll(keepCapacity: true) is called just before self.usernames.append(self.users[object["userId"] as! String]!).
Also, be aware that the two queries execute asynchronously. You have no guarantee that users has been populated when the second query's completion block is reached. You will most likely want to restructure your two queries into one compound query or nest them (not recommended).
Lastly, from the looks of it, you may want to use removeAll(keepCapacity: false) rather than keeping the capacity with removeAll(keepCapacity: true).
Related
I would like to add read-only example/tutorial data to my Core Data based macOS app.
I will include an SQL file in my application bundle containing the example data. My NSPersistentContainer will have 2 NSPersistentStores, one writable and one read only. I will only have a default configuration for my model since both stores will have the same model.
My UI will need to know if the data displayed is read only or not, for example, to stop this data being draggable.
I know that NSManagedObject does not support a readonly state, see and : Is it possible to return NSManagedObjects as read-only in Core Data? ...and the docs.
I think the best approach would be to add a readonly property to my NSManagedObject derived class that can be queried where necessary. However, I can't see how I could easily set this property! I can't find a direct link to an NSPersistentStore from an NSManagedObject.
I could set up an NSFetchRequest and specify the read only store and see if the NSManagedObject is in it, but that seems a little ridiculous.
Am I missing something more obvious here please?
With thanks to pbasdf for his suggestion...
I could find no straight-forward way to achieve this. I had to move away from using NSPersistentContainer to simplify my Core Data stack. However, I think this is a fairly elegant solution if you need a small subset of your graph to be readonly.
I subclassed NSPersistentStoreCoordinator to cache the NSManagedObjectIDs of any readonly store added to it:
class GraphStoreCoordinator: NSPersistentStoreCoordinator
{
override init(managedObjectModel model: NSManagedObjectModel)
{
readOnlyTestContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
super.init(managedObjectModel: model)
readOnlyTestContext.persistentStoreCoordinator = self
NotificationCenter.default
.addObserver(forName: .NSPersistentStoreCoordinatorStoresDidChange,
object: self, queue: nil) { [unowned self] notification in
// userInfo will be in this form for add/remove keys - not supporting migration here
guard let userInfo = notification.userInfo as? [String: [NSPersistentStore]] else {
unhandledError("Invalid userInfo for NSPersistentStoreCoordinatorStoresDidChange.") }
userInfo[NSAddedPersistentStoresKey]?.forEach { self.didAddStore($0) }
userInfo[NSRemovedPersistentStoresKey]?.forEach { self.didRemoveStore($0) }
}
}
deinit {
NotificationCenter.default
.removeObserver(self, name: .NSPersistentStoreCoordinatorStoresDidChange, object: self)
}
private func didAddStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
var addedObjects = Set<NSManagedObjectID>()
baseEntityNames.forEach { entityName in
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
fetchRequest.affectedStores = [store]
do {
let addedEntityObjects = try readOnlyTestContext.fetch(fetchRequest)
addedObjects = addedObjects.union(addedEntityObjects.map { $0.objectID })
} catch {
unhandledError("Failed to fetch all \(entityName) for read only check: \(error)") }
}
readOnlyObjects[store.identifier] = addedObjects
}
private func didRemoveStore(_ store: NSPersistentStore) {
guard store.isReadOnly else { return }
readOnlyObjects.removeValue(forKey: store.identifier)
}
/// Returns the minimum set of entities that can be fetched for readonly checking
private lazy var baseEntityNames: [String] = {
return managedObjectModel.entitiesByName.compactMap { $1.superentity == nil ? $0 : nil }
}()
private var readOnlyTestContext: NSManagedObjectContext
/// Readonly objectIDs keyed per persistent store
private var readOnlyObjects = [String : Set<NSManagedObjectID>]()
internal func isObjectReadOnly(_ objectID: NSManagedObjectID) -> Bool {
return readOnlyObjects.contains(where: { $1.contains(objectID) } )
}
}
I then added an extension to NSManagedObject to that queries its NSPersistentStoreCoordinator for read-only status:
public extension NSManagedObject
{
/// Does this managed object reside in a read-only persistent store?
var isReadOnly: Bool {
guard let coordinator = managedObjectContext?
.persistentStoreCoordinator as? GraphStoreCoordinator else {
unhandledError("Should only check readonly status in a GraphStoreCoordinator") }
return coordinator.isObjectReadOnly(objectID)
}
}
I'm trying to delete an entire record out of coreData. I've retrieved the data and placed it in an array for manipulation (I have another function that lets the user edit the data using this method and it works fine) But I can't figure out how to just delete the record. [.remove(at: index)] doesn't work and neither does the code below. I can set all the fields to empty but that's not what I want, I want the record gone completely.
I went through the solutions given for similar problems but to no avail
#IBAction func Delete(_ sender: UIButton) { // The delete function
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let context = appDelagate.persistentContainer.viewContext
var destArray = [DestinationsOne]() // The data array
do {
try destArray = context.fetch(request) as! [DestinationsOne]} //Fetching the data and placing it in the array
catch{
//error message
}
for index in (0..<destArray.count - 1){ //Go through the records
if destArray[index].destID == IDTitle!{ //Picks the record to edit
let object = destArray[index]
context.delete(object
}
appDelagate.saveContext()
}
I figured this one out. I'm posting the solution in case anyone else has the same question
func deleteRecords() -> Void { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let result = try? moc.fetch(fetchRequest)
let resultdata = result as! [DestinationsOne] // result as entity
for object in resultdata { // Go through the fetched result
if object.destID == self.IDTitle{ // If there is a match
moc.delete(object) // delete the object
}
}
do {
try moc.save() // Save the delete action
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
deleteRecords() // Call the function
Why not applying a predicate to search this particular record. It's much more efficient than looping through a huge list.
func deleteRecords() { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<DestinationsOne>(entityName: "DestinationsOne")
let predicate = NSPredicate(format: "destID == %#", self.IDTitle)
fetchRequest.predicate = predicate
do {
let resultdata = try moc.fetch(fetchRequest) // no type cast needed
if let objectToDelete = resultdata.first {
moc.delete(objectToDelete) // delete the object
try moc.save() // Save the delete action
}
} catch {
print("Could not save error: ", error)
}
}
Here are some issues with your code:
viewContext should be treated as readonly - you should use performBackgroundTask for all changes to core-data
You are fetching ALL of the entities and then then going through each one to find the one you want to delete. It is a lot faster to have core-data only fetch the one you want. You can do this by setting a predicate to the fetch request.
Instead of displaying your records by doing a fetch and using the array as a model, it is better to use a NSFetchedResultsController to do the fetch and manage the results. The fetchedResultsController will keep the data in sync when objects are changed, inserted or deleted. It also has delegate methods that will inform you when there are changes so you can update your view.
remove appDelagate.saveContext from your project. Apple's template code is wrong. You should never be writing to the viewContext so you should never have a reason to save it.
where is IDTitle being set? are you sure it is not nil?
(minor) for index in (0..<destArray.count - 1){ can be replaced with for (index, element) in destArray.enumerated() { which is clearer to read.
In Swift 2 the following code was working:
let request = NSFetchRequest(entityName: String)
but in Swift 3 it gives error:
Generic parameter "ResultType" could not be inferred
because NSFetchRequest is now a generic type. In their documents they wrote this:
let request: NSFetchRequest<Animal> = Animal.fetchRequest
so if my result class is for example Level how should I request correctly?
Because this not working:
let request: NSFetchRequest<Level> = Level.fetchRequest
let request: NSFetchRequest<NSFetchRequestResult> = Level.fetchRequest()
or
let request: NSFetchRequest<Level> = Level.fetchRequest()
depending which version you want.
You have to specify the generic type because otherwise the method call is ambiguous.
The first version is defined for NSManagedObject, the second version is generated automatically for every object using an extension, e.g:
extension Level {
#nonobjc class func fetchRequest() -> NSFetchRequest<Level> {
return NSFetchRequest<Level>(entityName: "Level");
}
#NSManaged var timeStamp: NSDate?
}
The whole point is to remove the usage of String constants.
I think i got it working by doing this:
let request:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Level")
at least it saves and loads data from DataBase.
But it feels like it is not a proper solution, but it works for now.
The simplest structure I found that works in 3.0 is as follows:
let request = NSFetchRequest<Country>(entityName: "Country")
where the data entity Type is Country.
When trying to create a Core Data BatchDeleteRequest, however, I found that this definition does not work and it seems that you'll need to go with the form:
let request: NSFetchRequest<NSFetchRequestResult> = Country.fetchRequest()
even though the ManagedObject and FetchRequestResult formats are supposed to be equivalent.
Here are some generic CoreData methods that might answer your question:
import Foundation
import Cocoa
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T
{
let entityName = T.description()
let context = app.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let record = T(entity: entity!, insertInto: context)
return record
}
func recordsInTable<T: NSManagedObject>(_ type : T.Type) -> Int
{
let recs = allRecords(T.self)
return recs.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func query<T: NSManagedObject>(_ type : T.Type, search: NSPredicate?, sort: NSSortDescriptor? = nil, multiSort: [NSSortDescriptor]? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let predicate = search
{
request.predicate = predicate
}
if let sortDescriptors = multiSort
{
request.sortDescriptors = sortDescriptors
}
else if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteRecord(_ object: NSManagedObject)
{
let context = app.managedObjectContext
context.delete(object)
}
func deleteRecords<T: NSManagedObject>(_ type : T.Type, search: NSPredicate? = nil)
{
let context = app.managedObjectContext
let results = query(T.self, search: search)
for record in results
{
context.delete(record)
}
}
func saveDatabase()
{
let context = app.managedObjectContext
do
{
try context.save()
}
catch
{
print("Error saving database: \(error)")
}
}
Assuming that there is a NSManagedObject setup for Contact like this:
class Contact: NSManagedObject
{
#NSManaged var contactNo: Int
#NSManaged var contactName: String
}
These methods can be used in the following way:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
let contacts = query(Contact.self, search: NSPredicate(format: "contactName == %#", name))
for contact in contacts
{
print ("Contact name = \(contact.contactName), no = \(contact.contactNo)")
}
deleteRecords(Contact.self, search: NSPredicate(format: "contactName == %#", name))
recs = recordsInTable(Contact.self)
print ("Contacts table has \(recs) records")
saveDatabase()
This is the simplest way to migrate to Swift 3.0, just add <Country>
(tested and worked)
let request = NSFetchRequest<Country>(entityName: "Country")
Swift 3.0 This should work.
let request: NSFetchRequest<NSFetchRequestResult> = NSManagedObject.fetchRequest()
request.entity = entityDescription(context)
request.predicate = predicate
I also had "ResultType" could not be inferred errors. They cleared once I rebuilt the data model setting each entity's Codegen to "Class Definition". I did a brief writeup with step by step instructions here:
Looking for a clear tutorial on the revised NSPersistentContainer in Xcode 8 with Swift 3
By "rebuilt" I mean that I created a new model file with new entries and attributes. A little tedious, but it worked!
What worked best for me so far was:
let request = Level.fetchRequest() as! NSFetchRequest<Level>
I had the same issue and I solved it with the following steps:
Select your xcdatamodeld file and go to the Data Model Inspector
Select your first Entity and go to Section class
Make sure that Codegen "Class Definition" is selected.
Remove all your generated Entity files. You don't need them anymore.
After doing that I had to remove/rewrite all occurences of fetchRequest as XCode seem to somehow mix up with the codegenerated version.
HTH
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func loadItemsCategory() {
let request: NSFetchRequest<Category> = Category.fetchRequest()
do {
categoryArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}
I am using Parse and have created a subclass of PFObject. When creating objects it makes things much easier. Once objects are created, I am experimenting with querying the database and accessing the custom properties I created. What I am finding is that I cannot use dot notation to access the properties when I am working the the PFObjects returned from the query. Is this normal?
Here is subclass I created.
import Foundation
import UIKit
import Parse
class MessagePFObject: PFObject
{
#NSManaged var messageSender : String
#NSManaged var messageReceiver : String
#NSManaged var messageMessage : String
#NSManaged var messageSeen : Bool
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Custom Query method.
override class func query() -> PFQuery?
{
let query = PFQuery(className: MessagePFObject.parseClassName())
query.includeKey("user")
query.orderByDescending("createdAt")
return query
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
init(messageSenderInput: String?, messageReceiverInput: String?, messageMessageInput: String?)
{
super.init()
self.messageSender = messageSenderInput!
self.messageReceiver = messageReceiverInput!
self.messageMessage = messageMessageInput!
self.messageSeen = false
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override init()
{
super.init()
}
}
//++++++++++++++++++++++++++++++++++++ EXTENSION +++++++++++++++++++++++++++++++++++++++++++++++++++
extension MessagePFObject : PFSubclassing
{
class func parseClassName() -> String
{
return "MessagePFObject"
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override class func initialize()
{
var onceToken: dispatch_once_t = 0
dispatch_once(&onceToken) {
self.registerSubclass()
}
}
}
Here is my query and what I am required to do to access the properties. createdAt, updatedAt, etc are all available with dot notation but none of my custom properties are. You can see I access messageSeen with element.objectForKey("messageSeen").
let predicate = NSPredicate(format: "messageSender == %# OR messageReceiver == %#", self.currentUser!.username!, self.currentUser!.username!)
let query = messagePFObject.queryWithPredicate(predicate)
query!.findObjectsInBackgroundWithBlock ({ (objects, error) -> Void in
if error == nil && objects!.count > 0
{
for element in objects!
{
print(element)
print(element.parseClassName)
print(element.objectId)
print(element.createdAt)
print(element.updatedAt)
print(element.objectForKey("messageSeen"))
}
}
else if error != nil
{
print(error)
}
})
If this is normal then that is fine. I just want to make sure I am not missing something.
Take care,
Jon
Your object subclass has to implement the PFSubclassing protocol and you need to call MessagePFObject.registerSubclass() in your app delegate.
The parse documentation is very good : https://parse.com/docs/ios/guide#objects-subclasses
I'm trying to pass data from iPhone -> Watch via Watch Connectivity using background transfer via Application Context method.
iPhone TableViewController
private func configureWCSession() {
session?.delegate = self;
session?.activateSession()
print("Configured WC Session")
}
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let matchupDict = ["matchupSaved" : MatchupObject]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
I'm getting error twice printed in the log (I have two matchups in Parse so maybe it knows there's two objects and thats why its throwing two errors too?):
Configured WC Session
error
error
So I haven't even gotten to the point where I can print it in the Watch app to see if the matchups passed correctly.
Watch InterfaceController:
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
let matchupWatch = applicationContext["matchupSaved"] as? String
print("Matchups: %#", matchupWatch)
}
Any ideas? Will post any extra code that you need. Thanks!
EDIT 1:
Per EridB answer, I tried adding encoding into getParsePassData
func getParsePassData () {
let gmtTime = NSDate()
// Query Parse
let query = PFQuery(className: "data")
query.whereKey("dateGame", greaterThanOrEqualTo: gmtTime)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil
{
if let objectsFromParse = objects as? [PFObject]{
for MatchupObject in objectsFromParse
{
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
let matchupDict = ["matchupSaved" : data]
do {
try self.session?.updateApplicationContext(matchupDict)
print("getParsePassData iPhone")
} catch {
print("error")
}
}
}
}
}
}
But get this in the log:
-[PFObject encodeWithCoder:]: unrecognized selector sent to instance 0x7fbe80d43f30
*** -[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
EDIT 2:
Per EridB answer, I also tried just pasting the function into my code:
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
But get this error on the first line of the function:
"Use of unresolved identifer MatchupObject"
I'm sure I must not be understanding how to use EridB's answer correctly.
EDIT 3:
NSCoder methods:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
//super.init(coder: aDecoder)
configureWCSession()
// Configure the PFQueryTableView
self.parseClassName = "data"
self.textKey = "matchup"
self.pullToRefreshEnabled = true
self.paginationEnabled = false
}
Error
You are getting that error, because you are putting a NSObject (MatchupObject) which does not conform to NSCoding inside the dictionary that you are going to pass.
From Apple Docs
For most types of transfers, you provide an NSDictionary object with
the data you want to send. The keys and values of your dictionary must
all be property list types, because the data must be serialized and
sent wirelessly. (If you need to include types that are not property
list types, package them in an NSData object or write them to a file
before sending them.) In addition, the dictionaries you send should be
compact and contain only the data you really need. Keeping your
dictionaries small ensures that they are transmitted quickly and do
not consume too much power on both devices.
Details
You need to archive your NSObject's to NSData and then put it in the NSDictionary. If you archive a NSObject which does not conform to NSCoding, the NSData will be nil.
This example greatly shows how to conform a NSObject to NSCoding, and if you implement these things then you just follow the code below:
//Send the dictionary to the watch
func sendObjectToWatch(object: NSObject) {
//Archiving
let data = NSKeyedArchiver.archivedDataWithRootObject(MatchupObject)
//Putting it in the dictionary
let matchupDict = ["matchupSaved" : data]
//Send the matchupDict via WCSession
self.session?.updateApplicationContext(matchupDict)
}
//When receiving object from the other side unarchive it and get the object back
func objectFromData(dictionary: NSDictionary) -> MatchupObject {
//Load the archived object from received dictionary
let data = dictionary["matchupSaved"]
//Deserialize data to MatchupObject
let matchUpObject = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MatchupObject
return matchUpObject
}
Since you are using Parse, modifying an object maybe cannot be done (I haven't used Parse in a while, so IDK for sure), but from their forum I found this question: https://parse.com/questions/is-there-a-way-to-serialize-a-parse-object-to-a-plain-string-or-a-json-string which can help you solve this problem easier than it looks above :)