xyk blog

最近は iOS 開発の記事が多めです。

UIStackView に背景色を設定する

検証環境:
Xcode 11.3
Swift 5.1.3

UIStackView の backgroundColor プロパティに色を設定しても描画されません。
(※追記: iOS 14 から UIStackView の backgroundColor で背景色を付けられるようになりました。)
そこで UIStackView の layer に CAShapeLayer を追加してそこに色を設定します。
UIStackView のサブクラスとして実装します。
IBInspectablecolorプロパティにStoryboard上で色を設定することで確認できるようにしています。

import UIKit

@IBDesignable
class MyStackView: UIStackView {
    
    @IBInspectable
    var color: UIColor?
    
    override var backgroundColor: UIColor? {
        get {
            return color
        }
        set {
            color = newValue
            setNeedsLayout()
        }
    }

    private lazy var backgroundLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        self.layer.insertSublayer(layer, at: 0)
        return layer
    }()
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        CATransaction.begin()
        CATransaction.setDisableActions(true) // CALayerの暗黙的アニメーションは不要なのでオフにする
        
        backgroundLayer.path = UIBezierPath(rect: bounds).cgPath
        backgroundLayer.fillColor = backgroundColor?.cgColor

        CATransaction.commit()
    }
}

さらに UIStackView をタップ中は背景色が少し暗めの色にハイライトするように改良します。
少し暗めの色に変化させるための leap メソッドはこちらの記事で定義したものを使っています。

import UIKit

@IBDesignable
class MyStackView: UIStackView {
    
    @IBInspectable
    var color: UIColor?
    
    override var backgroundColor: UIColor? {
        get {
            guard let color = color else { return nil }
            return isTracking ? color.lerp(to: .gray, progress: 0.3) : color
        }
        set {
            color = newValue
            setNeedsLayout()
        }
    }
    
    private var isTracking: Bool = false {
        didSet {
            setNeedsLayout()
        }
    }
    
    private lazy var backgroundLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        self.layer.insertSublayer(layer, at: 0)
        return layer
    }()
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        
        backgroundLayer.path = UIBezierPath(rect: bounds).cgPath
        backgroundLayer.fillColor = backgroundColor?.cgColor

        CATransaction.commit()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        isTracking = true
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        isTracking = false
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        isTracking = false
    }
}