I have a app with TabBar, under the main tabbar there are navigation controllers, under these there are 'branches' of tableviewcontollers.
How can I make two unrelated, non segue-connected, under two different tab icons viewcontrollers exchange information between them?
Tried with delegating but I can't get the instance variable to the delegator from the delegatee (there's no relation between them, no segue.destinationviewcontroller etc)
any ideas?
practical:
the app shows list of subitems (the parent tableview has the items), andon the other tab the recent items that were selected are getting added (but maxiumum 10 and sorted by most recent).. been breaking my ** on it...
thanks
I find NSNotifications not the best way to do this since you do couple the unrelated TableViewControllers. I think the flow of information in an application is crucial.
The solution I would personally favor is having a central class, that manages the global data for your application. This can be the AppDelegate class or an arbitrary manager class that manages the flow of data and "pulls the strings" and mediates between the different independent ViewControllers.
Example:
Using this code you can get hold of your UITabBarController and set your class as the delegate of your ViewControllers etc.:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UITabBarController *rootTabBarController = (UITabBarController *)self.window.rootViewController;
MyFirstTableViewController *firstVC = [rootTabBarController.viewControllers objectAtIndex:0];
firstVC.delegate = self; // Get informed about events in the first ViewController
MySecondTableViewController *secondVC = [rootTabBarController.viewControllers lastObject]; // Assuming you only have two Tabs
// Once you have the rootTabBarController you can cast it to the corresponding ViewController and access any nested UIViewControllers
return YES;
}
#pragma mark - MyFirstTableViewControllerDelegate
-(void)firstTableViewController:(MyFirstTableViewController *)sender didSomethingFancy:(MyFancyObject *)fancy{
// Do stuff like fetching some data, based on the event from the first ViewController
// Maybe tell the secondVC to refresh its data etc. etc.
}
Have you considered posting an NSNotification, passing the data you want to transfer in userInfo?
From the Apple documentation:
The object sending (or posting) the notification doesn’t have to know
what those observers are. Notification is thus a powerful mechanism
for attaining coordination and cohesion in a program. It reduces the
need for strong dependencies between objects in a program (such
dependencies would reduce the reusability of those objects).
The class sending a notification does it as follows:
[[NSNotificationCenter defaultCenter] postNotificationName: <notification-name>
object: self
userInfo: <your-user-info>];
and the class listening for the notification registers for notifications using:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(someMethod:)
name: <notification-name>
object: nil];
and has a method:
- (void) someMethod: (NSNotification *) notification
{
NSDictionary * myInfo = notification.userInfo;
//do something with myInfo
}
You can use #property and #synthesize to create default getters and setters. You can aso create your own getters and setters manually.
Related
In my application delegate I create a NSManagedObjectContext property that I want to pass to the window controller I launch in applicationDidFinishLaunching:.
This is basically my code in AppDelegate.m:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSStoryboard *storyboard = [NSStoryboard storyboardWithName:#"Main" bundle:nil];
self.setupWindowController = [storyboard instantiateControllerWithIdentifier:#"setup"];
self.setupWindowController.managedObjectContext = self.managedObjectContext;
[self.setupWindowController showWindow:self];
}
In MyWindowController.m I have the following method:
- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext {
((MyViewController *)self.contentViewController).managedObjectContext = managedObjectContext;
}
And this is a method from MyViewController.m:
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([segue.destinationController isKindOfClass:self.class]) {
((MyViewController *)segue.destinationController).managedObjectContext = self.managedObjectContext;
}
}
This approach worked pretty well until I had to programmatically launch different windows from the application delegate.
The problem now is that prepareForSegue:sender: for the initial view is called before I'm able to set the MyWindowController.managedObjectContext, so it'll only pass nil with the following segues.
How on earth should I pass objects from my app delegate to the initial window controller before it sets up the view hierarchy?
Am I approaching all this the wrong way?
I suggest reducing the dependencies among controllers by making a data model layer in your application.
The data model would manage the information that the controllers use and provide an application-specific facade in front of CoreData. The data model object(s) can be shared instances or can be requested from a known object, such as the app delegate.
That way, the model is only referenced where needed and doesn't clutter interfaces just as a pass-through object.
I want to be able to use a blue object box to delegate control over an NSOutlineView. The blue object box would be hooked up to my primary controller, so it'd just be a data source and control the content of the NSOutlineView.
The problem I'm having is that I have no control over the Channel Data Source. I'm simply calling a declared method with some test NSLog inside of it, and it doesn't get called. The outlet doesn't get instantiated.
Here's the connections of the blue object box (ChannelDataSource)
Here's the connections of File's Owner for my primary controller.
So you see, I want to do something like [dataSource callMyMethod]; with the final aim that I have control over the contents for the NSOutlineView.
Any ideas?
EDIT
The application is structured whereby my primarily app delegate looks like this:
#implementation MyAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
controller = [[MainController alloc] init];
[controller showWindow];
}
#end
Then in the MainController I have something along the following lines:
#implementation MainController
-(id)init {
self = [super init];
if (self) {
// loads of random stuff
[dataSource myMethod];
}
return self;
}
So "Channel Data Source" blue object box is dataSource. At this point in the application life cycle, it's null, which isn't what I was expecting. At the same time, it's still a bit of black magic to me. If you have a blue object box, at what point is it instantiated? Obviously this isn't hooked up correctly though.
EDIT EDIT
Further to my points above, and trying to fix the problem, is this actually a good way to go about it? I'm looking at this thinking it's not meeting a decent MVC architecture, because ultimately the blue object box's owning class is storing and managing the data. Is there a better way to go about managing what's in your NSOutlineView?
EDIT EDIT EDIT
So I have my app delegate, which is strangely a class all by itself that instantiates the main controller. Don't ask me why I did this, it was very early code. So my app delegate (root entry point) has this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
controller = [[MyController alloc] initWithWindowNibName:#"MainWindow"];
[controller showWindow:nil]; // this doesn't open the window
[controller loadWindow]; // this does open the window
}
And the declaration of the controller:
#interface MyController : NSWindowController
Which contains the following method declaration in it:
-(void)windowDidLoad {
[dataSource insertChannel:#"test" forServer:#"test2"];
}
I have a breakpoint in windowDidLoad and it definitely doesn't get called.
Ideas?
There's still a few things you didn't clarify, but I can do some guessing. First, I'm assuming that MainController is a subclass of NSWindowController. If so, you should be using initWithWindowNibName: instead of just init, otherwise how would the controller know what window to show when you address showWindow: to it? Second, even if you do that, and change your init method to initWithWindowNibNamed:, what your wrote won't work, because the init is too early in the process to see your outlet, datasource. If you just log dataSource it will come up null. A better place to put that code would be in windowDidLoad, as everything will have been set up by then (this will be called after showWindow:). So, in my little test project, this is what I did.
In the app delegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.cont = [[Controller alloc] initWithWindowNibName:#"Window"];
[self.cont showWindow:nil];
}
In the Controller.M I have this:
- (void)windowDidLoad {
NSLog(#"%#",self.dataSource);
[self.dataSource testMethod];
}
In IB, in the Window.xib file, I set the class of the file's owner to Controller, and the class of the blue cube to ChannelDataSource. EVerything was hooked up the same way you showed in your post.
I am creating some sample applications to understand the concepts of view navigation, binding etc in cocoa.
Here is the scenario:
I have a window that has a tab view(2 tabs) in MainMenu.Xib.
I have a text field in the first tab and label in the second tab. I want both of them to reflect the same value and I want to do this using binding. Also, I don't want to use the views provided to me along with the tab view.
These are the steps I have done.
The view of each tab view item is set separately in the applicationDidFinishLaunching: method using the following code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
//initialize view controllers
view1=[[ViewTab1 alloc] initWithNibName:#"ViewTab1" bundle:nil];
view2=[[ViewTab2 alloc] initWithNibName:#"ViewTab2" bundle:nil];
//set views
[[[myTabView tabViewItems] objectAtIndex:0]setView:view1.view];
[[[myTabView tabViewItems] objectAtIndex:1]setView:view2.view];
}
myTabView is the outlet reference of the tab view from MainMenu.xib in AppDelegate.
ViewTab1 is the name of the first view controller (and the xib).
ViewTab2 is the name of the second view controller (and the xib).
ViewTab1 has one single text field (and an associated label). I have bound this to a variable(name) declared in AppDelegate.
ViewTab2 has a label. I have bound this also to the same variable in AppDelegate.
The variable, 'name' is initialized in the init method of AppDelegate.
AppDelegate.h
....
NSString *name;
....
#property(strong) ViewTab1 *view1;
#property(strong) ViewTab2 *view2;
#property (assign) IBOutlet NSTabView *myTabView;
#property (strong) NSString *name;
....
AppDelegate.m
....
#synthesize myTabView;
#synthesize view1,view2;
#synthesize name;
....
- (id)init {
self = [super init];
if (self) {
name=#"dummy";
}
return self;
....
Apart from this I haven't done any coding in my program.
In the ViewTab1.xib I got an object and made it an instance of AppDelegate and then connected the delegate reference of the Application object(NSApplication) to the same object. (I hope this is the right way of getting the AppDelegate object.)
I did the same in ViewTab2.xib
Then I bound the text field in ViewTab1 and label in ViewTab2 to this variable in AppDelegate.
When I run the program both the text field and label shows "dummy". But when I change the value in the text field, its not reflected in the label in the second tab( i.e. ViewTab2).
Please tell me what I'm doing wrong.
How to establish binding to the same App delegate object from any loaded Nib?
Yes, I know this frustrated situation as described in question... after many weeks and hundreds pages of documentation for KVO - Notifications - Bindings I think there is one very simple solution for that.
As we can find in some information sources the nib-loading process produce new instances of members... and we need to use binding connection to the old one.
Note that bindings made in InterfaceBuilder are redirect to these new instances automatically after loading nib
Why not redirect the pointer of App delegate to the old instance?
In method where you loads your nib you can test which object is app delegate before and just after nib load.
If the new one isn’t the same as the previous one you can redirect it as you want.
This simple example works for me in Xcode3 under 10.5.8 with target to OSX10.5 / i386:
// ***** SOMEWHERE IN DEFAULT APP-DELEGATE.m IMPLEMENTATION
- (IBAction) createOtherWindowFromNib: (id)sender
{
// ensure that app delegate is set as you want...
[NSApp setDelegate:self];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE BEFORE NIB LOAD: %# ", [[NSApp delegate] description]);
// we can bind members of the nib to this controller over proxy object named "File’s Owner"
NSWindowController *otherWinCapo = [[NSWindowController alloc] initWithWindowNibName: #"OtherTestWindow"];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE AFTER NIB LOAD: %# ", [[NSApp delegate] description]);
// make some test for delegates before/after here if you need ...
// usually your bindings made inside "OtherTestWindow.xib" by IB doesn’t works in this moment
// ... and some redirection if needed
[NSApp setDelegate:self];
// afer that the bind made in IB inside "OtherTestWindow.xib"
// referred to (proxy object) "Application.delegate.myBOOL" (Bind to:Application, Model Key Path:delegate.myBOOL)
// react to changes of myBOOL placed in default app delegate object as expected
// simultaneously in every open instance of "OtherTestWindow.xib"
[otherWinCapo showWindow: otherWinCapo.window]; // we need populate the window instance on screen to see it
}
I think the problem is that the objects in your xibs that you set to the app delegate class create 2 different instances of the app delegate, so changing the value of the text field changes the value of name in one instance but not in the other. That's what you're doing wrong, unfortunately, I can't think of a solution at this time.
Have you turned on 'Continuously Updates Value' in the NSTextField controls?
See this example.
Scenario, in context of a card game:
User moves a card on the screen. As a result of a move, coordinates of the card change. If card is found to be in some specific location, we'd like to make sure Card object (Model) is updated to include those coordinates.
But View should not be talking to the Model directly .., so
Instead of updating Card directly, View is going to notify its Controller that "Card has landed". Upon receiving this notification, i'd like for a Controller to update Card's location instead of the View (Controller updates Model)
Question 1:
Am i thinking about this type of scenario correctly?
Question 2:
Is it possible to send data to a controller along with a notification?
You do not need NSNotifications for your scenario: a straightforward delegate-based approach should do.
The view should define a delegate interface, and provide a non-retaining delegate property. The controller should implement the delegate interface, and set itself as view's delegate. The view would then notify its delegate without even knowing that it notifies the controller. The controller would then pass the notification along to the model.
#protocol CardDelegate
-(void)cardHasLanded:(SOCard*)card atPosition:(SOPosition*)pos;
#end
#interface MyView
#property (weak, nonatomic,readwrite) id<CardDelegate> delegate;
#end
#implementation MyViewController
-(id)init { // This should be in your designated initializer
self = [super init];
if (self) {
MyView *view = [[MyView alloc] init];
view.delegate = self;
self.view = view;
}
return self;
}
-(void)cardHasLanded:(SOCard*)card atPosition:(SOPosition*)pos {
// Update the model
}
#end
#implementation MyView
#synthesize delegate;
-(void) doSomething {
// ...
if (cardHasLanded) {
[delegate cardHasLanded:card atPosition:pos];
}
// ... more code
}
#end
That's what the userInfo dictionary and object of NSNotification are used for. In this case, it's the object you want. For example:
// In your model header file
extern NSString * const CardMovedNotification;
// In your model implementation file
NSString * const CardMovedNotification = #"CardMoved";
...
[[NSNotificationCenter defaultCenter] postNotificationName:CardMovedNotification object:theCardThatMoved];
Your obversers then can get the card via [notification object]. If you need to pass more information, you'd create a dictionary and pass that as userInfo, via postNotificationName:object:userInfo:. The observer then can query it via [notification userInfo].
I agree that no notifications are needed. Usually for UI object coordinates I won't use a model object because the coordinates are coupled to the UI object (or else where will you draw it ?). What you do need to do is register your controller to the touch end event passing in the UI object where the touch was made and updating (in the responding controller method) the object's coordinates if they indeed within the desired location. (as done with buttons and other UI objects)
So it seems to me like there's a Catch-22 situation here. Note the following widely (and smartly) held stances on application architecture:
Apple (and most alpha devs) recommend not using a singleton or accessing the app delegate singleton for retrieving a NSManagedObjectContext. Rigidity, poor design, etc. OK-- I agree!
Iterating over a UITabbarController's child view controllers and giving each child VC a reference to the context (dependency injection) is also stupid, because you are loading every tab in your application at application launch time, just to pass a reference. This also goes against everything Apple recommends for application architecture.
How do you resolve this? Do you use NotificationCenter to post a notification when a view controller awakes from a nib, and have the app delegate pass in the context reference then? That's about the only way I can think of that jives with both #1 and #2, but it also feels like some contortion to me.
Is there a more elegant way?
Edit: doing a notification when initializing a view controller can be a race condition, since if you're doing things with Storyboard, your tabbar's child view controllers tend to be initialized (though sans-view loading) at launch. So you'd have to do such a notification in viewDidLoad, which is a bad idea vis-a-vis MVC convention. It also ties your hands from doing anything with data models (like pre-caching for performance) before the user does anything view-related.
When you pass NSManagedObject instances to a view controller, that view controller can retain those objects. It can then get to the NSManagedObjectContext through those managed objects by calling
-[NSManagedObject managedObjectContext]
I'm not sure if this will work in your particular case, but often it will. The app delegate or the root view controller creates a context and then passes along managed objects.
Update
If you need to use the context in multiple places, there's another pattern that I've found useful:
Subclass NSManagedObjectContext:
#interface MyManagedObjectContext : NSManagedObjectContext
+ (MyManagedObjectContext *)mainThreadContext;
#end
i.e. a singleton context for the UI / main thread. This is cleaner than using the App delegate, since other classes won't have to get to the App delegate, but can use this class directly. Also the initialization of the store and model can be encapsulated into this class:
#implementation MyManagedObjectContext
+ (MyManagedObjectContext *)mainThreadContext;
{
static MyManagedObjectContext *moc;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
moc = [[self alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
// Setup persistent store coordinator here
});
return moc;
}
#end
I see that some time has passed since you posted your question, but I have a different approach that I would like to share with you and everybody else.
I assume that you want to inject the managed object context in all/some of the view controllers that are shown as tabs of the UITabViewController and that you are using a Storyboard with a UITabBarController as the rootViewController.
In your AppDelegate header make your AppDelegate to implement the UITabBarControllerDelegate protocol.
#interface AppDelegate : UIResponder <UIApplicationDelegate, UITabBarControllerDelegate>
...
In your AppDelegate implementation add the following UITabBarControllerDelegate method. It will take care of setting the managed object context in any view controller that has the property.
- (BOOL) tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController {
if ([viewController respondsToSelector:#selector(setManagedObjectContext:)]) {
if ([viewController performSelector:#selector(managedObjectContext)] == nil) {
[viewController performSelector:#selector(setManagedObjectContext:) withObject:self.managedObjectContext];
}
}
return YES;
}
In your application:didFinishLaunchingWithOptions: set self as the UITabBarController delegate.
UITabBarController *tabBarController = (UITabBarController *) self.window.rootViewController;
tabBarController.delegate = self;
Sadly the first view controller to be loaded is not ready at that time (in tabBarController.selectedViewController), and doesn't invoke the delegate either. So the easiest way to set the first one is to observe the selectedViewController property of the TabBarController
[tabBarController addObserver:self forKeyPath:#"selectedViewController"
options:NSKeyValueChangeSetting context:nil];
and set it there.
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
[object removeObserver:self forKeyPath:#"selectedViewController"];
UIViewController *viewController = [change valueForKey:NSKeyValueChangeNewKey];
if ([viewController respondsToSelector:#selector(setManagedObjectContext:)]) {
if ([viewController performSelector:#selector(managedObjectContext)] == nil) {
[viewController performSelector:#selector(setManagedObjectContext:) withObject:self.managedObjectContext];
}
}
}
I would create a core data provider class, which is a singleton. This class could either just provide the persistent store so that each view controller can create it's own context if needed. Or you could use the provider class more like a manager and return new (if needed) context. It should of course also setup a merge notification to each context so that you can listen to changes between threads.
With this setup each view controller can ask the provider/manager for the context and the provider/manager will handle everything internally and return a context for the view controller.
What do you think?
IMHO I find it more elegant not to do Core Data heavy lifting in a ViewController.
I'd suggest that you encapsulate data manipulation into an object and let the object reference/encapsulate your Core Data stack.
When your application and datamodel grows, you want to be really strict with how you pass MOCs. This becomes even more important if youre using concurrency and have to deal with multiple MOCs. Besides, your ViewControllers only need a MOC for FetchedResultsControllers. In most other cases youre fine passing ManagedObjects.