This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
What should my Objective-C singleton look like?
I am trying to understand the use of singletons. I have red to be careful with them, but also that they can have their positive uses.
My Scenario:
At the moment I have a test Project set up. One ViewController has a button that needs to perform an action.
The FirstViewController has a UIWebView on it.
I am using Storyboard and ContainerView, so I am able to see both ViewControllers at the same time.
In the First ViewController I have this code in my .m file:
static FirstViewController *sharedController = nil;
+ (FirstViewController *)sharedController {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
//BBCSermonTabBarViewController *myVC = (BBCSermonTabBarViewController *)[storyboard instantiateViewControllerWithIdentifier:#"BBCNews"];
if(sharedController == nil)
sharedController = (FirstViewController *)[storyboard instantiateViewControllerWithIdentifier:#"firstViewController"];
return sharedController;
}
And I also have a method that changes the alpha it like so:
-(void)hideWebView
{
_webView.alpha = 0.3;
}
Now in my Second View controller I have this code:
-(IBAction)hideWebViewFromAnotherViewController
{
[[FirstViewController sharedController] hideWebView];
}
Should that action button now change the alpha of the webView in the other ViewController?
if not what am I doing wrong??
Thanks in advance:-)
I can appreciate your goal to understand singletons better, but I would suggest not using a singleton unless you have to.
I don't think it is appropriate in most UI scenarios(I have never needed one) to have singletons. I suggest one of the following methods for communicating between objects:
Hold a reference to objects that you want to communicate with. Simply add a property and save a reference to the class that needs to be called later. You can make it a weak reference if it works in your scenario.
Use the delegate pattern that is common with iOS/Objective-c apps. Same as above, except define a protocol instead. Normally the property is called delegate. This allows other views to communicate using a common interface.
Use notification center. I don't prefer this option for most cases, but if there is an event that a lot of views might need to know about, and you don't want to deal with passing references to objects, it might be a good option.
Singletons work best for non-UI code in my experience, when you actually need to rely on the singleton behavior of instantiating the class upon first use. In your situation it looks like the only reason you're using a singleton is to make that view controller accessible across your entire app.
For safety's sake, and because with libdispatch it's so easy, you should protect the singleton creator with a dispatch_once() call instead of doing an if() check.
+ (FirstViewController *)sharedController {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
static dispatch_once_t token;
dispatch_once(&token, ^{
sharedController = (FirstViewController *)[storyboard instantiateViewControllerWithIdentifier:#"firstViewController"];
});
return sharedController;
}
The token is used like a semaphore to protect the block and make sure it only gets called once during the run of your program; it's protected from races and simultaneous reads.
It should work. Potential problems: _webView is nil, [FirstViewController sharedController] is not returning a valid reference. Set a breakpoint on hideWebViewFromAnotherViewController and step through, making sure everything is defined when you think it is.
Related
This problem sounds quite basic but I don’t understand what I am overlooking.
I am trying to push a new view controller into a navigation controller, however the topViewController remains unaffected.
#import "TNPViewController.h"
#interface TNCViewController : UIViewController <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
#implementation TNCViewController
-(void)userDidSelectNewsNotification:(NSNotification*)note
{
TNPViewController *nextViewController = [[TNPViewController alloc] init];
[[self navigationController] pushViewController:nextViewController animated:YES];
UIViewController *test = [[self navigationController] topViewController];
}
The test shows an instance of TNCViewController instead of TNPViewController. How is this possible?
UPDATE
Thanks for everyone's participation. The method name indicating notifications is a red herring. I found the problem, as Stuart had mentioned previously but deleted later on. (As I have high reputation score, I still can see his deleted post).
My initial unit test was this:
-(void)testSelectingNewsPushesNewViewController
{
[viewController userDidSelectNewsNotification:nil];
UIViewController *currentTopVC = navController.topViewController;
XCTAssertFalse([currentTopVC isEqual:viewController], #"New viewcontroller should be pushed onto the stack.");
XCTAssertTrue([currentTopVC isKindOfClass:[TNPViewController class]], #"New vc should be a TNPViewController");
}
And it failed. Then I set a breakpoint and tried the test instance above and it still was showing the wrong topviewcontroller.
At least the unit test works if I change
[[self navigationController] pushViewController:nextViewController animated:YES];
to
[[self navigationController] pushViewController:nextViewController animated:NO];
A better solution is to use an ANIMATED constant for unit tests to disable the animations.
This doesn't really answer your question about why your navigationController is not pushing your VC. But it is a suggestion about another possible approach.
You could instead add a new VC on the Storyboard and simply activate the segue when the userDidSelectNewsNotification method is activated. Then change the information accordingly to the event in the VC, specially since you are initializing it every time anyway.
This is something of a stab in the dark, but the issue is hard to diagnose without more information.
I see you're trying to push the new view controller in response to a notification. Are you sure this notification is being handled on the main thread? UI methods such as pushing new view controllers will fail (or at least behave unpredictably) when not performed on the main thread. This may also go some way to explaining the odd behaviour of topViewController returning an unexpected view controller instance.*
Ideally, you should guarantee these notifications are posted on the main thread, so they will be received on that same thread. If you cannot guarantee this (for example if you're not responsible for posting the notifications elsewhere in your code), then you should dispatch any UI-related code to the main thread:
- (void)userDidSelectNewsNotification:(NSNotification *)note
{
dispatch_async(dispatch_get_main_queue(), ^{
TNPViewController *nextViewController = [[TNPViewController alloc] initWithNibName:#"TNPViewController" bundle:nil];
[self.navigationController pushViewController:nextViewController animated:YES];
});
}
Also, it appears you are not initialising TNPViewController using the designated initialiser (unless in your subclass you are overriding init and calling through to initWithNibName:bundle: from there?). I wouldn't expect this to cause the transition to fail entirely, but may result in your view controller not being properly initialised.
In general, you might be better creating your view controllers in a storyboard and using segues to perform your navigation transitions, as #Joze suggests in his answer. You can still initiate these storyboard segues in code (e.g. in response to your notification) with performSegueWithIdentifier:, but again, be sure to do so on the main thread. See Using View Controllers in Your App for more details on this approach.
*I originally wrote an answer trying to explain the unexpected topViewController value as being a result of deferred animated transitions. While it is true that animated transitions are deferred, this does not prevent topViewController from being set to the new view controller immediately.
So I have a view controller FavoritesViewController and I have an instance of that view controller:
FavoritesViewController *FVC=[[FavoritesViewController alloc]init];
If I have two other view controllers, HomeViewController and SettingsViewController how do I have it so that I can push to that one particular instance "FVC" from both view controllers. I guess the real question is how/where can I initialize that instance "FVC" so that it is recognized by both view controllers and don't initialize it in HomeViewController or SettingsViewController.
Thanks
When you look at the MVC pattern, the object that you should really care about sharing is the model, not the controller. Your questions suggests to me that maybe the ViewController is also performing the responsibility of being the model.
If this is true, you might want to create another class (called Favorites, perhaps) and follow the suggestions in some of the previous answers to make it a singleton, if necessary. Doing it this way, rather than making the VC a singleton also has the benefit of working the same way whether you are using storyboards, xib, or code.
Maybe you have a good reason to need to share the VC itself, but I thought it would be worthwhile to question that premise.
If you want a single instance of your favorites view controller that is shared throughout your app, make it a singleton. Do a Google search on the singleton design pattern in iOS. The idea is that you would add a class method sharedFavoritesController that would always return the same instance, and use that.
The class method would look something like this:
+(FavoritesViewController *) sharedFavoritesVC;
{
static FavoritesViewController *_sharedFavoritesVC;
if (! _sharedFavoritesVC)
_sharedFavoritesVC = [[FavoritesViewController alloc] init;
return _sharedFavoritesVC;
}
Then #import the header for your FavoritesViewController class, and any time you need to invoke it, use:
[FavoritesViewController sharedFavoritesVC] to get a pointer to it.
Way 1 :
Declare
extern FavoritesViewController *FVC;
Way 2 :
If this is your rootViewController then in other viewcontrollers
FavoritesViewController *fvc=(FavoritesViewController*)appDelegateObj.rootViewController;
Way 3 :
use singleton
+(FavoritesViewController *) sharedInstance;
{
static FavoritesViewController *SVC;
if (! SVC)
SVC = [[FavoritesViewController alloc] init;
return SVC;
}
You could wrap a singleton pattern around that view controller.
Or, less elegant but more common, instantiate it in you AppDelegate and fetch it from there.
Be aware that the same instance of a view controller can only be once in the stack of view controllers. Plus - in principle - there is nothing wrong with having multiple instances of the same view controller class.
Edit in reply to your comment:
Not that I recommend that but this is how you would fetch a property sharedFavoritesViewController (which may refer to an instance of your FavoritesViewController) when your app delegate class is named MyAppDelegate:
FavoritesViewController localVar = [(MyAppDelegate)[[UIApplication sharedApplication] delegate] sharedFavoritesViewController];
I have a tricky question here..Please help..
I have One ViewController Called "DemoViewController" two different Xib's (Demo1Controller.xib & Demo2Controller.xib) are linked to the DemoViewController, Will load the Xib based upon the Condition..
I have navigation controller implemented in AppDelegate, Currently I am pushing this view controller(DemoViewController) with the XIB Demo1Controller, When User Taps a button in Demo1Controller, I need to load the same Viewcontroller i.e, DemoViewcontroller with Xib Demo2Controller..
Can this Possible?? Or Do i need to maintain the Different Classes for two Xib's
Let me know if you have any questions...
As a ViewController is just an object like any other object you can stack as many of them as you need on top of each other. Creating as many instances of that object as you want.
When you instantiate them you can do:
UIViewController *viewController = [[DemoViewController alloc] initWithNibName:#"Demo1Controller" bundle:nil];
or
UIViewController *viewController = [[DemoViewController alloc] initWithNibName:#"Demo2Controller" bundle:nil];
As long as the IBOutlets and delegate are set up correctly on both .xib's and they are set up using the same Custom class in IB. (Third icon from the left in the inspector panel, at the top.) If you fail to set them up properly it will simply crash on build and run.
And you can also check out a similar question I answered with a different approach some time ago.
Another approach
TRY THIS
- (id)init
{
if (YES)
self = [super initWithNibName:#"VC1" bundle:nil];
else
self = [super initWithNibName:#"VC2" bundle:nil];
return self;
}
I'm not sure if this is helpful to you, or appropriate for your situation, but I would probably suggest a slightly different approach (obviously, without knowing very much about your specific situation).
I would probably suggest that, rather than having one class have two different nibs, instead you have one class that has all of the common behaviour that these two 'screens' share, and then two concrete subclasses of this common parent class for each of the 'screens'. I am assuming that there are slight behavioural differences between the two.
You could then create an instance of your concrete subclasses with the specific nib name, as usual:
SubclassOneViewController *viewController = [[SubclassOneViewController alloc] initWithNibName:#"SubclassOneViewController" bundle:nil];
In short, I want to know "How to change Runtime User defined attributes before instantiating a View Controller"
Why I Need this
I am creating an application which uses multiple storyboards. I have a main storyboard with a UIViewController designated as LinkViewController. It has a string attribute, which tells it which storyboard has to be linked. now what I want to do is, I want to change that attribute at appDelegate & then instantiate the viewController. So far not able to do it.
This is what I am doing:
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
EffLinkHomeVC *rootController = [storyboard instantiateViewControllerWithIdentifier:#"linkView"];
rootController.storyBoardName = #"wxyzForiPhone";
self.window.rootViewController = rootController;
I have found several other ways to work around. But I just want to know more clearly about altering "Runtime User defined attributes". Thank you all. :)
As far as I know you can only do this with proxy/external objects available in nibs.
Check the answer here to see a nice example of their usage.
By the looks of it this functionality is hidden or removed from storyboards. The only documented ways of configuring are static. In your case it would be statically configured with the properties from the storyboard you are using. If this suffices, you could use the key value mechanism to statically configure a different value for each storyboard.
Other than that you only have the normal post init viewController methods.
My problem is:
I've created a project object. In this project object I need to call a method in a ViewController.
The method I need to call draws some objects in the ViewController principal view using a modified -
(void)drawRect:(CGRect)rect method.
The only thing I need to do is call this method on the actual instance (the instance that is created when the app starts) of the ViewController from a method in the project class.
The method created (to draw the objects) works. I tested it by calling it from the ViewDidLoad method of the ViewController. In the project method I tried for example this code:
-(void)drawProject {
UIStoryboard *mainStoryboard =[UIStoryboard storyboardWithName:#"Main" bundle:nil];
IYViewController *projectViewController = [[IYViewController alloc] init];
projectViewController = (IYViewController *)[mainStoryboard instantiateViewControllerWithIdentifier:#"project"];
[projectViewController drawProject];
}
I named the principal, the target ViewController "project" in interface builder.
The problem is that anyway the ViewController instance that I create is not the instance of IYViewController that is displayed at runtime.
I didn't find any real good solution that works for me, so please help! Maybe the solution is create a delegate of a class but I didn't really understand that so please if this is the right solution help me code it!
Yes, the instance that you alloc and init is not the one that you load from the storybaoard. There is no need to alloc and init it. If you don't ARC then you even created a memory leak there.
-(void)drawProject {
UIStoryboard *mainStoryboard =[UIStoryboard storyboardWithName:#"Main" bundle:nil];
IYViewController *projectViewController;
projectViewController = (IYViewController *)[mainStoryboard instantiateViewControllerWithIdentifier:#"project"];
[projectViewController drawProject];
}
Well, this will not solve the issue. (If comments could be formatted properly then I added a comment rather than an answer). Your real problem is somewhere else and I fear that you may not yet know how to explain it so that we understand it.
Give it a try. Explain in a bit more detail.
Are you sure that your drawProject is really executed? To which class belongs the drawProject Method that we are looking at?
What exactly do you do in drawRect.
Besides, drawRect is a method of a view, not a view controller. If you implemented that for a view controller then it may not be called at all.