swift 创建uiview的渐变边框,减少崩溃

fykwrbwg  于 5个月前  发布在  Swift
关注(0)|答案(1)|浏览(46)

我有这个自定义代码,我写了一段时间,以便有一个渐变边界上我的意见。唯一的问题是,它已慢慢成为我的顶部崩溃在我的应用程序。
我在crashlytics中得到的crashlog指向线super.layoutSubviews(),这对我来说没有多大意义。
任何人都有任何改进的想法,或不同的方法来做同样的事情,可以导致更少的崩溃?我得到120-200一周从这个观点。

class GradientBorderView: UIView {
    var enableGradientBorder: Bool = false

    var borderGradientColors: [UIColor] = [] {
        didSet {
            setNeedsLayout()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        guard enableGradientBorder && !borderGradientColors.isEmpty  else {
            layer.borderColor = nil
            return
        }
        let gradient = UIImage.imageGradient(bounds: bounds, colors: borderGradientColors)
        layer.borderColor = UIColor(patternImage: gradient).cgColor
    }
}

extension UIImage {

    static func imageGradient(bounds: CGRect, colors: [UIColor]) -> UIImage {
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = bounds
        gradientLayer.colors = colors.map(\.cgColor)

        // This makes it left to right, default is top to bottom
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)

        let renderer = UIGraphicsImageRenderer(bounds: bounds)

        return renderer.image { gradientLayer.render(in: $0.cgContext) }
    }
}

字符串

qlzsbp2j

qlzsbp2j1#

坚韧很难说为什么会崩溃,除非你能提供重现它的代码。
但是,您可能会发现将CAGradientLayer.mask一起使用是一个更好,更有效的选择:

class MyGradientBorderView: UIView {
    
    var enableGradientBorder: Bool = false { didSet { setNeedsLayout() } }
    
    var borderGradientColors: [UIColor] = [] { didSet { setNeedsLayout() } }
    
    var gradientBorderWidth: CGFloat = 8.0 { didSet { setNeedsLayout() } }
    
    let gLayer: CAGradientLayer = CAGradientLayer()
    let mskLayer: CAShapeLayer = CAShapeLayer()

    override func layoutSubviews() {
        super.layoutSubviews()

        guard enableGradientBorder, gradientBorderWidth > 0.0, !borderGradientColors.isEmpty  else {
            gLayer.removeFromSuperlayer()
            return
        }
        if gLayer.superlayer == nil {
            layer.addSublayer(gLayer)
        }

        gLayer.frame = bounds
        gLayer.colors = borderGradientColors.map(\.cgColor)
        gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
        gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        
        let w: CGFloat = gradientBorderWidth
        
        mskLayer.path = UIBezierPath(rect: bounds.insetBy(dx: w * 0.5, dy: w * 0.5)).cgPath
        mskLayer.fillColor = UIColor.clear.cgColor
        mskLayer.strokeColor = UIColor.red.cgColor  // any opaque color
        mskLayer.lineWidth = w
        gLayer.mask = mskLayer
    }
    
}

字符串

**编辑 *-解决圆角的需求. *

