简体   繁体   中英

How to load NSView from Xib with Swift 3

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

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()

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

If you want to create NSView loaded from .xib to eg 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:

Xib 文件名和类名必须同名

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 eg:

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 eg 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)]
    )
}

// create your xib similar with classname and boom !

import Cocoa

class CustomView: NSView {
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
    }
    override init(frame : NSRect) {
        super.init(frame: frame)
    }
    
    required init?(coder : NSCoder) {
        super.init(coder: coder)
        setup()
        
        self.wantsLayer = true
        self.layer?.backgroundColor = NSColor.red.cgColor
    }
    
    
    private func setup() {
        let bundle = Bundle(for: type(of: self))
        let nib = NSNib(nibNamed: .init(String(describing: type(of: self))), bundle: bundle)!
        nib.instantiate(withOwner: self, topLevelObjects: nil)
    }
    
    
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM