How to run multi methods in main thread synchronously? - swift

I have a long CoreData process (GlobalData.shared.resetData function) that takes around 4 seconds and I want to present a loading indicator.
Both action (core data process and showing loading indicator) must run in Main Queue,
Do you know why the showing loading indicator always happened after CoreData process?
#IBAction func resetTapped(_ sender: UIButton) {
tableView.backgroundColor = .green
showLoading(loadingText: nil)
GlobalData.shared.resetData(completion: {
self.refreshGlobalData()
})
}
I added that line to change table background color for testing. Background color always changed after core data process (GlobalData.shared.resetData function).

When you change the background color like that, you're changing the "model", but you won't see the results until the next time the main thread redraws, which won't happen until your code relinquishes control of the run loop (finishes running). You could try dispatch_async-ing the Core Data op, which may allow the run loop to draw before starting your operation. But really, as commenters have mentioned, don't do something that takes four seconds on the main thread.

Related

NSImageView update delayed when set from an NSTimer

I'm working on a project in Swift that requires strict control of image display and removal timings in certain sections. I'm finding that when I set the image property of an NSImageView from inside a block that's fired by a Timer, the actual display of the image on the screen is delayed by up to a second after the assignment is complete. (This is measured by eyeballing it and using a stopwatch to gauge the time between when an NSLog line is written and when the image actually appears on-screen.)
Triggering image display with a click appears to happen instantaneously, whether it's done by setting the image property of an existing NSImageView, or constructing one on the spot and adding it as a subview.
I have attempted to reduce the behavior down to a fairly simple test case, which, after basic setup of the view (loading the images into variables and laying out several target image locations, which are NSImageViews stored to the targets array), sets a Timer, with an index into the targets array stored in its userInfo property, to trigger the following method:
#objc func testATimer(fromTimer: Timer) {
if let targetLocation = fromTimer.userInfo as? Int {
NSLog("Placing target \(targetLocation)")
targets[targetLocation].image = targetImage
OperationQueue.main.addOperation {
let nextLocation = targetLocation + 1
if (nextLocation < self.targets.count) {
NSLog("Setting timer for target \(nextLocation)")
let _ = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(ViewController.testATimer(fromTimer:)), userInfo: nextLocation, repeats: false)
}
}
}
}
The amount of time observed between the appearance of the log entry "Placing target x" and that of the image that is set to be displayed in the very next line seems to average between 0.3 and 0.6 seconds. This is far beyond the delay that this project can tolerate, as the image will only be on screen for a total of 1 second at a time.
My best guess as to why this is happening is that it is something to do with Timers running on a separate thread from the main display thread; however, I do not have enough experience with multi-threaded programming to know exactly how to mitigate that.
Any assistance would be most appreciated, and if I've left out information that would make answering easier (or possible) I'm more than happy to give it.
Well, after poking at it with some helpful people on in #macdev on irc.freenode.net, I found that the source of the problem was the program scaling the image down on the fly every time it set it to the NSImageView. Reducing the size of the image ahead of time solved the problem. (As did setting the image once, then hiding and showing the NSImageView instead.)

Background Process as NSOperation or Thread to monitor and update File

I want to check if a pdf file is changed or not, and if is changed i want to update the corresponding view. I don't know if it's more suitable to use a background process as a Thread or as an NSOperation to do this task. The Apple Documentation says: "Examples of tasks that lend themselves well to NSOperation include network requests, image resizing, text processing, or any other repeatable, structured, long-running task that produces associated state or data.But simply wrapping computation into an object doesn’t do much without a little oversight".
Also, if I understood correctly from the documentation, a Thread once started can't be stopped during his execution while an NSOperation could be paused or stopped and also they could rely on dependency to wait the completion of another task.
The workflow of this task should be more or less this diagram:
Task workflow
I managed to get the handler working after the notification of type .write has been sent. If i monitor for example a *.txt file everything works as expected and i receive only one notification. But i am monitoring a pdf file which is generated from terminal by pdflatex and thus i receive with '.write' nearly 15 notification. If i change to '.attrib' i get 3 notification. I need the handler to be called only once, not 15 or 3 times. Do you have any idea how can i do it or is not possible with a Dispatch Source? Maybe there is a way to execute a dispatchWorkItem only once?
I have tried to implement it like this(This is inside a FileMonitor class):
func startMonitoring()
{
....
let fileSystemRepresentation = fileManager.fileSystemRepresentation(withPath: fileStringURL)
let fileDescriptor = open(fileSystemRepresentation, O_EVTONLY)
let newfileMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor,
eventMask: .attrib,
queue: queue)
newfileMonitorSource.setEventHandler(handler:
{
self.queue.async
{
print(" \n received first write event, removing handler..." )
self.newfileMonitorSource.setEventHandler(handler: nil)
self.test()
}
})
self.fileMonitorSource = newfileMonitorSource
fileMonitorSource!.resume()
}
func test()
{
fileMonitorSource?.cancel()
print(" restart monitoring ")
startMonitoring()
}
I have tried to reassign the handler in test(), but it's not working(if a regenerate the pdf file, what is inside the new handler it's not executed) and to me, doing in this way, it seems a bit boilerplate code. I have also tried the following things:
suspend the DispatchSource in the setEventHandler of startMonitoring() (passing nil), but then when i am resuming it, i get the remaining .write events.
cancel the DispatchSource object and recall the startMonitoring() as you can see in the code above, but in this way i create and destroy the DispatchSource object everytime i receive an event, which i don't like because the cancel() function shoul be called in my case only when the user decide to disable this feauture i am implementing.
I will try to write better how the workflow of the app should be so you can have an more clear idea of what i am doing:
When the app starts, a functions sets the default value of some checkboxes of the window preference. The user can modify this checkboxes. So when the user open a pdf file, the idea is to launch in a background thread the following task:
I create a new queue call it A and launch asynch an infinite while where i check the value of the UserDefault checkboxe (that i use to reload and update the pdf file) and two things could happen
if the user set the value to off and the pdf document has been loaded there could be two situations:
if there is no current monitoring of the file (when the app starts): continue to check the checkboxe value
if there is currently a monitoring of the file: stop it
if the user set value to on and the pdf document has been loaded in this background thread (the same queue A) i will create a class Monitor (that could be a subclass of NSThread or a class that uses DispatchSourceFileSystemObject like above), then i will call startMonitoring() that will check the date or .write events and when there is a change it will call the handler. Basically this handler should recall the main thread (the main queue) and check if the file can be loaded or is corrupted and if so update the view.
Note: The infinite while loop(that should be running in the background), that check the UserDefault related to the feature i am implementing it's launched when the user open the pdf file.
Because of the problem above (multiple handlers calls), i should use the cancel() function when the user set checkboxe to off, and not create/destroy the DispatchSource object everytime i receive a .write event.

