如何在 Interface Builder 中使用自定义 NSView?

How to use custom NSView in Interface Builder?

我看过很多关于如何在 Interface Builder 中创建自定义视图的教程。所有都是 iOS 但 MacOS 应该相似,不是吗?我尝试了几种方法,但 none 完全成功。 init(coder:) 调用 NIB 实例化(通过 Bundle.main.loadNibNamedNSNib 依次调用 init(coder:) 并以无限递归结束,如果我 class 主要在我的笔尖中查看我的习惯 class

如果我使用标准 class 则将文件所有者设为我的自定义 class 效果更好但仍然不正确。

是否有使用 AppKit 创建自定义控件的有效示例?我最接近的显示自定义控件,但自动布局设置的 none 有效。

一定很简单,但我还没想好。

这是我目前的情况:

  1. 一个新的 class MyControl


import Cocoa</p>

<p>@IBDesignable
class MyControl: NSView {</p>

<pre><code>@IBOutlet var customView: NSView!  // The top level NSView
@IBOutlet weak var insideButton: NSButton!  // The button inside the view

let myName: String

required init?(coder: NSCoder) {
    super.init(coder: coder)

    if Bundle.main.loadNibNamed("MyControl", owner: self, topLevelObjects: nil) {
        addSubview(customView)
    }
}

}

  1. 基于 NSView 的 nib 包含一个居中的 NSButton。 File's Owner class 设置为 MyControl,顶层视图保持为 NSView

  2. Main.storyboard 有一个自定义视图 class 编辑为 MyControl 居中并设置了高度和宽度。

当我查看 Main.storyboard 时,它有一个自定义视图的框架,但它是空白的。

当我运行应用程序时window显示的是空白。

经过大量搜索和 Apple 支持的大量帮助,我发现在 AppKit 中创建和使用自定义控件非常容易。只是它就像一把开锁的钥匙,除非你把它弄对了,否则你什么也得不到。

我创建了一个示例项目并将其发布到 GitHub 此处:https://github.com/ctgreybeard/SwiftCustomControl

这是一个小项目,我希望我已经对它进行了充分的评论,以便其他人能够理解它。

过程的要点是这样的:

  1. 在 Interface Builder 中创建一个 XIB 和一个 NSView 的子class。它们应该是相同的名称,但这不是必需的。
  2. 对于 XIB,将 File's Owner 的 class 更改为您的新 class 名称。
  3. 按照您的意愿构建新的自定义控件。
  4. 在您的 class 中包含一个 IBOutlet,引用 XIB 中的顶级 NSView。还包括您的控件需要的任何其他操作或插座。
  5. 创建初始值设定项required init?(coder: coder)
  6. 在该初始化程序中:
    1. 使用 let newNib = NSNib(nibNamed: myName, bundle: Bundle(for: type(of: self))) 加载笔尖,其中 myName 是 XIB 的名称。
    2. newNib.instantiate(withOwner: self, topLevelObjects: nil) 新的 NSNib
    3. 从旧的顶级 NSView 中重新创建所有现有约束,用 self 替换旧的 NSView。在 constraints 属性 的 for 循环中执行此操作。或者,您可以简单地创建您所知道的约束。
    4. self.addSubview for all the old top-level subviews 这很容易在旧 NSView 中的 subviews 数组的 for 循环中完成。
    5. 应用您在上面创建的新约束数组。

大功告成...自定义控件现在应该正确显示在 Interface Builder 和应用程序中。

评论:这个,虽然很简单,但真的不应该有这个必要。我应该能够在 XIB 的顶级 NSView 中简单地使用我的自定义 class 名称并完成它。 init 中的代码只是将顶级 NSView 替换为我们的自定义视图。

你必须先在你的 xib 中添加约束,否则它不会工作。我有同样的问题。代码只是重新创建这些约束,但如果它们不是一开始就添加的,那么它就没有什么可重构的。

您还必须在 super.init(coder: coder)

之后添加以下两行
wantsLayer = true
canDrawSubviewsIntoLayer = true

我测试过了。有效!

这里有一些与 Bills 解决方案一起使用的代码:

我们可以创建一个协议 LoadableNib 让我们的自定义视图符合要求并扩展它以获得此功能(如果符合):

protocol LoadableNib {
    var contentView: NSView! { get }
}

extension LoadableNib where Self: NSView {

    func loadViewFromNib() {
        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)

        let contentConstraints = contentView.constraints
        contentView.subviews.forEach({ addSubview([=10=]) })

        for constraint in contentConstraints {
            let firstItem = (constraint.firstItem as? NSView == contentView) ? self : constraint.firstItem
            let secondItem = (constraint.secondItem as? NSView == contentView) ? self : constraint.secondItem
            addConstraint(NSLayoutConstraint(item: firstItem as Any, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: secondItem, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
        }
    }
}

然后在我们的自定义视图中class:

class YourViewClass: NSView, LoadableNib {

    @IBOutlet var contentView: NSView!

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

不要忘记按照 Bill 的回答设置 xib(class 文件所有者到您的自定义视图 class,以及一个容器 NSView 来保存视图内容和连接到 contentView 出口。

基本步骤:

  1. MyView.swift 如下所示
  2. 创建一个同名的 xib 文件,例如 MyView.xib
  3. 设置文件所有者 > Class > MyView
  4. 将根视图连接到 contentView IBOutlet

-

class MyView: NSView {

    @IBOutlet var contentView: NSView!

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        setup()
    }

    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)

        addSubview(contentView)
        // Autolayout code using cartograhpy library
        constrain(self, contentView) { view, subview in
            subview.edges == view.edges
        }
    }

}

这与Bill 的步骤类似,但我认为Bill 的步骤6 可以简化。 contentView可以简单地添加到自定义视图中,然后添加一个约束来填充。

也在我的blog post.

对于到达此页面但发现接受的答案对于他们想要实现的目标来说似乎过于复杂的任何人...

我觉得这个视频很有帮助:https://www.youtube.com/watch?v=Iwm47M7st14&t=648s

简而言之,以背景颜色为例:

import Cocoa
@IBDesignable
class SelectionView: NSView {
    
    @IBInspectable var backgroundColor: NSColor? {
        didSet { needsDisplay = true }
    }
    
    override var wantsUpdateLayer: Bool {
        return true
    }
    
    override func updateLayer() {
        guard let layer = layer else { return }
        layer.backgroundColor = backgroundColor?.cgColor
    }
} 

...并且只需将 Interface Builder 中自定义视图的 class 的名称设置为您创建的自定义 NSView subclass 的名称(上面示例代码中的 SelectionView ).