I am a new developer. I'm using Swift 4.2 and Xcode 10.2.
I'm trying to show a spinner while a photo is being saved. I am simulating a slow connection on my iPhone in Developer Mode to test it. Using the code below the spinner does not show up. The view goes right to the end where the button shows and says the upload is complete (when it isn't). I tried putting it all into a DispatchQueue.global(qos: .userinitiated).async and then showing the button back on the main queue. I also tried putting the showSpinner on a DispatchQueue.main and then savePhoto on a .global(qos: .utility). But I cleary don't understand the GCD processes.
Here's my code:
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// Can show the loading bar here.
}
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
What types of DispatchQueues should I use and where should I put them?
Here is the savePhoto code:
static func savePhoto(image:UIImage, progressUpdate: #escaping (Double) -> Void) {
// Get data representation of the image
let photoData = image.jpegData(compressionQuality:0.1)
guard photoData != nil else {
print("Couldn't turn the image into data")
return
}
// Get a storage reference
let userid = LocalStorageService.loadCurrentUser()?.userId
let filename = UUID().uuidString
let ref = Storage.storage().reference().child("images/\(String(describing: userid))/\(filename).jpg")
// Upload the photo
let uploadTask = ref.putData(photoData!, metadata: nil) { (metadata, error) in
if error != nil {
// An error during upload occurred
print("There was an error during upload")
}
else {
// Upload was successful, now create a database entry
self.createPhotoDatabaseEntry(ref: ref, filename: filename)
}
}
uploadTask.observe(.progress) { (snapshot) in
let percentage:Double = Double(snapshot.progress!.completedUnitCount /
snapshot.progress!.totalUnitCount) * 100.00
progressUpdate(percentage)
}
}
Since the code that saves the photo is asynchronous , so your current code removes the spinner directly after it's added before the upload is complete
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// remove spinner when progress is 100.0 = upload complete .
if pct == 100.0 {
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
}
}
Here you don't have to use GCD as firebase upload runs in another background thread so it won't block the main thread
Related
I am downloading an image in my view controller. I want to update my image every time the app enters the foreground. I tried to call the function to download the image from the scene delegate, but unfortunately, I get the error "Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value", when I try that.
This is my code to download the image which works fine except when I call it from the scene delegate.
let urlSession = URLSession(configuration: .default)
let url = URL(string: "https://jarisstoriesphotographyphoto.files.wordpress.com/2020/06/menu1.png")!
// Create Data Task
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, _, error) in
if let error = error {
print(error)
}
if let data = data {
DispatchQueue.main.async {
// Create Image and Update Image View
// self?.imageView.image
self?.imageView.image = UIImage(data: data)
}
}
}
// Start Data Task
dataTask.resume()
This is the code I used in my scene delegate. I also tried to call the download function in the "willConnectTo" but that gave me the same error.
let viewController = ViewController()
func sceneWillEnterForeground(_ scene: UIScene) {
viewController.downloadImage()
}
Help is very appreciated.
If you want to start a download task every time the app enters foreground, within a view controller then you should do the task in viewWillAppear of the view controller. Here's an example:
class ViewController: UIViewController {
// ...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let urlSession = URLSession(configuration: .default)
let url = URL(string: "https://jarisstoriesphotographyphoto.files.wordpress.com/2020/06/menu1.png")!
// Create Data Task
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, _, error) in
if let error = error {
print(error)
}
if let data = data {
DispatchQueue.main.async {
// Create Image and Update Image View
// self?.imageView.image
self?.imageView.image = UIImage(data: data)
}
}
}
// Start Data Task
dataTask.resume()
}
}
I'm uploading 2 files to the storage, once both are done a Firestore document should be created. This works perfectly if the app is in the foreground.
However if the app is put into the background while still uploading, the .success observer code does not get executed, the .progress observer still executes until it's 100% and then everything just stops.
Once I put my app back into the foreground the code gets executed again (sometimes successfully, sometimes with an error telling me to check the server response).
Button that calls the upload functions:
#IBAction func postButtonClicked(_ sender: Any) {
print("Post button clicked, trying to post...")
//Make sure we are logged in and have a video
guard let videoURL = videoURL else {
print("We don't have a video, why are we trying to post?")
dismiss(animated: true, completion: nil)
return
}
//1 Create and start uploading thumbnail
createThumbnail(from: videoURL) { (image) in
if let image = image {
self.uploadThumbnailToStorage(image: image)
}
}
//2 Start uploading video
uploadVideoToStorage(from: videoURL)
dismiss(animated: true, completion: nil)
}
And the video upload function:
func uploadVideoToStorage(from url: URL) {
print("Start uploading video...")
let storageRef = storage.reference()
let videoRef = storageRef.child("videos/\(postRef.documentID)_video")
// Upload the file to the path "videos"
let uploadTask = videoRef.putFile(from: url, metadata: nil) { metadata, error in
guard let metadata = metadata else {
// Uh-oh, an error occurred!
return
}
// Metadata contains file metadata such as size, content-type.
let size = metadata.size
print("Video file size: \(size)")
}
//Observe uploadTask
uploadTask.observe(.success) { snapshot in
snapshot.reference.downloadURL { (url, error) in
guard let downloadURL = url else {
if let error = error {
print("Error in video success observer: \(error.localizedDescription)")
}
return
}
print("Video upload success.")
self.videoString = downloadURL.absoluteString
self.videoUploadComplete = true
self.checkIfUploadsAreComplete()
}
}
uploadTask.observe(.progress) { snapshot in
let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount)
/ Double(snapshot.progress!.totalUnitCount)
print("Uploading video: \(percentComplete.rounded())%")
}
}
The checkIfUploadsAreComplete() function checks if both upload tasks are complete and then writes the document, but I don't think that code is relevant.
I have tried putting the uploadVideoToStorage() function into the background like so:
DispatchQueue.global(qos: .background).async {
// Request the task assertion and save the ID.
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TestVideoUpload") {
// End the task if time expires.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
// Send the data synchronously.
DispatchQueue.main.async {
self.uploadVideoToStorage(from: videoURL)
}
// End the task assertion.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
But that didn't do anything, any thoughts?
I've been messing around with dispatch groups and am wondering how the placement of the notify callback of a dispatch group affects when the callback will be called. I'm reading data from my database and then fetching a picture for each item in the data that was read. When I have the notify outside of the initial database read block I notice it gets called immediately, however when the notify is inside the block it behaves the proper way. Here is my code:
override func viewDidAppear(_ animated: Bool) {
let ref = FIRDatabase.database().reference().child("users").child((FIRAuth.auth()?.currentUser?.uid)!).child("invites")
ref.observeSingleEvent(of: .value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in snapshots {
self.dispatchGroup.enter()
let info = petInfo(petName: child.value! as! String, dbName: child.key)
print(info)
self.invitesData[info] = UIImage()
let picRef = FIRStorage.storage().reference().child("profile_images").child(info.dbName+".png")
picRef.data(withMaxSize: 1024 * 1024) { (data, error) -> Void in
if error != nil {
print(error?.localizedDescription ?? "Error getting picture")
}
// Create a UIImage, add it to the array
self.invitesData[info] = UIImage(data: data!)!
self.dispatchGroup.leave()
}
}
self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
print("YOOO")
self.invitesArray = Array(self.invitesData.keys)
print(self.invitesArray)
self.inviteTable.reloadData()
})
}
})
}
This code behaves properly when the notify is within the original database read block. However if I place this after the ref.observeSingleEvent block the notify gets called immediately.
Can somebody please explain this to me?
Yes. Asynchronous code :-)
Code execution runs all the way through to the end of the function, and then the completion handler will be called
What I have
I don't understand the nature of the problem.
I have 2 View Controllers :
1) FeedViewController ,which shows events in tableview
2) EventViewController, pushed when you press on event.
When Feed Loads, it start asynchronous loadings of all images of all events.
It's done for each event by following function:
EventsManager().loadProfilePictureById(event.profilePictureID as String, currentProgress: event.profilePictureProgress, completionHandler: {
(progress:Double, image:UIImage!, error:NSError!) -> Void in
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
if (error == nil){
if (self.tableView.headerViewForSection(index) != nil){
var header:eventHeaderView = self.tableView.headerViewForSection(index) as! eventHeaderView
header.updateProfilePicture(
self.eventsManager.events[index].profilePictureID as String,
progress: self.eventsManager.events[index].profilePictureProgress,
image: self.eventsManager.events[index].profilePicture)
}
}else{
println("Error:" + error.description)
}
})
Here is how I push EventViewController:
func PushEventViewController(sender:UITapGestureRecognizer)->Void{
let ViewSender = sender.view!
let selectedRow = ViewSender.tag
//let Cell:HomeEventTableViewCell = TimelineEventTable.cellForRowAtIndexPath(SelectedIndexPath) as HomeEventTableViewCell
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
let VC:EventViewController = self.storyboard?.instantiateViewControllerWithIdentifier("EventViewController") as! EventViewController
VC.event = self.eventsManager.events[selectedRow]
self.navigationController?.pushViewController(VC, animated: true)
})
}
Problem
The problem is that if I press on event and push EventViewController before image is downloaded (completion handlers are still getting called) it crashes app.
Assumptions
I struggled with this for days and couldn't find a solution,
but my assumptions are that completion handler called after
Crash happens when it tries to execute following line after EventViewController was pushed:
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
I assume when new view controller is pushed , event object which is used in completion handler is being deallocated
Found out where the problem was, the issue was that variable event.profilePictureProgress was declared as dynamic var (I was going to take advantage of that to add observer to it after).
I'm working on an app in iOS wherein I need to start spinning a UIActivityIndicatorView, upload an image to a server, and when the upload is completed, stop spinning the activity indicator.
I'm currently using XCode 7 Beta and am testing the app on the iOS simulator as an iPhone 6 and iPhone 5. My issue is that the activity indicator won't end immediately after file upload, but several (~28 seconds) later. Where should I place my calls to cause it to end?
I have an #IBOutlet function attached to the button I use to start the process, which contains the startAnimating() function, and which calls a dispatch_async method that contains the call to uploadImage, which contains the signal, wait, and stopAnimating() functions.
Note that
let semaphore = dispatch_semaphore_create(0)
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
are defined at the top of my class.
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
} // end dispatch_async
} // works with startAnimating() and stopAnimating() in async but not with uploadImage() in async
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
dispatch_semaphore_signal(self.semaphore)
}
}
})! // end dataTaskWithResult
task.resume()
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
} // end uploadImage
This is just one version of my code, I have moved several things around several different ways. I have tried this:
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
dispatch_semaphore_signal(self.semaphore)
} // end dispatch_async
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
}
And several, several other ways of moving my code around to attempt to get the activity indicator to display for the duration of the image upload and then immediately quit. In some cases the spinner doesn't appear at all for the duration of program execution. I read this post and this question and have migrated my dispatch_semaphore_wait and stopAnimating() to the uploadImage() method to circumvent this, but can't find enough information in the UIActivityIndicatorView documentation about the UI updating to know any other way of updating it, though I believe this might be at the core of the problem.
All I need is for the spinner to start before the upload process begins (dataTaskWithRequest) and end once it has succeeded or failed. What am I doing wrong?
Instead of using semaphores, you could just dispatch directly to the main thread in your async task,
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
}
}
// dispatch to main thread to stop activity indicator
dispatch_async(disptach_get_main_queue()) {
self.activityIndicatorView.stopAnimating()
}
})! // end dataTaskWithResult
task.resume()
} // end uploadImage