this is a follow-up to this question (custom UIView how-to with IB / Xcode 4.5.1 and iOS Simulator 6.0) but not necessary to read - this is my first time trying to create a custom UIView and there is clearly something that I'm not getting so thx in advance for any help that you can provide.
I have a custom UIView that is derived from Interface Builder. I want to set the set the size to 200w x 200h and the backgroundColor to Green. When I created I did the following:
Created a new Custom UIView via File -> New -> Objective-C Class called Tview
Created a new view via File -> New -> User Interface -> View and called it tnib
in the Canvas, I deleted out the View and added a new View from the object inspector and set the class name to Tview. Also, in the Attributes inspector, I set the size to Freeform and the background color to Green. In the Size Inspector, I set the width to 200 and the height to 200.
In my Tview.m I set up with the following (I have made some updates based upon other SO questions but it is unclear whether those are still current or accurate):
#implementation Tview
-(id) initWithCoder:(NSCoder *)aDecoder
{
NSLog(#"in initWithCoder");
if((self = [super initWithCoder:aDecoder])) {
[self setUpView];
}
return self;
}
-(void)awakeFromNib
{
NSLog(#"in awakeFromNib");
[self setUpView];
}
-(void)setUpView
{
NSLog(#"I am in setUpView");
NSArray *subviewArray=[[NSBundle mainBundle] loadNibNamed:#"tnib" owner:self options:nil];
UIView *mainView = [subviewArray objectAtIndex:0];
[self addSubview:mainView];
}
#end
In my ViewController.xib, I drag out a UIView and set the custom class name to Tview. I #import the Tview.h file and drag from the ViewController.xib to Tview.h and create following property:
#property (strong, nonatomic) IBOutlet Tview *myTview;
I build and run and get an infinite loop of hte following:
2013-03-04 06:49:05.452 Nibtest2[44524:11303] in initWithCoder
2013-03-04 06:49:05.455 Nibtest2[44524:11303] I am in setUpView
2013-03-04 06:49:05.456 Nibtest2[44524:11303] in initWithCoder
2013-03-04 06:49:05.458 Nibtest2[44524:11303] I am in setUpView
2013-03-04 06:49:05.459 Nibtest2[44524:11303] in initWithCoder
2013-03-04 06:49:05.460 Nibtest2[44524:11303] I am in setUpView
until it eventually crashes.
What am I doing wrong here?
thx in advance
Your code sets up an infinite recursion: initWithCoder: calls setUpView, which instantiates a new UIView from a bundle, thus calling initWithCoder: indirectly, and completing the cycle.
You should not access the NIB in the code that is called when the same NIB is read. You should read the NIB in the code of the containing object, not in the code of the Tview object itself. Otherwise, you get an infinite cycle.
You need to remote the setUpView altogether, along with its calls from the awakeFromNib and the initWithCoder:. It appears that the connection that you made in the interface builder by dragging out from object inspector is already causing the NIB file to load correctly, as evidenced by the call of initWithCoder:. The only line that you may need is this:
[self addSubview:mainView];
However, it shouldn't be in the Tview's code: it needs to be moved to the parent controller that has the myTview property, and it should be modified to
[self.view addSubview:_myTview];
Add this line to viewDidLoad. It should take care of the problem.
Actually my resolution to this problem was, to load the view in a viewDidLoad in my CustonViewController where I wanted to use the view like that:
myAccessoryView = NSBundle.mainBundle().loadNibNamed("MyAccessoryView", owner: self, options: nil)[0] as! MyAccessoryView
Don't load the view in a loadView() method! The loadView method serves for loading the view for your custom ViewController.
Related
I am completely stumped and have been researching for days. Probably something really simple that I am missing.
I have a ViewController which contains a custom UIView called GameView, and a UIView called buttonBox which contains a "next level" button. What I am trying to achieve is when the level is completed in GameView, it fires a function in my ViewController which shows the buttonBox so the user can click the "next level" button. It simply will not work.
I have attempted this in 3 ways, neither have worked:
Creating an IBOutlet in the ViewController, connecting it to the hidden UIView (and it was definitely connected) and calling setHidden:NO.
Calling the [self.view viewWithTag:xxx] and then calling setHidden:NO.
Using hidden=NO instead of setHidden:NO.
Relevant code for ViewController as follows:
#interface PlayViewController : UIViewController
#property GameView *gv;
#property (strong, nonatomic) IBOutlet UIView *buttonBox;
-(void) showButtonBox;
#end
#implementation PlayViewController
#synthesize buttonBox;
...
- (IBAction)showButtonBox {
UIView *uiv = (UIView*) [self.view viewWithTag:999];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Showing box function");
NSLog(#"%#", uiv);
uiv.hidden = NO;
});
}
#end
And my custom view:
#implementation GameView
...
dispatch_async(bgQueue, ^{
_loopRunning = true;
//NSLog(#"Calling main loop...");
while ([self loopRunning])
{
...
PlayViewController * pvc = [[PlayViewController alloc]init];
[pvc showButtonBox];
...
}
#end
The thing is, the variable uiv is returning null in NSLog, which is obviously why hidden is not working, but I have no idea why. It also didn't work when I was using IBOutlet.
Also, current output from NSLog is as follows:
2015-11-24 00:18:38.612 ib[12579:1264539] Showing box function
2015-11-24 00:18:38.612 ib[12579:1264539] (null)
Thanks in advance.
Correct Answer:
The problem was that I was using StoryBuilder to build my UI, but by using the alloc init method was creating a new view controller (which is never shown) instead of correctly referencing the view controller which was being displayed. This is achieved by passing the view controller being displayed to the view in the viewDidLoad function, see below:
#implementation PlayViewController
#synthesize buttonBox;
#synthesize gv;
- (void)viewDidLoad
{
[super viewDidLoad];
gv = [self.view viewWithTag:777];
[gv setPlayViewController:self];
}
...
Man, it's simple. Let's take a look at:
#implementation GameView
...
dispatch_async(bgQueue, ^{
_loopRunning = true;
//NSLog(#"Calling main loop...");
while ([self loopRunning])
{
...
PlayViewController * pvc = [[PlayViewController alloc]init];
[pvc showButtonBox];
...
}
#end
Here we have the issue:
dispatch_async(bgQueue, ^{
I assume, bgQueue stands for "background queue", which means this is not served by the main thread (the UI thread).
Having that said, it's quite naive to expect
[pvc showButtonBox];
to work properly. Just move this code into the main thread. For instance, you can just wrap the aforementioned line of code into a dispatch_async on the main queue. That should solve your probem, if your outlets and/or tags are OK. Cheers.
[[PlayViewController alloc]init];
This creates a new instance of PlayViewController. Where have you defined your outlets and views?
In a storyboard? You can't use this initialiser - nothing from the storyboard will be picked up, you have to use a segue or initializeViewControllerWithIdentifier:.
In a xib file? Is it called PlayViewController.xib? If not, it won't be picked up by the initialiser. Plain alloc/init of a view controller will only find a nib file as described in the documentation of the nibName property.
Do you really want alloc / init at all? Do you actually want to make a new view controller, or is one already on the screen?
From your comments it seems option 3 is the right answer. The PlayViewController is already on the screen, alloc/init is creating a new instance of it, which is never being put on screen, which never loads any views regardless of storyboards or nibs.
You need to get a reference to the existing instance of PlayViewController. Without knowing the structure of your app it's not too easy to say how that's done - is it presenting the game view? Is the game view a subview of the view controller's view? You may need to pass in a reference (weak) to the game view when it is created, at viewDidLoad, or set up an outlet in the storyboard.
I created a custom UITableViewCell (for this example, let's just say the subclass is MyViewCell) which has an Nib file associated to it MyViewCell.xib. The nib contains a UITableViewCell with one subview UIView (named cardContainer) that's simply a rectangle with a blue background. I want to add a drop shadow around the UIView, so I added set the layer properties in the -initWithCoder call:
#implementation MyViewCell
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self initView];
}
return self;
}
- (void) initView
{
UIBezier Path*shadowPath =[UIBezierPath bezierPathWithRect:view.bounds];
[self.cardContainer.layer setShadowColor: [UIColor blackColor].CGColor];
[self.cardContainer.layer setShadowOpacity: 0.8];
[self.cardContainer.layer setShadowRadius:3.0];
[self.cardContainer.layer setShadowOffset: CGSizeMake(2.0,2.0)];
view.layer.shadowPath = shadowPath.CGPath;
}
#end
The problem I'm having is that these layer properties aren't being drawn. If I call the -initView call within awakeFromNib or drawRect it's drawn as expected. My question: why doesn't my original code work? Where should I be calling initView? Is there some view lifecycle? I understand that the initWithCoder doesn't have the outlets connected, but it didn't crash at runtime.
I read through Apple documentation around Views and searched through the SO questions without finding an answer. I found this SO answer, but again doesn't explain.
Hey I found a better way to do this ,just add some runtime attributes for your subview cardContainer
like this
no more code in .m file anymore.
EDIT:
From:NSNibAwaking Protocol
Important: Because the order in which objects are instantiated from an archive is not guaranteed, your initialization methods should not send messages to other objects in the hierarchy. Messages to other objects can be sent safely from within awakeFromNib—by which time it’s assured that all the objects are unarchived and initialized (though not necessarily awakened, of course).
You need to add this,
self.cardContainer.layer.masksToBounds = NO;
edit #1
please read the edit #1 at the bottom of the question; I'm trying to figure out all the ways nib's can be used in creating custom views so all the examples deal with nib files.
end
I have been trying to figure out all the techniques for creating custom views that use nibs in controllers. I have listed the ways at the bottom of this question.
Here's the code for my TRtestview (one implication of how I have this set-up is that initWithCoder will always be called):
// TRtestview.m
-(id)initWithCoder:(NSCoder *)aDecoder{
self=[super initWithCoder:aDecoder];
if (self) {
if (self.subviews.count == 0) {
UIView *myView=[[[NSBundle mainBundle] loadNibNamed:#"testview" owner:nil options:nil] lastObject];
myView.userInteractionEnabled=YES; // doesn't do anything
[self addSubview:myView];
}
}
NSLog(#"about to return here in initWithCoder");
return self;
}
edit 1
-(void)setup
{
UIView *myView=[[[NSBundle mainBundle] loadNibNamed:#"testview" owner:nil options:nil] lastObject];
[self addSubview:myView];
}
- (id)initWithFrame:(CGRect)frame
{
NSLog(#"at top of initWithFrame");
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
in TRViewController.m, I tried setting thisView to userInteractionEnabled but it didn't change things:
- (void)viewDidLoad
{
[super viewDidLoad];
self.thisView.userInteractionEnabled=YES;
This has worked for the scenario of calling initWithFrame (technique #1) and loadNibNamed (technique #2). It will load the view by dragging it out from the Object Inspector and then setting the Class in the Custom Class attribute inspector (technique #3). However, the button is non-functional and thus this way doesn't work.
How would I create a custom view with a nib using technique #3 and allow for user interaction?
thx
edit 1
I'd really like to understand all the ways that a custom view can be created and instantiated. Ideally, I'd like to be able to have custom views that can be created via initWithFrame, loadNibNamed, or dragged from the object library.
It seems like the scenarios are:
initWithFrame from a ViewController; if using with a nib, I call loadNibNamed in my initWithFrame - I need to prevent recursive loading
loadNibNamed from a ViewController (which will call initWithCoder) and would want to load only once
dragging an instance of a UIView and then setting the custom class to my custom UIView (TRtestview in this case). This will call initWithCoder.
It looks like you're loading testview from inside the testview class initialiser with a hacky check for subviews to prevent it from becoming recursive. Since the code outside probably also loads the nib, what you're actually doing here is loading the nib twice, once inside the other. If you set up the owner then the inner one will be setting the outlets on the outer one.
It's just a guess as there is not enough information here to know for sure - but if you are setting outlets in your view class for some of the sub-views within it then you must supply an owner object to loadNibNamed:owner:options: with the type of your class. So you could pass self as owner in your code and this might work. However it's really the wrong way to use nibs for single-views.
Instead of having the single view in your nib file being the TRtestView class, have the single-view just as a holder for your other internal views and set the "File's Owner" place-holder object to have the class TRtestview. Create an outlet for the holder and assign it then do something like this:
TRtestview* testview = [[TRtestview alloc] initWithFrame:desiredFrame];
[[NSBundle mainBundle] loadNibNamed:#"testview" owner:testview options:nil];
Now all the outlets in testview will be set when the nib is loaded, including your holder which you can add as a sub-view.
Also, if you want to use a TRtestview within a different nib (for example TestViewController), you can do this by dragging in a view and setting that object's class to TRtestview - this will create a TRtestview and call its initWithCoder and you can assign it to an outlet in TestViewController called testview. Then in the awakeFromNib for TestViewController you would put the second line of code from above, passing in the testview outlet as the owner object.
Edit:
It's tricky to see what you're trying to do exactly, but loading the nib from inside any of the view's init methods is wrong (loading the nib creates the view object, not the other way around). I think I misunderstood at first where the IBOutlets that you want to set are, probably the answer you're looking for is actually that you can load the nib but from a class constructor method like this:
+ (MyCustomView*)newCustomView
{
MyCustomView* view = [[NSBundle mainBundle] loadNibNamed:#"testview" owner:nil options:nil][0];
// Do custom setup things to view here...
// Perhaps based on additional inputs to this method.
view.doStuff;
return view;
}
I have a UIImageView subclass (AdImageView) placed in my view controller's NIB. This AdImageView knows how to go off and load a remote image into itself using some asynchronous calls invoked from willMoveToSuperview. I know this works because if I add the AdImageView through my own code and tell it before it gets added to a super view which image to load it works great. However when switching setup to use a NIB, I'm not sure when to set the remote path on the AdImageView.
I can't do it in any of the VC's init methods, because the NIB objects aren't yet setup. By the time the VC's viewDidLoad gets called AdImageView's willMoveToSuperview: is already called. I tried setting some IBOutlets telling AdImageView who to ask for the image url, but that is also called after AdImageView's willMoveToSuperview:.
I could start the remote image loading in AdImageView's awakeFromNib, but I want to continue to also have the option to programmatically add an AdImageView to a view controller. Putting the code there then puts me in a situation where I could remote image loading reinvoked in willMoveToSuperview: unless I protect it with some sort of flag that just feels ugly.
So where does state setup code for Interface Builder objects normally occur?
(Sorry there is no code for this question, but it's more about strategy)
This doesn't really sound like a view's job but you could do something like this
#interface AdImageView : UIImageView
#property (nonatomic, copy) NSString *path
#end
#implementation AdImageView
- (void)setPath:(NSString *)path
{
if (_path != path) {
_path = [path copy];
[self loadAd];
}
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self loadAd];
}
- (void)loadAd
{
// This will only load when the view is on screen
if (self.window) {
// ... fetch ad etc
}
}
#end
Suppose you implement a custom table view and a custom view controller (which mostly mimics UITableViewControllers behaviour, but when initialized programmatically, ...
#interface Foo : MyCustomTableViewController ...
Foo *foo = [[Foo alloc] init];
... foo.view is kind of class MyCustomTableView instead of UITableView:
// MyCustomTableView.h
#protocol MyTableViewDelegate <NSObject, UITableViewDelegate>
// ...
#end
#protocol MyTableViewDataSource <NSObject, UITableViewDataSource>
// ...
#end
#interface MyCustomTableView : UITableView
// ...
#end
// MyCustomTableViewController.h
#interface MyCustomTableViewController : UIViewController
// ...
#end
How should you implement/override init methods in correct order/ways so that you could create and use an instance of MyCustomTableView both by subclassing MyCustomTableViewController programmatically or from any custom nib file by setting custom class type to MyCustomTableView in Interface Builder?
It important to note that this is exactly how UITableView (mostly UIKit for that matter) works right now: a developer could create and use either programmatically or by creating from nib, whether be it File owner's main view or some subview in a more complex hierarchy, just assign data source or delegate and you're good to go...
So far I managed to get this working if you subclass MyCustomTableViewController, where I will create an instance of MyCustomTableView and assign it to self.view in loadView method; but couldn't figure out how initWithNibName:bundle:, initWithCoder:, awakeFromNib, awakeAfterUsingCoder:, or whatever else operates. I am lost in life cycle chain and end up with a black view/screen each time.
Thanks.
It is a real mystery how the UITableViewController loads its table regardless of if one is hooked up in interface builder, however I have came up with a pretty good way to simulate that behavior.
I wanted to achieve this with a reusable view controller that contains a MKMapView, and I figured out a trick to make it happen by checking the background color of the view.
The reason this was hard is because any call to self.view caused the storyboard one to load or load a default UIView if didnt exist. There was no way to figure out if inbetween those 2 steps if the user really didn't set a view. So the trick is the one that comes from a storyboard has a color, the default one is nil color.
So now I have a mapViewController that can be used in code or in storyboard and doesn't even care if a map was set or not. Pretty cool.
- (void)viewDidLoad
{
[super viewDidLoad];
//magic to work without a view set in the storboard or in code.
//check if a view has been set in the storyboard, like what UITableViewController does.
//check if don't have a map view
if(![self.view isKindOfClass:[MKMapView class]]){
//check if the default view was loaded. Default view always has no background color.
if([self.view isKindOfClass:[UIView class]] && !self.view.backgroundColor){
//switch it for a map view
self.view = [[MKMapView alloc] initWithFrame:CGRectZero];
self.mapView.delegate = self;
}else{
[NSException raise:#"MapViewController didn't find a map view" format:#"Found a %#", self.view.class];
}
}
The strategy I've used when writing such classes has been to postpone my custom initialization code as late as possible. If I can wait for viewDidLoad or viewWillAppear to do any setup, and not write any custom code in init, initWithNibName:bundle: or similar methods I'll know that my object is initialized just like the parent class no mater what way it was instantiated. Frequently I manage to write my classes without any overrides of these init methods.
If I find that I need to put my initialization code in the init methods my strategy is to write just one version of my initialization code, put that in a separate method, and then override all the init methods. The overridden methods call the superclass version of themselves, check for success, then call my internal initialization method.
If these strategies fail, such that it really makes a difference what way an object of this class is instantiated, I'll write custom methods for each of the various init methods.
This is how I solved my own issue:
- (void)loadView
{
if (self.nibName) {
// although docs states "Your custom implementation of this method should not call super.", I am doing it instead of loading from nib manually, because I am too lazy ;-)
[super loadView];
}
else {
self.view = // ... whatever UIView you'd like to create
}
}