Big Problem with easy Three20 - three20

I have no idea why, but when I start my app, I have from the beginning 2 "Back"-possibilities in my navigator.
So here is my code:
#import "AnAppDelegate.h"
#import "LoginViewController.h"
#import "MainScreenViewController.h"
#import "AdminViewController.h"
#import "Three20/Three20.h"
#implementation AnAppDelegate
#synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self.window makeKeyAndVisible];
TTNavigator* navigator = [TTNavigator navigator];
navigator.persistenceMode = TTNavigatorPersistenceModeAll;
TTURLMap* map = navigator.URLMap;
[map from:#"*" toViewController:[TTWebController class]];
[map from:#"tt://launcher/" toViewController:
[AdminViewController class]];
if (![navigator restoreViewControllers]) {
[navigator openURLAction:
[TTURLAction actionWithURLPath:#"tt://launcher"]];
}
return YES;
}
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)URL {
[[TTNavigator navigator] openURLAction:[TTURLAction actionWithURLPath:URL.absoluteString]];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
/*
Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
*/
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
/*
Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
*/
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
/*
Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
*/
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
/*
Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
*/
}
- (void)applicationWillTerminate:(UIApplication *)application
{
/*
Called when the application is about to terminate.
Save data if appropriate.
See also applicationDidEnterBackground:.
*/
}
- (void)dealloc
{
[_window release];
[super dealloc];
}
#end
It's no matter which View I try to add here:
[map from:#"tt://launcher/" toViewController:
[AdminViewController class]];
Every view, even a complete empty one, reference 2 times "back" to itself from the start of my app.

Found it after a total working time of 2 hours for this bug...
Instead
[map from:#"tt://launcher/" toViewController:
[AdminViewController class]];
it should be
[map from:#"tt://launcher/" toSharedViewController:
[AdminViewController class]];

Related

Compling Xcode Project Application Windows are expected to have a root view controller

How do i fix this error? iphone simulator works but when I use ipad i get this in console.. 2013-05-25 20:59:17.556 YourRSS[2717:c07] Application windows are expected to have a root view controller at the end of application launch
//
// AppDelegate.m
// YourRSS
//
// Created by Mohammad Komeili on 5/17/13.
// Copyright (c) 2013 MOMEKS. All rights reserved.
//
#import "AppDelegate.h"
#import "ViewController.h"
#import "ViewController_iPad2.h"
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] ;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.viewController = [[ViewController alloc] initWithNibName:#"ViewController_iPhone" bundle:nil];
self.navigationController = [[UINavigationController alloc] initWithRootViewController:self.viewController];
}
/* }else{
self.viewController = [[ViewController alloc] initWithNibName:#"ViewController_iPad2" bundle:nil];
self.navigationController = [[UINavigationController alloc] initWithRootViewController:self.viewController];
}
*/
self.navigationController.navigationBarHidden = YES;
self.window.rootViewController = self.navigationController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application
{
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
#end
The part of the code that is commented out was what would have executed if you were not on an iPhone (see: UIUserInterfaceIdiomPhone). Since you're skipping over the creation of the navigation controller when you're on an iPad, it stays nil...which gives you the obvious error.

Local Notification is displayed without didFinishLaunchingWithOptions:

I have quite an understanding issue with displaying local notifications.
As far as I was reading in other threads, one has first create and schedule the local notification with the application.
For displaying that notification, one has to use the delegates didFinishLaunchingWithOptions: (if app is in background operation) and didReceiveLocalNotification: (if app is in foreground).
Now even though I did NOT change the didFinishLaunchingWithOptions: method, the notification is already getting viewed when my app is in the background.
It wouldn't be that big of a problem if the didFinishLaunchingWithOptions: would at least be used when I specify it at all. But it doesn't.
So my problem is, that even though I haven't used the didFinishLaunchingWithOptions: method, the notification is getting displayed. When the user clicks on the notification, the app gets to foreground and the didReceiveLocalNotification: method is triggered and the notification is displayed again.
What I originally wanted to do is to cancelAllLocalNotifications on execution of didFinishLaunchingWithOptions:, but since it is not getting executed, I'm kinda stuck here.
Ok, there might be a workaround with applicationWillEnterForeground: but honestly, I'd like to understand, why the notification is getting displayed even without having specified that in didFinishLaunchingWithOptions:.
All of your help is really appreaciated!! Thanks!!
//
// myNotificationsClass.m
//
#import "myNotificationsClass.h"
#implementation myNotificationsClass
//Sets up a Local Notification with Message, TimeFromNow, BadgeNumber and UserInfo
//no Class Instance for calling this method needed!!
+ (void)setupLocalNotificationsWithMessage: (NSString *) message andTimeFromNow: (NSTimeInterval) seconds andAlertAction: (NSString *) alertAction andBadgeNumber: (NSInteger) badgeNumber andUserInfo: (NSDictionary *) infoDict {
//[[UIApplication sharedApplication] cancelAllLocalNotifications];
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
// create date/time information
localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:seconds];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
//setup Appearence and Message
localNotification.alertBody = message; //#"Time to get up!";
localNotification.alertAction = alertAction;
localNotification.soundName = UILocalNotificationDefaultSoundName;
localNotification.applicationIconBadgeNumber = badgeNumber;
localNotification.userInfo = infoDict;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}
#end
//overwrites the viewWillAppear: Method from the primary Class to display a Test Notification
#implementation UIViewController (localNotification)
- (void)viewWillAppear:(BOOL)animated {
[myNotificationsClass setupLocalNotificationsWithMessage:#"First Test after 2 Seconds" andTimeFromNow:2 andAlertAction:#"GoTo iSnah" andBadgeNumber:7 andUserInfo:nil];
}
#end
//receive Local Notifications even if the App is in Foreground
//overwrites the primery method
#implementation UIResponder (localNotificationForeground)
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleName"]
message:notification.alertBody
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alertView show];
//reset Badge
application.applicationIconBadgeNumber = 0;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Because I don't want the Notification to be displayed twice
[[UIApplication sharedApplication] cancelAllLocalNotifications];
UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (notification) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleName"]
message:notification.alertBody
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alertView show];
//reset Badge
application.applicationIconBadgeNumber = 0;
}
return YES;
}
#end
You are very close. If the app is running in the background, the user responding to the notification will not cause the app to "launch". "Launch" means the app is not running at all (foreground or background). So only put code in there that you want to execute when the app launches or starts as a result of the user responding to your local notification.
So what you need for your didReceiveLocalNotification method is something to check the application state to see if it was in the foreground or in the background when the user responded to the local notification.
You might do something like the following to differentiate your logic between foreground and background:
- (void)application:(UIApplication *)application didReceiveLocalNotification: (UILocalNotification *)notification
if ( application.applicationState == UIApplicationStateActive ){
NSLog(#"Was already in the foreground");
}
else{
NSLog(#"Was in the background");
}
}
Minor point, however. Documentation states that the didReceiveLocalNotification method is invoked after application:didFinishLaunchingWithOptions: (if that method is implemented). So if you have both methods implemented you'll need to make sure the combination of the 2 work correctly in a "launch" situation.

Doing something after NSOpenPanel closes

I have an NSOpenPanel and I want to do some validation of the selection after the user has clicked OK. My code is simple:
void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
if (returnCode == NSFileHandlingPanelOKButton) {
// do my validation
[self presentError:error]; // uh oh, something bad happened
}
}
[openPanel beginSheetModalForWindow:[self window]
completionHandler:openPanelHandler];
[self window] is an application-modal window. The panel opens as a sheet. So far so good.
Apple's docs say that the completion handler is supposed to be called "after the user has closed the panel." But in my case, it's called immediately upon the "OK/Cancel" button press, not upon the panel having closed. The effect of this is that the error alert opens above the open panel, not after the panel has closed. It still works, but it's not Mac-like.
What I would prefer is for the user to click OK, the open panel sheet to fold up, then the alert sheet to appear.
I guess I could present the alert using a delayed selector, but that seems like a hack.
Since the panel completion handler is invoked before the panel has effectively been closed,1 one solution is to observe NSWindowDidEndSheetNotification on your modal window:
Declare an instance variable/property in your class to hold the validation error;
Declare a method that will be executed when the panel is effectively closed. Define it so that if presents the error on the current window;
Have your class listen to NSWindowDidEndSheetNotification on [self window], executing the method declared above when the notification is sent;
In the panel completion handler, if the validation fails then assign the error to the instance variable/property declared above.
By doing this, the completion handler will only set the validation error. Soon after the handler is invoked, the open panel is closed and the notification will be sent to your object, which in turn presents the validation error that has been set by the completion handler.
For example:
In your class declaration, add:
#property (retain) NSError *validationError;
- (void)openPanelDidClose:(NSNotification *)notification;
In your class implementation, add:
#synthesize validationError;
- (void)dealloc {
[validationError release];
[super dealloc];
}
- (void)openPanelDidClose:(NSNotification *)notification {
if (self.validationError) [self presentError:error];
// or [[self window] presentError:error];
// Clear validationError so that further notifications
// don't show the error unless a new error has been set
self.validationError = nil;
// If [self window] presents other sheets, you don't
// want this method to be fired for them
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSWindowDidEndSheetNotification
object:[self window]];
}
// Assuming an action fires the open panel
- (IBAction)showOpenPanel:(id)sender {
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(openPanelDidClose:)
name:NSWindowDidEndSheetNotification
object:[self window]];
void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
if (returnCode == NSFileHandlingPanelOKButton) {
// do my validation
// uh oh, something bad happened
self.validationError = error;
}
};
[openPanel beginSheetModalForWindow:[self window]
completionHandler:openPanelHandler];
}
1If you think this behaviour is wrong, consider filing a bug report with Apple. I don’t really remember whether an error should be presented over an open/save panel.

