如何在Swift中使用XIB文件初始化/实例化自定义UIView类

164

我有一个名为MyClass的类,它是UIView的子类,我想用一个XIB文件来初始化它。我不确定如何使用名为View.xib的xib文件来初始化这个类。

class MyClass: UIView {

    // what should I do here? 
    //init(coder aDecoder: NSCoder) {} ?? 
}

5
请参考iOS9 Swift 2.0的完整源代码示例https://github.com/karthikprabhuA/CustomXIBSwift和相关线程https://dev59.com/7WAf5IYBdhLWcg3wQQu4#31957247。 - karthikPrabhu Alagu
11个回答

295

我测试了这段代码,它运行得非常好:

class MyClass: UIView {        
    class func instanceFromNib() -> UIView {
        return UINib(nibName: "nib file name", bundle: nil).instantiateWithOwner(nil, options: nil)[0] as UIView
    }    
}

初始化视图并按以下方式使用:

var view = MyClass.instanceFromNib()
self.view.addSubview(view)

或者

var view = MyClass.instanceFromNib
self.view.addSubview(view())

更新 Swift >=3.x 和 Swift >=4.x

class func instanceFromNib() -> UIView {
    return UINib(nibName: "nib file name", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIView
}

1
应该写成 var view = MyClass.instanceFromNib()self.view.addSubview(view),而不是 var view = MyClass.instanceFromNibself.view.addSubview(view())。这只是一个小建议,以改进答案 :) - Logan
1
在我的情况下,视图稍后初始化!如果是self.view.addSubview(view),那么view应该是var view = MyClass.instanceFromNib()。 - Ezimet
2
@Ezimet 那么关于视图内部的 IBActions 呢?在哪里处理它们?比如我有一个按钮在我的视图(xib)中,如何处理该按钮的 IBAction 点击事件呢? - Qadir Hussain
9
应该返回"MyClass"而不仅仅是UIView吗? - Kesong Xie
10
IBOutlets在这种方法中无法工作...我收到了以下错误信息:"此类未对键执行值编码兼容操作" - Radek Wilczak
显示剩余7条评论

91

尽管Sam的解决方案没有考虑不同的bundle(使用NSBundle:forClass可以帮助解决),并且需要手动加载,也就是输入代码,但它已经很出色了。

如果你想要完全支持你的Xib Outlets、不同的Bundle(在框架中使用!)并在Storyboard中获得良好的预览,请尝试这个:

// NibLoadingView.swift
import UIKit

/* Usage: 
- Subclass your UIView from NibLoadView to automatically load an Xib with the same name as your class
- Set the class name to File's Owner in the Xib file
*/

@IBDesignable
class NibLoadingView: UIView {

    @IBOutlet weak var view: UIView!

    override init(frame: CGRect) {
        super.init(frame: frame)
        nibSetup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        nibSetup()
    }

    private func nibSetup() {
        backgroundColor = .clearColor()

        view = loadViewFromNib()
        view.frame = bounds
        view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        view.translatesAutoresizingMaskIntoConstraints = true

        addSubview(view)
    }

    private func loadViewFromNib() -> UIView {
        let bundle = NSBundle(forClass: self.dynamicType)
        let nib = UINib(nibName: String(self.dynamicType), bundle: bundle)
        let nibView = nib.instantiateWithOwner(self, options: nil).first as! UIView

        return nibView
    }

}

按照惯例使用你的xib,即将Outlets连接到文件所有者并将文件所有者类设置为你自己的类。

用法:只需从NibLoadingView子类化自己的视图类,并在Xib文件中将类名设置为File's Owner

不再需要额外的代码。

功劳归于功臣:从DenHeadless的GH上修改了此分支。 我的Gist:https://gist.github.com/winkelsdorf/16c481f274134718946328b6e2c9a4d8


9
这个解决方案会打破插座连接(因为它将已加载的视图添加为子视图),如果将NibLoadingView嵌入到XIB中并从init?(coder:)中调用nibSetup,会导致无限递归。 - redent84
3
感谢您的评论和投票。如果您再看一遍,它应该替换���前的SubView(没有新的实例变量被赋值)。您对无限递归的问题是正确的,如果在IB中遇到这个问题,应该省略它。 - Frederik Winkelsdorf
1
如上所述,“将插座连接到文件所有者并将文件所有者类设置为您自己的类。” 将插座连接到文件所有者。 - Yusuf X
2
我总是对使用从xib加载视图的方法感到不舒服。我们基本上是在一个也是Class A子类的视图中添加了一个Class A的子类视图。难道没有什么方法可以防止这种迭代吗? - Prajeet Shrestha
2
@PrajeetShrestha 这可能是由于 nibSetup() 覆盖了从 Storyboard 加载的背景颜色为 .clearColor()。但如果在实例化后通过代码执行,它应该可以工作。无论如何,正如所说的,更优雅的方法是基于协议的方法。所以,这里有一个链接供您参考:https://github.com/AliSoftware/Reusable。我现在正在使用类似的方法处理 UITableViewCells(在我发现那个非常有用的项目之前实现的)。希望对你有帮助! - Frederik Winkelsdorf
显示剩余5条评论

