Swift 3 DispatchQueue.global (download images from Firebase and UI frozen) - ios

I have a problema with this method:
func DownloadImages(uid: String, indice: Int) {
DispatchQueue.global(qos: .background).async {
let refBBDD = FIRDatabase.database().reference().child("users").child(uid)
refBBDD.observeSingleEvent(of: .value, with: { snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let profileImageUrl = snapshotValue?.value(forKey: "profileImageUrl") as! String
let storage = FIRStorage.storage()
var reference: FIRStorageReference!
if(profileImageUrl == "") {
return
}
print("before")
reference = storage.reference(forURL: profileImageUrl)
reference.downloadURL { (url, error) in
let data = NSData(contentsOf: url!)
let image = UIImage(data: data! as Data)
print("image yet dowload ")
self.citas[indice].image = image
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadRows(at: [IndexPath(row: indice, section: 0)], with: .none)
//self.tableView.reloadData()
print("image loaded")
})
}
print("after")
})
}
}
I want to download images in background mode. I want follow using app, but the UI has frozen until methods not entry in reloadRows.
Is it possible run in true background mode and can i follow using the app??
Trace program:
before
after
before
after
...
before
after
before
after
image yet dowload --> here start UI frozen
image loaded
image yet dowload
image yet dowload
...
image yet dowload
image yet dowload
image loaded
image loaded
...
image loaded
image loaded
image yet dowload ------> here UI is not frozen
image loaded

The problem is caused by this line: let data = NSData(contentsOf: url!).
That constructor of NSData should only be used to read the contents of a local URL (a file path on the iOS device), since it is a synchronous method and hence if you call it to download a file from a URL, it will be blocking the UI for a long time.
I have never used Firebase, but looking at the documentation, it seems to me that you are using the wrong method to download that file. You should be using func getData(maxSize size: Int64, completion: #escaping (Data?, Error?) -> Void) -> StorageDownloadTask instead of func downloadURL(completion: #escaping (URL?, Error?) -> Void), since as the documentation states, the latter only "retrieves a long lived download URL with a revokable token", but doesn't download the contents of the URL itself.
Moreover, you shouldn't be force unwrapping values in the completion handler of a network request. Network requests can often fail for reasons other than a programming error, but if you don't handle those errors gracefully, your program will crash.

Your problem is in this line let data = NSData(contentsOf: url!) and let's now see what apple says about this method below
Don't use this synchronous method 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.
So it clearly states that this method will block your User Interface.

Related

How to handle errors while saving multiple images to the user's device?

My app as the functionality of choosing multiple images from the app main screen, and save the selected images to the user gallery.
As an example (image from google):
After the user clicking "save" I am doing the following in order to save the chosen images to the user's gallery.
Running through all of the images and saving each image that on clicked.
func saveSelectedImagesToDevice() {
for imageList in imagesListCells {
for image in imageList.images {
if image.selectionState == .onClicked {
downloadImage(from: image.url)
}
}
}
}
Downloading each image
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
guard let image = UIImage(data: data) else {return}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let _ = error {
self.delegate?.savedImage(proccessMsg: "Error adding images to the gallery, pleast make sure you enabled photos permissions in phone setting")
}
}
The thing is, because the saving process is asynchronies, in case error occurs in one of the process of downloading an image, I would like to stop all of the other asynchronies processes that running on the background.
At the moment, in case of error, the error been called for each one of the images.
Any ideas how can I manage it different in order to keep the process asynchronies but to be able to stop all processes in case of error?
You would have to change completely the architecture of the download to make it cancellable. A data task is cancellable, but yours is not because you have not retained any way of referencing it.
Apple suggests to not using the shared instance if you want to create multiple sessions. You could try to achieve this by creating a single session instance and invalidate it as soon as you receive an error.
Keep in mind that if you want to re-start the session you need to re instantiate a new one.
e.g.
let session = URLSession(configuration: .default)
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
session.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
print("You have an error: ",error.localizedDescription)
self.session.invalidateAndCancel()
}else if let data = data,
let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}.resume()
}

Post to Instagram by opening Instagram app – iOS, Swift

