How to load NSView from Xib with Swift 3

2020-03-28 03:05发布

问题:

How to load NSView from Xib properly?

My code:

var topLevelArray: NSArray? = nil
let outputValue = AutoreleasingUnsafeMutablePointer<NSArray>(&topLevelArray)

if Bundle.main.loadNibNamed("RadioPlayerView", owner: nil, topLevelObjects: outputValue) {
    let views = outputValue.pointee
    return views.firstObject as! RadioPlayerView
}

topLevelArray = nil
return nil

The problem is "outputValue" is a auto-release pointer, and as soon as I return from the function, the program crash with ACCESS_BAD_ADDRESS

回答1:

I made an protocol and extension to do this:

import Cocoa

protocol NibLoadable {
    static var nibName: String? { get }
    static func createFromNib(in bundle: Bundle) -> Self?
}

extension NibLoadable where Self: NSView {

    static var nibName: String? {
        return String(describing: Self.self)
    }

    static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? {
        guard let nibName = nibName else { return nil }
        var topLevelArray: NSArray? = nil
        bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray)
        guard let results = topLevelArray else { return nil }
        let views = Array<Any>(results).filter { $0 is Self }
        return views.last as? Self
    }
}

Usage:

final class MyView: NSView, NibLoadable {
    // ...
}

// create your xib called MyView.xib

// ... somewhere else:

let myView: MyView? = MyView.createFromNib()


回答2:

I solved this problem with a slightly different approach. Code in Swift 5.

If you want to create NSView loaded from .xib to e.g. addSubview and constraints from code, here is example:

public static func instantiateView<View: NSView>(for type: View.Type = View.self) -> View {
    let bundle = Bundle(for: type)
    let nibName = String(describing: type)

    guard bundle.path(forResource: nibName, ofType: "nib") != nil else {
        return View(frame: .zero)
    }

    var topLevelArray: NSArray?
    bundle.loadNibNamed(NSNib.Name(nibName), owner: nil, topLevelObjects: &topLevelArray)
    guard let results = topLevelArray as? [Any],
        let foundedView = results.last(where: {$0 is Self}),
        let view = foundedView as? View else {
            fatalError("NIB with name \"\(nibName)\" does not exist.")
    }
    return view
}

public func instantiateView() -> NSView {
    guard subviews.isEmpty else {
        return self
    }

    let loadedView = NSView.instantiateView(for: type(of: self))
    loadedView.frame = frame
    loadedView.autoresizingMask = autoresizingMask
    loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints

    loadedView.addConstraints(constraints.compactMap { ctr -> NSLayoutConstraint? in
        guard let srcFirstItem = ctr.firstItem as? NSView else {
            return nil
        }

        let dstFirstItem = srcFirstItem == self ? loadedView : srcFirstItem
        let srcSecondItem = ctr.secondItem as? NSView
        let dstSecondItem = srcSecondItem == self ? loadedView : srcSecondItem

        return NSLayoutConstraint(item: dstFirstItem,
                                  attribute: ctr.firstAttribute,
                                  relatedBy: ctr.relation,
                                  toItem: dstSecondItem,
                                  attribute: ctr.secondAttribute,
                                  multiplier: ctr.multiplier,
                                  constant: ctr.constant)
    })

    return loadedView
}

If there is no .xib file with the same name as the class name, then code will create class from code only. Very good solution (IMO) if someone wants to create the view from code and xib files in the same way, and keeps your code organized.

.xib file name and class name must have the same name:

In .xib file you should only have one view object, and this object has to have set class:

All you need to add in class code is instantiateView() in awakeAfter e.g.:

import Cocoa

internal class ExampleView: NSView {
   internal override func awakeAfter(using coder: NSCoder) -> Any? {
      return instantiateView() // You need to add this line to load view
   }

   internal override func awakeFromNib() {
      super.awakeFromNib()
      initialization()
   }
}

extension ExampleView {
   private func initialization() {
      // Preapre view after view did load (all IBOutlets are connected)
   }
}

To instantiate this view in e.g. ViewController you can create view like that:

let exampleView: ExampleView = .instantiateView() or

let exampleView: ExampleView = ExampleView.instantiateView()

but Swift have problems sometimes with instantiate like that:

let exampleView = ExampleView.instantiateView()

in viewDidLoad() in your controller you can add this view as subview:

internal override func viewDidLoad() {
    super.viewDidLoad()

    exampleView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(exampleView)
    NSLayoutConstraint.activate(
        [exampleView.topAnchor.constraint(equalTo: view.topAnchor),
         exampleView.leftAnchor.constraint(equalTo: view.leftAnchor),
         exampleView.rightAnchor.constraint(equalTo: view.rightAnchor),
         exampleView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]
    )
}