Swift: How to let background thread wait for user input? - swift

I have a background thread which needs some user input. Since it is not recommended to call an NSAlert window from a background thread, I like to do this in the main thread. But how can I let the background thread wait till the NSAlert window is closed?
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// some background task
dispatch_async(dispatch_get_main_queue()) {
// UI task asking for user input:
let alert = NSAlert()
alert.messageText = "Some text"
alert.informativeText = "Some information"
alert.addButtonWithTitle("Yes")
alert.addButtonWithTitle("No")
result = alert.runModal()
}
// some background task, treating user input (Yes/No)
}

The runModal method is synchronous. If you want to wait for it to finish, rather than dispatching asynchronously to the main queue with dispatch_async, you can most easily achieve what you asked for by dispatching it synchronously with dispatch_sync, e.g.:
let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)
dispatch_async(queue) {
// some background task
var result: NSModalResponse!
dispatch_sync(dispatch_get_main_queue()) { // note, `dispatch_sync`
// UI task asking for user input:
let alert = NSAlert()
...
result = alert.runModal()
}
// some background task, treating user input (Yes/No)
}
A more subtle problem rests with the synchronous nature runModal. This will block the main thread. This is not generally a good idea. It can interfere with other tasks your app might be performing (timers, animations, network operations using the main queue, etc.) that might require the main thread.
So, instead of the synchronous runModal, you might consider presenting the NSAlert asynchronously with beginSheetModalForWindow, with a completion handler that dispatches additional background processing to the background queue, e.g.:
let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)
dispatch_async(queue) {
// some background task
dispatch_async(dispatch_get_main_queue()) { // note, `dispatch_async` is OK here
// UI task asking for user input:
let alert = NSAlert()
...
alert.beginSheetModalForWindow(NSApp.mainWindow!) { result in
dispatch_async(queue) {
// some background task, treating user input (Yes/No)
}
}
}
}
This achieves what you want (triggering additional background tasks when the modal is dismissed), but has the virtue of not blocking the main thread at all.

You can use semaphore.
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// some background task
//Create a semaphore with count = 0
let semaphore = dispatch_semaphore_create(0)
dispatch_async(dispatch_get_main_queue()) {
// UI task asking for user input:
let alert = NSAlert()
alert.messageText = "Some text"
alert.informativeText = "Some information"
alert.addButtonWithTitle("Yes")
alert.addButtonWithTitle("No")
result = alert.runModal()
//Signal semaphore
dispatch_semaphore_signal(semaphore)
}
//Wait for semaphore
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
// some background task, treating user input (Yes/No)
}
You should not "wait" in the main thread. But background threads are other things.

Related

Show please wait overlay in swift asynchronously

I am using the answer located here for creating a "please wait" overlay. And I also used this answer for the asynchronous logic. What I am trying to do is create this overlay asynchronously. Ultimately, what I would like to do is just create a method which shows this overlay, and when that method call returns I want to be sure that the overlay has been fully presented. So something like this:
Helper.showPleaseWaitOverlay()
doSomeOtherTask() // when we get here, overlay should be fully presented
Helper.hidePleaseWaitOverlay()
I realize I could do something like this (e.g. use the completion callback of the present method) :
Helper.showPleaseWaitOverlay() {
doSomeOtherTask()
Helper.hidePleaseWaitOverlay()
}
But I really am just curious as to why the below code doesn't work. What ends up happening is that the group.wait() call just hangs and never returns.
What am I doing wrong?
// create a dispatch group which we'll use to keep track of when the async
// work is finished
let group = DispatchGroup()
group.enter()
// create the controller used to show the "please wait" overlay
var pleaseWaitController = UIAlertController(title: nil, message: "Please Wait...", preferredStyle: .alert)
// present the "please wait" overlay as an async task
DispatchQueue.global(qos: .default).async {
// we must perform the GUI work on main queue
DispatchQueue.main.async {
// create the "please wait" overlay to display
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
self.pleaseWaitController!.view.addSubview(loadingIndicator)
self.present(self.pleaseWaitController!, animated: true) {
// the "please wait" overlay has now been presented, so leave the dispatch group
group.leave()
}
}
}
// wait for "please wait" overlay to be presented
print("waiting for please wait overlay to be presented")
group.wait() // <---- Call just hangs and never completes
print("done waiting for please wait overlay to be presented")

