IBInspectable 和从 xib 为 Interface Builder 加载视图时出现问题

Trouble with IBInspectable and loading a view from xib for Interface Builder

我有一个带有自己的 .xib 的自定义 class。我正在尝试合并 IBInspectable 以更改其背景颜色和 IB 中子视图的背景。

在 IB 中,当我将 'Custom Class' 留空并添加用户定义的运行时属性 'bgColor' 然后尝试 运行 应用程序时,我得到 "...this class is not key value coding-compliant for the key bgColor."应用 运行s 没有崩溃,但没有将颜色应用到视图。

当我将 'Custom Class' 设置为 "AxesView" 和 运行 时,应用程序崩溃并显示 "Thread 1: EXC_BAD_ACCESS (code=2, address)" 并突出显示 func setupFromNib() 中的行 "guard let view = Self.nib.instantiate..." NibLoadableExtension.swift

有没有办法让这些东西一起工作,或者这是 either/or 情况?

在视图控制器中:

var axesView = AxesView()
override func viewDidLoad() {
    super.viewDidLoad()
    axesView.frame = self.view.bounds
    self.view.addSubview(axesView)
}

完整 ViewController.swift

var axesView = AxesView()

override func viewDidLoad() {
    super.viewDidLoad()
    axesView.frame = self.view.bounds

    axesView.bgColor = .clear
    axesView.lineColor = .red
    self.view.addSubview(axesView)
    axesView.contentView.translatesAutoresizingMaskIntoConstraints = false

    axesView.contentView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
    axesView.contentView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
    axesView.contentView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
    axesView.contentView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touch = touches.first! as UITouch
    let p = touch.location(in: self.view)
    axesView.vLine.center.x = p.x
    axesView.hLine.center.y = p.y
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    let touch = touches.first! as UITouch
    let p = touch.location(in: self.view)
    axesView.vLine.center.x = p.x
    axesView.hLine.center.y = p.y
    self.view.setNeedsDisplay()
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    touchesMoved(touches, with: event)
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    touchesEnded(touches, with: event)
}

AxesView.swift

@IBDesignable class AxesView: UIView, NibLoadable {

    @IBOutlet var contentView: UIView!
    @IBOutlet weak var hLine: UIView!
    @IBOutlet weak var vLine: UIView!

    @IBInspectable var bgColor: UIColor = UIColor.white {
        didSet {
            contentView.backgroundColor = bgColor
        }
    }

    @IBInspectable var lineColor: UIColor = UIColor.cyan {
        didSet {
            hLine.backgroundColor = lineColor
            vLine.backgroundColor = lineColor
        }
    }

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

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

NibLoadableExtension.swift

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }

        addSubview(view)
    }
}

您的代码工作正常(大部分)。

确保您在正确的对象上设置了 Class:

将您的 setupFromNib() 功能更改为:

func setupFromNib() {
    guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }

    addSubview(view)
    view.frame = self.bounds
    view.autoresizingMask = [.flexibleWidth, .flexibleHeight]

    // layout code...
}

这是它添加到 IB 中的视图控制器后的样子:

这是代码的样子:

class AxesTestViewController: UIViewController {

    var axesView = AxesView()

    override func viewDidLoad() {
        super.viewDidLoad()
        axesView.frame = self.view.bounds
        axesView.bgColor = .blue
        axesView.lineColor = .red
        self.view.addSubview(axesView)
    }

}


编辑

在评论中进行讨论后,这里有一种使用 @IBDesignable xib 和 "draggable" 十字准线的方法。

这使用约束并修改 centerXcenterY 上的 .constant 以移动线条。我还将您的 touches... 函数移到了自定义视图中,以使事情更有条理。

完整的示例代码如下,包括 NibLoadable 代码(为了比较,我将您的控件重命名为 TapAxesView):

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.backgroundColor = .clear
        view.frame = self.bounds
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
}

@IBDesignable
class TapAxesView: UIView, NibLoadable {

    @IBOutlet var hLine: UIView!
    @IBOutlet var vLine: UIView!

    @IBOutlet var vLineCenterX: NSLayoutConstraint!
    @IBOutlet var hLineCenterY: NSLayoutConstraint!

    @IBInspectable var bgColor: UIColor = UIColor.white {
        didSet {
            self.backgroundColor = bgColor
        }
    }

    @IBInspectable var lineColor: UIColor = UIColor.cyan {
        didSet {
            hLine.backgroundColor = lineColor
            vLine.backgroundColor = lineColor
        }
    }

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

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

    func updateCenter(_ point: CGPoint) -> Void {
        // prevent centers from moving outside the bounds
        let halfW = (bounds.size.width / 2.0)
        let halfH = (bounds.size.height / 2.0)
        let x = point.x - halfW
        let y = point.y - halfH
        vLineCenterX.constant = min(max(x, -halfW), halfW)
        hLineCenterY.constant = min(max(y, -halfH), halfH)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch
        let p = touch.location(in: self)
        updateCenter(p)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch
        let p = touch.location(in: self)
        updateCenter(p)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesMoved(touches, with: event)
    }

}

class TapAxesTestViewController: UIViewController {

    var axesView = TapAxesView()