Show Login Screen for iPhone when app becomes active

From other threads here I have gotten a modal view "login screen" to show whenever my app starts for the first time. What I want now is to have the login screen show whenever the app is brought back into the foreground (i.e. its running in the background and is brought up again) so the user can log back in.
Here's my code from the applicationDidFinishLaunchingWithOptions
LoginViewController *loginViewController = [[LoginViewController alloc] initWithNibName:#"LoginView" bundle:nil];
if (lockScreenOnOff)
{
[self.tabBarController presentModalViewController:loginViewController animated:YES];
[self.window makeKeyAndVisible];
}
where the modal login view is shown if a switch (lockScreenOnOff) is on. How can I accomplish this?
Implement applicationDidEnterBackground: and applicationDidBecomeActive: in your application delegate. (UIApplicationDelegate class reference)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Create all view controllers
[self lockApplicationAnimated:NO]; // Lock on launching
[self.window makeKeyAndVisible];
return YES;
}
- (void)lockApplicationAnimated:(BOOL)animated
{
[self.tabBarController presentModalViewController:self.loginViewController
animated:animated];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Lock the application
// Also, save all user data because the application might terminate soon
[self lockApplicationAnimated:NO];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Hooray, the user returned!
// Screen should be locked because the applicationDidEnterBackground
}
Additionally you can also use NSTimer to invalidate a session after a certain amount of time.