I have an Instagram scheduling app and I am trying to open this (see image below) in Swift 5.x. The goal is simple: save Image to Firebase, once it is time to post, notification!, user clicks on the notification and this (image below) opens up with the appropriate image/video to post. Everything works except for opening Instagram with the appropriate photo/video. I have tried this:
func postToInstagram(image: URL) {
let videoFileUrl: URL = image
var localId: String?
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoFileUrl)
localId = request?.placeholderForCreatedAsset?.localIdentifier
}, completionHandler: { success, error in
// completion handler is called on an arbitrary thread
// but since you (most likely) will perform some UI stuff
// you better move everything to the main thread.
DispatchQueue.main.async {
guard error == nil else {
// handle error
print(error)
return
}
guard let localId = localId else {
// highly unlikely that it'll be nil,
// but you should handle this error just in case
return
}
let url = URL(string: "instagram://library?LocalIdentifier=\(localId)")!
guard UIApplication.shared.canOpenURL(url) else {
// handle this error
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
})
}
and this:
func postToInstagram(image: URL, igURL: String) {
let urlStr: String = "instagram://app"
let url = URL(string: igURL)
if UIApplication.shared.canOpenURL(url!) {
print("can open")
UIApplication.shared.open(url!, options: [:], completionHandler: nil)
}
}
To no avail. The latter code works, but only opens the Instagram app itself, which is fine, but I would like to open the View in the image below rather than Instagram's home screen. I also tried changing the URL to "instagram://share" and this works but goes to publish a regular post, whereas I want the user to decide what they want to do with their image.
This is where I want to go:
Note: For everyone who will be telling me this and whoever will wonder: Yes, my URL schemes (LSApplicationQueriesSchemes) are fine. And, just to clarify, I need to fetch the image/video from Firebase before posting it.

Downloading Images in order of url's - iOS Swift