Handling large operations Swift

I'm working with NSAttributed strings with large amount of characters 100 000, 1m. How should I handle operations like iterating all of the characters, changing color, foreground and background. It works fine but it's slow, it freezes for a while and then works fine.
Modify them on a background thread. You can add a progress UI for the user
DispatchQueue.global().async {
// modify attributed string
DispatchQueue.main.async {
// update UI
}
}

Simple waiting for value from async thread

I've got 2 basic methods - viewDidLoad and viewDidAppear. According to my App philosophy, when view controller loads, it fetches data from base and starts to sort it with some predicates. Fetching process is long, so I dispatched it to global queue. When my view appears, it obviously do not get the value from array(which compiles in load method) and crashes. So I need viewDidAppear to wait till at least one object will be appended to array.
Kind of semaphores or temp values?
Thanks in advance!
P.S. Each item in array represent struct with data which composes UI. User interact with this UI, so it has to be loaded once with the first item from array. To switch to next item, user just clicks "next" and UI changes according to next item from array. That's why I want the data to fetch in background and allow user to work immediately. (It's impossible to jump on 5th, 10th or 1001st element immediately, there will be enough time to fetch data before user gets on these page numbers)
P.P.S Still no right decision :(
You should using a nested dispatch block, like so:
func fetch(completion block:(() -> Void)?) {
// Run fetch on background thread, to prevent the main thread (and hence your UI) from being 'blocked'.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
//
// Fetch data...
//
dispatch_async(dispatch_get_main_queue(), {
block?()
})
})
}
fetch(completion: {
// Update your UI
})

Asynchronous function execution?

in my iOS app I do the following.
viewDidAppear(){
// Load a spinner in a view on the top
[DSBezelActivityView newActivityViewForView:self.view];
// Execute code that require 3 seconds
...
// Stop the spinner
[DSBezelActivityView removeViewAnimated:YES];
}
The problem is that the spinner doesn't appear, because the the cpu is working hard (something similar). It's like that the code betweek the start and stop has precedence on the rendering of the view.
I would love to find a way to show effectively the start of the spinner, without using a timer to delay the code execution.
Thanks
If you have a method like
-(void) showSpinner:(UIView*)view {
dispatch_async(dispatch_get_main_queue(), ^{
[DSBezelActivityView newActivityViewForView:view];
});
}
there are several ways to call it from a different thread. Choose one from the following:
[NSThread detachNewThreadSelector:#selector(showSpinner:) toTarget:self withObject:self.view];
// or
[self performSelectorInBackground:#selector(showSpinner:) withObject:self.view];
// or
NSInvocationOperation *invOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(showSpinner:) object:self.view];
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:invOperation];
// or
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self showSpinner:self.view];
});
Alt + click for details.
Move code between start and stop activity indicator into separate thread because it's blocking main thread. That's why activity indicator is not showing.
Edit: Example
I agree with the 1st answer with a couple of modifications. I just went through this exact same problem. The problem is that anything graphical is going to automatically move to the background updating when you have code that takes time to get through. Throwing the spinner to the background is what it essantially doing anyway. What you want is (sadly) for you main code to run in the background and the spinner to run in the foreground. I know this sounds bad, but in some cases allowing your code to run a bit slower to give indication that the app is doing something useful is beneficial to the user.
In order to get the spinner to work:
1) Take all the code that takes the 3 seconds to run, and put that into a function that is a void function
2) Instantiate your spinner but store it to a variable that is accessible outside your viewDidAppear routine.
3) Startup a new NSTimer with that runs continuously with an increment of about every quarter second or so. I will define what goes into the routine that gets called every cycle later.
4) Call the routine you created in step 1 using the performSelectorInBackground capability. This essentially is now going to run your startup (3 seconds worth) in the background which is really the only way to allow the animated spinner to show up and truly animate.
5) In the routine you created in step 1, add a line of code right at the top that updates a (global to the object) boolean to true, stating that we are in the middle of our main 3 second routine.
6) At the end of the routine defined in step 1 add a line of code setting the same global defined in step 5 to false indicating that our 3 second routine is completed.
7) In the timer routine, we now want to do something that looks like the following:
// If busy that start the spinner
if(YES == busy){
[spinner startAnimating];
}else{
[spinner stopAnimating];
// Here we can also stop and deallocate the timer
}
If you need more aid on this subject, I can indeed provide exact code. Take a look at the example app that I have developed for the Pepperdine News Group. When you press a button, the spinner comes up on the top right of the screen.
http://itunes.apple.com/us/app/pepperdine-graphic-for-iphone/id516343215?mt=8