UIKit Tips

188 阅读2分钟

给UILabel设置文字渐变色

// requiredSize 为 frame.size
label.textColor = .init(patternImage: .gradient(size: label.requiredSize,
                                                startColor: .hex("#FFECBA"),
                                                endColor: .hex("#FFBE8D"),
                                                startPoint: .init(x: 0.5, y: 0),
                                                endPoint: .init(x: 0.5, y: 1)))

extension UIImage {
    
    static func gradient(size: CGSize,
                         startColor: UIColor, endColor: UIColor,
                         startPoint: CGPoint, endPoint: CGPoint) -> UIImage {
        
        let colorspace = CGColorSpaceCreateDeviceRGB()
        let gradientLocations: [CGFloat] = [0.0, 1.0]
        let colors: CFArray = [startColor.cgColor, endColor.cgColor] as CFArray
        let gradient = CGGradient(colorsSpace: colorspace, colors: colors, locations: gradientLocations)
        let format = UIGraphicsImageRendererFormat()
        format.scale = UIScreen.main.scale
        
        let render = UIGraphicsImageRenderer(bounds: .init(origin: .zero, size: size), format: format)
        
        let image = render.image { context in
            context.cgContext.drawLinearGradient(gradient!,
                                                 start: CGPoint(x: size.width * startPoint.x, y: size.height * startPoint.y),
                                                 end: CGPoint(x: size.width * endPoint.x, y: size.height * endPoint.y),
                                                 options: .drawsAfterEndLocation)
        }
        
        return image
    }
}

给UIView添加渐变圆角

extension UIView {
    
    /// 添加渐变圆角(基于frame)
    func addGradientBorder(opacity: Float, colors: [UIColor], startPoint: CGPoint, endPoint: CGPoint, borderWidth: CGFloat, cornerRadius: CGFloat) {
        let frame = self.bounds
        
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = frame
        gradientLayer.opacity = opacity
        gradientLayer.colors = colors.map({ $0.cgColor })
        gradientLayer.startPoint = startPoint
        gradientLayer.endPoint = endPoint
        
        let borderInset = borderWidth / 2.0
        let maskFrame = CGRect(x: borderInset, y: borderInset, width: frame.width - borderInset, height: frame.height - borderInset)
        
        let maskLayer = CAShapeLayer()
        maskLayer.lineWidth = borderWidth
        maskLayer.path = UIBezierPath(roundedRect: maskFrame, cornerRadius: cornerRadius).cgPath
        maskLayer.fillColor = UIColor.clear.cgColor
        maskLayer.strokeColor = UIColor.black.cgColor
        
        gradientLayer.mask = maskLayer
        
        self.layer.addSublayer(gradientLayer)
    }
}

给UIView添加自定义圆角+阴影

// ⚠️ 阴影在下,圆角在上,否则阴影会被覆盖,导致无效
class CornerShadowView: UIView {
    
    lazy var cornerView = UIView(backgroundColor: .white)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        ez_size = .init(width: App.width, height: 120.wScale)
        ez_addShadow(ofColor: .black, radius: 3.5, offset: .init(width: 1.5, height: 2), opacity: 0.1)
        
        addSubview(cornerView)
        
        cornerView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        layoutIfNeeded() // 如果不这么做,第一次显示不出来。。。神奇
        
        cornerView.ez_roundCorners([.topLeft, .topRight], radius: 18.wScale)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    /// 添加阴影
    /// - Parameters:
    ///   - color: 颜色
    ///   - radius: 半径
    ///   - offset: 偏移量
    ///   - opacity: 透明度
    func ez_addShadow(ofColor color: UIColor, radius: CGFloat, offset: CGSize, opacity: Float) {
        layer.shadowColor = color.cgColor
        layer.shadowOffset = offset
        layer.shadowRadius = radius
        layer.shadowOpacity = opacity
        layer.masksToBounds = false
    }
    
    /// 添加自定义角落圆角(基于frame)
    /// - Parameters:
    ///   - corners: [UIRectCorner]
    ///   - radius: 半径
    func ez_roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
        let maskPath = UIBezierPath(
            roundedRect: bounds,
            byRoundingCorners: corners,
            cornerRadii: CGSize(width: radius, height: radius))
        
        let shape = CAShapeLayer()
        shape.path = maskPath.cgPath
        layer.mask = shape
    }
}

关于UIStackView的一个小知识点

把UIStackView中某个视图hidden后,UIStackView的布局会进行更新,只展示没有hidden的视图, 例如,你有5个视图平等分显示,设置某个视图hidden之后,就会变成4个视图平等分了。 如果hidden某个视图后,不想更改其他视图布局,那么可以设置alpha = 0

如何在键盘光标处插入、删除字符?

// 系统输入控件都遵守了UITextInput协议
class UITextView : UIScrollView, UITextInput {}
class UITextField : UIControl, UITextInput {}

@MainActor public protocol UIKeyInput : UITextInputTraits {
    
    /// 是否有文字?用于判断删除、完成键是否可用。
    var hasText: Bool { get }
    
    /// 在光标处插入字符串
    func insertText(_ text: String)
    
    /// 从光标处删除字符串
    func deleteBackward()
}

关于UIView通过AutoLayout获取frame的时机

// 假定当前View里有一个hStackView,然后titleLabel又在stack中

override func layoutSubviews() { // 父视图会先调用
    super.layoutSubviews()
    
    logDebug(#function, titleLabel) // 此时frame == .zero
    
    hStackView.layoutIfNeeded() // 当label的父视图布局完后
    
    logDebug(#function, titleLabel) // label.frame != .zero
}

通过代码调用主动进入后台

let suspend = #selector(NSXPCConnection.suspend)
if UIApplication.shared.responds(to: suspend) {
    UIApplication.shared.perform(suspend)
}

等宽字体可解决倒计时Label跳动的问题

/// 样式更接近系统默认字体
@available(iOS 9.0, *)
open class func monospacedDigitSystemFont(ofSize fontSize: CGFloat, weight: UIFont.Weight) -> UIFont

/// 样式差异较大,谨慎使用
@available(iOS 13.0, *)
open class func monospacedSystemFont(ofSize fontSize: CGFloat, weight: UIFont.Weight) -> UIFont

后续补充。。。