当前位置: 首页 > 知识库问答 >
问题:

使用xib创建可重用的UIView(并从情节提要加载)

姜志行
2023-03-14

好的,StackOverflow上有很多关于这个的帖子,但是没有一个对解决方案特别清楚。我想创建一个带有随附的xib文件的自定义UIView。这些要求是:

  • 没有单独的UIViewController–一个完全独立的类

我目前的做法是:

>

  • 重写-(id)initAnd Frame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    

    在我的视图控制器中使用-(id)initWithFrame:以编程方式实例化

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

    这很好(尽管从未调用[super init],并且简单地使用加载的nib的内容设置对象似乎有点可疑——这里有建议在这种情况下添加子视图,这也很好)。但是,我也希望能够从故事板实例化视图。因此,我可以:

    >

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

    当然,这是行不通的,因为无论我使用上面的方法,还是我以编程方式实例化,都最终在输入-(无效)初始化子视图并从文件加载nib时递归调用-(id)initAnd Coder:

    其他几个SO问题涉及到这一点,比如这里,这里,这里和这里。但是,给出的答案都不能令人满意地解决问题:

    • 一个常见的建议似乎是将整个类嵌入到UIViewController中,并在那里进行nib加载,但这对我来说似乎是次优的,因为它需要添加另一个文件作为包装器

    有没有人能就如何解决此问题提供建议,并在定制的UIView中以最小的麻烦/无精简控制器包装获得工作插座?或者有没有一种替代的、更干净的方法来使用最少的样板代码?

  • 共有3个答案

    温智明
    2023-03-14

    initAnd Coder:方法中替换自己将失败,并出现以下错误。

    'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
    

    相反,您可以使用coder:(而不是awakeFromNib)将解码对象替换为awakafter。比如:

    @implementation MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    

    当然,这也会引起递归调用问题。(故事板解码-

    @interface MyCustomView : UIView
    @property (assign, nonatomic) BOOL xib
    @end
    

    并仅在“MyCustomView”中设置“用户定义的运行时属性”。xib'。

    赞成的意见:

    • 没有

    缺点:

    • 根本不起作用:setXib:将在使用coder:

    通常,在xib中有子视图,但在故事板中没有。

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        if(self.subviews.count > 0) {
            // loading xib
            return self;
        }
        else {
            // loading storyboard
            return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                  owner:nil
                                                options:nil] objectAtIndex:0];
        }
    }
    

    赞成的意见:

    • 界面生成器中没有技巧

    缺点:

    • 故事板中不能有子视图
    static BOOL _loadingXib = NO;
    
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        if(_loadingXib) {
            // xib
            return self;
        }
        else {
            // storyboard
            _loadingXib = YES;
            typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                               owner:nil
                                                             options:nil] objectAtIndex:0];
            _loadingXib = NO;
            return view;
        }
    }
    

    赞成的意见:

    • 简单
    • 接口生成器中没有技巧。

    缺点:

    • 不安全:静态共享标志是危险的

    例如,将\u NIB\u MyCustomView声明为MyCustomView的子类。并且,仅在XIB中使用\u NIB\u MyCustomView而不是MyCustomView

    我的客户视图。h:

    @interface MyCustomView : UIView
    @end
    

    MyCustomView. m:

    #import "MyCustomView.h"
    
    @implementation MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In Storyboard decoding path.
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    
    @interface _NIB_MyCustomView : MyCustomView
    @end
    
    @implementation _NIB_MyCustomView
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In XIB decoding path.
        // Block recursive call.
        return self;
    }
    @end
    

    赞成的意见:

    • 如果MyCustomView

    缺点:

    • 前缀\u NIB\ucode>在xib界面生成器中的技巧

    类似于d)但在故事板中使用子类,即XIB中的原始类。

    在这里,我们声明MyCustomViewProtoMyCustomView的子类。

    @interface MyCustomViewProto : MyCustomView
    @end
    @implementation MyCustomViewProto
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        // In storyboard decoding
        // Returns MyCustomView loaded from NIB.
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    @end
    

    赞成的意见:

    • 非常安全
    • 清洁;在MyCustomView中没有额外的代码。
    • 没有显式的如果检查相同的d)

    缺点:

    • 需要在情节提要中使用子类

    我认为e)是最安全、最干净的策略。因此,我们在这里采用这一点。

    在“awakafterusingcoder:”中加载nibNamed:后,您必须从脚本实例中解码的self中复制多个属性<代码>帧和自动布局/自动调整大小属性尤其重要。

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        // copy layout properities.
        view.frame = self.frame;
        view.autoresizingMask = self.autoresizingMask;
        view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
    
        // copy autolayout constraints
        NSMutableArray *constraints = [NSMutableArray array];
        for(NSLayoutConstraint *constraint in self.constraints) {
            id firstItem = constraint.firstItem;
            id secondItem = constraint.secondItem;
            if(firstItem == self) firstItem = view;
            if(secondItem == self) secondItem = view;
            [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                                attribute:constraint.firstAttribute
                                                                relatedBy:constraint.relation
                                                                   toItem:secondItem
                                                                attribute:constraint.secondAttribute
                                                               multiplier:constraint.multiplier
                                                                 constant:constraint.constant]];
        }
    
        // move subviews
        for(UIView *subview in self.subviews) {
            [view addSubview:subview];
        }
        [view addConstraints:constraints];
    
        // Copy more properties you like to expose in Storyboard.
    
        return view;
    }
    

    正如您所看到的,这是一些样板代码。我们可以将它们实现为“类别”。在这里,我扩展了常用的UIView loadFromNibcode。

    #import <UIKit/UIKit.h>
    
    @interface UIView (loadFromNib)
    @end
    
    @implementation UIView (loadFromNib)
    
    + (id)loadFromNib {
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
    
    - (void)copyPropertiesFromPrototype:(UIView *)proto {
        self.frame = proto.frame;
        self.autoresizingMask = proto.autoresizingMask;
        self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
        NSMutableArray *constraints = [NSMutableArray array];
        for(NSLayoutConstraint *constraint in proto.constraints) {
            id firstItem = constraint.firstItem;
            id secondItem = constraint.secondItem;
            if(firstItem == proto) firstItem = self;
            if(secondItem == proto) secondItem = self;
            [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                                attribute:constraint.firstAttribute
                                                                relatedBy:constraint.relation
                                                                   toItem:secondItem
                                                                attribute:constraint.secondAttribute
                                                               multiplier:constraint.multiplier
                                                                 constant:constraint.constant]];
        }
        for(UIView *subview in proto.subviews) {
            [self addSubview:subview];
        }
        [self addConstraints:constraints];
    }
    

    使用此选项,您可以像以下那样声明MyCustomViewProto

    @interface MyCustomViewProto : MyCustomView
    @end
    
    @implementation MyCustomViewProto
    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
        MyCustomView *view = [MyCustomView loadFromNib];
        [view copyPropertiesFromPrototype:self];
    
        // copy additional properties as you like.
    
        return view;
    }
    @end
    

    XIB:

    故事板:

    结果:

    邹嘉致
    2023-03-14

    我将此作为一个单独的帖子添加,以更新Swift的发布情况。LeoNatan描述的方法可以完美地在Object-C中工作。但是,更严格的编译时检查可以防止在从Swift中的xib文件加载时将自己分配给。

    因此,除了将从xib文件加载的视图添加为自定义UIView子类的子视图之外,别无选择,而不是完全替换self。这类似于原始问题中概述的第二种方法。使用该方法的Swift类的大致轮廓如下:

    @IBDesignable // <- to optionally enable live rendering in IB
    class ExampleView: UIView {
    
        required init(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            initializeSubviews()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            initializeSubviews()
        }
    
        func initializeSubviews() {
            // below doesn't work as returned class name is normally in project module scope
            /*let viewName = NSStringFromClass(self.classForCoder)*/
            let viewName = "ExampleView"
            let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                                   owner: self, options: nil)[0] as! UIView
            self.addSubview(view)
            view.frame = self.bounds
        }
    
    }
    

    这种方法的缺点是在视图层次结构中引入了一个额外的冗余层,当使用LeoNatan在Objective-C中概述的方法时,该层不存在。但是,这可能被视为一种必要的邪恶,是Xcode中设计事物的基本方式的产物(在我看来,很难将自定义UIView类与UI布局以一种在故事板和代码上都能一致工作的方式链接起来,这似乎很疯狂)–以前从未在初始值设定项中大量替换过self似乎是一种特别易于理解的方式,尽管每个视图基本上有两个视图类似乎也不太好。

    尽管如此,这种方法的一个令人高兴的结果是,我们不再需要在interface builder中将视图的自定义类设置为我们的类文件,以确保在分配给self时行为正确,因此,当发出loadNibNamed()时,对init(coder-aDecoder:NSCoder)的递归调用被中断(通过不在xib文件中设置自定义类,将调用普通UIView的init(coder-aDecoder:NSCoder),而不是我们的自定义版本)。

    即使我们不能直接对存储在xib中的视图进行类定制,我们仍然能够使用出口/操作等将视图的文件所有者设置为我们的自定义类后,将视图链接到我们的父UIView子类:

    在下面的视频中可以找到一段视频,演示如何使用这种方法一步一步地实现这种视图类。

    钱德元
    2023-03-14

    请注意,这个质量保证(像许多一样)实际上只是具有历史意义。

    (事实上,苹果终于在一段时间前添加了故事板参考,让它变得更加容易。)

    这是一个典型的故事板,到处都是容器视图。一切都是容器视图。这就是你制作应用程序的方式。

    (奇怪的是,KenC的回答正好说明了过去如何将xib加载到一种包装器视图中,因为您不能真正“分配给自己”。)

     类似资料:
    • 我读过使用XCode故事板来实例化使用XIB进行设计的视图控制器,但我在Swift(使用XCode 6 Beta 6)中遇到了困难。我想知道是我做错了什么,还是这个功能不再可用了? 我创建了一个简单的存储库https://github.com/jer-k/storyboardtesting-swift,它展示了上述方法。 我通过添加并重写到init来解决这个问题 但我想知道是否仍有可能让故事板为我

    • 问题内容: 在XCode 6中创建一个新项目不允许禁用Storyboard。您只能选择Swift或Objective-C,而不能使用Core Data。 我尝试删除情节提要,并从项目中删除主情节提要,然后从didFinishLaunching手动设置窗口 在AppDelegate中,我有以下内容: 但是,XCode给我一个错误: 类“ AppDelegate”没有初始化程序 有人成功吗? 问题答案

    • 问题内容: 我正在这样做,我想使用CollectionView,但我还没有看到原型单元,也不知道在这种情况下如何使用CollectionView,有人可以帮助我吗? 我尝试以这种方式使用,但是比UICollectionView要花很多时间并且很难管理 问题答案: 使用UICollectionView的主要方法是通过编程方式管理逻辑。 首先,创建一个继承自的新类。选择是否要包含xib来轻松设计单元格

    • 问题内容: 我有一个没有情节提要的应用程序,所有UI创建都是通过代码完成的,我得到了一个我可以使其在iPhone上使用的应用程序,因为该应用程序最初仅是为iPad设计的,因此当您在列表中选择一行时,大师认为,它在iPhone上什么也不做,但在iPad上工作正常。 所以我的问题是我可以创建并执行允许在方法上显示“详细视图”的segue 吗? 到目前为止,这是我所做的: 但是当运行并选择一行时,应用程

    • 问题内容: 我要执行以下操作: 我已经尝试过了,但是它似乎并不总是可以工作。它在一个上下文中可以工作,但是在另一个上下文中,相同的代码在第二个“ Class.forName(” MyClass“)”上崩溃了…… 总是调用带来正确的课堂,并尝试过,但没有区别。不知何故,在某些情况下,第二个Class.forName找到了该类,而在其他情况下,它只是坏了……我错过了什么吗? 问题答案: 我发现我的代码

    • 问题内容: 这是我的UIView类: 它应该只填满屏幕。当我在情节提要中添加具有固定到屏幕边缘的约束的视图时,启动应用程序时它将崩溃。如上面的代码所述,出现错误。 通过此init函数以编程方式创建和添加子类: 但是我希望它可以通过Storyboard重复使用,而无需添加代码。我的代码有什么问题,甚至有可能我想要什么? 谢谢! 问题答案: 启动应用程序时崩溃 因为那是您的代码 告诉 它要做的。 意思