I have 10 urls in an array and when 4 of them downloaded I need to display them. Im using Semaphores and groups to implement . But looks like im hitting deadlock. Not sure how to proceed. Please advice how I can
Simulating same in playground:
PlaygroundPage.current.needsIndefiniteExecution = true
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async(group: group) {
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
nums.append(i)
print("Hola image \(i)")
if nums.count == 4 {
print("4 downloaded")
semaphore.signal()
group.leave()
}
}
if nums.count == 4 {
break
}
}
group.notify(queue: DispatchQueue.main) {
print(nums)
}
I get this in o/p console
> Downloading image 1
> Downloading image 2
> Downloading image 3
> Downloading image 4
Semaphores(41269,0x70000ade5000) malloc: *** error for object 0x1077d4750: pointer being freed was not allocated
Semaphores(41269,0x70000ade5000) malloc: *** set a breakpoint in malloc_error_break to debug
I'm expecting to print [1,2,3,4] in order
I know im trying to access a shared resource in async but not sure how I can fix this. Please advice
Also How can I use this with semaphore's if I want to download 4,4,2 tasks at a time so it display [1,2,3,4,5,6,7,8,9,10] in my ouput
Your title says “Downloading Images in order of url’s”, but your code snippet is not attempting to do that. It appears to be attempting to use semaphores to constrain the download to four images at a time, but it won’t guarantee that they’ll be in order.
It is commendable that this code snippet isn’t attempting to download them in order, sequentially, one after another, because that would impose a huge performance penalty. It is also good that this code snippet is constraining this degree of concurrency to something reasonable, thereby avoiding exhausting worker threads or causing some of the latter requests to timeout. So, the idea of using semaphore to allow concurrent image download, but constrain it to four at a time, is a fine approach; we only need to sort the results at the end if you want them in order.
But before we get to that, let’s tackle a bunch of problems in the supplied code snippet:
You are calling group.enter() and semaphore.wait() for every iteration (which is correct), but group.leave() and semaphore.signal() only when i is 4 (which is not correct). You want to leave and signal for every iteration.
Obviously, that break call is not needed, either.
So, to fix this “do four at a time” process, one can simplify this code:
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async() { // NB: the `group` parameter is not needed
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
nums.append(i)
print("Hola image \(i)")
semaphore.signal()
group.leave()
}
}
group.notify(queue: .main) {
print(nums)
}
That will download four images at a time and will call your group.notify closure when they’re all done.
While the above fixes the semaphore and group logic, there is yet another problem lurking in the above code snippet. It is updating that nums array from multiple background threads, but Array is not thread-safe. So you should synchronize those updates to that array. An easy way to achieve this is to dispatch that update back to the main thread. (Any serial queue would have been fine, but the main thread works fine for this purpose.)
Also, since one should never call wait on the main queue, so I’d suggest that you explicitly dispatch this entire for loop to a background thread:
DispatchQueue.global(qos: .utility).async {
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async() {
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
DispatchQueue.main.async {
nums.append(i)
print("Hola image \(i)")
}
semaphore.signal()
group.leave()
}
}
group.notify(queue: .main) {
print(nums)
}
}
That is now the correct “do four at a time and let me know when it’s done.”
OK, now that we’re downloading all of the images properly, let’s figure out how to sort the results. Frankly, I think it’s easier to follow what’s going on if we imagine that we have some image download method, like so, that downloads a particular image:
func download(_ url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) { ... }
Then the routine to (a) download the images, no more than four at a time; and (b) return the results back in order, might look like:
func downloadAllImages(_ urls: [URL], completion: #escaping ([UIImage]) -> Void) {
DispatchQueue.global(qos: .utility).async {
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 4)
var imageDictionary: [URL: UIImage] = [:]
// download the images
for url in urls {
group.enter()
semaphore.wait()
self.download(url) { result in
defer {
semaphore.signal()
group.leave()
}
switch result {
case .failure(let error):
print(error)
case .success(let image):
DispatchQueue.main.async {
imageDictionary[url] = image
}
}
}
}
// now sort the results
group.notify(queue: .main) {
completion(urls.compactMap { imageDictionary[$0] })
}
}
}
And you’d call it like so:
downloadAllImages(urls) { images in
self.images = images
self.updateUI() // do whatever you want to trigger the update of the UI
}
FWIW, the “download single image” routine might look like:
enum DownloadError: Error {
case notImage
case invalidStatusCode(URLResponse)
}
func download(_ url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
completion(.failure(error!))
return
}
guard 200..<300 ~= response.statusCode else {
completion(.failure(DownloadError.invalidStatusCode(response)))
return
}
guard let image = UIImage(data: data) else {
completion(.failure(DownloadError.notImage))
return
}
completion(.success(image))
}
}
And this is using the Swift 5 Result enumeration. If you’re using an earlier version of Swift, you can define a simple rendition of this enum yourself:
enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
Finally, it’s worth noting a few other alternatives:
Wrap your network request in asynchronous Operation subclass and add them to an operation queue whose maxConcurrentOperationCount is set to 4. If you’re interested in this approach, I can supply some references.
Use an image downloading library like Kingfisher.
Instead of manual downloading of all the images, use the UIImageView extension (such as provided by Kingfisher) and completely abandon the “download all images” process at all, and move to a pattern where you simply instruct your image views to asynchronously retrieve the images in either a just-in-time manner (or prefetching).

iOS - Swift 3 Share Extension Preview Image

