4

我们当前自定义视图的最佳实践是:

  1. 在 Nib 中构建自定义视图。
  2. 在视图控制器中,以编程方式加载 Nib,从加载的对象数组中获取自定义视图(我们在 UIView 类别方法中执行此操作+loadInstanceFromNib)。
  3. 添加自定义视图作为子视图,设置其框架。

我们真正想要的是在视图控制器 Nib 中“嵌入”自定义视图 Nib。如果做不到这一点,至少我们想在视图控制器 Nib 中添加和定位一个自定义视图实例(不看到它的内容)。

我们已经非常接近以下解决方案:

@implementation CustomView

static BOOL loadNormally;

- (id) initWithCoder:(NSCoder*)aDecoder {
    id returnValue = nil;
    if (loadNormally) { // Step 2
        returnValue = [super initWithCoder:aDecoder];
        loadNormally = !loadNormally;
    } else {            // Step 1
        loadNormally = !loadNormally;
        returnValue = [CustomView loadInstanceFromNib];
    }
    return returnValue;
}

- (id) initWithFrame:(CGRect)frame {
    loadNormally = YES;
    self = (id) [[CustomView loadInstanceFromNib] retain];
    self.frame = frame;
    return self;
}
// ...
@end

如果我们以编程方式实例化自定义视图,我们使用-initWithFrame:,它将从 Nib 加载视图(它将调用-initWithCoder:并直接转到标记为“步骤 2”的 if 分支),设置其框架,并将其保留计数设置为 1。

然而,如果我们在视图控制器 Nib 中实例化自定义视图,(诚然相当难看的)静态loadNormally变量最初是NO:我们从“步骤 1”开始,在确保我们将立即使用-initWithCoder:. 从自定义视图 Nib 加载意味着我们回到-initWithCoder:,这次使用loadNormally==YES,即我们让 Nib 加载机制完成其工作并返回自定义视图实例。

结果,总结:

  • 好的:它工作!我们在 Interface Builder 中有“可插入”的自定义视图!
  • 坏处:丑陋的静态变量……:-/
  • 丑陋的: 自定义视图的一个实例被泄露了!这就是我希望你帮助的地方——我不明白为什么。有任何想法吗?
4

3 回答 3

3

我们最终得到了一种更好的方法,它涉及-awakeAfterUsingCoder:在我们的自定义视图中进行覆盖,将从视图控制器 Nib 加载的对象替换为从“嵌入式” Nib (CustomView.xib) 加载的对象。

我在一篇广泛的博客文章中写了我们如何将自定义视图 Nib 嵌入到其他 Nib中。

代码是这样的:

// CustomView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
    if (theThingThatGotLoadedWasJustAPlaceholder) {
        // load the embedded view from its Nib
        CustomView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([CustomView class]) owner:nil options:nil] objectAtIndex:0];

        // pass properties through
        theRealThing.frame = self.frame;
        theRealThing.autoresizingMask = self.autoresizingMask;

        [self release];
        self = [theRealThing retain];
    }
    return self;
}
于 2011-07-20T12:09:29.777 回答
1

杨的回答很好……但是仍然会发生“发送到已释放实例的消息”。我通过使用“自我”分配解决了这个问题。

因此,如果您使用 ARC,则必须允许这种“自我”分配。(阅读https://blog.compeople.eu/apps/?p=142了解更多信息)

要在 ARC 项目中实现这一点,请在文件中添加“-fno-objc-arc”标志编译器设置。然后在这个文件中做NO-ARC编码(比如dealloc设置nils,调用super dealloc等)

此外,客户端 nib 的 viewcontroller 应该使用 strong 属性来保存 awakeFromNib 返回的实例。在我的示例代码中,customView 的引用如下:


@property ( strong , nonatomic) IBOutlet CustomView* customView;


最后,我使用copyUIPropertiesTo :和在我的UIView+Util类别中定义的 loadNibNamed为属性处理和 nib 加载添加了一些其他改进。

所以awakeAfterUsingCoder:代码现在

#import "UIView+Util.h"
...
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder
{
    // are we loading an empty “placeholder” or the real thing?
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);

    if (theThingThatGotLoadedWasJustAPlaceholder)
    {
        CustomView* customView = (id) [CustomView loadInstanceFromNib];
        // copy all UI properties from self to new view!
        // if not, property that were set using Interface buider are lost!
        [self copyUIPropertiesTo:customView];

        [self release];
        // need retain to avoid deallocation
        self = [customView retain];
    }
    return self;
}

UIView+Util 类别代码为

@interface UIView (Util)
   +(UIView*) loadInstanceFromNib;
   -(void) copyUIPropertiesTo:(UIView *)view;
@end

连同它的实施

#import "UIView+Util.h"
#import "Log.h"

@implementation UIView (Util)

+(UIView*) loadInstanceFromNib
{ 
    UIView *result = nil; 
    NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner: nil options: nil];
    for (id anObject in elements)
    { 
        if ([anObject isKindOfClass:[self class]])
        { 
            result = anObject;
            break; 
        } 
    }
    return result; 
}

-(void) copyUIPropertiesTo:(UIView *)view
{
    // reflection did not work to get those lists, so I hardcoded them
    // any suggestions are welcome here

    NSArray *properties =
    [NSArray arrayWithObjects: @"frame",@"bounds", @"center", @"transform", @"contentScaleFactor", @"multipleTouchEnabled", @"exclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"opaque", @"clearsContextBeforeDrawing", @"hidden", @"contentMode", @"contentStretch", nil];

    // some getters have 'is' prefix
    NSArray *getters =
    [NSArray arrayWithObjects: @"frame", @"bounds", @"center", @"transform", @"contentScaleFactor", @"isMultipleTouchEnabled", @"isExclusiveTouch", @"autoresizesSubviews", @"autoresizingMask", @"clipsToBounds", @"backgroundColor", @"alpha", @"isOpaque", @"clearsContextBeforeDrawing", @"isHidden", @"contentMode", @"contentStretch", nil];

    for (int i=0; i<[properties count]; i++)
    {
        NSString * propertyName = [properties objectAtIndex:i];
        NSString * getter = [getters objectAtIndex:i];

        SEL getPropertySelector = NSSelectorFromString(getter);

        NSString *setterSelectorName =
            [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];

        setterSelectorName = [NSString stringWithFormat:@"set%@:", setterSelectorName];

        SEL setPropertySelector = NSSelectorFromString(setterSelectorName);

        if ([self respondsToSelector:getPropertySelector] && [view respondsToSelector:setPropertySelector])
        {
            NSObject * propertyValue = [self valueForKey:propertyName];

            [view setValue:propertyValue forKey:propertyName];
        }
    }    
}
于 2012-06-07T15:10:05.023 回答
0

There is an alternative way to do this:

say you use View1 in your Interface Builder, then you create another view called View2, View2 has a corresponding View2.xib file, you have linked the outlets in View2.m and View2.xib.

Then, in View1.m, write this:

-(void)awakeFromNib
{
    NSArray *topObjects = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil];
    self.subContentView = topObjects.firstObject]
    [self addSubview:self.subContentView];
}

With this, you can use View1 in places where you need to put your custom view in Interface Builder, thus make View1 reusable in Interface Builder without writing any more code.

于 2014-09-02T10:54:10.200 回答