ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

类中的插座允许我设置/获取视图的属性

我目前的做法是:

覆盖 -(id)initWithFrame: -(id)initWithFrame:(CGRect)frame { self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] objectAtIndex:0]; self.frame = 框架;回归自我;以编程方式使用 -(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 的内容设置对象似乎有点可疑 - 这里有对 add a subview in this case 的建议,它也可以正常工作)。但是,我也希望能够从情节提要中实例化视图。所以我可以:

在情节提要的父视图上放置一个 UIView 将其自定义类设置为 MyCustomView Override -(id)initWithCoder: - 我见过的最常符合以下模式的代码:-(id)initWithCoder:(NSCoder * )aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self initializeSubviews]; } 返回自我; } -(id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initializeSubviews]; } 返回自我; } -(void)initializeSubviews { typeof(view) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] objectAtIndex:0]; [自我添加子视图:视图]; }

当然,这不起作用,因为无论我使用上述方法,还是以编程方式进行实例化,两者最终都会在输入 -(void)initializeSubviews 并从文件加载 nib 时递归调用 -(id)initWithCoder:

其他几个 SO 问题处理此问题,例如 herehereherehere。但是,给出的答案都不能令人满意地解决问题:

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

任何人都可以就如何解决此问题提供建议,并在自定义 UIView 中获得工作插座,而无需大惊小怪/没有瘦控制器包装器?或者是否有另一种更简洁的方式来使用最少的样板代码?

你有没有得到满意的答案?我目前正在为此而苦苦挣扎。正如您所提到的,所有其他答案似乎都不够好。如果您在过去几个月中发现了什么,您总是可以自己回答这个问题。
为什么在 iOS 中创建可重用视图如此困难?
实际上,您链接到的答案使用完全相同的方法(尽管您的答案不包括 rect 函数的 init,这意味着它只能从情节提要中初始化,而不能以编程方式初始化)
关于这个非常古老的 QA,Apple 终于推出了 STORYBOARD REFERENCES ... developer.apple.com/library/ios/recipes/… ... 就是这样,唷!

C
Community

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

如今 多年来,iOS 中的一切都只是一个容器视图。 Full tutorial here

(事实上,Apple 终于在不久前添加了 Storyboard References,使其变得更加容易。)

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

https://i.stack.imgur.com/CdGH7.png

(出于好奇,KenC 的回答准确地显示了过去如何将 xib 加载到一种包装器视图中,因为您不能真正“分配给自己”。)


这样做的问题是,您最终会为所有嵌入的内容视图提供大量 ViewController。
嗨@BogdanOnu!你应该有很多很多的视图控制器。对于“最小”的东西——你应该有一个视图控制器。
这非常有用——感谢@JoeBlow。使用容器视图显然是一种替代方法,也是一种避免直接处理 xib 的所有复杂性的简单方法。然而,创建可重用组件以供跨项目分发/使用似乎并不是 100% 令人满意的替代方案,因为它需要将所有 UI 设计直接嵌入到情节提要中。
在这种情况下,我对使用额外的 ViewController 没有什么问题,因为它只包含在 xib 案例中属于自定义 View 类的程序逻辑,但是与情节提要的紧密耦合意味着我不确定容器视图能否完全解决这个问题。也许在大多数实际情况下,视图是特定于项目的,因此这是最好和最“标准”的解决方案,但我很惊讶仍然没有简单的方法来打包自定义视图以供故事板使用。我的程序员分而治之的冲动是令人发痒的;)
此外,随着在 Xcode 6 中引入自定义 UIView 子类的实时渲染,我不确定我是否相信以这种方式使用 xibs 创建视图的前提现在已被弃用
S
Suragch

我将此作为单独的帖子添加,以更新 Swift 发布的情况。 LeoNatan 描述的方法在 Objective-C 中完美运行。但是,在 Swift 中从 xib 文件加载时,更严格的编译时检查会阻止分配 self

因此,没有选择,只能将从 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 之前似乎从来都不是一种特别可解释的做事方式,尽管每个视图基本上有两个视图类似乎也不是很好。

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