UIBezierPath(roundedRect: ...有很多bug。
cornerRadius大约是短边的1/3时,最大的问题就开始发挥作用了。
因此,如果框架是300 x 60(可能是为标签提供边框),而我们有一个20cornerRadius,事情可能会变得非常难看。
而不是压倒性的细节,参考这些SO职位(除其他外):

现在,* 如果 * 你将永远有一个清晰的背景,* 如果 * 你永远不会接近1/3的半径与侧边比,使用上面的代码UIBezierPath(roundedRect: ...应该不是问题。
然而,这里有另一种方法来实现您的设计目标,您可能会发现更可靠和灵活。
我们将保持视图背景颜色清晰,为所需的背景颜色添加一个“fillLayer”,我们将使用“plain”CALayer遮罩渐变层,并设置其边框属性。

class AnotherGradientBorderView: UIView {
    
    // override backgroundColor, because we want this view
    //  to always have a clear background
    // to avoid edge anti-aliasing artifacts
    //  we use the fillLayer as the background color
    override var backgroundColor: UIColor? {
        set {
            super.backgroundColor = .clear
            self.bkgColor = newValue ?? .clear
        }
        get {
            return self.bkgColor
        }
    }
    
    public var enableGradientBorder: Bool = false { didSet { setNeedsLayout() } }
    
    public var borderGradientColors: [UIColor] = [] { didSet { setNeedsLayout() } }
    
    public var gradientBorderWidth: CGFloat = 8.0 { didSet { setNeedsLayout() } }
    
    public var cornerRadius: CGFloat = 20.0 { didSet { setNeedsLayout() } }

    private var bkgColor: UIColor = .clear { didSet { setNeedsLayout() } }
    
    private let gLayer: CAGradientLayer = CAGradientLayer()
    private let fillLayer: CALayer = CALayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        layer.addSublayer(fillLayer)
        layer.addSublayer(gLayer)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        
        fillLayer.backgroundColor = bkgColor.cgColor

        let w: CGFloat = gradientBorderWidth
        let rad: CGFloat = cornerRadius

        self.layer.cornerRadius = rad

        let mskLayer = CALayer()
        mskLayer.frame = bounds
        mskLayer.cornerRadius = rad
        mskLayer.borderWidth = w
        mskLayer.borderColor = UIColor.black.cgColor    // any opaque color
        gLayer.mask = mskLayer

        if enableGradientBorder, gradientBorderWidth > 0.0, !borderGradientColors.isEmpty {
            
            gLayer.opacity = 1.0
            
            gLayer.frame = bounds
            
            gLayer.colors = borderGradientColors.map(\.cgColor)
            gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
            gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)

            // to avoid edge anti-aliasing artifacts
            //  inset and adjust cornerRadius of fillLayer
            //  if gradient border is showing
            fillLayer.frame = bounds.insetBy(dx: w * 0.5, dy: w * 0.5)
            fillLayer.cornerRadius = rad - w * 0.5

        } else {

            gLayer.opacity = 0.0

            fillLayer.frame = bounds
            fillLayer.cornerRadius = rad
            
        }
        
    }
    
}

示例控制器

class ViewController: UIViewController {
    
    var gbViews: [AnotherGradientBorderView] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let g = view.safeAreaLayoutGuide

        for _ in 1...4 {
            
            let lbl = UILabel()
            lbl.textAlignment = .center
            lbl.font = .systemFont(ofSize: 20.0, weight: .bold)
            lbl.text = "Label behind/underneath the gradient border view."
            lbl.numberOfLines = 0
            lbl.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(lbl)

            let v = AnotherGradientBorderView()
            v.backgroundColor = .black.withAlphaComponent(0.20)
            v.borderGradientColors = [.red, .yellow]
            v.enableGradientBorder = true
            v.cornerRadius = 20.0
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            NSLayoutConstraint.activate([
                v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                v.widthAnchor.constraint(equalToConstant: 120.0),
                v.heightAnchor.constraint(equalToConstant: 80.0),
                
                lbl.centerXAnchor.constraint(equalTo: v.centerXAnchor),
                lbl.centerYAnchor.constraint(equalTo: v.centerYAnchor),
                lbl.widthAnchor.constraint(equalToConstant: 200.0),
            ])
            
            gbViews.append(v)
        }
        
        gbViews[2].cornerRadius = 0.0
        gbViews[3].cornerRadius = 0.0
        
        gbViews[1].backgroundColor = .systemBlue
        gbViews[3].backgroundColor = .systemBlue
        
        NSLayoutConstraint.activate([
            gbViews[0].topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            gbViews[1].topAnchor.constraint(equalTo: gbViews[0].bottomAnchor, constant: 8.0),
            gbViews[2].topAnchor.constraint(equalTo: gbViews[1].bottomAnchor, constant: 8.0),
            gbViews[3].topAnchor.constraint(equalTo: gbViews[2].bottomAnchor, constant: 8.0),
        ])
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        gbViews.forEach { v in
            v.enableGradientBorder.toggle()
        }
    }
}


输出-请注意,在第一个和第三个示例中,我使用.black.withAlphaComponent(0.20)作为背景色,而不是.clear-否则当我们关闭渐变边界时,我们不会看到任何东西:
x1c 0d1x的数据
点击任何地方切换渐变边界:


相关问题