Swift 3 - 自定义按钮(图像和文本)只有在没有插座时才能正确绘制?

Swift 3 - Custom button (image and text) only draws correctly if there's no outlet?

编辑:虽然我接受了一个帮助我在运行时正确绘制按钮的答案,但我还有其他问题。我怀疑,其根源是 为什么给我的自定义按钮一个插座会干扰它的绘制方式。我仍然需要知道这是为什么。(请参阅下面的回答)

我有自己的按钮 class,它扩展了 UIButton(见下文)并有几个 IBInspectable 属性,如边框 width/color、圆角半径,甚至 start/end 渐变背景颜色.我还使用这些属性以编程方式设置图像和标题的插图,这样我就可以考虑各种屏幕尺寸。

以前我有一个问题,如果我在故事板中更改 "View as" 设备,假设从 iPhone SE 到 iPhone 7,然后刷新视图和 运行 在物理 iPhone SE 上,它将使用 iPhone 7 的插图进行部署,而不是使用物理设备自己的屏幕尺寸计算插图。但是,我 几乎 通过在自定义按钮 class.

中覆盖 draw 解决了这个问题

我现在的问题是重写的 draw 方法仅被调用(或似乎仅在以下情况下有效)按钮没有 ViewController class 放在.

例如:

*img为占位符;插图适合真实的 imgs。

bottom-right 按钮的渐变和角绘制正确,而其他 3 个按钮则没有。 我已经确认向该按钮添加一个插座会使它的行为与其他按钮一样,并且从其他 3 个中移除任何一个插座都会使其正确绘制

供参考,网点刚好

@IBOutlet weak var button1: UIButton!
@IBOutlet weak var button2: UIButton!
@IBOutlet weak var button3: UIButton!

直接在class MyViewController: UIViewController {声明下。

此外,我没有忘记在界面生成器中为每个按钮设置自定义 Class。

这是按钮class:

import UIKit

@IBDesignable
class ActionButton: UIButton {

    override init(frame: CGRect){
        super.init(frame: frame)
        setupView()
    }
    required init(coder aDecoder: NSCoder){
        super.init(coder: aDecoder)!
        setupView()
    }

    @IBInspectable var cornerRadius: CGFloat = 0{
        didSet{
            setupView()
        }
    }
    @IBInspectable var startColor: UIColor = UIColor.white{
        didSet{
            setupView()
        }
    }
    @IBInspectable var endColor: UIColor = UIColor.black{
        didSet{
            setupView()
        }
    }
    @IBInspectable var borderWidth: CGFloat = 0{
        didSet{
            self.layer.borderWidth = borderWidth
        }
    }
    @IBInspectable var borderColor: UIColor = UIColor.clear{
        didSet{
            self.layer.borderColor = borderColor.cgColor
        }
    }

    private func setupView(){
        let colors:Array = [startColor.cgColor, endColor.cgColor]
        gradientLayer.colors = colors
        gradientLayer.cornerRadius = cornerRadius
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        self.setNeedsDisplay()
    }

    var gradientLayer: CAGradientLayer{
        return layer as! CAGradientLayer
    }

    override func draw(_ rect: CGRect) {

        //Draw image

        setupView()

        let btnW = self.frame.size.width
        let btnH = self.frame.size.height

        //Compute and set image insets
        let topBtnInset = btnH * (-5/81)
        let bottomBtnInset = -4 * topBtnInset
        let leftBtnInset = btnW / 4 //btnW * (35/141)
        let rightBtnInset = leftBtnInset

        self.imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)

        //Compute and set title insets
        let topTitleInset = btnH * (47/81)
        let bottomTitleInset = CGFloat(0)
        let leftTitleInset = CGFloat(-256) //Image width
        let rightTitleInset = CGFloat(0)

        self.titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)

        //Draw text

        //Default font size
        self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 15)

        if(btnH > 81){
            self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
        }else if(btnH > 97){
            self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
        }

    }

    override class var layerClass: AnyClass{
        return CAGradientLayer.self
    }

}

请忽略我处理插图、字体大小等的奇怪方式。

有谁知道为什么为按钮指定 outlet 会导致它无法正确绘制图层?

