How to manipulate views while for-loop - objective-c

I want to manipulate views while in a for loop. I manipulate a view in the for loop, then the operations for the view are done at once after the for loop has ended. I tried to use other threads like GCD, but I noticed that a view is in the main thread. The operations are back to the main thread and they are put off after the for loop finishes.
What I want to do is update UITextView's text while in the for loop. If I can't operate the for loop in another thread, how can I do that? Are there other ways to do that?

Solution 1: Use a timer
In order to progressively add text to a textview, you can use an NSTimer.
Requirements
in your interface - the following ivars or properties:
UITextView *textView;
NSNumber *currentIndex;
NSTimer *timer;
NSString *stringForTextView;
Assuming the string is created and the textview is set up, you can create a function to create the timer and kick it off:
- (void) updateTextViewButtonPressed
{
timer = [NSTimer scheduledTimerWithTimeInterval:.5
target:self
selector:#selector(addTextToTextView)
userInfo:nil
repeats:YES];
}
- (void) addTextToTextView
{
textView.text = [string substringToIndex:currentIndex.integerValue];
currentIndex = [NSNumber numberWithInt:currentIndex.integerValue + 1];
if(currentIndex.integerValue == string.length)
{
[_timer invalidate];
timer = nil;
}
}
This is a basic working implementation, and you can vary it to pass in the string as userInfo for the timer, if it is not present at the class level. Then you could access it in your addTextToTextView selector with sender.userInfo. You can also adjust the timer interval and how exactly the text is added. I used half a second and character by character concatenation as an example.
Solution 2: Use a loop
Requirements
NSString *string
UITextview *textView
- (void) updateTextViewButtonPressed
{
// perform the actual loop on a background thread, so UI isn't blocked
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^()
{
for (int i = 0; i < string.length; i++)
{
// use main thread to update view
dispatch_async(dispatch_get_main_queue(), ^()
{
textView.text = [string substringToIndex:i];
});
// delay
[NSThread sleepForTimeInterval:.5];
}
});
}

Related

Allow user input during loop execution for macOS

