performBackgroundTask Fault Error when fetching data in Core Data - swift

I am encoutering a strange issue:
I am using a backgroundFetch to fetch the data from Core Data.
func fetchDataFromCoreData(completion:#escaping()->Void)
{
let appdel = UIApplication.shared.delegate as! AppDelegate
let context = appdel.persistentContainer.viewContext
appdel.persistentContainer.performBackgroundTask { (context) in
let fetchReq = NSFetchRequest<NSFetchRequestResult>(entityName: "FreeApps")
do
{
let data = try context.fetch(fetchReq) as? [NSManagedObject]
self.resultData = data
print(self.resultData ?? "data is empty")
DispatchQueue.main.async{
completion()
}
}
catch
{
print("fetch error")
}
}
}
Now in my view Controller, in my table cell:
let myDict = itunesViewModelObj.resultData?[indexPath.row] as? NSManagedObject
print(myDict?.value(forKey: "name") as? String ?? "no name")myDict shows as fault but valefor key comes nil
Now if I comment the performBackgroundTask line data comes properly.
Please help as what can be the issue with backgroundTask.
Any suggestions will be highly appreciated!
Thanks in advance!

The PersistentContainer operates on the main queue. As the name of the property implies, this managed object context is designed to be used in combination with the application's user interface. Maybe you need to dispatch back to the main queue to interact with UIApplicationDelegate and PersistentContainer.
PerformBackgroundTask is generally used for updates to core data. If you are doing fetches you should use the main viewContext.
Maybe using...
DispatchQueue.main.async {
// your code
}

Related

Can't fill my collection views with API data by using Alamofire

There is an api (https://docs.api.jikan.moe/#section/Information). I get data from it, but I can’t display them in my collection views in any way. The data should come, I checked. I implement filling the collection view cells through the view model ViewController <-> ViewModel and with Network Manager API Manager
The result is just white collectionView - Screen
For the first time I decided to work with Alamofire and apparently I don’t understand something. Please tell me what is the problem. Link to github in case someone needs it.
Updated
The problem might be with asynchronous coding. And i still have no ideas to fix it, cause don't understand the GCD as well. Screen
func fetchRequest(typeRequest: TypeRequest) -> [AnimeModel] {
var animeModels: [AnimeModel] = []
switch typeRequest {
case .name(let name):
let urlString = "https://api.jikan.moe/v4/anime?q=\(name)"
AF.request(urlString).response { response in
guard let data = response.data else { return print("NO DATA FOR - \(name)") }
do {
let json = try JSON(data: data)
let title = json["data"][0]["title_english"].string ?? "Anime"
let imageURL = json["data"][0]["images"]["jpg"]["image_url"].string ?? ""
let image = AnimeModel.downloadImage(stringURL: imageURL)
animeModels.append(AnimeModel(image: image, title: title))
print(".NAME ANIME MODELS - \(animeModels)")
} catch let error {
print(error.localizedDescription)
}
}
}
print("BEFORE RETURN ANIME MODELS - \(animeModels)")
return animeModels // returns empty array and then "animeModel.append()" is applied
}

NSFetchRequestResult returns duplicate data from the database

I have a simple entity with identifier (constraint) and name fields. In ViewController I try to add data to the database without worrying about duplicates and there really is no duplicate data in the database, but when I try to get records, I get twice as many of them. As I found out, this only happens when I try to write something to the database, and regardless of whether the attempt was successful, the data goes to my NSFetchRequestResult. What is the reason for this behavior?
DB content now:
ViewController:
class ViewController: UIViewController {
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
//if comment this loop I won't get duplicates
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
try? moc.save() //if the code is not executed for the first time, then the attempt is unsuccessful due to identifier constraint
}
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Entity")
let fetchedEntities = try? moc.fetch(fetch) as? [Entity]
print(fetchedEntities!.count) // Output: 12 (actually only 6 records in the db)
}
}
Change your code where you create the objects to
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
}
do {
try moc.save()
} catch {
moc.reset()
}
This way you will remove the faulty (duplicate) objects from the context

Fetching data from Core Data on application launch

I need to check for items in my Core Data when the app launches. this is what I wrote so far, but I don't think this is the best practice (although it is seems to be working) is there any other way? better way to acheive what I need?
func getMealsFromCoreData() -> [Meal]{
var retrivedMeals = [Meal]()
let appDelegate = UIApplication.shared.delegate as? AppDelegate
let managedContext = appDelegate?.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Meal")
do {
let result = try managedContext?.fetch(fetchRequest)
for data in result as! [NSManagedObject]{
retrivedMeals.append(data as! Meal)
}
}catch{
print("Failed to fetch.")
}
return retrivedMeals
}
func uploadMeals(){
var mealsToUpload = [Meal]()
let dispatchQueue = DispatchQueue(label: "mealQueue", qos: .background)
dispatchQueue.async {
mealsToUpload = self.getMealsFromCoreData()
for meal in mealsToUpload {
print(meal)
}
}
}
Upload meal should upload to meals to the screen after fetching everything (maybe not the best function name).
This is what I wrote in the AppDelegate in didFinishLaunchingWithOptions:
DataManager.manager.uploadReports()
I get in getMealsFromCoreData a purple warnning that says appDelegate should run on the main queue.
I'm really confused with this CoreData + Moving CoreData fetch from the main thread to the background.
Would really appericiate your help guys.
Am I doing something wrong? What is the best practice for this?
Well, the warning says what you are doing wrong.
The best practice depends on the use case. But it is certainly not fetching all data on application start. Fetch the data when you need it, as much data as you need (to display).
Maybe have a NSFetchedResultsController to maintain the data for your view if that is an option for you.

downloading and caching images from url asynchronously

I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.

Show indicator when save core data Swift

I have a button to save picture data in core data but when I push it, it is freezing because size of the data is big. I did try to use dispatch_async but it didn’t work. How do I create the icon/indicator showing that it is loading/bookmarking rather than just freezing?
#IBAction func save() {
let content = self.foodMenu?["content"].string
let urlString = self.foodMenu?["thumbnail_images"]["full"]["url"]
let urlshare = NSURL(string: urlString!.stringValue)
let imageData = NSData(contentsOfURL: urlshare!)
let images = UIImage(data: imageData!)
dispatch_async(dispatch_get_main_queue(), {
if let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext {
self.foodClass = NSEntityDescription.insertNewObjectForEntityForName("Foods",
inManagedObjectContext: managedObjectContext) as! Foods
self.foodClass.content = content
self.foodClass.image = UIImageJPEGRepresentation(images, 1)
var e: NSError?
if managedObjectContext.save(&e) != true {
println("insert error: \(e!.localizedDescription)")
return
}
}
First, it is unlikely it is the save that is slow. I would suspect that your creation of the JPEG representation is the slow part.
Second, you are wanting to hide a problem by putting up a spinner. That really is bad for the user experience. Far better to do the following (yes it is more code);
Move your image creation and saving to a background queue.
Restructure your Core Data stack so that your saves to disk are on a private queue.
This involves using a background queue and multiple contexts in Core Data but getting this data processing off the User Interface thread is the right answer.