几个想法:

  1. draw(_:)方法用于描边路径、绘制图像等,用于渲染给定时刻的视图。您不应该更新图层、UIKit 子视图等。

    在您的代码段中,您的 draw(_:) 正在调用 setupView,然后调用 setNeedsDisplay(理论上可以触发 draw(_:) 再次被调用)。我建议只为那些需要手动绘制的东西保留 draw(_:)。 (就像现在一样,您可能可以完全消除此方法,因为这里没有任何东西正在绘制任何东西。)

  2. 对于子视图(它们的框架等)的任何手动配置,都应移至 layoutSubviews,只要 OS 确定子视图可能会被调用需要调整它们的 frame 值。

  3. 如果您对可设计视图有任何疑问,值得确认您将它们放在单独的目标中。它最大限度地减少了在 IB 中渲染时需要重新编译的内容。它还可以防止主要目标中的问题影响在 IB 中呈现这些可设计对象的能力。 (当 Apple 引入可设计视图时,它们非常具体,应该将它们放在一个单独的目标中。)

    我在 Xcode 中的可设计视图也遇到过间歇性问题,但这些问题通常可以通过退出 Xcode、清空派生数据文件夹并重新启动来解决。

我必须承认,我不知道为什么向可设计视图添加插座会影响视图的呈现方式。不过,我知道当我执行上述操作时,无法重现您的问题。

无论如何,我像这样调整了你的 ActionButton,它似乎对我有用:

@import UIKit

@IBDesignable
public class ActionButton: UIButton {

    public override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        setupView()
    }

    public required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        setupView()
    }

    @IBInspectable public var cornerRadius: CGFloat = 0     { didSet { setupView() } }

    @IBInspectable public var startColor: UIColor = .white  { didSet { setupView() } }

    @IBInspectable public var endColor: UIColor = .black    { didSet { setupView() } }

    @IBInspectable public var borderWidth: CGFloat = 0      { didSet { layer.borderWidth = borderWidth } }

    @IBInspectable public var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }

    private func setupView() {
        let colors = [startColor.cgColor, endColor.cgColor]
        gradientLayer.colors = colors
        gradientLayer.cornerRadius = cornerRadius
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
    }

    private var gradientLayer: CAGradientLayer {
        return layer as! CAGradientLayer
    }

    public override func layoutSubviews() {
        super.layoutSubviews()

        let btnW = frame.width
        let btnH = frame.height

        //Compute and set image insets
        let topBtnInset = btnH * (-5/81)
        let bottomBtnInset = -4 * topBtnInset
        let leftBtnInset = btnW / 4 //btnW * (35/141)
        let rightBtnInset = leftBtnInset

        imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)

        //Compute and set title insets
        let topTitleInset = btnH * (47/81)
        let bottomTitleInset = CGFloat(0)
        let leftTitleInset = CGFloat(-256) //THIS MUST BE EQUAL TO -IMAGE WIDTH
        let rightTitleInset = CGFloat(0)

        titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)

        //Adjust text font
        if btnH > 81 {
            titleLabel?.font = .boldSystemFont(ofSize: 17)
        } else if btnH > 97 {
            titleLabel?.font = .boldSystemFont(ofSize: 20)
        } else {
            //Default font size
            titleLabel?.font = .boldSystemFont(ofSize: 15)
        }
    }

    public override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }

}

我接受了 Rob 的回答,因为它更有可能对其他人有用,但我发现我的问题具体是什么,如果有人有类似的机会问题。

显然在我放置这些按钮的视图控制器的代码中,我忘记了 enables/disables 那些自定义按钮的功能,以及设置它们的背景颜色(纯色) .这就是为什么这些按钮看起来不对的原因。第 4 个按钮从未受到此功能的影响。这不是其他 3 个有插座的事实,而是当我移除插座时我注释掉了对该函数的调用。

然而,这个问题解决了其他问题!

-我有一个非常草率的 UIButton 实现,它覆盖并调用了 UI 功能不正确,导致旋转冻结等问题。 Rob(已接受)的回答也解决了这些问题。

Interface Builder 中的

-"Refresh All Views" 挂在 "Signing product" 上,这是由调用 self.titleLabel?.font 引起的。根据 Apple 开发者论坛上的 some answers,在 layoutSubViews() 中这样做很糟糕!