    override func viewDidLoad() {
        super.viewDidLoad()
        axesView.frame = self.view.bounds

        axesView.bgColor = .clear
        axesView.lineColor = .red
        self.view.addSubview(axesView)

        axesView.translatesAutoresizingMaskIntoConstraints = false

        axesView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
        axesView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
        axesView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
        axesView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
    }

}

这里是 TapAxesView.xib 文件的来源:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina4_7" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TapAxesView" customModule="MiniScratch" customModuleProvider="target">
            <connections>
                <outlet property="hLine" destination="wry-9o-V8F" id="uCc-eL-sSS"/>
                <outlet property="hLineCenterY" destination="Txd-hz-fX2" id="OUH-HO-ghG"/>
                <outlet property="vLine" destination="x0E-M7-ETl" id="BaY-4q-4RA"/>
                <outlet property="vLineCenterX" destination="pAM-XU-BDo" id="fgf-lE-dn3"/>
            </connections>
        </placeholder>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB">
            <rect key="frame" x="0.0" y="0.0" width="375" height="382"/>
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wry-9o-V8F">
                    <rect key="frame" x="0.0" y="189" width="375" height="4"/>
                    <color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="4" id="OqP-vn-hAj"/>
                    </constraints>
                </view>
                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="x0E-M7-ETl">
                    <rect key="frame" x="185.5" y="0.0" width="4" height="382"/>
                    <color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <constraints>
                        <constraint firstAttribute="width" constant="4" id="cqZ-JL-4vH"/>
                    </constraints>
                </view>
            </subviews>
            <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            <constraints>
                <constraint firstItem="wry-9o-V8F" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="Txd-hz-fX2"/>
                <constraint firstItem="wry-9o-V8F" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="bcB-iZ-vbV"/>
                <constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="x0E-M7-ETl" secondAttribute="bottom" id="hfl-4K-VZq"/>
                <constraint firstItem="wry-9o-V8F" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="ma5-u8-0U4"/>
                <constraint firstItem="x0E-M7-ETl" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="pAM-XU-BDo"/>
                <constraint firstItem="x0E-M7-ETl" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="z8W-cZ-2Bi"/>
            </constraints>
            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
            <point key="canvasLocation" x="138.40000000000001" y="24.287856071964018"/>
        </view>
    </objects>
</document>

编辑 2

也许值得一试...自定义 @IBDesignable 视图仅通过 代码 ... 不需要 xib 文件(或 nib 加载)。此外,这使用 CALayer 作为 "cross-hair-lines" 而不是子视图。做一点 "lighter weight."

@IBDesignable
class LayerAxesView: UIView {

    var hLine: CALayer = CALayer()
    var vLine: CALayer = CALayer()

    var curX: CGFloat = -1.0
    var curY: CGFloat = -1.0

    let lineWidth: CGFloat = 4.0

    @IBInspectable var bgColor: UIColor = UIColor.white {
        didSet {
            self.backgroundColor = bgColor
        }
    }

    @IBInspectable var lineColor: UIColor = UIColor.cyan {
        didSet {
            hLine.backgroundColor = lineColor.cgColor
            vLine.backgroundColor = lineColor.cgColor
        }
    }

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

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

    override func prepareForInterfaceBuilder() {
        commonInit()
    }

    func commonInit() -> Void {
        if hLine.superlayer == nil {
            layer.addSublayer(hLine)
            layer.addSublayer(vLine)
            hLine.backgroundColor = lineColor.cgColor
            vLine.backgroundColor = lineColor.cgColor
            backgroundColor = bgColor
        }
    }

    override func layoutSubviews() {
        // if curX and curY have not yet been set,
        //    such as on init or when used in Storyboard / IB,
        //    initialize to center of view
        if curX == -1 {
            curX = bounds.midX
            curY = bounds.midY
        }
        hLine.frame = CGRect(x: bounds.minX, y: curY - lineWidth * 0.5, width: bounds.maxX, height: lineWidth)
        vLine.frame = CGRect(x: curX - lineWidth * 0.5, y: bounds.minY, width: lineWidth, height: bounds.maxY)
    }

    func updateCenter(_ point: CGPoint) -> Void {
        // prevent centers from moving outside the bounds
        curX = max(min(bounds.maxX, point.x), bounds.minX)
        curY = max(min(bounds.maxY, point.y), bounds.minY)
        // disable CALayer's built-in animation
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        setNeedsLayout()
        layoutIfNeeded()
        CATransaction.commit()
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch
        let p = touch.location(in: self)
        updateCenter(p)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first! as UITouch
        let p = touch.location(in: self)
        updateCenter(p)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesMoved(touches, with: event)
    }

}

class TapAxesTestViewController: UIViewController {

    var axesView: LayerAxesView = {
        let v = LayerAxesView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.lineColor = .red
        v.bgColor = UIColor.blue.withAlphaComponent(0.5)
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addSubview(axesView)

        // respect safe area
        let g = view.safeAreaLayoutGuide

        // constrain axesView to safe area with 40-pts "padding"
        NSLayoutConstraint.activate([
            axesView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            axesView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            axesView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            axesView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
        ])
    }

}