78

从Swift 2.0开始,你可以添加协议扩展。在我看来,这是一个更好的方法,因为返回类型是Self而不是UIView,所以调用者不需要转换为视图类。

import UIKit

protocol UIViewLoading {}
extension UIView : UIViewLoading {}

extension UIViewLoading where Self : UIView {

  // note that this method returns an instance of type `Self`, rather than UIView
  static func loadFromNib() -> Self {
    let nibName = "\(self)".characters.split{$0 == "."}.map(String.init).last!
    let nib = UINib(nibName: nibName, bundle: nil)
    return nib.instantiateWithOwner(self, options: nil).first as! Self
  }

}

4
这个方案比被选中的答案更好,因为它不需要强制转换,并且在将来创建任何其他 UIView 子类时也可以重复使用。 - user3344977
3
我尝试使用Swift 2.1和Xcode 7.2.1。有时它可以工作,但在通过互斥锁的其他情况下会出现无法预测的挂起。直接在代码中使用最后两行代码,将最后一行修改为var myView = nib.instantiate... as! myViewType,每次都可以正常工作。 - Paul Linsay
1
@jr.root.cs 是的,这个答案很好,所以没有人应该修改它。这是Sam的答案,不是你的。如果你想评论它,请留下评论;如果你想发布新的/更新的版本,请在自己的帖子中进行。编辑旨在修复拼写错误/缩进/标签,而不是在其他人的帖子中添加您的版本。谢谢。 - Eric Aya
1
你也可以使用 let nibName = String(describing: Self.self)(Swift 3)来获取类的名称。 - OliverD
我总是使用这种方法并且没有问题地添加约束。请检查您是否已激活约束,或尝试使用开源约束简化框架,例如https://github.com/theappbusiness/TABSwiftLayout。 - Sam
显示剩余3条评论

42

这是Frederik对Swift 3.0的回答。

/*
 Usage:
 - make your CustomeView class and inherit from this one
 - in your Xib file make the file owner is your CustomeView class
 - *Important* the root view in your Xib file must be of type UIView
 - link all outlets to the file owner
 */
@IBDesignable
class NibLoadingView: UIView {

    @IBOutlet weak var view: UIView!

    override init(frame: CGRect) {
        super.init(frame: frame)
        nibSetup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        nibSetup()
    }

    private func nibSetup() {
        backgroundColor = .clear

        view = loadViewFromNib()
        view.frame = bounds
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.translatesAutoresizingMaskIntoConstraints = true

        addSubview(view)
    }

    private func loadViewFromNib() -> UIView {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
        let nibView = nib.instantiate(withOwner: self, options: nil).first as! UIView

        return nibView
    }
}

一行代码拯救了我的一天,那就是 view.translatesAutoresizingMaskIntoConstraints = true。谢谢 :) - Parth Patel

33

Swift 4

在我的情况下,我需要将数据传递到自定义视图中,因此我创建了一个静态函数来实例化该视图。

  1. Create UIView extension

    extension UIView {
        class func initFromNib<T: UIView>() -> T {
            return Bundle.main.loadNibNamed(String(describing: self), owner: nil, options: nil)?[0] as! T
        }
    }
    
  2. Create MyCustomView

    class MyCustomView: UIView {
    
        @IBOutlet weak var messageLabel: UILabel!
    
        static func instantiate(message: String) -> MyCustomView {
            let view: MyCustomView = initFromNib()
            view.messageLabel.text = message
            return view
        }
    }
    
  3. Set custom class to MyCustomView in .xib file. Connect outlet if necessary as usual. enter image description here

  4. Instantiate view

    let view = MyCustomView.instantiate(message: "Hello World.")
    

如果自定义视图中有按钮,我们如何在不同的视图控制器中处理它们的操作? - jayant rawat
你可以使用协议委托。在这里看一下 https://dev59.com/wYnda4cB1Zd3GeqPBqCv#29603255。 - axunic
在xib中加载图像失败,出现以下错误。无法从标识符为“test.Testing”的包中加载nib引用的“_IBBrokenImage_”图像。 - Ravi Raja Jangid

33

从xib加载视图的通用方法:

示例:

let myView = Bundle.loadView(fromNib: "MyView", withType: MyView.self)

实现:

extension Bundle {

