【问题标题】:How to use custom NSView in Interface Builder?如何在 Interface Builder 中使用自定义 NSView?
【发布时间】:2017-06-27 20:12:10
【问题描述】:

我看过很多关于如何在 Interface Builder 中创建自定义视图的教程。所有都是针对iOS的,但MacOS应该是相似的,不是吗?我尝试了一些方法,但没有一个是完全成功的。 init(coder:) 调用 NIB 实例化(通过 Bundle.main.loadNibNamedNSNib 依次调用 init(coder:) 并在我将 nib 中的主视图归类为自定义类时以无限递归结束

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

是否有一个使用 AppKit 创建自定义控件的示例?我来的最接近的显示自定义控件,但没有任何自动布局设置工作。

应该很简单,但我还没弄明白。

这是我目前所拥有的:

  1. 一个新的 MyControl 类


import Cocoa

@IBDesignable class MyControl: NSView {

@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 类设置为MyControl,顶层视图保持为NSView

  2. Main.storyboard 有一个自定义视图,分类为 MyControl,居中设置了高度和宽度。

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

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

【问题讨论】:

  • 显示你正在使用的代码;试图描述它是没有帮助的。
  • 我现在所拥有的是一团混乱。我正在寻找的是我可以开始使用的其他人的代码。如果您真的认为发布我的代码会有所帮助,那么我将尝试简化并发布它,但我正在盲目地工作,我所做的一切都没有正常工作。
  • 你的描述和代码看起来就像一个按钮在一个视图里面一个视图里面一个视图里面 -> 无限递归。 initWithCoder 调用 loadNibNamed,后者调用 initWithCoder。你想完成什么?
  • 我正在尝试创建一个自定义的可重用控件。我想要结束的是一个图像和文本字段,并用一些代码捆绑在一起。我希望能够在 Interface Builder 中使用它。

标签: macos swift3 xcode8 macos-sierra appkit


【解决方案1】:

经过大量搜索和 Apple 支持的大量帮助后,我发现在 AppKit 中创建和使用自定义控件非常容易。只是它就像一把锁上的钥匙,除非你做对了,否则你将一无所获。

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

这是一个小项目,我希望我已经完整地评论它,以便其他人能够理解它。

这个过程的要点是这样的:

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

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

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

【讨论】:

  • 谢谢,这真的帮助了我!如果您有兴趣,我提交了一个拉取请求,该请求缩短了复制约束的 for 循环。
  • 不客气。我已经合并了拉取请求。谢谢你。
  • @IBDesignable 真的会导致您的自定义视图显示在 IB 中吗?我得到“无法呈现...的实例:代理崩溃”。
  • 是的,它确实出现了。我在构建它的代理方面也遇到了问题。注意意外的 nil 值。当它为 IB 构建时,您不会在“真实”版本中看到 nil 实例。
  • 我没有在 XCode 9 中使用过它,但如果你使用 @IBDesignable 它应该会出现。检查构建日志以了解 IB 中的故障。对意外的 nil 值要加倍小心。
【解决方案2】:

以下是 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($0) })

        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 YourViewClass: NSView, LoadableNib {

    @IBOutlet var contentView: NSView!

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

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

【讨论】:

    【解决方案3】:

    基本步骤:

    1. MyView.swift 如下所示
    2. 创建一个同名的xib文件,例如MyView.xib
    3. 设置文件的所有者>类>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
            }
        }
    
    }
    

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

    还有我的blog post

    【讨论】:

      【解决方案4】:

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

      您还必须在 super.init(coder: coder) 之后添加以下两行

      wantsLayer = true
      canDrawSubviewsIntoLayer = true
      

      我测试过了。有效!

      【讨论】:

        【解决方案5】:

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

        我发现这个视频很有帮助: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 中自定义视图的类名称设置为您创建的自定义 NSView 子类的名称(上面示例代码中的 SelectionView)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-04
          • 1970-01-01
          • 1970-01-01
          • 2011-12-05
          相关资源
          最近更新 更多