从xib加载视图的正确方法是啥?

Posted

技术标签:

【中文标题】从xib加载视图的正确方法是啥?【英文标题】:What is the correct way to load view from xib?从xib加载视图的正确方法是什么? 【发布时间】:2018-03-25 17:39:28 【问题描述】:

到目前为止,我发现的所有资源都建议使用此代码的变体从xib 文件加载视图,但始终实现相同的逻辑:

required init?(coder aDecoder: NSCoder) 
    super.init(coder: aDecoder)
    var view = (Bundle.main.loadNibNamed("MyCustomView", owner: self, options: nil)![0])
    self.addSubview(view as! UIView)

这对我来说看起来不正确,因为该函数属于 UIView 的子类,并将预期设计(xib 的设计)添加为子视图。在这种情况下,如果我们向self 添加了不同的 UI 元素,那么它与xib 文件中的 UI 元素属于不同的父元素。

所以层次结构看起来像这样:

---self (UIView)  
    ---Other UI elements added by code in `self`
    ---xib (UIView)
        ---UI elements in xib file

这不是不必要的包装吗?如果是这样,有没有办法将xib文件中的所有视图直接添加到self?还是有其他更优雅的方式从xib 文件加载设计?

【问题讨论】:

我觉得奇怪的是,你找到的“所有资源”都是这样。有两件事让我觉得不寻常:首先,笔尖是从init(NSCoder) 构造函数中加载的。此构造函数通常在从 nib(或故事板)加载时使用;换句话说,从另一个 nib 加载时加载辅助 nib 很奇怪——一个 nib 通常包含 整个 视图层次结构。其次,owner 通常是一个视图控制器,而不是另一个视图。 @JamesBucanek 是的,这实际上很奇怪。我发现可以将self 分配给从objective-c 的笔尖加载的view,这就是人们过去这样做的方式。对于您强调的​​第二点,这是真的,但我的目的是创建一个可重用的视图,我可以将其添加到 inside 其他视图中。当您从viewcontroller 加载笔尖时,您无法将该视图添加到另一个视图中,它有自己的屏幕。我不明白这是一个如何被忽视的问题:整个viewcontroller 实践基于 MVC 模型,但同时创建可重用对象很乏味 这不是真的。您可以拥有任意数量的视图控制器,只要您喜欢/需要。视图控制器可用于管理视图中的任意视图子集,并且非常适合在不同的视图层次结构中重用 MVC 模式。我会用答案来说明... 【参考方案1】:

重用一组复杂的子视图的一种方法是定义一个嵌入式视图控制器。您首先定义您的***视图控制器。在其中,您定义了一个插座并将其连接到您的子控制器的一个实例(也在 nib 中定义)。您还将子控制器的 view 连接到*** nib 视图层次结构中的占位符 UIView

class ViewController: UIViewController 

    @IBOutlet var childController: ReusableViewController?

    override func viewDidLoad() 
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    

在子控制器中发生了手误。它的awakeFromNib 函数在加载超级控制器时被调用。然后,子控制器使用它所连接的“占位符”UIView 将其视图层次结构插入到***视图中。

class ReusableViewController: UIViewController 

    override func viewDidLoad() 
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    

    override func awakeFromNib() 
        super.awakeFromNib()
        // This controller manages a reusable view defined by a seperate nib.
        // When defined in the parent nib, its view is connected to a placeholder view object.
        // When the nib is loaded, the secondary nib is loaded and replaces the placeholder.
        let placeholder = self.view!
        Bundle.main.loadNibNamed("ReusableViewController", owner: self, options: nil)
        // (the nib connects the view property to the actual view, replacing the placeholder)
        placeholder.superview?.insertSubview(self.view, aboveSubview: placeholder)
        // (do something about constraints here?)
        placeholder.removeFromSuperview()
    


这种安排的优点是子控制器可以拥有它需要的任何绑定、出口、动作、连接、属性和业务逻辑,并且可以被整齐地封装和重用。

这有点小技巧,因为它缩短了子控制器的 view-did-load 生命周期(因为控制器从不加载自己的视图)。为了解决这个问题,您可以定义另一个属性,将子控制器指向一个占位符视图或应该插入自身的容器视图。然后将 Bundle.main.loadNibName(blah, blah, blah) 替换为 let replacement = self.view 并让视图控制器完成所有加载。


这是一个完成的解决方案,使用原始发布者提供的故事板在完成此工作后

