문제

I have a UIView viewForRootVc that is a declared property for a UIView subclass NewView. It's NewView's responsibility to initialize viewForRootVc, but then a subclass NewViewSubClass sets its background color. Once this is all done, the root view controller RootVc adds viewForRootVc as a subview to its view.

But this doesn't work. viewForRootVc doesn't actually get added.

It DOES work, however, if I do any of the following three things (keep in mind that I am using ARC):

  1. Rather than setting the background color of viewForRootVc in NewViewSubClass, I set it in NewView. And then, rather than initializing an instance of NewViewSubClass, I simply initialize an instance of NewView.
  2. When initializing viewForRootVc in NewView, I call the setter (i.e. self.viewForRootVc) rather than just using direct assignment.
  3. List viewForRootVc as an ivar in the header file of NewView.

I don't understand why any of these three things are necessary.

Here's the code that does NOT work: RootVc.m

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    NewView *newView = [[NewViewSubClass alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];

    //logs 0.00000, 0.00000    
    NSLog(@"%f, %f",newView.viewForRootVc.frame.size.width, newView.viewForRootVc.frame.size.height);

    [self.view addSubview:newView.viewForRootVc];

    //logs 0
    NSLog(@"subviews: %i",[self.view.subviews count]);
}

NewView.h

@property (nonatomic, retain) UIView *viewForRootVc;

NewView.m

@synthesize viewForRootVc;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        viewForRootVc = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    }
    return self;
}

NewViewSubClass.m

@synthesize viewForRootVc;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        viewForRootVc.backgroundColor = [UIColor greenColor];
    }
    return self;
}

One thing I noticed is that if I do specify viewForRootVc as an ivar, I don't need to put @synthesize viewForRootVc; in the implementation file of NewViewSubClass.

I always understood declared properties to effectively achieve the following:

  1. Allow other objects to access them.
  2. Allow a setter to be called when desired.
  3. If not using ARC, calling the setter will automatically retain the property if you specified retain in the property declaration.
  4. Under ARC, an ivar is automatically generated for any declared properties.

But clearly there is more to it than this. I'm not sure if it's a scope issue, or if my non-working code above ends up creating two distinct versions of viewForRootVc -- perhaps one that has the correct frame as specfied in NewView's init method and one that has the correct background color as specified in NewViewSubClass's init method, but neither has both the right frame and color.

I'm very much hoping someone can clarify once and for all for me the implications of declared properties vs. ivars, as well as calling the setter to set a declared property vs. assigning it a value directly.

도움이 되었습니까?

해결책

So the issue here is this line in NewViewSubClass.m:

@synthesize viewForRootVc;

What the compiler ends up doing there is implementing a new getter/setter pair for the subclass. This is distinct from the previous getter/setter pair that were implemented for the same property in the superclass.

In addition, something else happens here and in the previous @synthesize directive, which is that the compiler also generates an ivar to back this property. However, this ivar that is created is visible only within each particular implementation. So in other words, each of the two @synthesize directives ends up creating its own distinct ivar.

So then we get to this bit of code here in NewViewSubClass.m:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {

        // this ivar is the one from the subclass. It is still nil at this point!
        viewForRootVc.backgroundColor = [UIColor greenColor];
    }
    return self;
}

Here viewForRootVc is the ivar from the subclass. It is nil at this point. In fact, in your case it is always nil and is never set. Because if you look at this code from NewView.m:

@synthesize viewForRootVc;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {

        // Here viewForRootVc is the ivar generated for this class
        // this is not the same as the ivar with the same name in our subclass

        viewForRootVc = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    }
    return self;
}

The superclass is setting its ivar called 'viewForRootVc', not the ivar with the same name generated in the subclass.

I hope that makes sense. So let's look at the three ways in which you "fixed" this to understand exactly what happened in each case:

  1. Set the background in NewView -> This basically uses the correct ivar (the one from NewView), which effectively avoids the problem we've been talking about.

  2. Calling the setter from NewView -> This is the interesting one. By calling the setter, you are actually calling the setter implemented in the subclass. This in turn, uses the ivar in the subclass, and so everything "works" (although, I think this is not ideal.)

  3. Explicitly declaring the ivar -> This avoids the issue in a different way. By declaring the ivar, you prevent the compiler from generating its own ivar from the @synthesize directive. And because ivars are, by default, @protected, that ivar is also visible in the subclass, so both sets of setters/getters end up using the same ivar.

The right fix, IMO, is let ivars be private to a class (@synthesized ivars are hidden and therefore effectively private), and have subclasses reference the property getter/setter as needed. And then of course don't @synthesize the property again in your NewViewSubclass.m

I hope that helps.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top