I am adding about 3000 MKOverlays to my map, and as you can imagine, it takes a while, up to about eight seconds sometimes. I'm looking for a way to use threading to improve performance, so the user can move the map around while overlays are being added. Preferably, the overlays would be added sequentially, starting with the just the ones within the map's region. I have tried something along these lines with GCD:
- (MKOverlayView*)mapView:(MKMapView*)mapView viewForOverlay:(id)overlay {
__block MKPolylineView* polyLineView;
//do the heavy lifting (I presume this is the heavy lifting part, but
// because this code doesn't compile, I can't actually *test* it)
// on a background thread
dispatch_async(backgroundQueue, ^ {
polyLineView = [[[MKPolylineView alloc] initWithPolyline:overlay] autorelease];
[polyLineView setLineWidth:11.0];
//if the title is "1", I want a blue line, otherwise red
if([((LocationAnnotation*)overlay).title intValue]) {
[polyLineView setStrokeColor:[UIColor blueColor]];
} else {
[polyLineView setStrokeColor:[UIColor redColor]];
}
//return the overlay on the main thread
dispatch_async(dispatch_get_main_queue(), ^(MKOverlayView* polyLineView){
return polyLineView;
});
});
}
But because GCD blocks are defined with void parameter and return types, this code doesn't work- I get an incompatible pointer type error on the return line. Is there something I am missing here, or another way to thread this? Or perhaps an entirely different way to improve the performance of the overlay-adding process? I appreciate any and all help!
Edit:
I have found that the problem is not where I actually add the overlays here:
for(int idx = 1; idx < sizeOverlayLat; idx++) {
CLLocationCoordinate2D coords[2];
coords[0].latitude = [[overlayLat objectAtIndex:(idx - 1)] doubleValue];
coords[0].longitude = [[overlayLong objectAtIndex:(idx - 1)] doubleValue];
coords[1].latitude = [[overlayLat objectAtIndex:idx] doubleValue];
coords[1].longitude = [[overlayLong objectAtIndex:idx] doubleValue];
MKPolyline* line = [MKPolyline polylineWithCoordinates:coords count:2];
[line setTitle:[overlayColors objectAtIndex:idx]];
[mapViewGlobal addOverlay:line];
}
Adding all 3000 takes maybe 100ms here. The part that takes a long time (I assume) is where I actually create the overlays, in the first method I showed.
There is a little gap between what you want and what the compiler can do. When you are calling dispatch_async, you are actually telling the CPU "here, have this chunk of code, and run it whenever you feel like it, not now, not blocking my user interface thread". But, your method has to return now. There is simply no way for you to create anything in a background thread, because you are going to have to wait for it anyway before mapView:viewForOverlay: returns, since it has to return something.
This method is not the place to use GCD or any background code. If your problem is the addition of a big number of overlays at once, I would split all the overlays into chunks of say 100 and add them to the map with a delay of 100ms between each batch.
Related
I am developing an IOS 10.x application using UICollectionView and would like to change the image of specific cells, at regular interval.
The code below shows the current implementation. Even though it should change the cell background image every half of second, it changes the images immediately disregarding the NSThread SleepAt interval of 0.5 seconds.
I suspect something about the main thread handling or the ReloadItem method but hasn't reached a clear conclusion. Any insight is very welcome! Thank you.
NSNumber* originalCardSelected;
int position;
for (int i = 0; i < [opponentOriginalCardArray count]; i++) {
originalCardSelected = [opponentOriginalCardArray objectAtIndex:i];
position = [self convertLevelDataPositionToCellViewPosition:[originalCardSelected intValue]];
NSMutableArray *tmpBackgroundAssets = [self getNewAssetBackgroundBasedOnBackgroundType:playbackBackground Index:position];
self.backgroundAssets = tmpBackgroundAssets;
dispatch_async(dispatch_get_main_queue(), ^{
[collectionViewRoot reloadItemsAtIndexPaths:[collectionViewRoot indexPathsForVisibleItems]];
});
[NSThread sleepForTimeInterval:0.5];
}
You should use performBatchUpdates(_:completion:) methods and add your for loop inside it.
Remember to not keep strong references inside the block to avoid retain cycles.
EDIT:
and in the completion block, you can check if finished and add your NSThread methods
I'm doing a pretty simple iOS/ObjC program. Click a button, it's loops thru a for..next and displays counters and pics.
for (int i=0; i <= numberOfExercises; i++) {
exerciseName = [exercises objectAtIndex:j][0];
[self.lastLabel setText:exerciseName];
[self.lastLabel setNeedsDisplay];
usleep(1000000);
NSLog(#"Count: %d Name:%#", i, exerciseName);
}
However, it's not updating the actual textfield on the screen.
I've tried everything I know of and there's just something I can't see.
(IBAction)btnExerciseClicked:(id)sender {
for (int i=0; i <= numberOfExercises; i++) {
exerciseName = [exercises objectAtIndex:j][0];
exerciseImageName = [exercises objectAtIndex:j][1];
exerciseImageName = [exerciseImageName stringByAppendingString:#".jpg"];
self.exerciseImage.image = [UIImage imageNamed:exerciseImageName];
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
[self.exerciseImage setNeedsDisplay];
}
I've put the block in there:
double delayInSeconds = 0.1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(#"inside the block");
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
[self.exerciseImage setNeedsDisplay];
});
And I've done a direct assign before the loop starts:
exerciseName = [exercises objectAtIndex:5][0];
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
NSLog(#"Name: %#", self.ExerciseName.text);
I've even written a separate method for it:
-(void) myCycleDisplay: (NSString *) imageName
nameOfExercise: (NSString *) exerciseName
voiceOver: (BOOL) useVoiceOver
countDown: (BOOL) useCountDown
beep: (BOOL) useBeep{
[self.ExerciseName setText:exerciseName];
[self.ExerciseName setNeedsDisplay];
self.exerciseImage.image = [UIImage imageNamed:imageName];
}
And after all that, STILL no display until AFTER the method is done. Put in a delay: usleep(1000000);
I've confirmed that the data is in the element. I've setup the element in code and in IB.
The app is pretty simple, a button is pressed on the screen, data is loaded, an array is walked and display items are updated based on elements in the array.
The data is in the textfield, this has been confirmed. It even display them, but only after the method is exited.
Once the method is exited, the text/pic are displayed properly.
So, as a test, I made the for..next loop run twice and clicked the button over and over. Sure enough it displayed properly AFTER leaving the method.
I can't get it to update the display while IN the method. (this also includes the slider).
Why do I have to exit a method to get the display to update?
I think the easiest solution to your problem is to dump the whole for-loop into another queue.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
for (int i=0; i <= numberOfExercises; i++) {
NSString *exerciseName = [exercises objectAtIndex:i][0];
dispatch_async(dispatch_get_main_queue(), ^{
[self.lastLabel setText:exerciseName];
[self.lastLabel setNeedsDisplay];
});
usleep(1000000);
NSLog(#"Count: %d Name:%#", i, exerciseName);
}
});
There are three additional changes.
Changed [exercises objectAtIndex:j] to [exercises objectAtIndex:i]
I think this was a mistake on your part
Made exerciseName local to the block.
I could have made the declaration of exerciseName __block, but it's easier to just make the whole thing local.
Wrapped setting the label in dispatch_async(dispatch_get_main_queue(), ^{ … });
UI updates must be made on the main queue.
Note, this is a bad solution. You should rethink your approach entirely. I would move this logic into it's own separate class, then use notifications to get the UI to update.
When you call usleep(1000000) you are blocking the main queue. UI updates happen on the main queue, but you're blocking that queue, so they don't happen. Also, UI updates in general don't happen until you finish the current pass through the event loop-- from the user tapping your button, through your method doing whatever it needs to do, continuing until your method finishes. Then UIKit updates the UI. You need to let your method finish because that's how UIKit works.
I'm not exactly sure what you're trying to do. If you want to update your UI at intervals, look into NSTimer.
I hope I understood the problem well enough to give a relevant answer and I apologize if I didn't. Here's how I see it:
There is a set of exercises that have to be done one after another;
Exercises take certain amount of time;
The name of the exercise and a relevant image are displayed when exercise starts.
The current implementation does precisely that: display is updated and the system sleeps for a certain amount of time before updating the display again.
The problem with that approach is that UIKit frameworks that is commonly used when doing interactive things on iOS devices is what could be called "indirect". For instance, when .text property of a label is updated, text is not drawn, instead, the system is notified that display should be updated. UIKit periodically checks whether updates are requested and if they are, the display is redrawn.
All of this is done on the "main queue" by sequentially executing blocks of code added to it (as a side note, this is to unlike the way Javascript work in a browser). This means that as long as some block of code is executed, everything else of the main queue will not be and the app will stop being interactive and updating display. Thus, the way to use UIKit is by quickly notifying it of necessary changes and finishing the function. Consequently, one never does long calculations or "sleeps" on on the main queue.
In context of this question, this leaves a problem of timed updates: we want to update the UI after some time has expired. There are 2 solution that come to mind:
The "typical" one would to be use the NSTimer class;
If constant updates to the UI are necessary, there is a CADisplayLink class and a convenient [UIScreen displayLinkWithTarget:selector:] method to create one on, say, [UIScreen mainScreen]. What it does is, it calls the selector in time for screen updates (currently, that's about 60 times per second but you can configure it to be called less frequently). It is ideal for things like count-down timers and frequently updating UI elements such as progress bars.
(I've switched to Swift and the latest API so the method names might not be exactly right in Objective-C).
Please let me know if I misunderstood the question or if you need sample code or further clarification.
Good luck!
I want to be able to update / reload a ScatterPlot on iOS Devices during runtime. To be specific, I record Audio input, do some funny stuff and put the result as a simple NSNumber value into an array. Whenever that happens, reloadData is called on the Plot I want to update but sadly, exactely nothing happens. Basically, I do the same as described in the answer here: real time plotting on iPhone using core plot? . It just doesn't work, so I assume I made some stupid mistake.
This the relevant method:
-(NSNumber *) numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index {
double val = (index/5.0) -10;
if (fieldEnum == CPTScatterPlotFieldX) {
return [NSNumber numberWithDouble:val];
} else {
if ([plot.identifier isEqual: #"X Squared Plot"]) {
return [NSNumber numberWithDouble:10-(0.25*val*val)];
} else if( [plot.identifier isEqual:#"LivePlot"]) {
if (!_isMeasuring) {
return [NSNumber numberWithFloat:0.0f];
} else {
printf("%f", [[_plotIndizesAndVolume objectAtIndex:index] floatValue]);
return [NSNumber numberWithFloat:[[_plotIndizesAndVolume objectAtIndex:index] floatValue]];
}
} else {
return 0;
}
}
}
The X Squared Plot is irrelevant to my needs for now, it's just a reference. The LivePlot is the one I need and want to update. The _plotIndizesAndVolume is the NSMutableArray I store my values in. It has 500 Elements, all of which are initialized with [NSNumber numberWithFloat:0.0f] at the beginning of my execution. The Plot has 500 indizes as well, therefor we don't get out of bounds or whatever.
The code executes fine, the method is being called, the printf statement works and correctly displays the floats, the Plot just doesn't update.
I also tried
if (index < [_plotIndizesAndVolume count]) {
return [NSNumber numberWithFloat:[_plotIndizesAndVolume objectAtIndex:index]];
} else {
return [NSNumber numberWithFloat:0.0f];
}
without having the 500 0.0fs in the array, so that I just access values differently from 0.0f. This of course is better, it just doesn't work either.
Any ideas?
Update 1:
If I fill the _plotIndizesAndVolume with just random numbers at the beginning of my application, the plot perfectly follows these numbers.
If I printf in the method I can perfectly read the updated values once reload has been called on the plot.
However, these updated values aren't shown in the plot. The plot remains the way it is after calling -reloadData on it, no matter which values changed in the _plotIndizesAndVolume-Array.
So, from what I can see: I can access the array properly, I can update and read the new values properly, the plot still stays the same and doesn't change. I'm confused. Probably I still made some random stupid mistake, I just don't see it.
Update2:
Well, it seems as if my problem isn't the reloadData itself, but from where I call it. Some more context: I'm using novocaine to measure decibel via the device microphone. This works fine. I then want to visualize part of the measured dB in a graph through corePlot (which I described above). I got a method called startMeasuring which I call somewhen and then - obviously - start measuring decibel. Whenever that happens I put the measured value int _plotIndicesAndVolume and call reloadData on the plot.
The method looks like this (and is largely copied directly from the novocain examples):
-(void) startMeasuring {
if (_isMeasuring) {
return;
}
__weak ViewController * wself = self;
self.ringBuffer = new RingBuffer(32768, 2);
self.audioManager = [Novocaine audioManager];
// MEASURE SOME DECIBELS!
// ==================================================
__block float dbVal = 0.0;
__block int count = 0;
__block int limiter = 0;
[self.audioManager setInputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels) {
vDSP_vsq(data, 1, data, 1, numFrames*numChannels);
float meanVal = 0.0;
vDSP_meanv(data, 1, &meanVal, numFrames*numChannels);
float one = 1.0;
vDSP_vdbcon(&meanVal, 1, &one, &meanVal, 1, 1, 0);
dbVal = dbVal + 0.2*(meanVal - dbVal);
float reducedVal = (dbVal + 66.0f)/3;
if (!wself.measuringPaused && limiter > 10) {
[wself.plotIndizesAndVolume replaceObjectAtIndex:count withObject:[NSNumber numberWithFloat:reducedVal]];
[[wself.graph plotWithIdentifier:#"LivePlot"] reloadData];
limiter = -1;
++count;
}
++limiter;
}];
[self.audioManager play];
_isMeasuring = true;
}
I call reloadData from within the measuring block. I don't know exactely much about using blocks, I just assumed I could do that. If I, however, try to change the data in the array and afterwards reload the plot manually from somewhere else, it works and the plot updates accordingly. Is the problem related to my call of the method from within the block?
Even then I'm still unsure why it doesn't work, as the printf statement works perfectly - so I'm under the assumption that the updateData method is correctly invoked and the values are correctly updated.
Update 3:
If I call reloadData from somewhere else during the measurement, the graph updates just fine and according to whatever is in _plotIndizesAndVolume. It simply doesn't update if I call it from inside the block (even if I call another method from within the block which in turn calles reloadData). However, I need the live update. I assume I could reload the plot once every x miliseconds as well (going to try that next), I'm still curious as to why it doesn't work in the first place.
Update 4: As assumed in Update 3, if I call reloadData from somewhere else through a NSTimer, it does exactely what I want it to do. Why can't I call it from within the block then?
Call -reloadData (and any other Core Plot methods) from the main thread.
Probably you need to update your plot space x-range/y-range in your timer
CPTXYPlotSpace *plotSpace = (CPTXYPlotSpace *)self.graph.defaultPlotSpace;
[plotSpace scaleToFitPlots:[self.graph allPlots]];
CPTPlot *thePlot = [self.graph plotWithIdentifier:kIdentifier];
[thePlot reloadData];
I have 10 UIImageViews on the screen. They are all in an array called posArray. I also have another UIImageView that is dragged by user and can hit those other 10. What is the easies way of displaying a simple NSlog message if the one object hits any of the other 10?
Right now i'm using touchesBegin, touchesMoved to move my one object and this array below to test if the one objects hits any of the other ten.
I'm just thinking that there is an easier, less memory spending way, way of doing this for some reason.
for (int i = 0; i < [posArray count]; i++) {
UIImageView *tempPos;
tempPos = [posArray objectAtIndex:i];
if (CGRectIntersectsRect(red1.frame, tempPos.frame)) {
red1.center = CGPointMake(tempPos.center.x, tempPos.center.y);
NSLog(#"position piece touched");
}
}
You can also use fast enumeration to get some more speed. Also you can add a break statement after you found one match (if you just need one match):
for (UIImageView * tempPos in posArray){
if (CGRectIntersectsRect(red1.frame, tempPos.frame)) {
red1.center = CGPointMake(tempPos.center.x, tempPos.center.y);
NSLog(#"position piece touched");
break;
}
}
I'm having problems adding subviews to a scrollview sequentially.
I've got a JSON response coming back from the server which I parse into an array of Business objects, and I send off to the function updateCarousel, which looks like this:
-(void) updateCarousel: (NSArray *)response{
if(response && response.count>0){
int i=0;
self.scrollView.hidden=NO;
[self.scrollView setNeedsDisplay];
self.pageControl.hidden=NO;
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:NO];
for (Business *business in response){
if (i >= MAX_INITAL_SEARCH_RESULTS)
break;
CGRect frame;
frame.origin.x = self.scrollView.frame.size.width * i;
frame.origin.y = 0;
frame.size = scrollView.frame.size;
CardView *cardView = [[CardView alloc] initWithBusinessData:business andFrame:frame];
//I've tried the following code with and without wrapping it in a GCD queue
dispatch_queue_t addingQueue = dispatch_queue_create("adding subview queue", NULL);
dispatch_async(addingQueue, ^{
[self.scrollView addSubview:cardView];
});
dispatch_release(addingQueue);
cardView.backgroundColor = [UIColor colorWithWhite:1 alpha:0];
i++;
self.scrollView.contentSize = CGSizeMake(i*(self.scrollView.frame.size.width), self.scrollView.frame.size.height);
self.pageControl.numberOfPages=i;
}
}else{
self.scrollView.hidden=YES;
self.pageControl.hidden=YES;
NSLog(#"call to api returned a result set of size 0");
}
The result - despite the many things I've tried - is always the same: the scrollView adds the subviews all at once, not as they are processed through the loop. I don't understand how this is possible. If I add a sleep() at the end of the loop, it somehow waits for the whole loop to be over before it shows the subviews as added. How does it even know how long the results array is? I'm at my wits' end, please help.
I assume you are not using any extra threads to process the data.
What you experience is the application is stuck executing your method. Even though you add your subviews one by one (with a sleep between them), no other piece of code is executed to handle your adding.
1. You can use another thread to load the data and add subviews but this needs to be synchronized to the main thread (more complicated).
2 You can break your method in multiple calls. Between 2 calls of your load method, other pieces of code are allowed to execute and this means the scrollview will be able to process/display your subviews one by one.
You will need to change your loading method to something like this:
- (void)updateCarouselStep:(NSNumber*)loadIndex
{
if (response && response.count > 0)
{
// Here add only a subview corresponding to loadIndex
// Here we schedule another call of this function if there is anything
if (loadIndex < response.count - 1)
{
[self performSelector:#selector(updateCarouselStep:) withObject:[NSNumber numberWithInt:(loadIndex+1) afterDelay:0.5f];
}
}
}
This is just one basic solution to the problem. For example, you need to take into consideration what happens if you update response data before you finish loading the previous one.