I'm currently building a share extension that accepts URLs. As part of this I've customised my share screen as outlined in a previous question to create a full screen view controller. This is all working fine. However in the default share composer view I noticed there was a preview image of the web page. I'm trying to access this in my extension but I can't seem to get hold of it.
Specifically I've been trying to use the method
loadPreviewImage
https://developer.apple.com/reference/foundation/nsitemprovider/1403925-loadpreviewimage
You will note in the docs that this says the following for the completion handler
completion​Handler
A completion handler block to execute with the results. The first parameter of this block must be a parameter of type NSData, NSURL, UIImage (in iOS), or NSImage (in macOS) for receiving the image data. For more information about implementing the block, see Completion​Handler.
However if I try to set this as a UIImage in my completion block I get an error of
Cannot convert value of type '(UIImage, _) -> ()' to expected argument
type 'NSItemProvider.CompletionHandler!'
example code where itemProvider is confirmed to be an instance of NSItemProvider via guard statements
itemProvider.loadPreviewImage(options: nil) { (image: UIImage, error) in
}
The docs for the completion Handler say to set this to what type you want and it will attempt to coerce the data to the type you specify. Has anyone seen this before? I'm not sure what to do here as I can't see what I'm doing wrong.
https://developer.apple.com/reference/foundation/nsitemprovider/completionhandler
If all else fails I'll look at using some Javascript to get an image from the dom but I would have liked the preview image that Apple seemed to provide
I don't know why the code in
itemProvider.loadPreviewImage(options: nil) { (image: UIImage, error) in
}
not called when Post button tapped.
My round way is saving the preview image in method
override func configurationItems() -> [Any]! {
}
as
let inputItem: NSExtensionItem = self.extensionContext?.inputItems[0] as! NSExtensionItem
let itemProvider = inputItem.attachments![0] as! NSItemProvider
if (itemProvider.hasItemConformingToTypeIdentifier("public.url")) {
itemProvider.loadPreviewImage(options: nil, completionHandler: { (item, error) in // 画像を取得する
if let image = item as? UIImage {
if let data = UIImagePNGRepresentation(image) {
self.photoNSURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("preview.png") as NSURL!
do {
try data.write(to: self.photoNSURL as URL, options: .atomic)
} catch {
print("\(#file)#\(#function)(\(#line)): error: \(error.localizedDescription)")
}
}
}
})
}

How to Upload video files in background?

I am doing a app in which i need to upload videos taken from iPhone Camera and upload it to a server. When the app is in foreground the video gets uploaded but i don't know how to do it in background when the app is inactive. I used AFNetworking to do the multipart data upload. Here is the code i tried
var task:NSURLSessionUploadTask!
let FILEPICKER_BASE_URL = "My server Url"
var isUploading : Bool = false
var videoPath : String!
func upload(dictMain: NSMutableDictionary)
{
isUploading = true
let uuid = NSUUID().UUIDString + "-" + (getUserId().description)
let request = AFHTTPRequestSerializer().multipartFormRequestWithMethod("POST", URLString: FILEPICKER_BASE_URL, parameters: nil, constructingBodyWithBlock: { (fromData) in
do {
try fromData.appendPartWithFileURL(NSURL(fileURLWithPath: videoPath) , name: "fileUpload", fileName: uuid, mimeType: "video/quicktime")
}catch{
print(error)
}
}, error: nil)
let manager:AFURLSessionManager = AFURLSessionManager(sessionConfiguration: NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("Your video is uploading"));
task = manager.uploadTaskWithStreamedRequest(request, progress: nil , completionHandler: { (response, responseObject, error) in
NSLog("Resposne Object: \(responseObject)")
post(dictMain,response: responseObject!)
})
task.resume()
}
Also I don't know how to know my upload progress. I tried to use the following block in the progress parameter
{ (progress) in
print("\((progress))")
}
But it does not work the complier shows error
Cannot convert value of type '(_) -> _' to expected argument type 'AutoreleasingUnsafeMutablePointer<NSProgress?>' (aka 'AutoreleasingUnsafeMutablePointer<Optional<NSProgress>>')
Could any one share a snippet that really works. As i googled there are very very few basic tutorials on NSURLSession.uploadTaskWithStreamedRequest and all of them i tried didn't work on swift 2.2
I am using Xcode 7.3 Swift 2.2
Points I Know:
Background upload works only with File paths and not NSData. so i took the video url path from the UIImagePickerController function func video(videoPath: NSString, didFinishSavingWithError error: NSError?, contextInfo info: AnyObject)
To do background file transfer i have to enable them in the Target->Capabilities -> Background Modes -> Background fetch
Must set the Session manager with NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(""). But i don't know why and where that identifier is being used.
I know this is a long post. But if it is answered this will surely be helpful to many developers.
I fixed it by calling the upload function inside a DispatchQueue Background queue
DispatchQueue.global(qos: .background).async { upload() }

Resources