即使我们不能直接对存储在 xib 中的视图进行类自定义,在将视图的文件所有者设置为我们的自定义类之后,我们仍然可以使用 outlets/actions 等将视图链接到我们的“父”UIView 子类:

https://i.stack.imgur.com/9p0ib.png

可以找到演示使用此方法逐步实现此类视图类的视频in the following video


嗨,乔——感谢您的评论,这很大胆(就像您的许多其他评论一样!)正如我在回复您的回答时所说,我同意在大多数情况下容器视图可能是最好的方法,但在这些情况下在必须跨项目(或分布式)使用视图的情况下,至少对我来说确实有意义,而且对其他人来说似乎也有替代方案。您可能个人认为这是不好的风格,但也许您可以让这里的许多其他帖子建议如何做到这一点以供参考,并让人们自己判断。这两种方法似乎都很有用。
谢谢你。我在 Swift 中尝试了许多不同的方法,但没有成功,直到我听取了您关于将 nib 的类保留为 UIView 的建议。我同意苹果从来没有让这件事变得简单是疯狂的,现在这几乎是不可能的。容器并不总是答案。
r
rintaro

步骤1。从情节提要中替换自我

initWithCoder: 方法中替换 self 将失败并出现以下错误。

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

相反,您可以将解码的对象替换为 awakeAfterUsingCoder:(不是 awakeFromNib)。喜欢:

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

第2步。防止递归调用

当然,这也会导致递归调用问题。 (情节提要解码 -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...)
因此您必须检查情节提要中是否调用了当前 awakeAfterUsingCoder:解码过程或XIB解码过程。你有几种方法可以做到这一点:

a) 使用仅在 NIB 中设置的私有 @property。

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

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

优点:

没有任何

缺点:

根本不起作用: setXib: 将在 awakeAfterUsingCoder 之后调用:

b) 检查 self 是否有任何子视图

通常,您在 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];
    }
}

优点:

Interface Builder 中没有任何技巧。

缺点:

故事板中不能有子视图。

c) 在 loadNibNamed 期间设置静态标志:调用

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;
    }
}

优点:

简单的

Interface Builder 中没有任何技巧。

缺点:

不安全:静态共享标志很危险

d) 在 XIB 中使用私有子类

例如,将 _NIB_MyCustomView 声明为 MyCustomView 的子类。并且,仅在您的 XIB 中使用 _NIB_MyCustomView 而不是 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 中没有明确的 if

缺点:

在 xib Interface Builder 中添加前缀 _NIB_ 技巧

相对较多的代码

e) 在情节提要中使用子类作为占位符

d) 类似,但在 Storyboard 中使用子类,在 XIB 中使用原始类。

在这里,我们将 MyCustomViewProto 声明为 MyCustomView 的子类。

@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 中没有额外的代码。

没有明确的 if 检查与 d)

缺点:

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

我认为 e) 是最安全和最干净的策略。所以我们在这里采用它。

第 3 步。复制属性

在 'awakeAfterUsingCoder:' 中的 loadNibNamed: 之后,您必须从 self 复制几个属性,这些属性是 Storyboard 的解码实例。 frame 和 autolayout/autoresize 属性尤其重要。

- (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+loadFromNib 代码。

#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

西布:

https://i.stack.imgur.com/2dpTM.png

故事板:

https://i.stack.imgur.com/8FGiy.png

结果:

https://i.stack.imgur.com/JOl5G.png


解决方案比最初的问题更复杂。要停止递归循环,您只需设置 File's Owner 对象,而不是将内容视图声明为 MyCustomView 类类型。
这只是 a) 简单的初始化过程但复杂的视图层次结构和 b) 复杂的初始化过程但简单的视图层次结构的权衡。 n'est-ce过去了吗? ;)
这个项目有下载链接吗?
L
Léo Natan