NSOutlineView not refreshing when objects added to managed object context from NSOperations

Background
Cocoa app using core data Two
processes - daemon and a main UI
Daemon constantly writing to a data store
UI process reads from same data
store
Columns in NSOutlineView in UI bound to
an NSTreeController
NSTreeControllers managedObjectContext is bound to
Application with key path of
delegate.interpretedMOC
NSTreeControllers entity is set to TrainingGroup (NSManagedObject subclass is called JGTrainingGroup)
What I want
When the UI is activated, the outline view should update with the latest data inserted by the daemon.
The Problem
Main Thread Approach
I fetch all the entities I'm interested in, then iterate over them, doing refreshObject:mergeChanges:YES. This works OK - the items get refreshed correctly. However, this is all running on the main thread, so the UI locks up for 10-20 seconds whilst it refreshes. Fine, so let's move these refreshes to NSOperations that run in the background instead.
NSOperation Multithreaded Approach
As soon as I move the refreshObject:mergeChanges: call into an NSOperation, the refresh no longer works. When I add logging messages, it's clear that the new objects are loaded in by the NSOperation subclass and refreshed. It seems that no matter what I do, the NSOutlineView won't refresh.
What I've tried
I've messed around with this for 2 days solid and tried everything I can think of.
Passing objectIDs to the NSOperation to refresh instead of an entity name.
Resetting the interpretedMOC at various points - after the data refresh and before the outline view reload.
I'd subclassed NSOutlineView. I discarded my subclass and set the view back to being an instance of NSOutlineView, just in case there was any funny goings on here.
Added a rearrangeObjects call to the NSTreeController before reloading the NSOutlineView data.
Made sure I had set the staleness interval to 0 on all managed object contexts I was using.
I've got a feeling this problem is somehow related to caching core data objects in memory. But I've totally exhausted all my ideas on how I get this to work.
I'd be eternally grateful to anyone who can shed any light as to why this might not be working.
Code
Main Thread Approach
// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
// Delay to allow time for the daemon to save
[self performSelector:#selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}
-(void)refreshTrainingEntriesAndGroups {
NSSet *allTrainingGroups = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
[interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];
NSError *saveError = nil;
[interpretedMOC save:&saveError];
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// In window controller class
-(void)refreshTrainingView {
[trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
[trainingView reloadData];
}
NSOperation Multithreaded Approach
// In App Delegate (just the changed method)
-(void)refreshTrainingEntriesAndGroups {
JGRefreshEntityOperation *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
NSOperationQueue *refreshQueue = [[NSOperationQueue alloc] init];
[refreshQueue setMaxConcurrentOperationCount:1];
[refreshQueue addOperation:trainingGroupRefresh];
while ([[refreshQueue operations] count] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// JGRefreshEntityOperation.m
#implementation JGRefreshEntityOperation
#synthesize started;
#synthesize executing;
#synthesize paused;
#synthesize finished;
-(void)main {
[self startOperation];
NSSet *allEntities = [imoc fetchAllObjectsForEntityName:entityName];
for(id thisEntity in allEntities)
[imoc refreshObject:thisEntity mergeChanges:YES];
[self finishOperation];
}
-(void)startOperation {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isStarted"];
[self setStarted:YES];
[self setExecuting:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isStarted"];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setStalenessInterval:0];
[imoc setUndoManager:nil];
[imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
}
-(void)finishOperation {
saveError = nil;
[imoc save:&saveError];
if (saveError) {
NSLog(#"Error saving. %#", saveError);
}
imoc = nil;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
[self setExecuting:NO];
[self setFinished:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(id)initWithEntityName:(NSString *)entityName_ {
[super init];
[self setStarted:false];
[self setExecuting:false];
[self setPaused:false];
[self setFinished:false];
[NSThread setThreadPriority:0.0];
entityName = entityName_;
return self;
}
#end
// JGRefreshEntityOperation.h
#interface JGRefreshEntityOperation : NSOperation {
NSString *entityName;
NSManagedObjectContext *imoc;
NSError *saveError;
BOOL started;
BOOL executing;
BOOL paused;
BOOL finished;
}
#property(readwrite, getter=isStarted) BOOL started;
#property(readwrite, getter=isPaused) BOOL paused;
#property(readwrite, getter=isExecuting) BOOL executing;
#property(readwrite, getter=isFinished) BOOL finished;
-(void)startOperation;
-(void)finishOperation;
-(id)initWithEntityName:(NSString *)entityName_;
-(void)mergeChanges:(NSNotification *)notification;
#end
UPDATE 1
I just found this question. I can't understand how I missed it before I posted mine, but the summary is: Core Data wasn't designed to do what I'm doing. Only one process should be using a data store.
NSManagedObjectContext and NSArrayController reset/refresh problem
However, in a different area of my application I have two processes sharing a data store with one having read only access and this seemed to work fine. Plus none of the answers to my last question on this topic mentioned that this wasn't supported in Core Data.
I'm going to re-architect my app so that only one process writes to the data store at any one time. I'm still skeptical that this will solve my problem though. It looks to me more like an NSOutlineView refreshing problem - the objects are created in the context, it's just the outline view doesn't pick them up.
I ended up re-architecting my app. I'm only importing items from one process or the other at once. And it works perfectly. Hurrah!

Resources