I have an Objective C MacOS project In Xcode 12.3 with a loop containing code that writes to user interface controls and may display alerts. When the loop runs, the cursor becomes a rotating rainbow disc. Clicking on a toolbar item (or any user interface control) has no effect until the loop has terminated.
I would like to have a toolbar item accept user clicks during loop execution. Whilst running the loop in a separate thread would allow this, substantial recoding would be required to remove the interface references and alerts from the loop code.
Is there a way of pausing the loop execution to check for input from user controls such as toolbar items? Adding [[NSRunloop mainRunLoop] runUntilDate:[NSDate datewithTimeIntervalSinceNow:0.5]];at the start of the loop code does not achieve this.
I've tried running the loop code (runBatch) in a separate thread using
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
[self runBatch];
dispatch_sync(dispatch_get_main_queue(), ^{
});
});
The loop code is contained in runBatch, which sets and reads various UI controls and these are are flagged as only being accessible from the main thread at run time. The project builds OK. Placing these UI interactions on the main thread after async queue completion would be difficult.
An example of code showing the problem is below. The project consists of a window with an NSTextField (outlet textData) and three buttons, two of which run a loop and the third (Stop) sets a stop flag. The runMain shows the index in textData, but when it runs only the final value appears and the Stop button is not responsive. The cursor becomes a coloured wheel after about 3 seconds when it is moved off the Start button.
When the loop is run on the background thread, the Stop button is responsive but textData cannot be updated from the background thread.
What I would like is for textData to show the index value while the loop is running.
AppDelegate.h
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property (weak) IBOutlet NSTextField *textData;
#end
AppDelegate.m
#import "AppDelegate.h"
#interface AppDelegate ()
#property (strong) IBOutlet NSWindow *window;
#end
#implementation AppDelegate
#synthesize textData;
static bool stopBatch = false;
- (IBAction)runMain:(id)sender {
stopBatch = false;
[self runMain];
}
- (IBAction)stopClick:(id)sender {
stopBatch = true;
}
- (IBAction)runBackground:(id)sender {
stopBatch = false;
[self runBatchBackground];
}
-(void) runMain{
[textData setStringValue:#"Start"];
[textData displayIfNeeded];
NSString * iString = #"0";
for (int i=0;i<=10000 ;i++)
{
iString= [NSString stringWithFormat: #"%d",i];
[textData setStringValue:iString];
[textData displayIfNeeded];
if(stopBatch)
{
break;
}
}
NSString *iStringFinal = iString;
}
-(void)runBatchBackground{
[textData setStringValue:#""];
NSString * __block iString = #"0";
dispatch_queue_t backgroundQueue = dispatch_queue_create("Network",nil);
dispatch_async(backgroundQueue, ^(void){
for (int i=0;i<=10000000 ;i++)
{
iString= [NSString stringWithFormat: #"%d",i];
//[self->_textData setStringValue:iString];
//[self->_textData displayIfNeeded];
if(stopBatch)
{
break;
}
}
NSString *iStringFinal = iString;
});
}
#end
After some experimentation I found a simpler solution than that kindly provided by #willeke. Using runMain code as shown below, adding a timerCalled method and adding a class variable iVal allowed the Stop button action to be executed while the loop was running. It appears that the 10000 timer requests are queued and then executed without blocking the main loop (and access to user controls) until timerCalled is exited using a return statement as shown. Is there anything wrong with this approach?
-(void) runMain{
for (int i=0;i<10000 ;i++)
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(timerCalled) userInfo:nil repeats:NO];
}
}
-(void)timerCalled{
if(stopBatch) return;
for (int i=0;i<10;i++)
{
iVal++;
iString= [NSString stringWithFormat: #"%ld",iVal];
[textData setStringValue:iString];
}
}
Here you go
- (void)runBatchBackground {
[self.textData setStringValue:#""];
NSString * __block iString = #"0";
dispatch_queue_t backgroundQueue = dispatch_queue_create("Network",nil);
dispatch_async(backgroundQueue, ^(void){
for (int i = 0; i <= 10000000; i++)
{
// Simulate some processing
// If the code on the background thread runs faster than the code
// on the main thread, then the main thread is lagging behind and doesn't
// have time to process events.
[NSThread sleepForTimeInterval:0.25];
iString = [NSString stringWithFormat: #"%d",i];
// Execute UI code on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
[self.textData setStringValue:iString];
//[self.textData displayIfNeeded]; displayIfNeeded is not needed
});
if (self->stopBatch)
{
break;
}
}
});
}

Draw the contents of an array modified in the background

I have a background thread that runs and modifies the contents of a NSMutableArray within an object. This takes a long time to run (several hours) and I periodically want to draw the contents of an array within the drawRect of a NSView to check on progress and see the intermediate results.
My object has a protocol with a method called: didChange:
// How I start my background thread
[self performSelectorInBackground:#selector(startProcessing) withObject:nil];
- (void)startProcessing {
myObject.delegate = self;
[myObject start];
}
// My protocol implementation
- (void)myObjectDidChange:(myObjectClass *)sender {
[myView setNeedsDisplay:YES];
}
// My View's drawRect (pseudo code)
- (void)drawRect {
[myObject drawInContext:context];
}
All works, except that the NSMutableArray backing all this is being changed whilst the drawing takes place. How should I do this? Do I somehow pause the processing in the background thread whilst the update is taking place?
EDIT: This is the sort of display I am drawing (although much more complicated):
Any help appreciated.
If you are doing something in background thread and you want to update UI, its usually done on the main thread, so in your object did change you would do it, probably like this:
// My protocol implementation
- (void)myObjectDidChange:(myObjectClass *)sender {
dispatch_async(dispatch_get_main_queue(), ^{
[self drawRect]; //Or any drawing function you are trying to do
});
}
I have done it using NSLock to lock the outer loop of the start and the drawInContext methods. I am still not sure if this is the best approach and will not accept this answer for a few days in case there is a better answer out there.
- (void)start {
for(int i=0; i < MAX; i++) {
[self.updateLock lock];
....
[self.updateLock unlock];
}
}
- (void)drawInContext:(CGContextRef)context {
[self.updateLock lock];
...
[self.updateLock unlock];
}

TextField will not update in Cocoa application

I am creating a simple countdown application in Cocoa. When I click the "Start Countdown" button, I get the spinning mouse indicator. After the correct amount of time (for example, if "2" was put in the box, the application would wait 2 seconds), the TextField skips right to 0. Here is my code:
#import "APPAppDelegate.h"
#implementation APPAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
}
- (IBAction)StartCountdown:(id)sender {
int counterNow, counterLater;
counterNow = [_FieldCount intValue];
while (counterNow>1){
counterNow = [_FieldCount intValue];
counterLater = counterNow - 1;
[NSThread sleepForTimeInterval:1.0f];
[_FieldCount setIntValue:counterLater];
}
}
#end
You need to let the run loop run in order for updates to make it to the screen. You're changing the value, then sleeping the thread. A better method would be to have an NSTimer which sets the text of the text field, and then calls -setNeedsDisplay:YES on it. Something like this:
-(void)fireTimer:(NSTimer*)timer
{
counterNow = [_FieldCount intValue];
counterNow--;
[_FieldCount setIntValue:counterNow];
}
This assumes that _FieldCount is an NSTextField. You'd set this up by creating a repeating timer. If you wanted it to repeat once per second, you could do the following:
NSTimer* secondsTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(fireTimer:) userInfo:nil repeats:YES];

Cocoa threads being cantankerous

In short, I would like to, in Objective-C cocoa, program something that functions the same way as the following Java pseudocode:
public class MainClass
{
public void mainmethod() //Gets called at start of program
{
UILabel label = CreateAButton();
new DaemonClass(label).start();
//Do things without being interrupted by the Daemon class sleeping or lagging
}
}
public class DaemonClass extends Thread
{
public UILabel label;
public DaemonClass(UILabel lbl)
{
setDaemon(true);
label = lbl;
}
public void run()
{
int i = 0;
while(true)
{
i++;
i = i%2;
UILabel.setText("" + i);
Thread.sleep(1000);
}
}
}
In other words... I'd like to spawn a daemon thread that can be as slow as it likes, without interrupting the progress or speed of any other threads, INCLUDING, the main one.
I have tried using things like the Dispatch Queue, as well as NSThread.
When using either of these, I tried to create a simple label-changer thread that toggled the label's text from 1 to 0, indefinitely. It appeared to me, the user, to constantly be locked either at 1, or 0, randomly chosen at startup.
When using either of these, and attempting to use [NSThread sleepForTimeInterval:1];, the thread would stop executing all together after the sleepForTimeInterval call.
Furthermore, having skimmed the docs, I picked up on the fact that the run loop is not called while [NSThread sleep... is sleeping!
If it is any help, I was invoking my threads from the - (void)viewDidLoad; method.
My question for you is:
How do I stop [NSThread sleepForTimeInterval:1]; from crashing my thread, OR:
How do I start a daemon thread that invokes a method or code block (preferably a code block!)
P.S. if it makes any difference, this is for iOS
The reason for the problems you've seen is most likely that UIKit isn't thread-safe, i.e. you can only use a UILabel from the main thread. The easiest way to do that is to enqueue a block on the main queue (which is associated with the main thread) using GCD:
dispatch_async(dispatch_get_main_queue(), ^{
myLabel.text = #"whatever";
});
First of all you cannot manage UIKit in the background threads. In order to set the text to UILabel you need to use main thread.
Judging by the type of task you want to achieve, you should use NSTimer. You can set the time interval it should be called and stop and resume it anytime.
#property (strong, nonatomic) NSTimer *timer; //in your .h file
- (void)startChangingLabelText {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(checkTime:)
userInfo:nil
repeats:YES];
}
- (void)stopChangingLabelText {
[timer invalidate], self.timer = nil;
}
- (void)checkTime:(NSTimer *)timer {
int rand = arc4random() % 2;
if (rand)
label.text = #"true";
else
label.text = #"false";
}

NSTextField waits until the end of a loop to update

Here's the problem: I have some code that goes like this
otherWinController = [[NotificationWindowController alloc] init];
for (int i = 0; i < 10; i++) {
[otherWinController showMessage:[NSString stringWithFormat:#"%d", i]];
NSLog(#"%d", i);
sleep(1);
}
where otherWinController is a subclass of NSWindowController that I am using to update my window as changes happen in code, and the alloc init method simply opens up the nib and shows the window. showMessage is a method that changes an NSTextView to display whatever text is in the parameter.
in the NSLog, the text changes every second and just counts to ten. However for the showMessage method, the text is blank for a full ten seconds and then just displays the number 10. any thoughts??
FTR, the showMessage method is simply
- (void)showMessage:(NSString *)text {
[[self message] setStringValue:text];
}
not that it should matter, that's pretty basic.
You can probably achieve the desired effect right inside your loop, if you explicitly give the run loop some time to run:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 0.1]];
The problem is that you’re blocking the main thread in your for loop and user interface updates happen on the main thread. The main thread run loop will only spin (and consequently user interface updates will take place) after the method containing that for loop finishes executing.
If you want to update that text field every second, you should use a timer. For instance, considering otherWinController is an instance variable, declare a counter property in your class and:
otherWinController = [[NotificationWindowController alloc] init];
self.counter = 0;
[otherWinController showMessage:[NSString stringWithFormat:#"%d", self.counter]];
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(updateCounter:)
userInfo:nil
repeats:YES];
In the same class, implement the method that’s called whenever the time has been fired:
- (void)updateCounter:(NSTimer *)timer {
self.counter = self.counter + 1;
[otherWinController showMessage:[NSString stringWithFormat:#"%d", self.counter]];
if (self.counter == 9) {
[timer invalidate];
// if you want to reset the counter,
// self.counter = 0;
}
}
Views don't get updated until the end of the run loop; your for loop doesn't let the run loop continue, so all the view updates you make are just done after your for loop exits.
You should either use an NSTimer or performSelector:withObject:afterDelay: to change the display in a loop-like fashion.
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(changeTextFieldsString:)
userInfo:nil
repeats:YES];
Then your timer's action will change the view's image:
- (void)changeTextFieldsString:(NSTimer *)tim {
// currStringIdx is an ivar keeping track of our position
if( currStringIdx >= maxStringIdx ){
[tim invalidate];
return;
}
[otherWinController showMessage:[NSString stringWithFormat:#"%d", currStringIdx]]
currStringIdx++;
}
You also generally don't want to use sleep unless you're on a background thread, because it will lock up the rest of the UI; your user won't be able to do anything, and if you sleep long enough, you'll get the spinning beach ball.
I think that by calling sleep(1) you block the main thread, which must draw your changes. So the display is not updated. The task manager will not interrupt your function. You shouldn't use sleep in this case. Please take a look at NSTimer class. It has a static method scheduledTimerWithTimeInterval, which you should use.
static UIViewController *controller = nil;
.....
{
.....
otherWinController = [[NotificationWindowController alloc] init];
controller = otherWinController;
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(timerTick:) userInfo:nil repeats:YES]; //set timer with one second interval
.....
}
- (void) timerTick:(NSTimer*)theTimer {
static int i = 0;
[controller showMessage:[NSString stringWithFormat:#"%d", i]];
NSLog(#"%d", i);
if (++i == 10) {
[theTimer invalidate];
i = 0;
}
}