Activity indicator while creating csv file

I am trying to show an activity indicator while creating a csv file, but it does not show. I am guessing I should use dispatch_async somehow, but I cant figure out how to do this in swift 3.
var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
override func viewDidLoad() {
super.viewDidLoad()
// activity indicator
activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 100 ,y: 200,width: 50,height: 50)) as UIActivityIndicatorView
activityIndicator.hidesWhenStopped = true
activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
activityIndicator.center = self.view.center
self.view.addSubview(activityIndicator)
}
func writeToCsv() {
self.activityIndicator.startAnimating() // start the animation
let fileName = "events.csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
var csvText = self.name! + "\n"
csvText += "Date,Start time,End time\n"
// create rest of comma-separated string
for event in self.events! {
let newLine = "\(event.date),\(event.startTime),\(event.endTime)\n"
csvText.append(newLine)
}
// write to csv
do {
try csvText.write(to: path!, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Failed to create file")
print(error)
}
// create and present view controller with send options
let vc = UIActivityViewController(activityItems: [path as Any], applicationActivities: [])
self.present(vc, animated: true, completion: nil)
self.activityIndicator.stopAnimating() // stop the animation
}
Err, alright bit hard to answer this without a bit more context about your view setup. First of all, make sure your activity indicator is visible without calling the writeCsv method, so you know your view hierarchy is correct. ( I.E. It could be that it is hidden behind some other subview )
Next, in Swift3 Dispatch has been changed to a newer API. I'm not sure whether on OSX they use the raw libdispatch Swift wrapper, but in any case you access it like this:
Background default queue:
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { /* code */ }
Main thread:
DispatchQueue.main.async { /* Mainthread code ( UIKit stuff ) */ }
Your own custom queue for CSV generation blocks:
let queue = DispatchQueue(label: "csvgenerator.queue")
queue.async { /* code */ }
Now for your animating / stopAnimation, make sure you call your UIKit related code from the mainthread to prevent weird glitechs and or crashes
Namely:
DispatchQueue.main.async {
self.activityIndicator?.startAnimating()
}
Another good idea might be to use NSOperationQueue instead. It internally uses GCD I believe, but it does integrate very well into iOS and might make some of the dispatching a lot easier to implement. I myself always use GCD instead, but I have never really had long queeu's of work that needed to be done. One of the advantages of NSOperationQueue is that it is a lot more user friendly in terms of cancelling dispatch blocks.
An interesting session video about NSOperationQueue in the WWDC app written by Dave Delong: WWDC Videos 2015
A small minor changes I'd make to your writeCSV method:
guard let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName) else {
// Should throw an error here, or whatever is handy for your app
return
}
Try to avoid forced unwrapping at all stages where possible.
In methods that have this, you can for instance add "throws" to the end of the function definition so you can use try without the do and catch block, while also being able to throw errors in your guard statement so whatever calls writeCsv can catch the error and more easily display it to the user.

Realm accessed from incorrect thread - dispatch async

I'm trying to do some sorting on a different thread, however i keep getting Realm accessed from incorrect thread. Below is what i'm tried so far.
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
dispatch_async(dispatch_get_main_queue()) {
self.lastLoadedPage = self.lastLoadedPage + 1
if numberOfResults < self.limit {
//Hide FooterView
self.moreDataAvailable = false
self.hideTableViewFooter()
}
}
}
Assuming that you're calling the shown code snippet from the main thread, e.g. from a view controller as it seems, you're likely using a RLMRealm instance from the main thread on another thread.
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// Get a Realm for the current thread
RLMRealm *realm = [RLMRealm defaultRealm];
// do some task with that instance instead
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
// …

Progress Indicator does not animate

