Auto - 优雅的iPhone等比例精准适配工具(一. 纯代码)

前言

很多时候我们苦于需要精准的适配各个屏幕尺寸的UI, 通常根据某一种倍数计算的结果并不能满足精准的需求, 随着iPhone设备不同尺寸的增加 这种需求更加迫切, 当然我说的这些都是属于对于产品细节要求苛刻 追求完美的那一部分, 如果你觉得随便适配适配看着还行就够了, 那么现在就可以X掉这个页面了.

问题

各种适配方案中, 针对不同尺寸iPhone适配的最佳方案莫过于等比例适配 (即按照基准屏幕宽度计算出一个比例值, 再按照这个比例值计算出其他宽度屏幕的值), 计算方法大家都会 加减乘除嘛, 但是如何可以优雅的封装 并在开发中更简单的使用就是一个问题了.

解决方案

先说说思路吧.

首先 对于交换某些UI类的某些方法实现 增加等比例计算处理 这类的利用Method Swizzling的实现方案我并不赞同, 例如交换一下UILabel类的setFont方法的实现 增加一些将原有FontSize按照等比例计算的操作等等吧, 可能这类问题仁者见仁智者见智, 但我个人的观点是"对类本身的入侵性太强 极容易出现不可预知的问题" 试想各种工具类都利用Method Swizzling的方式来处理 那么你根本不清楚当你调用一个方法时被其他扩展进行了怎样的处理, 同时遇到Crash时 也很难定位罪魁祸首是谁造成的.

Method Swizzling在我看来只适用于某些快速补救的情景, 如果过分依赖于它 那么整个项目会变得极不稳定, 它在某些情况下对整个项目的健壮性破坏是致命的.

还有一点, UI类无数 每个类会影响到布局效果的属性/方法也是层出不穷, 每一个都要扩展处理会不会累死?

我的思路:

上升一个维度, 既然针对各个UI类进行扩展处理不合适, 那么为什么不直接对数值进行扩展呢?

对各种数值类型进行扩展 增加一个等比例计算转换方法, 开发时即可以灵活的控制数值要不要进行等比例计算, 也可以对于各个UI类不造成任何影响.

设(yi)想(yin)中的使用状态:

label.font = .systemFont(ofSize: 16.auto())

第一步 声明一个自动计算的协议

protocol AutoCalculationable {
    
    /// 自动计算
    ///
    /// - Returns: 结果
    func auto() -> Double
}

针对Double扩展 添加默认实现

extension Double: AutoCalculationable { }

extension AutoCalculationable where Self == Double {
    
    func auto() -> Double {
        guard UIDevice.current.userInterfaceIdiom == .phone else {
            return self
        }
        
        let base = 375.0
        let screenWidth = Double(UIScreen.main.bounds.width)
        let screenHeight = Double(UIScreen.main.bounds.height)
        let width = min(screenWidth, screenHeight)
        return self * (width / base)
    }
}

这里说明一下为什么要用一个协议, 而不是直接对Double进行扩展, 主要是为了方便使用者自定义计算处理逻辑, 不一定所有人的auto()实现都是按照375屏幕宽度计算的, 当不想使用默认实现时, 可以通过扩展Double重写auto()方法的实现来自定义计算逻辑, 如下:

extension Double {
    /// 扩展Double类 重写auto()实现
    func auto() -> Double {
        // ... 自定义计算处理
        return self
    }
}

Double类型的扩展已经加好了, 其他类型怎么办呢?CGFloat, Float, Int, Int8, Int32, Int64.... 所有扩展都写一遍? 不可能的, 这辈子都不可能的.

查看DoubleFloat等浮点类型的声明 我们可以看到所有浮点类型都实现了一个叫做BinaryFloatingPoint的协议, 利用Swift强大的扩展 可以这样做:

extension BinaryFloatingPoint {
    
    func auto() -> Double {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
}

这里再解释一波, 为什么要转成String再转成Double?

转换计算的逻辑其实只要在一个扩展中进行就够了, 其他数值类型的扩展可以转换成这个类型 直接调用该类型的方法就可以了, 这样的好处就是计算逻辑汇总到了一处, 更方便日后调整维护.

Double无疑是最合适的类型 (Swift中浮点字面量的默认类型, 计算时精度比单浮点更准确), 所以你懂得~

那么为什么转成String?

查看Double结构体的初始化方法可以看到并没有给出通过BinaryFloatingPoint协议类型初始化的方法,
但是我们必须要将这任意浮点类型转换Double, 才能使用Double扩展中的auto()进行统一的计算, 苦思冥想, 终于想到了可以利用万能的String来做一个中间者, StringDouble是必然完全阔以的, 最终我将任意浮点类型先转成String, 再将String转成所需的Double, 调用auto()进行计算并返回结果.

Int, Int8, Int32, Int64....类型和Double同理, 它们都实现了一个叫BinaryInteger的协议.

extension BinaryInteger {
    
    func auto() -> Double {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
}

继续继续, 所有数值类型的扩展都搞定了, 现在任何一个数值类型都可以调用auto()方法进行等比例计算了.

调用试试看:

/// Integer
print(1994.auto())
print(Int(1994).auto())
print(Int8(1994).auto())
print(Int16(1994).auto())
print(Int32(1994).auto())
print(Int64(1994).auto())
print(UInt(1994).auto())
print(UInt8(1994).auto())
print(UInt16(1994).auto())
print(UInt32(1994).auto())
print(UInt64(1994).auto())

/// Floating
print(1994.0.auto())
print(Float(1994).auto())
print(Float32(1994).auto())
print(Float64(1994).auto())
print(Float80(1994).auto())
print(Double(1994).auto())
print(CGFloat(1994).auto())

很好, 完美编译.

不过! 所有的auto()方法返回值都是Double, 在Swift的强类型限制中就会出现如下尴尬的情况 各种类型转换:

label.font = .systemFont(ofSize: CGFloat(16.auto()))

别怕, 上泛型:

extension BinaryInteger {
    
    func auto() -> Double {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
    func auto<T: BinaryInteger>() -> T {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
    func auto<T: BinaryFloatingPoint>() -> T {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
}

extension BinaryFloatingPoint {
    
    func auto() -> Double {
        let temp = Double("\(self)") ?? 0
        return temp.auto()
    }
    func auto<T: BinaryInteger>() -> T {
        let temp = Double("\(self)") ?? 0
        return T(temp.auto())
    }
    func auto<T: BinaryFloatingPoint>() -> T {
        let temp = Double("\(self)") ?? 0
        return T(temp.auto())
    }
}

再调用来看看, 完美

label.font = .systemFont(ofSize: 16.auto())

看一下实际使用场景的样子 :

containerView.snp.makeConstraints { (make) in
      make.left.equalTo(10.auto())
      make.top.equalToSuperview()
      make.bottom.equalToSuperview()
      make.right.equalTo(-10.auto())
 }

总结

以上方案可以很好地支持不同设备快捷精准的等比例适配需求, 并且保持良好的扩展性和灵活性, 这里也充分利用了Swift强大的语法特性, 为适配创造了更多的可能.

如果你有更好的想法 欢迎Issues留言讨论, 我是LEE, 下个轮子见.

上一篇 -> 全尺寸精准适配
下一篇 -> 等比例精准适配(二. 可视化)