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
Related
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 know that SDWebImage loads the image in a background thread so you're not blocking the UI/main thread when this downloading is going on. Furthermore, it will also disk-cache all the images you've downloaded and will NEVER re-download an image from the same URL.
So I wonder if there is something similar or the same for videos?
Something to note: I add Videos as Sublayer.
let videoURL = URL(string: postArray[indexPath.item].media[0].videoURLString!)//need to do error handlin here
print(videoURL as Any, "<-- video url in dispkay")
let player = AVPlayer(url: videoURL! as URL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = CGRect(x: -8, y: 0, width: 138, height: 217)//cell.frame
cell.imageOrVideoView.layer.addSublayer(playerLayer)
//Other code and play()
This was recommended in the past but it seems like it does something different or at the very leased has too much extra functionality I dont need.
Update:
What I am testing:
DispatchQueue.global(qos: .default).async(execute: {
var downloadedData: Data? = nil
if let url = URL(string: videoURL) {
do {
downloadedData = try Data(contentsOf: url)
} catch {
print(error, "downloaded Data failed")
}
}
if downloadedData != nil {
// STORE IN FILESYSTEM
var cachesDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
var file = URL(fileURLWithPath: cachesDirectory).appendingPathComponent(videoURL).absoluteString
do {
try downloadedData?.write(to: URL(string: file)!)
} catch {
print(error, "error dowloading data and writing it")
}
// STORE IN MEMORY
if let downloadedData = downloadedData {
memoryCache?.setObject(downloadedData as AnyObject, forKey: videoURL as AnyObject)
}
}
// NOW YOU CAN CREATE AN AVASSET OR UIIMAGE FROM THE FILE OR DATA
})
I do not understand however if I should do something right after the last line or if I should do it after the }) or if I need to add a Update UI there.
So I was able to solve the problem with the following:
Swift 4:
import Foundation
public enum Result<T> {
case success(T)
case failure(NSError)
}
class CacheManager {
static let shared = CacheManager()
private let fileManager = FileManager.default
private lazy var mainDirectoryUrl: URL = {
let documentsUrl = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return documentsUrl
}()
func getFileWith(stringUrl: String, completionHandler: #escaping (Result<URL>) -> Void ) {
let file = directoryFor(stringUrl: stringUrl)
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
DispatchQueue.global().async {
if let videoData = NSData(contentsOf: URL(string: stringUrl)!) {
videoData.write(to: file, atomically: true)
DispatchQueue.main.async {
completionHandler(Result.success(file))
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])
completionHandler(Result.failure(error))
}
}
}
}
private func directoryFor(stringUrl: String) -> URL {
let fileURL = URL(string: stringUrl)!.lastPathComponent
let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)
return file
}
}
Usage:
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
switch result {
case .success(let url):
// do some magic with path to saved video
break;
case .failure(let error):
// handle errror
print(error, " failure in the Cache of video")
break;
}
}
This is my code to accomplish the upload task:
let image = UIImage(named: "12.jpeg")
let fileManager = FileManager.default
let imageData = UIImageJPEGRepresentation(image!, 0.99)
let path = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent("\(imageData!).jpeg")
fileManager.createFile(atPath: path as String, contents: imageData, attributes: nil)
let fileUrl = NSURL(fileURLWithPath: path)
uploadRequest?.bucket = "testrawdata"
uploadRequest?.key = "test/loodfd.jpeg"
uploadRequest?.contentType = "image/jpeg"
uploadRequest?.body = fileUrl as URL!
uploadRequest?.serverSideEncryption = AWSS3ServerSideEncryption.awsKms
uploadRequest?.uploadProgress = { (bytesSent, totalBytesSent, totalBytesExpectedToSend) -> Void in
DispatchQueue.main.async(execute: {
print("bytes sent \(bytesSent), total bytes sent \(totalBytesSent), of total \(totalBytesExpectedToSend)")
})
}
transferManager?.upload(uploadRequest).continue(with: AWSExecutor.mainThread(), withSuccessBlock: { (taskk: AWSTask) -> Any? in
if taskk.error != nil {
// Error.
} else {
// Do something with your result.
}
return nil
})
}
I know I don't need to apply it to image, but this is just an example, by default I'm going to send files like 100mb.
When I put my phone into airplane mode during the transfer then turn the network on again, it does not finish the upload task.
Docs are not saying explicitly what should I do to resume interrupted task.
Here is what I tried:
I put initialization of request and manager into viewDidLoad() to assure I'm not creating another request
class ViewController: UIViewController {
var uploadRequest:AWSS3TransferManagerUploadRequest!
var transferManager: AWSS3TransferManager!
override func viewDidLoad() {
super.viewDidLoad()
uploadRequest = AWSS3TransferManagerUploadRequest()
transferManager = AWSS3TransferManager.default()
}
and tried to call
func resumeTransfer() {
transferManager?.resumeAll(nil)
}
But it does not work.
Thanks in advance
It turns out that Transfer Utility is the right tool to accomplish this task
func uploadData(data: NSData) {
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = progressBlock
let transferUtility = AWSS3TransferUtility.default()
transferUtility.uploadData(
data as Data,
bucket: "test",
key: "test/test.jpeg",
contentType: "image/jpeg",
expression: expression,
completionHander: completionHandler).continue(successBlock: { (task) -> AnyObject! in
if let error = task.error {
NSLog("Error: %#",error.localizedDescription);
}
if let exception = task.exception {
NSLog("Exception: %#",exception.description);
}
if let _ = task.result {
NSLog("Upload Starting!")
// Do something with uploadTask.
}
return nil;
})
}
This way all upload stuff happens in the background, I don't have to worry about app being killed by the iOS, about networks problem etc.
One can even specify
configuration?.allowsCellularAccess = false
in AWSServiceConfiguration
to resume the task only when wifi is available.
I'm currently displaying a video in my app and I want the user to be able to save it to its device gallery/album photo/camera roll.
Here it's what I'm doing but the video is not saved in the album :/
func downloadVideo(videoImageUrl:String)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
//All stuff here
print("downloadVideo");
let url=NSURL(string: videoImageUrl);
let urlData=NSData(contentsOfURL: url!);
if((urlData) != nil)
{
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0];
let fileName = videoImageUrl; //.stringByDeletingPathExtension
let filePath="\(documentsPath)/\(fileName)";
//saving is done on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
urlData?.writeToFile(filePath, atomically: true);
print("videoSaved");
})
}
})
}
I'va also look into this :
let url:NSURL = NSURL(string: fileURL)!;
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(url);
let assetPlaceHolder = assetChangeRequest!.placeholderForCreatedAsset;
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: self.assetCollection)
albumChangeRequest!.addAssets([assetPlaceHolder!])
}, completionHandler: saveVideoCallBack)
But I have the error "Unable to create data from file (null)". My "assetChangeRequest" is nil. I don't understand as my url is valid and when I go to it with a browser, it download a quick time file.
If anyone can help me, it would be appreciated ! I'm using Swift and targeting iOS 8.0 min.
Update
Wanted to update the answer for Swift 3 using URLSession and figured out that the answer already exists in related topic here. Use it.
Original Answer
The code below saves a video file to Camera Roll. I reused your code with a minor change - I removed let fileName = videoImageUrl; because it leads to incorrect file path.
I tested this code and it saved the asset into camera roll. You asked what to place into creationRequestForAssetFromVideoAtFileURL - put a link to downloaded video file as in the example below.
let videoImageUrl = "http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4"
DispatchQueue.global(qos: .background).async {
if let url = URL(string: urlString),
let urlData = NSData(contentsOf: url) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let filePath="\(documentsPath)/tempFile.mp4"
DispatchQueue.main.async {
urlData.write(toFile: filePath, atomically: true)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
}) { completed, error in
if completed {
print("Video is saved!")
}
}
}
}
}
Swift 3 version of the code from #Nimble:
DispatchQueue.global(qos: .background).async {
if let url = URL(string: urlString),
let urlData = NSData(contentsOf: url)
{
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let filePath="\(documentsPath)/tempFile.mp4"
DispatchQueue.main.async {
urlData.write(toFile: filePath, atomically: true)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
}) { completed, error in
if completed {
print("Video is saved!")
}
}
}
}
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: video.url!)}) {
saved, error in
if saved {
print("Save status SUCCESS")
}
}
following #Nimble and #Yuval Tal solution, it is much more preferable to use the URLSession dataTask(with:completionHandler:) method to download a file before writing it as stated in the warning section of NSData(contentsOf:) Apple documentation
Important
Don't use this synchronous initializer to request network-based URLs.
For network-based URLs, this method can block the current thread for
tens of seconds on a slow network, resulting in a poor user
experience, and in iOS, may cause your app to be terminated.
Instead, for non-file URLs, consider using the
dataTask(with:completionHandler:) method of the URLSession
a correct implementation could be :
let defaultSession = URLSession(configuration: .default)
var dataTask: URLSessionDataTask? = nil
func downloadAndSaveVideoToGallery(videoURL: String, id: String = "default") {
DispatchQueue.global(qos: .background).async {
if let url = URL(string: videoURL) {
let filePath = FileManager.default.temporaryDirectory.appendingPathComponent("\(id).mp4")
print("work started")
self.dataTask = self.defaultSession.dataTask(with: url, completionHandler: { [weak self] data, res, err in
DispatchQueue.main.async {
do {
try data?.write(to: filePath)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: filePath)
}) { completed, error in
if completed {
print("Saved to gallery !")
} else if let error = error {
print(error.localizedDescription)
}
}
} catch {
print(error.localizedDescription)
}
}
self?.dataTask = nil
})
self.dataTask?.resume()
}
}
}
One more advantage is that you can pause, resume and terminate your download by calling the corresponding method on dataTask: URLSessionDataTask .resume() .suspend() .cancel()
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.