How to use a common target object to handle action

2019-02-11 05:37发布

问题:

I'm beginning to try and use IB where previously I used code. I have created nibs for iPad/iPhone Portrait/Landscape so I have four possible views. All have files owner set to UIViewController, all have an object added from the palette set to my NSObject subclass "TargetObject". No problem setting IBActions/IBOutlets, just ctrl-drag as usual to create the code in one nib, then go and right-click the object and drag the actions/outlets to the right controls in the other nibs so that the connection exists in all nibs. The idea is that the app delegate kicks off the appropriate view controller for the current device and then that view controller loads the right view depending on orientation.

(Maybe with autoresizing mastery this approach is not necessary - I want to know if it is possible to do it this way because getting the layouts to rearrange themselves just right just with those IB controls is very, very frustrating.)

The idea behind these one-to-many views and target objects is that I can use them anywhere in the app. For example, where I have something displayed which is an object in core data and I would like to allow detail view/edit in multiple places in the app. It seems like a very MVC way to do things.

The problem comes in loadView of the view controller when I do:

NSArray *portraitViewArray = [[NSBundle mainBundle] loadNibNamed:@"Weekview_iPad_Portrait" owner:self options:nil];
portraitWeekView = [portraitViewArray objectAtIndex:0];

NSArray *landscapeViewArray = [[NSBundle mainBundle] loadNibNamed:@"Weekview_iPad_Landscape" owner:self options:nil];
landscapeWeekView = [landscapeViewArray objectAtIndex:0];

// this object is intended to be a common target for portrait and landscape arrays. 
weekViewTarget = [portraitViewArray objectAtIndex:1];

UIInterfaceOrientation interfaceOrientation = self.interfaceOrientation;

if(interfaceOrientation == UIInterfaceOrientationPortrait || interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
    self.view = portraitWeekView;
else 
    self.view = landscapeWeekView;

As you guessed, everything is fine in portrait orientation but in landscape any interaction with the target object crashes. The weekViewTarget object refers to an object created with the portrait view, so the landscape view is looking for a different instance that hasn't been held with a reference. In the landscape view _viewDelegate is nil - it is private so I can't fix it up in code.

I can hold onto the landscape target object with a reference too, but then there will be problems whenever the orientation changes - in case of any stateful controls like UITableView the datasource/delegate will change resulting in strange user experience on rotation. Unless I ensure the target object implements only simple actions and uses a separate, shared object to implement datasource/delegate objects for any table views or text views required.

One thing I have thought of is to make the target a singleton. Seems like a hack and here will be times that can't work.

What's the right way to create multiple views implemented as nibs using a common target object to do stuff to the model?

  • progress - things begin to work if I give each target a pointer to its own type:

    WeekViewTarget *forwardTarget;
    

and let the real handler's forwardTarget = nil while the unused handler's forwardTarget is set as follows:

weekViewTarget = [portraitViewArray objectAtIndex:1];
forwardedWeekViewTarget = [landscapeViewArray objectAtIndex:1];
forwardedWeekViewTarget.forwardTarget = weekViewTarget;

and then do the following in each IBAction:

- (IBAction)timeBlockTapped:(id)sender {

    if(forwardTarget != nil) {
        [forwardTarget performSelector:@selector(timeBlockTapped:) withObject:sender];
    } else {
        NSLog(@"Got a tap event with sender %@", [sender description]);
    }
}

Still doesn't feel right though.

回答1:

Adam, it sounds like you want a proxy (aka external) object.

Your Problem

NSBundle and UINib return auto-released arrays when instantiating object graphs from NIBs. Any object you don't specifically retain will be released when the array is released at the end of the event loop. Obviously objects inside the nib that reference each other will be retained as long as their owner is retained.

So you 2nd instance of your action target object is being released because you're not hanging on to it.

What you really want is just a single instance of your target object that both NIBs can reference. So you can't have your NIB instantiate an instance of that object. But your NIB needs to be able to make reference to that object instance!

Oh dear, what to do?!

Solution: Proxy/External Objects

Basically an external object is a proxy you place in a NIB. You then feed in the actual instance that when you instantiate the NIB's object graph. The NIB loader replaces the proxy in the NIB with your externally provided instance and configures any targets or outlets you've assigned to the proxy in Interface Builder.

How To Do It

  1. In Interface Builder drag in an "External Object" rather than NSObject instance. It will appear in the upper section along with File's Owner in Xcode 4.x.
  2. Select it and in the "Identity Inspector" set it's class to the appropriate type.
  3. In the "Attributes Inspector" set the Identifier string to "TargetObject" (or whatever string you want to use in the code below).
  4. Profit!

The Code

id owner = self; // or whatever
id targetObject = self.targetObject; // or whatever...

// Construct a dictionary of objects with strings identifying them.  
// These strings should match the Identifiers you specified in IB's 
// Attribute Inspector.
NSDictionary *externalObjects = [NSDictionary dictionaryWithObjectsAndKeys:
                                    targetObject, @"TargetObject",
                                    nil];

// Load the NIBs ready for instantiation.
UINib *portraitViewNib = [UINib nibWithNibName:@"Weekview_iPad_Portrait" bundle:nil];
UINib *landscapeViewNib = [UINib nibWithNibName:@"Weekview_iPad_Landscape" bundle:nil];

// Set up the NIB options pointing to your external objects dictionary.
NSDictionary *nibOptions = [NSDictionary dictionaryWithObjectsAndKeys:
                               externalObjects, UINibExternalObjects, 
                               nil];

// Instantiate the objects in the nib passing in the owner 
// (coincidentally also a proxy in the NIB) and your options dictionary.
NSArray *portraitViewArray =  [portraitViewNib instantiateWithOwner:owner
                                                            options:nibOptions];
NSArray *landscapeViewArray = [landscapeViewNib instantiateWithOwner:owner
                                                             options:nibOptions];

self.view = [(UIInterfaceOrientationIsPortrait(interfaceOrientation)) ? portraitViewArray : landscapeViewArray) objectAtIndex:0];

Notes

You're using NSBundle to load NIBs. You should be using UINib. It is much faster at reading the NIB files. Also, it separates the NIB loading from the object instantiation. Which means you can load the NIB in your init, awakeFromNib, or viewDidLoad and not instantiate the contents of the NIB. Then at the last possible moment when you need the actual objects in the NIB to you can call instantiateWithOwner.

Say in the case of using a UITableViewController you would load your table cell nibs in viewDidLoad but not actually instantiate them until you needed a cell of that type in -tableView:cellForRowAtIndexPath:.



回答2:

On the basis of interfaceOrientation you can identify the orientation and reset the frame/s of controls in xib. For more, this will guide you...