    static func loadView<T>(fromNib name: String, withType type: T.Type) -> T {
        if let view = Bundle.main.loadNibNamed(name, owner: nil, options: nil)?.first as? T {
            return view
        }

        fatalError("Could not load view with type " + String(describing: type))
    }
}

将输出视图的类型作为UIView子类的最佳答案。 - jfgrang
这是Bundle的扩展,但它假定始终从主Bundle加载。在单元测试中无法使用。 - Marcin Kapusta

30

Swift 3 的解答: 在我的情况下,我想在自定义类中拥有一个可以修改的 outlet:

class MyClassView: UIView {
    @IBOutlet weak var myLabel: UILabel!
    
    class func createMyClassView() -> MyClassView {
        let myClassNib = UINib(nibName: "MyClass", bundle: nil)
        return myClassNib.instantiate(withOwner: nil, options: nil)[0] as! MyClassView
    }
}

在.xib文件中,请确保Custom Class字段为MyClassView。不要担心File's Owner。

确保Custom Class是MyClassView

另外,请确保将MyClassView中的outlet连接到标签:

myLabel的出口

实例化:

let myClassView = MyClassView.createMyClassView()
myClassView.myLabel.text = "Hello World!"

1
如果所有者未设置,则返回“已加载“MyClas”nib,但视图插座未设置。” - jcharch

6

Swift 5.3

创建一个名为NibLoadingView的类,包含以下内容:

import UIKit

/* Usage:
- Subclass your UIView from NibLoadView to automatically load an Xib with the same name as your class
- Set the class name to File's Owner in the Xib file
*/

@IBDesignable
class NibLoadingView: UIView {

    @IBOutlet public weak var view: UIView!
    
    private var didLoad: Bool = false

    public override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.nibSetup()
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        self.nibSetup()
    }
    
    open override func layoutSubviews() {
        super.layoutSubviews()
        
        if !self.didLoad {
            self.didLoad = true
            self.viewDidLoad()
        }
    }
    
    open func viewDidLoad() {
        self.setupUI()
    }

    private func nibSetup() {
        self.view = self.loadViewFromNib()
        self.view.frame = bounds
        self.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.view.translatesAutoresizingMaskIntoConstraints = true

        addSubview(self.view)
    }

    private func loadViewFromNib() -> UIView {
        guard let nibName = type(of: self).description().components(separatedBy: ".").last else {
            fatalError("Bad nib name")
        }
        
        if let defaultBundleView = UINib(nibName: nibName, bundle: Bundle(for: type(of: self))).instantiate(withOwner: self, options: nil).first as? UIView {
            return defaultBundleView
        } else {
            fatalError("Cannot load view from bundle")
        }
    }
}

现在创建一个XIB和UIView类对,将XIB的所有者设置为UIView类并且作为“NibLoadingView”的子类。
现在您可以像使用“ExampleView()”,“ExampleView(frame: CGRect)”等一样初始化该类,也可以直接从故事板中使用。
您还可以像在“UIViewController”中一样使用“viewDidLoad”。此时,所有插座和布局都已呈现。
基于Frederik's answer

此代码的关键点是:view.translatesAutoresizingMaskIntoConstraints = true。 - Yogesh Patel

2

从.xib文件创建视图

let nib = UINib(nibName: "View1", bundle: nil) //View1 is a file name(View1.swift)
if let view = nib.instantiate(withOwner: self, options: nil).first as? UIView {
    // logic
}
//or
if let view = Bundle.main.loadNibNamed("View1", owner: self, options: nil)?.first as? UIView {
    // logic
}

由于.xib文件可以包含多个视图,所以您在这里使用数组(.first)。

例如

  1. 创建View1.xib
  2. 创建View1.swift,在代码中设置所有者(loadNibNamed())来创建类("View1")的实例
  3. File's OwnerView1.xib中设置为View1,允许连接插座和操作
import UIKit

class View1: UIView {
    @IBOutlet var contentView: UIView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.commonInit()
    }
    
    private func commonInit() {
        if let view = Bundle.main.loadNibNamed("View1", owner: self, options: nil)?.first as? UIView {
            addSubview(view)
            view.frame = self.bounds
        }
    }
}

注意事项:如果我们将Custom ClassFile's owner移动到Container View,则会出现错误(循环)。这是因为:

系统从Container View初始化实例,在commonInit()中再次初始化它。

.loadNibNamed

Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ff7bf6fbfc8)

2
如果有人想要以编程方式加载自定义视图和XIB,则以下代码可以完成此工作。
let customView = UINib(nibName:"CustomView",bundle:.main).instantiate(withOwner: nil, options: nil).first as! UIView
customView.frame = self.view.bounds
self.view.addSubview(customView)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接