I set up a label and a progress indicator to bind to the AppDelegate's progress property. I then perform work on a concurrent queue. As each task finishes, I increase the progress by 1.
My problem is that the label updates tick-by-tick as expected but the progress indicator doesn't. It updates once every 15 ticks or so. Any idea how to make the progress indicator to move with every tick?
A simplified example:
class AppDelegate: NSObject, NSApplicationDelegate {
dynamic var progress = 0
#IBAction func updateProgress(sender : AnyObject) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
guard self.progress < 100 else {
return
}
self.progress += 1
sleep(1)
self.updateProgress(sender)
}
}
}
In my experience, updating binding variable from a background queue sometimes lead to funny behaviors. The progress indicator not updating is an example. I've not figured out the "why" part though. My workaround is to do your work on the background queue, but update the binding variable on the main queue.
Try this (not tested):
#IBAction func updateProgress(sender : AnyObject) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
guard self.progress < 100 else {
return
}
dispatch_async(dispatch_get_main_queue()) {
self.progress += 1
}
sleep(1)
self.updateProgress(sender)
}
}
I noticed that your concurrent queue is anything but concurrent in the example. I assume you use background threads to perform multiple tasks at once. If so, incrementing progress on the main queue also help with race condition because the main queue is a serial one, so all progress increments are performed one by one.
Changes to UI should be update on the main thread. You should move the update progress to main thread.
dispatch_async(dispatch_get_main_queue()) {
self.updateProgress(sender)
}

Why does the iPhone seem to freeze when using a while loop with swift?

I am trying to take a picture every 2 seconds by using a while loop. but when I try this the screen freezes.
This is the function that takes the photo:
func didPressTakePhoto(){
if let videoConnection = stillImageOutput?.connectionWithMediaType(AVMediaTypeVideo){
videoConnection.videoOrientation = AVCaptureVideoOrientation.Portrait
stillImageOutput?.captureStillImageAsynchronouslyFromConnection(videoConnection, completionHandler: {
(sampleBuffer, error) in
if sampleBuffer != nil {
let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(sampleBuffer)
let dataProvider = CGDataProviderCreateWithCFData(imageData)
let cgImageRef = CGImageCreateWithJPEGDataProvider(dataProvider, nil, true, .RenderingIntentDefault)
let image = UIImage(CGImage: cgImageRef!, scale: 1.0, orientation: UIImageOrientation.Right)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
//Adds every image taken to an array each time the while loop loops which will then be used to create a timelapse.
self.images.append(image)
}
})
}
}
To take the picture I have a button which will use this function in a while loop when a variable called count is equal to 0, but when the end button is pressed, this variable is equal to 1, so the while loop ends.
This is what the startPictureButton action looks like:
#IBAction func TakeScreanshotClick(sender: AnyObject) {
TipsView.hidden = true
XBtnTips.hidden = true
self.takePictureBtn.hidden = true
self.stopBtn.hidden = false
controls.hidden = true
ExitBtn.hidden = true
PressedLbl.text = "Started"
print("started")
while count == 0{
didPressTakePhoto()
print(images)
pressed = pressed + 1
PressedLbl.text = "\(pressed)"
print(pressed)
sleep(2)
}
}
But when I run this and start the timelapse the screen looks frozen.
Does anyone know how to stop the freeze from happening - but also to add each image taken to an array - so that I can turn that into a video?
The problem is that the method that processes clicks on the button (TakeScreanshotClick method) is run on the UI thread. So, if this method never exits, the UI thread gets stuck in it, and the UI freezes.
In order to avoid it, you can run your loop on the background thread (read about NSOperation and NSOperationQueue). Occasionally you might need to dispatch something from the background thread to the UI thread (for instance, commands for UI updates).
UPDATE: Apple has a really great documentation (best of what I've seen so far). Have a look at this: Apple Concurrency Programming Guide.
You are calling the sleep command on the main UI thread, thus freezing all other activity.
Also, I can't see where you set count = 1? Wouldn't the while loop continue forever?