在故事板中使用可重用视图 添加一个Container View(不要与View混淆)到ViewController(故事板上的主视图),并将它的场景(控制器)类设置为我们刚刚定义的控制器(ReusableViewController)。@ 987654323@ 就是这样。通过这种方式,您可以将尽可能多的子视图添加到视图中,而无需将子视图包装在情节提要和代码中。Container View 的预期用途实际上被记录为正是为此目的here

【讨论】:

很抱歉,我已阅读但无法测试您的答案。我很高兴尝试这个,看起来很有希望。我有一个非常忙碌的一周,所以我会尽快将其标记为答案。可能在几天内,甚至可能在今天 这正是我想要的,谢谢。不过,我想补充一点,如果我可以在您的答案中添加如何将这个实现与故事板一起使用,我认为这将是一个完整的解决方案,但是由于我没有在问题中询问故事板,所以我不确定它是否将是适当的。你怎么看?我应该建议编辑,还是应该先编辑问题,或者甚至将情节提要实现添加到问题本身? 使用故事板不会有很大的变化。只需在情节提要中定义可重用视图控制器和子视图,为其分配一个标识符,然后将 Bundle.main.loadNibNamed(... 代码替换为 storyboard.instantiateViewController(identifier:... (我想了想,回想起来,它可能并不那么简单。但我认为一般原则仍然有效。如果你卡住了再写,我会尝试排序出故事板细节。) 其实我的意思是我知道答案:)。它实际上要简单得多:添加一个Container View(不要与View 混淆)并将它的view controller 设置为我们刚刚定义的控制器(ReusableViewController)。就是这样。 View 容器的预期用途实际上就是这样记录的 (developer.apple.com/library/content/featuredarticles/…),这很奇怪,因为即使 Apple 的可重用视图示例文档也根本没有提到它。如果你愿意,我可以建议编辑来解释故事板部分【参考方案2】:

通常,当您为视图实现 xib 时,它总是包含它所需的所有元素。但如果你有你可以尝试添加这样的元素

var view = (Bundle.main.loadNibNamed("MyCustomView", owner: self, options: nil)![0]) as! UIView

var lbl = UILabel()

lbl.frame  = //.....

view.addSubview(lbl)

self.addSubview(view)

【讨论】:

也许可以将新的 UI 元素添加到从 xib 文件加载的子视图中,但这不会在缩放时引起额外的努力,因为每个开发人员都可能使用不同的层次结构?我的意思是,建议的方式通常看起来像是不必要的包装。我想将您的答案标记为正确,因为您提供了有效的实现,但它并不能完全解决问题。不过,如果没有其他人想出更好的实现,我会直接将视图加载到 self【参考方案3】:

您可以在初始化期间加载视图的 xib:

init() 
    super.init(nibName: "ViewController", bundle: Bundle(identifier: "domain.AppName")!)
    self.loadView()

这就是你要找的吗?

【讨论】:

您使用的是哪个版本的 swift?当我尝试您的答案时,我收到以下错误:Incorrect argument labels in call (have 'nibName:bundle:', expected 'frame:options:') 您在ViewController 中使用它吗?如果是这样,那不是我的意图。我使用了在UIView 的类中实现的init 函数,而不是它的控制器。否则视图将无法重用【参考方案4】:

这是您一直想要的答案。您可以只创建您的CustomView 类,将它的主实例放在带有所有子视图和插座的xib 中。然后,您可以将该类应用于情节提要或其他 xib 中的任何实例。

无需摆弄 File's Owner,或将 outlet 连接到代理或以特殊方式修改 xib,或添加自定义视图的实例作为其自身的子视图。

这样做:

    导入 BFWControls 框架 将您的超类从UIView 更改为NibView(或从UITableViewCell 更改为NibTableViewCell

就是这样!

它甚至可以与 IBDesignable 一起使用,以便在设计时在情节提要中引用您的自定义视图(包括来自 xib 的子视图)。

您可以在此处阅读有关它的更多信息: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

您可以在此处获取开源 BFWControls 框架: https://github.com/BareFeetWare/BFWControls

下面是驱动它的NibReplaceable 代码的简单摘录,如果你好奇的话:https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

汤姆?

【讨论】:

以上是关于从xib加载视图的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确子类化从 Swift 中的 xib 加载的视图类?

从 XIB 加载的 UITableView Sectionheader 有时显示不正确

从 XIb 加载视图崩溃

在 UIScrollView 中未正确加载 XIB

从 XIB 加载 UIView 大小不正确

从 xib 加载视图控制器返回错误的帧