I'm trying to make an application that uses a web service to get some data from a directory, but I also need to save the data into the device, include images, to do so I'm using Alamofire and AlamofireImage framework for consuming the webservice. I save the generated objects in a database with Realm framework and for images I save the UIImage into a file.
Basically, the ViewController has a tableView which displays de data, but it seems laggy because of the images writing into files.
This is my writing function:
func saveImage(_ image: UIImage) {
if let data = UIImagePNGRepresentation(image) {
let name = "images/person_directory_\(id).png"
let docsDir = getDocumentsDirectory()
let filename = docsDir.appendingPathComponent(name)
let fm = FileManager.default
if !fm.fileExists(atPath: docsDir.appendingPathComponent("images").path) {
do {
try fm.createDirectory(at: docsDir.appendingPathComponent("images"), withIntermediateDirectories: true, attributes: nil)
try data.write(to: filename)
try! realm?.write {
self.imageLocal = name
}
}
catch {
print(error)
}
}
else {
do {
try data.write(to: filename, options: .atomic)
try! realm?.write {
self.imageLocal = name
}
}
catch {
print(error)
}
}
}
}
I call this function when Alamofire downloads the image
if person.imageLocal != nil, let image = person.loadLocalImage() {
print("Load form disk: \(person.imageLocal)")
cell.imgProfile.image = image
}
else if !(person.image?.isEmpty)! {
Alamofire.request(person.image!).responseImage(completionHandler: { (response) in
if response.result.isSuccess {
if let image = response.result.value {
person.saveImage(image)
cell.imgProfile.image = image
print("Downloaded: \(person.imageLocal)")
}
}
})
}
But the tableView looks laggy when scrolled and I was trying to make the writing operation into a diferent thread so it could get written without affecting the application performance by using DispatchQeue
DispatchQueue.global(qos: .background).async {
do {
try data.write(to: filename)
}
catch {
print(error)
}
}
But even so the applications stills laggy.
UPDATE:
I tryed this as Rob suggested:
func saveImage(_ image: UIImage) {
if let data = UIImagePNGRepresentation(image) {
let name = "images/person_directory_\(id).png"
do {
try realm?.write {
self.imageLocal = name
}
}
catch {
print("Realm error")
}
DispatchQueue.global().async {
let docsDir = self.getDocumentsDirectory()
let filename = docsDir.appendingPathComponent(name)
let fm = FileManager.default
if !fm.fileExists(atPath: docsDir.appendingPathComponent("images").path) {
do {
try fm.createDirectory(at: docsDir.appendingPathComponent("images"), withIntermediateDirectories: true, attributes: nil)
}
catch {
print(error)
}
}
do {
try data.write(to: filename)
}
catch {
print(error)
}
}
}
}
I can't dispatch the Realm writing becase Realm doesn't suport multithreading.
It stills scrolling laggy but not as much as the first time.
So the proper answer as #Rob gave it is
DispatchQueue.global().async {
do {
try data.write(to: filename)
}
catch {
print(error)
}
}
But just as important is to never use a reference to a UITableViewCell from an asynchronous call (again credit #Rob). For example, setting a cell value from the asynchronous parts of your code.
cell.imgProfile.image = image
UITableViewCells are re-used, so you don't know that the original cell is still used at the same index. For a test, scroll your list really fast so your images get loaded, if you see images appear in the wrong cell, its the re-use problem.
So from an asynchronous callback you need to figure out if the cell index for the new image is visible, go get the current cell for that index to set it's image. If the index isn't visible, then store/cache the image until it's index is scrolled into view. At that point it's cell will be created with UITableView.cellForRowAtIndexPath, and you can set the image there.
Related
I currently have a button that opens a TableViewController and loads the data using JSON like the following:
private func JSON() {
print(facility)
guard let url = URL(string: "https://example/example/example"),
let sample = value1.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "example1=\(example)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
self.JStruct = try JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
Then after I am done looking at the tableview I close it by doing:
self.dismiss(animated: true, completion: nil)
using a BarButtonItem.
The issue is every time the UIView opens it takes some time to load the data. Is there anyway to have the tableView load just once and when dismissed and re-opened just have the same data show that was already loaded before?
The best thing you can do is to store the data locally. Either use a local database or a plain text file to store the data. When you open the page check whether data is already present. If it is already present load it, and call the API in background silently to update the existing data. If data is not saved, call the API, load the data and save it locally.
func getFileURL() -> URL {
let fileName = "CacheData"
let documentDirURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = documentDirURL.appendingPathComponent(fileName).appendingPathExtension("json")
return fileURL
}
func createFile(data: Data) {
let fileURL = getFileURL()
do {
try data.write(to: fileURL)
} catch let e {
print(e.localizedDescription)
}
}
func loadData() -> Data? {
let fileURL = getFileURL()
do {
let data = try Data(contentsOf: fileURL)
return data
} catch let e {
print(e.localizedDescription)
}
return nil
}
In your viewDidLoad method do something like:
let fileURL = getFileURL()
if FileManager.default.fileExists(atPath: fileURL.path) {
if let data = loadData() {
do {
self.JStruct = try
JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
} catch {
print(error)
}
}
}
JSON()
And call the createFile when you get data from the API. You may need to write the file and load the file using a background queue to avoid overloading and freezing of your main thread.
I have several thousand images I want to download from a S3 bucket to an iOS App.
But I'm getting memory issues I'm unable to track down.
Here is my sketchy code:
let client = HttpClient<[SomeImage]>()
client.get(fromURL: URL(string: endpoint)!) {
(result, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let result = result {
let downloadGroup = DispatchGroup()
var count = 0
// just assembling a list of s3 keys to download here...
for item in result {
for image in (item.images ?? []) {
let prefix = "\(image.key)/"
for key in ["\(globalGetThumbnailS3Key(byImageKey: image.key))",
"\(globalGetPreviewS3Key(byImageKey: image.key))"] {
count = count + 1
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let data = data, let localDir = FileManager.default.applicationSupportURL {
do {
let imageURL = localDir.appendingPathComponent(key)
FileManager.default.directoryExistsOrCreate(localDir.appendingPathComponent(prefix))
try data.write(to: imageURL)
self.log(message: "downloaded \(prefix)\(key) to \(imageURL.absoluteString)", level: .verbose)
} catch let error {
self.log(message: "\(error)", level: .error)
return
}
}
}
bgSyncQueue.async(group: downloadGroup) {
self.transferUtility.downloadData(fromBucket: "\(globalDerivedImagesBucket)", key: key,
expression: nil,
completionHandler: completionHandler).continueWith {
(task) in
if let error = task.error {
// iirc, this error is caused, if the task couldnt be created due to being offline
self.log(message: "\(error)", level: .error)
return nil
}
if let result = task.result {
// do something with the task?
return nil
}
return nil
}
}
}
}
}
self.log(message: "\(count) images to download...", level: .debug)
bgSyncQueue.activate()
downloadGroup.notify(queue: DispatchQueue.main) {
self.log(message: "All items downloaded?!")
}
}
}
}
So I put all calls to the transfer utility in a serial dispatch queue, which is initially inactive. Then I activate the queue and downloading starts just fine. But after a while the app crashes with "Message from debugger: Terminated due to memory issue."
The app is only consuming about 100M of memory though. What am I overlooking?
Rob's suggestion to use the "downloadToUrl" method was the way to go, without using GCD on my part. Thanks again, Rob!
The transferUtility seems to be a fine tool, though very badly documented.
Here is the simple code used to download about 20k of images:
for key in keys {
let imageURL = localDir.appendingPathComponent(key.1)
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "failed downloading \(key.1): \(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return
}
DispatchQueue.main.async {
countingDown()
if let onProgress = self.onProgress {
onProgress(100.0 - ((100.0 / Double(total)) * Double(count)))
}
}
//self.log(message: "downloaded \(key.1)")
}
transferUtility.download(to: imageURL, bucket: "\(globalDerivedImagesBucket)", key: key.1, expression: nil, completionHandler: completionHandler).continueWith {
(task) in
if let error = error {
self.log(message: "\(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return nil
}
return nil
}
}
You may need to consider using an autoreleasepool to better manage the memory used by the bridged data types as detailed here
Exert from article (in case of link changes)
Consider the code:
func run() {
guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
return
}
for i in 0..<1000000 {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
}
Even though we’re in Swift, this will result in the same absurd memory spike shown in the Obj-C example! This is because the Data init is a bridge to the original Obj-C [NSDatadataWithContentsOfURL] -- which unfortunately still calls autorelease somewhere inside of it. Just like in Obj-C, you can solve this with the Swift version of #autoreleasepool; autoreleasepool without the #:
autoreleasepool {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
Disclaimer: I am no expert in Swift or Objective-C advanced memory management but I have used this in a similar scenario with good results.
I have below for loop to download files from server to my iPad 3 device.
Running hundred of files, I got error and app crash. The console shown me "Received memory warning. Same logic running on my iPad Air was passed. Anyone can adivse how to resolve the problem.
iPad 3 -> iOS 9.3.5
iPad Air -> iOS 10.3.3
func download() {
for (index, subJson): (String, JSON) in serverJson! {
for (_, subJson): (String, JSON) in subJson {
let filepath = subJson["path"].stringValue
let nUpdated = subJson["updated"].stringValue
if let oUpdated = localJson?[index].array?.filter({ $0["path"].string == filepath}).first?["updated"].stringValue {
if (oUpdated == nUpdated)
{
DispatchQueue.main.async { () -> Void in
self.progressView.progress = Float(self.count) / Float(self.totalCount)
}
count += 1
continue
}
}
var absPath = filepath
let strIdx = absPath.index(absPath.startIndex, offsetBy: 2)
if (absPath.hasPrefix("./"))
{
absPath = absPath.substring(from: strIdx)
}
let sourceUrl = URL(string: self.sourceUrl.appending(absPath))
do {
let fileData = try NSData(contentsOf: sourceUrl!, options: NSData.ReadingOptions())
try writeFileToLocal(absPath, fileData)
} catch {
print(error)
}
DispatchQueue.main.async { () -> Void in
self.progressView.progress = Float(self.count) / Float(self.totalCount)
}
//print("Path: \(absPath)")
//print("%: \(Float(count) / Float(totalCount))")
count += 1
}
}
do {
// Remove temp json file first if exists.
if fileManager.fileExists(atPath: oldManifestPath) {
try? fileManager.removeItem(atPath: oldManifestPath)
}
// Write temp json file to local.
try data?.write(to: oldManifestUrl)
self.defaults.set(hashes, forKey: "LastHash")
} catch {
print(error)
}
DispatchQueue.main.async {
self.progressView.isHidden = true
self.changeViewProperties(2)
}
}
}
private func writeFileToLocal(_ url:String, _ data:NSData) throws {
let url = URL(string: url)
let path = url?.deletingLastPathComponent().relativePath
let file = url?.lastPathComponent
let documentsPath = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let filePath = documentsPath.appendingPathComponent(path!)
var isDir: ObjCBool = false
if !FileManager.default.fileExists(atPath: (filePath?.path)!, isDirectory:&isDir) {
try FileManager.default.createDirectory(atPath: filePath!.path, withIntermediateDirectories: true, attributes: nil)
}
FileManager.default.changeCurrentDirectoryPath((filePath?.path)!)
try data.write(toFile: file!, options: .atomicWrite)
print("Update: \(filePath!), \(file!)")
FileManager.default.changeCurrentDirectoryPath(documentsPath.path!)
}
Then I call the function "download" in DispatchQueue.
DispatchQueue.global().async {
self.downloadFiles()
}
Memory waring is occuring as you are processing hundreds of Things on main thread just a Trick that you can try out if it helps you take that function in background queue instead of main Queue , you may get some ui warnings which you need to manage as updating ui in background thread
I do not know if it will help you or not but you an just give it a try for once
DispatchQueue.global(qos: .background).async
{
//BackGround Queue Update
self.downloadFiles()
DispatchQueue.main.async
{
//Main Queue Update With All UI
}
}
as background thread is always good to manage huge processing for example kingfisher library take data on main thread and return on main thread but process it in background thread
I'm downloading a picture from internet and storing its data locally then saving the path in my CoreData, this way:
getDataFromUrl(url!) { (data, response, error) in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
guard let data = data where error == nil else { return }
print(response?.suggestedFilename ?? "")
print("Download Finished")
let filename = self.getDocumentsDirectory().stringByAppendingPathComponent(userKey as! String + ".png")
data.writeToFile(filename, atomically: true)
user.setValue(filename, forKey: "avatar")
do {
try managedContext.save()
}
catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
}
The save does seem to work (I debugged by printing the data received and the data inside the file once copied and I don't have any managedContext error).
On the next view, I do use a UITableView and on cellForRowAtIndexPath
let path = authorArray.objectAtIndex(indexPath.row).objectAtIndex(0).objectForKey("avatar")! as! String
let name = authorArray.objectAtIndex(indexPath.row).objectAtIndex(0).objectForKey("name")
do {
let data = try NSData(contentsOfFile: path, options: NSDataReadingOptions())
let image = UIImage(data: data)
cell.profilePicture.image = image
cell.profilePicture.layer.cornerRadius = cell.profilePicture.layer.cornerRadius / 2;
cell.profilePicture.layer.masksToBounds = true;
}
catch {
print("failed pictures")
}
The thing is I get the photo on my cell.profilePicture but as soon as I do any modification elsewhere and relaunch my application from xCode, I get the error message. The pictures path did not change but the datas obtained from it are nil. I can't find a reason why it does work until I update the code. Any solutions to make it work everytime ?
As pbasdf stated on comments, I was storing the whole Document directory path instead of just the filename + extension. Documents directory changes on every build.
I'm trying to display and save images with Swift. On first hit, it shows the remote image on imageview, on second hit it shows blank imageview instead of it should be local image which saved on first hit.
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
var imagePath = paths.stringByAppendingPathComponent("images/\(id)/logo.jpg" )
var checkImage = NSFileManager.defaultManager()
if (checkImage.fileExistsAtPath(imagePath)) {
let getImage = UIImage(contentsOfFile: imagePath)
self.image?.image = getImage
} else {
dispatch_async(dispatch_get_main_queue()) {
let getImage = UIImage(data: NSData(contentsOfURL: NSURL(string: remoteImage)))
UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)
self.image?.image = getImage
}
}
Edit: This one worked for me.
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
var dirPath = paths.stringByAppendingPathComponent("images/\(id)" )
var imagePath = paths.stringByAppendingPathComponent("images/\(id)/logo.jpg" )
var checkImage = NSFileManager.defaultManager()
if (checkImage.fileExistsAtPath(imagePath)) {
let getImage = UIImage(contentsOfFile: imagePath)
self.image?.image = getImage
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
checkImage.createDirectoryAtPath(dirPath, withIntermediateDirectories: true, attributes: nil, error: nil)
let getImage = UIImage(data: NSData(contentsOfURL: NSURL(string: remoteImage)))
UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)
dispatch_async(dispatch_get_main_queue()) {
self.image?.image = getImage
return
}
}
}
To answer your main question, you're calling the wrong UIImage initializer. You should be calling UIImage(contentsOfFile: imagePath) in swift 2 and UIImage(contentsOf: imagePath) in swift 3.
Additionally, it looks like you're trying to do your remote fetch in the background with dispatch_async (or DispatchQueue in swift 3), but you're passing it the main queue, so you're actually blocking the main/UI thread with that. You should dispatch it to one of the background queues instead and then dispatch back to the main queue when you actually set the image in your UI:
Swift 3 :
DispatchQueue.global(qos: DispatchQoS.background.qosClass).async {
do {
let data = try Data(contentsOf: URL(string: self.remoteImage)!)
let getImage = UIImage(data: data)
try UIImageJPEGRepresentation(getImage!, 100)?.write(to: imagePath)
DispatchQueue.main.async {
self.image?.image = getImage
return
}
}
catch {
return
}
}
Swift 2 :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
let getImage = UIImage(data: NSData(contentsOfURL: NSURL(string: self.remoteImage)))
UIImageJPEGRepresentation(getImage, 100).writeToFile(imagePath, atomically: true)
dispatch_async(dispatch_get_main_queue()) {
self.image?.image = getImage
return
}
}
#Rob's answer re: fetching your remote image and saving it is really the best way to do this.
Your code that dispatches NSData(contentsOfURL:) (now known as Data(contentsOf:)) to the main queue. If you're going to use that synchronous method to request remote image, you should do this on a background queue.
Also, you are taking the NSData, converting it to a UIImage, and then converting it back to a NSData using UIImageJPEGRepresentation. Don't round-trip it though UIImageJPEGRepresentation as you will alter the original payload and will change the size of the asset. Just just confirm that the data contained an image, but then write that original NSData
Thus, in Swift 3, you probably want to do something like:
DispatchQueue.global().async {
do {
let data = try Data(contentsOf: URL(string: urlString)!)
if let image = UIImage(data: data) {
try data.write(to: fileURL)
DispatchQueue.main.async {
self.imageView?.image = image
}
}
} catch {
print(error)
}
}
Even better, you should use NSURLSession because you can better diagnose problems, it's cancelable, etc. (And don't use the deprecated NSURLConnection.) I'd also check the statusCode of the response. For example:
func requestImage(_ url: URL, fileURL: URL) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// check for fundamental network issues (e.g. no internet, etc.)
guard let data = data, error == nil else {
print("dataTask error: \(error?.localizedDescription ?? "Unknown error")")
return
}
// make sure web server returned 200 status code (and not 404 for bad URL or whatever)
guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
print("Error; Text of response = \(String(data: data, encoding: .utf8) ?? "(Cannot display)")")
return
}
// save image and update UI
if let image = UIImage(data: data) {
do {
// add directory if it doesn't exist
let directory = fileURL.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
// save file
try data.write(to: fileURL, options: .atomic)
} catch let fileError {
print(fileError)
}
DispatchQueue.main.async {
print("image = \(image)")
self.imageView?.image = image
}
}
}
task.resume()
}
Note, the just-in-time creation of the folder is only necessary if you haven't created it already. Personally, when I build the original path, I'd create the folder there rather than in the completion handler, but you can do this any way you want. Just make sure the folder exists before you write the file.
Regardless, hopefully this illustrates the main points, namely that you should save the original asset and that you should do this in the background.
For Swift 2 renditions, see previous revision of this answer.