您的问题是从 initWithCoder:(的后代)调用 loadNibNamed:loadNibNamed: 在内部调用 initWithCoder:。如果您想覆盖情节提要编码器,并始终加载您的 xib 实现,我建议使用以下技术。将一个属性添加到您的视图类,并在 xib 文件中,将其设置为预定值(在用户定义的运行时属性中)。现在,在调用 [super initWithCoder:aDecoder]; 后检查属性的值。如果是预定值,不要调用[self initializeSubviews];

所以,像这样:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

谢谢@LeoNatan!我接受这个答案,因为它是最初陈述的问题的最佳解决方案。但是,请注意,在 Swift 中不再可能 - 我已经添加了一些单独的注释,说明在这种情况下可能的解决方法。
@KenChatfield 我在我的 Swift 子项目中注意到了这一点,并对此感到恼火。我不确定他们在想什么,因为没有这个,很多 Cocoa/Cocoa Touch 内部实现在 Swift 中是不可能的。我敢打赌,当他们真正有时间专注于功能而不是错误时,将会有一些动态功能。 Swift 根本没有准备好,最糟糕的是开发工具。
这不是 hack,而是类集群的工作方式。实际上,允许返回作为返回类的子类的对象的返回并不存在技术问题。事实上,Cocoa 和 Cocoa Touch 的基石之一——类集群——是不可能实现的。糟糕的。一些框架,比如 Core Data,不能在 Swift 中实现,这使得它在我的大部分使用中都是无用的语言。
不知何故,它对我不起作用(iOS8.1 SDK)。我在 XIB 中设置了一个 restoreIdentifier 而不是运行时属性,而不是它的工作。例如,我在 xib 中设置了“MyViewRestorationID”,而不是在 initWithCoder 中:我检查了 ![[self restoreIdentifier] isEqualToString:@"MyViewRestorationID"]
这至少在 iOS 10 上不起作用,因为“用户定义的运行时属性”的绑定在 -initWIthCoder: 完成之前不会发生。
C
Community

不要忘记

两个重要的点:

将 .xib 的文件所有者设置为自定义视图的类名。不要在 IB 中为 .xib 的根视图设置自定义类名。

在学习制作可重用视图的过程中,我多次访问此问答页面。忘记以上几点让我浪费了很多时间试图找出导致无限递归发生的原因。此处的其他答案和elsewhere中提到了这些要点,但我只想在这里再次强调它们。

我的完整 Swift 步骤答案是 here


这就是我的答案,我已经设置了 BOTH - 你只需要文件所有者,如果你将视图的主要内容设置为自定义类,它会创建一个无限循环
B
Balazs Nemeth

有一个比上述解决方案更清洁的解决方案:https://www.youtube.com/watch?v=xP7YvdlnHfA

没有运行时属性,完全没有递归调用问题。我尝试了它,它使用来自故事板和来自具有 IBOutlet 属性(iOS8.1,XCode6)的 XIB 就像一个魅力。

祝你编码好运!


谢谢@ingaham!但是,视频中概述的方法与原始问题中提出的第二种解决方案相同(我在上面的答案中提供了 Swift 代码)。在这两种情况下,它涉及向包装 UIView 子类添加子视图,这就是为什么递归调用没有问题,也不需要依赖运行时属性或其他任何复杂的东西。如前所述,缺点是需要将多余的附加子视图添加到自定义 UIView 类中。正如所讨论的,这可能是目前最好和最简单的解决方案。
是的,你完全正确,这些是相同的解决方案。但是,冗余视图是必要的,但它是最干净且易于维护的解决方案。所以我决定用这个。
我相信冗余视图是完全自然的,永远不会有其他解决方案。请注意,您是在说“'某物'会去'这里'”......“这里”是自然存在的东西。那里必须有“东西”,一个“你要放置东西的地方”——这就是“视图”的定义。等等!事实上,Apple 的“容器视图”的东西正是……有一个“框架”,一个“持有者视图”(“容器视图”),你可以将某些东西放入其中。事实上,“冗余”视图解决方案正是手工制作的容器视图!只用苹果的。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