I am using swift and AWS DynamoDB for mobile app. I followed the tutorial and can save data successfully. However , when I try to load data , i found I the saving and loading data always come after all tasks in the viewdidload finished, so I can not pass the data out in the same view? Is there any way to save or retire data immediately ?
below is my code
mapper.query(Table.self, expression: queryExpress).continueWith{(task: AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
print("test")
if let error = task.error as NSError? {
print("The requst failed. Error: \(error)")
}
if let paginatedOutput = task.result {
for item in paginatedOutput.items
{
print("quring")
//pass info out to array
}
}
return nil
}
Fetching data from the network is an asynchronous action. You can't delay loading the screen while it completes. It may take a long time. It might not ever complete.
Your view controller must handle the case that it doesn't have data yet, and update itself when that data becomes available. The first step to this is avoiding making network queries in your view controller. View controllers should never directly query the network. They should query model objects that outlive the view controller. The model objects are responsible for making queries to the network and updating themselves with the results. Then the view controller will update itself based on the model. The name for this pattern is Model View Controller and is fundamental to Cocoa development. (Search around for many tutorials and discussions of this pattern.)
But regardless of where you make the queries and store the data, you will always have to deal with the case where the data is not yet available, and display something in the meantime. Nothing can fix this in a distributed system.
When the query finishes successful, load the data into your view. You can send the query in your viewDidLoad method, but you need to present the data when it arrives using another method you call when the data did arrive.
Related
I’m working on an app that uses the new sharing support in iOS 15 using NSPersistentCloudKitContainer. I do not see a way to tell Core Data that an object, or set of objects, is no longer shared.
For example, if I create a set of objects and then call share(_:to:completion:), the objects are shared properly and moved to a new, custom CKRecordZone, as expected.
Now, if the user stops sharing an object using UICloudSharingController, the CKShare that Core Data created is properly deleted from CloudKit, but the objects are still in the custom zone, and Core Data still has the original CKShare associated with those objects. So, when I call fetchShares(matching:), I still get the CKShare, but of course, that is no longer valid. In the past, with my own code, I’d use UISharingControllers delegate to get notified that the user stopped sharing, and then update my model. But there doesn’t seem to be a way to tell Core Data about the change.
Forcing Core Data to fetch CloudKit changes by either moving the app to background and then foreground, or by stopping the app and relaunching does not cause Core Data to notice the change to the share.
Does anyone know how to tell Core Data that these objects are no longer shared?
I worked around this by always checking to see if the share actually exists in CloudKit, rather than relying on the existence of a CKShare from fetchShares(matching:). I get the URL from the CKShare returned from fetchShares(matching:) and call this:
private func remoteShare(at url: URL) async throws -> CKShare? {
do {
let metadata = try await cloudKitContainer.shareMetadata(for: url)
return metadata.share
} catch let error as CKError {
if error.retryable {
throw RemoteError.retryable
} else if error.userInterventionRequiredError {
throw RemoteError.userInterventionRequired
} else if error.code == .unknownItem {
return nil
} else {
throw RemoteError.remoteFailure
}
} catch {
throw RemoteError.remoteFailure
}
}
}
If I get unknownItem that means there is no share on the remote, so the object is not actually shared.
The RemoteError is my custom error handling, and I have some extensions on CKError to categorize the errors.
Another workaround to the issue is to create a copy of the object being shared, and then deleting the original. It's not elegant, but it works in the meantime. (Hopefully Apple will address this at some point with a better solution.)
The copy will be placed in the default zone like it was before you shared the original. You'll be left with an empty share zone, but I dealt with that by running a background task upon app launch that deletes all empty share zones, so it doesn't get out of hand.
I haven't tried the solution posted by Dharman, but I imagine it's slower than querying the local cache. Using the method above, you can still use the "isShared" code from the demo app.
I'm currently creating an app which stores information in a database which the users device must have in order for certain parts of the app to respond differently.
My question is, when should I be calling for this data from firebase.
e.g.
In one part of my app, the app needs to know if the user is currently "connected" to another user. Currently, it checks this against the database as the user presses on the tab bar icon where this information needs to be known, which takes a couple seconds. (checked in the viewdidload() override func)
Should I be grabbing all data from the database before the first view controller is even displayed?
Is there a way to share this between all the view controllers?
If I could load all data from the database into global variables on the device that all view controllers can see this would seem much easier, however i'm not sure if this is good practice.
What would you recommend?
My database structure:
Basically, right now, when the user opens the app and logs in, I need the 'name' and 'family' of each user to be stored for use across the whole app globally across all classes and view controllers.
In terms of the list. when the user clicks the view controller where the list is, currently i'm just running code like this
self.ref.child("familys").child(email.replacingOccurrences(of: ".", with: "")).child("list").observe(.value, with: { (DataSnapshot) in
if DataSnapshot.hasChildren() == false{
print("No list")
return
}
self.tableList = DataSnapshot.value as! [String]
self.tableView.reloadData()
}) { (Error) in
print(Error)
}
then it goes ahead and updates the list with the array 'tableList'.
This means the first time the user clicks to get to the shopping view, there is some delay before the list populates.
I'm not sure what the standard way is to go about grabbing data like this and when it should be done in a way which minimises data usage and database access frequency.
I think you are on the right track. Although you may think you need all the data stored globally so you can access it from all view controllers, you don't. You can pass the data between viewControllers through segues. If you are making a tab based app, you just pull the data necessary for that ViewController in the viewDidLoad() (just like you are doing).
The name of the game is to structure your Firebase database so that when you pull data, you can pull as little as possible to fill all the fields in the View Controller. Since Firebase uses a JSON structure, there is no shame in saving the same data twice in order to make a search faster.
That being said, I think a currentUser global variable is useful in your case. Assuming you have a current user (one user logged in), I would just create a User class that mimics the Firebase and instantiate one global variable called currentUser. Your currentUser object should contain enough information to go and pull anything you need for filling ViewControllers. For example if your User class has an email attribute, you can do:
self.ref.child("familys").child(currentUser.email.replacingOccurrences(of: ".", with: "")).child("list").observe(.value, with: { (DataSnapshot) in
if DataSnapshot.hasChildren() == false{
print("No list")
return
}
self.tableList = DataSnapshot.value as! [String]
self.tableView.reloadData()
}) { (Error) in
print(Error)
}
Global variables should be avoided when possible, but I think just reducing it to one global variable should be enough to get you going.
As for why you are getting a delay, I'm not sure. When you are pulling that little data, it should be extremely fast. If there really is a noticeable delay when pulling a list of 2 items, the issue might be elsewhere (network, simulator, etc.).
Here is my context:
When I launch my app I fetch local data from CoreData and fill a tableview with it. At the same time I send an asynchronous request to my webservice to fetch what will be the new content of my tableview.
As soon as the request sends me a response I delete all the instances of the current NSManagedObjects and create new ones with the new data I got. Then I replace the datasource of my tableview to an array of the new NSManagedObjectContexts instances.
My problem:
I'm getting an error : CoreData could not fulfill a fault for ... if I scroll my tableview when the request finished and is triggering the deletion/creation of my tableview's data source.
I understand that this problem occurs because I'm trying to access an old NSManagedObject instance while doesn't exist anymore as it is explained in the doc : Apple doc. But I have no idea of what are the best practices in my case.
I don't want to block the user until my request finished but I have to prevent any error if he accesses "old" data while the request didn't finish (e.g : what if the user taps on a cell and I pass an instance of an NSManagedObject to another viewcontroller but when the request finishes this object doesn't exist anymore ?)
I would appreciate any help !
I highly recommend you to use NSFetchedResultsController since it's sole purpuse is:
You use a fetched results controller to efficiently manage the results returned from a Core Data fetch request to provide data for a UITableView object.
When using a fetched results controller it is much easier to handle the Core Data events like insert, delete, update.
You say you have three sections in your table view? That's no problem, NSFetchedResultsController can handle all of that.
Take a look at this. Apple provides a very nice set of instructions on how to configure and use NSFetchedResultsController.
I am using Core Data to see whether messages in a table view have been seen before by the user. The way I do this is to save the message Id to Core Data the first time it is seen, and then I run a fetch request when I update the table view to see if there is an entry in the persistent memory with the same Id.
Now what I want to know is how I should most effectively implement my fetch request, based on how time consuming it is. Should I either run a request that returns all saved message Ids as an array when the view is loaded, and then in cellForRowAtIndexPathcheck if that array contains that cell's message Id, or run the fetch request with a predicate in cellForRowAtIndexPath? The latter would be my preferred method, but If i have 100 or so cells I wondered if this would be poor etiquette.
Any help would be very much appreciated.
This is my fetch Request :
func persistQuery(predicateValueString: String!) -> Bool! {
let fetchRequest = NSFetchRequest(entityName: "LogItem")
let predicate = NSPredicate(format: "itemText == %#", predicateValueString)
fetchRequest.predicate = predicate
var didFindResult = true
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] {
if fetchResults.count == 0 {
didFindResult=false
}
}
return didFindResult
}
The best way is to use a NSFetchedResultsController. It will optimize the fetching and the memory footprint as well. It is specifically designed for table views.
To get started, take a look at the Xcode template (Master/Detail, check Core Data). It is really quite simple.
Make sure you also implement the delegate methods - they will automatically be called when your managed objects change, so there is only minimal code that is executed to update the UI (an only if the object is actually on screen).
Presumably each of your table view cells represent a LogItem (the NSManagedObject subclass) with a property to indicate the read status. Once you change that, the delegate method will try to update it based on the index path.
That's all there is to it. With the fetched results controller you get a lot of optimization for free, so I would strongly recommend using it whenever you populate a table view with Core Data entities.
I have an object that needs to be initialised with data from network and doesn't really make sense without the downloaded data. But it seems to me that doing an asynchronous network call in its init method is not a good idea because the object will not be ready to user right away and it might cause confusion. Should I only use a basic init method that will alloc init all its properties to create an empty object, and have other (non-init) methods populate the data from the network which will be called explicitly by other objects (such as the view controller using this object)? What would be a good way to approach this?
I think that the solution comes from having the right order of code running:
1) Go to network and fetch data (show the user acivity indicator in this time)
2) Server return response -> fetch the response into your object
3) Show the data or use it