I'm analyzing an image which takes some time and meanwhile I want to display a progress indicator. For this I'm using MBProgressHUD.
It almost works... I get this error: "Modifying layer that is being finalized". I guess it's due to the fact that I do pushViewController not in my main thread. Am I right? Any ideas on how to correct this issue?
My code:
- (IBAction)buttonReadSudoku:(id)sender
{
mbProgress=[[MBProgressHUD alloc] initWithView:self.view];
mbProgress.labelText=#"Läser Sudoku";
[self.view addSubview:mbProgress];
[mbProgress setDelegate:self];
[mbProgress showWhileExecuting:#selector(readSudoku) onTarget:self withObject:nil animated:YES];
}
- (void)readSudoku
{
UIImage *image = imageView.image;
image = [ImageHelpers scaleAndRotateImage:image];
NSMutableArray *numbers = [SudokuHelpers ReadSudokuFromImage:image];
sudokuDetailViewController = [[SudokuDetailViewController alloc] init];
[sudokuDetailViewController setNumbers:numbers];
[[self navigationController] pushViewController:sudokuDetailViewController animated:YES];
}
Define a new method to push your detail view controller and use -performSelectorOnMainThread:withObject:waitUntilDone: to perform it on the main thread. Don't try to make any UI changes from other threads.
All UI changes must be in the main thread, as you note. Rather than you off-main-thread method make any changes to the UI, send an NSNotification to the current viewController telling it to do the UI work.
This is an especially good route if you're crossing an MVC border, or if you already have a viewController that knows what to do so that writing a separate method results in duplicate code.
Related
It seems that UIPageViewController is holding the initial content view controller forever.
For example:
DataViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];
self.pageViewController.dataSource = self.modelController;
The startingViewController is never released until the pageViewController itself it released.
To reproduce this bug, just create a new project in XCode using the Page-Based Application template. And add 3 lines of code into DataViewController.m
#property NSInteger debugIndex; // file scope
NSLog(#"DataViewController[%d] created", self.debugIndex); // in viewDidLoad
NSLog(#"DataViewController[%d] dealloc", self.debugIndex); // in dealloc
And when you scroll the demo App in vertical orientation, you'll get logs like this:
DataViewController[0] created
DataViewController[1] created
DataViewController[2] created
DataViewController[1] dealloc
DataViewController[3] created
DataViewController[2] dealloc
DataViewController[4] created
DataViewController[3] dealloc
DataViewController[5] created
DataViewController[4] dealloc
DataViewController[6] created
DataViewController[5] dealloc
DataViewController[0] is never deallocated.
Any ideas about this?
Thanks!
Are you using transitionStyle UIPageViewControllerTransitionStyleScroll? I encountered the same or a similar problem which seemed to disappear when using page curl animations instead.
The problem was compounded for me because I was allowing a UISliderBar to set the position in the content. So on change of the UISliderBar, I was calling setViewControllers:direction:animated:completion: which caused more and more view controller references to get "stuck" in my UIPageViewController.
I am also using ARC. I have not found an acceptable way to force the UIPageViewController to let go of the extra view controller references. I will probably either end up using the page curl transition or implementing my own UIPageViewController equivalent using a UIScrollView with paging enabled so I can manage my own view controller cache instead of relying on UIPageViewController's broken view controller management.
I'm not sure you still got the problem, but I had the same problem and I found the solution.
I don't know the reason, but it works.
I'm setting the first viewController right after addSubview, rather than before addChlidViewController.
-(void)settingPageViewController{
if (!self.pageViewController) {
self.pageViewController = [[UIPageViewController alloc]initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
self.pageViewController.dataSource = self;
[self addChildViewController:self.pageViewController];
[self.pageViewController didMoveToParentViewController:self];
[self.containerView addSubview:self.pageViewController.view];
[self.pageViewController setViewControllers:#[[self viewcontrollerAtIndex:0]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
}
and the first viewController will dealloc in the right time.
also, I found if call
[self.pageViewController setViewControllers:#[[self viewcontrollerAtIndex:0]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished){
NSLog(#"finished : %d",finished);
}];
before addSubView and the completion block will not call.
and I reckon this block is the reason why the first viewController didn't dealloc.
I'll go find out why it didn't callback, and improve the answer~
cheers
After a few attempts to figure out what was happening on a similar issue, I noticed that in my project there were 2 reasons that caused a retain problem and resulted in having a UIPageViewController being forever retained.
1) there was a circular reference between the UIPageViewController and the UIViewcontroller that was presented (this was fixed by changing the properties to weak from strong in both classes)
2) and the main fix consisted in changing
[self setViewControllers:#[initialDetailsViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
to
__weak __typeof__(self) weakSelf = self;
[weakSelf setViewControllers:#[initialDetailsViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
I hope this helps someone
same problem here
i resolved it by keeping my initial viewController in the variable
and instead of creating the same vc on particular pageIndex i just reuse it
I had same problem and solved the following:
[startingViewController release]; where the end point of initialization.
then the first ViewController will be deallocated.
I am using MagicalRecord to help with core data operation.
I have a NSOperation subclass called OfflineRetrieveOperation. It retrieve the message from the server and save it.
the code is like this:
NSManagedObjectContext *context = [NSManagedObjectContext contextForCurrentThread];
Message *existMessage = [Message MessageWithMessageID:messageID inManagedObjectContext:context];
if (!existMessage) {
Message *message = [Message insertMessageWithProperties:properties inManagedObjectContext:context];
}
[context save];
The notification receiver is initialized like this:
- (id)init
{
self = [super init];
if (self != nil) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:[NSManagedObjectContext defaultContext]];
[self setContext:[NSManagedObjectContext context]];
}
return self;
}
the log:
-[NSManagedObjectContext(MagicalRecord) saveWithErrorHandler:](0x5906a0) Saving Context
-[NSManagedObjectContext(MagicalRecord) mergeChangesFromNotification:](0x37eab0) Merging changes to *** DEFAULT *** context *** on Main Thread ***
Everything seems work fine except I can't receive the NSManagedObjectContextDidSaveNotification at all, so that I can't know that I have already finish retrieving.
Your OfflineRetrieveOperation is probably creating its own NSManagedObjectContext. When you save that context it will fire the NSManagedObjectContextDidSaveNotification. However you have an observer that will only listen for a NSNotification being broadcast by the [NSManagedObjectContext defaultContext].
Change your observer to consume notifications from your OfflineRetrieveOperation internal NSManagedObjectContext instead of [NSManagedObjectContext defaultContext] and it should start receiving them.
So, I'm guessing you probably want to update the objects in the defaultContext when you save them in the background context. MagicalRecord actually already handles that case for you when you create a new context using the helper method. That is, when you do something like this:
NSManagedObjectContext *backgroundOperationContext =
[NSManagedObjectContext contextThatNotifiesDefaultContextOnMainThread];
The context method has already setup the notifications necessary to tell the default context to merge the changes when it saves in the background. All you need to do is keep your context alive in the background operation and call save when you're ready to persist data.
Behind the scenes, the context method is doing exactly when Marcus is suggesting, and that is, adding a notification to the notification center:
[NSNotificationCenter defaultCenter] addObserver:[NSManagedObjectContext defaultContext]
selector:...
name:NSManagedObjectContextDidSaveNotification
object:backgroundOperationContext]
This isn't exactly the code, but this is pretty much what it does.
Bottom line, forget worrying about observing and merging changes from a background context to the default context yourself, MagicalRecord takes care of it for you.
i want to create a basic push view scenario. I have the following code but it does not work. Nothing happens. Could someone tell me why?
testController *screen2 = [[testController alloc] initWithNibName:nil bundle:nil];
[self.navigationController pushViewController:screen2 animated:YES];
[screen2 release];
You should try to do an NSLog to see if screen2 is nil. Chances are it is not being appropriately loaded from a corresponding nib for some reason. As an aside, I'd highly recommend sticking to the convention of capitalizing class names. Speaking of which, did you maybe call the nib file TestController.nib? (That would cause the problem.)
i use a combination of queue and resultscontroller to update and display some coredata objects.
in my uitableviewcontroller i call every X second a method in my main controller object.
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(test:) userInfo:nil repeats:YES];
}
- (void)test:(NSTimer*)theTimer {
[[MainController instance] updatePersons];
}
In this method a custom NSOperation object will be added to my main Q.
- (BOOL)updatePersons {
UpdatePersonsOperation* u = [[UpdatePersonsOperation alloc] init];
[u setQueuePriority:NSOperationQueuePriorityVeryHigh];
[operationQ u];
[u release];
The operation itself creates a new managedobjectcontext and tries to download some xml from the web and tries to update the coredata database... (This code works!)
In my main controller i receive the context changed message and i use mergeChangesFromContextDidSaveNotification to merge and update my main object context. All resultscontroller use this main object context.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notificationManagedObjectContextDidSave:) name:
NSManagedObjectContextDidSaveNotification object:nil];
Actually everything works but when i insert a new object inside a NSOperation it takes 4-6 seconds till the UI updates and displays this new object... Furthermore the UI blocks... Its not possible to scroll or trigger a other interaction...
When i don't use a queue and i put my code to download and update the objects into a method inside my uitableviewcontroller class and i use this
[NSThread detachNewThreadSelector:#selector(codeFromUpdatePersonsOperation) toTarget:self withObject:nil];
Everything works very well without any delay or UI freeze...
Can someone explain me this behavoir?
Thanks
Another problem may have been that updating the UI needs to take place on the Main thread. I was experiencing the same issue that you reported, and eventually figured out that if you call
[target performSelectorOnMainThread:#selector(methodToUpdateUI:) withObject:dataFromRetrievalOperation waitUntilDone:NO];
This will cause the UI to update immediately when the thread has finished processing. Otherwise, it waits about 5 seconds before the UI animations take place.
Up to iOS 3.2, I used this kind of code to load UIImageView image in background, and it worked fine...
Code:
- (void)decodeImageName:(NSString *)name
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *newImage = [UIImage imageNamed:name];
[myImageView setImage:newImage];
[pool release];
}
...
[self performSelectorInBackground:#selector(decodeImageName:) withObject:#"ID"]
... even if [UIImageView setImage:] was not thread-safe !
But since iOS 4, it doesn't work any more... Images appear on screen two seconds after setImage call. And if I do a [myImageView performSelectorOnMainThread:#selector(setImage:) withObject:newImage waitUntilDone:YES] instead of [myImageView setImage:newImage], images appear immediately but seem to be re-decoded again on-the-fly (ignoring the previous [UIImage imageNamed:] which should have already decoded the image data), causing a pause on my main thread... Even if documentation says The underlying image cache is shared among all threads..
Any thought ?
Don’t do it in the background! It’s not thread-safe. Since an UIImageView is also an NSObject, I think that using -[performSelectorOnMainThread:withObject:waitUntilDone:] on it might work, like:
[myImageView performSelectorOnMainThread:#selector(setImage:) withObject:newImage waitUntilDone:NO];
And it’s UIImage which is newly made thread-safe. UIImageView is still not thread-safe.
performSelectorInBackground: runs a selector in a background thread. Yet setImage: is a UI function. UI functions should only be run on the main thread. I do not have insight into the particular problem, but this is the first gut feel about this code, and it may be that iOS4 handles the (non-supported) mechanism of running UI functions in background threads somehow differently.
If you're using iOS 4.0, you should really consider reading up on blocks and GCD. Using those technologies, you can simply replace your method with:
- (void)decodeImageName:(NSString *)name
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *newImage = [UIImage imageNamed:name];
dispatch_async(dispatch_get_main_queue(), ^{
[myImageView setImage:newImage];
}
[pool release];
}
Let's quote:
#property(nonatomic, readonly) CGImageRef CGImage
Discussion
If the image data has been purged because of memory constraints, invoking this method forces that data to be loaded back into memory. Reloading the image data may incur a performance penalty.
So you might be able to just call image.CGImage. I don't think CGImages are lazy.
If that doesn't work, you can force a render with something like
// Possibly only safe in the main thread...
UIGraphicsBeginImageContext((CGSize){1,1});
[image drawInRect:(CGRect){1,1}];
UIGraphicsEndImageContext();
Some people warn about thread-safety. The docs say UIGraphics{Push,Pop,GetCurrent}Context() are main-thread-only but don't mention anything about UIGraphicsBeginImageContext(). If you're worried, use CGBitmapContextCreate and CGContextDrawImage.