《iOS 10 day by day》是 shinobicontrols 公司编写的系列博客,介绍开发者需要了解的 iOS 10 新特性,每周更新。本系列翻译(文集地址)已取得官方授权。目录点此。仓薯翻译,欢迎指正:)
Shinobicontrols 为 iOS 和 Android 开发者提供高性能、响应式的 UI 控件 SDK,尤其是图表方面的控件。 官网 : shinobicontrols.com twitter : @shinobicontrols
本文翻译时参考了 simpletonking 的同一篇译文,在此感谢。Thank you simpletonking!
本周我们来看看新的 Measurement API,这是 Foundation 框架新增的一部分。这套 API 从表面上看平凡无奇,就是提供了一套单位换算的方法,例如公里与英里互相换算。
不过,仔细想想,我们确实在单位换算上浪费了太多时间。比如,你有一个角度值,但是用来旋转 view 的 API 只接受弧度值。或者,可能你的 app 里距离的计量单位是英里,但是要为了习惯用公里的用户转换成公里去显示。
在 iOS 10 之前,你可能已经自己写了一些单位换算的方法,或者用了第三方库。现在苹果提供了新的 API ,能解决大部分问题了。我们来看看它具体能做什么吧。
基本概念
本文使用 Swift 3 编写,在 Xcode 8 GM build 上编译运行。
我们要介绍一个单位空间(dimension)的概念,用 Measurement 的 model 类型把数值保存为某个单位空间的数据。所谓“单位空间”就是一组能互相转换的单位,比如克可以转换成千克,千克也能转换回来。每个单位空间都有一个基本单位,其他单位都用这个基本单位来表示(比如,容积的基本单位是升,而 1 毫升就是 0.001 升)。
建立度量值
先从简单的开始,假设我们有一品脱牛奶,想知道一品脱是多少公升。代码如下:
let milk = Measurement(value: 1, unit: UnitVolume.imperialPints)
milk.converted(to: .liters)
// 输出 0.568261 L
很简单吧!定义出了某个单位的度量值之后,就只能把它换算为同一个单位空间的其他单位。converted
这一步会自动进行类型检查,milk
变量的类型是 Measurement<UnitVolume>
,换算成的单位也要属于 UnitVolume
这个单位空间。只能在同一个单位空间之内互相换算,这种限制是显而易见的——不然,把公升换算成英里怎么换算?
运算符
Measurement API 支持对度量值使用运算符。
如果我们想要 5 品脱的牛奶,就可以写:
let fivePints = milk * 5
这样会创建一个新的度量值,接下来我们就可以把它换算成另一个单位:
fivePints.converted(to: .cups)
// 输出 11.8387708333333 cup
可以注意到,把代码放在 Playground 里,或者把度量值打印出来,末尾会自动带上它的单位。
当然,能用的运算符不只有乘号。还有其他的几种,比如双等号 ==
:
let kms = Measurement(value: 5, unit: UnitLength.kilometers)
let meters = Measurement(value: 5000, unit: UnitLength.meters)
kms == meters // true
以及加号:
kms + meters // 10000.0 m
Formatter
前面提到过,做本地化的时候我们经常需要为不同的 locale 显示不同的单位。
除了新的 Measurement API,苹果还提供了 MeasurementFormatter
,它能把度量值转化成格式化字符串。
默认情况下,measurement formatter 使用的是用户当前的 locale。下面我们手动更改这一点,更改前后分别打印同一段两个城市之间的距离,看看输出有什么变化:
let newcastleToLondon = Measurement(value: 248, unit: UnitLength.miles)
let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "fr")
formatter.string(from: newcastleToLondon) // 输出 399,166 km
formatter.locale = Locale(identifier: "en_GB")
formatter.string(from: newcastleToLondon) // 输出 248 mi
好棒!不费吹灰之力就完成了。
项目
我们已经大略看了一下 API 的基本用法,下面来上手玩一下吧。
我们来做一个小风车,转动速度与风力强度成比例,而风力强度可以用一个滑动条来调节。
风车就是一个简单的 UIView 子类。把它添加到 UIViewController 的 view 上,再加一些其他的基本 UI 控件:一个调节风速的滑动条,还有一个用 米/秒 和 英里/小时 两种单位显示风速的 label。如果想看完整的 playground,欢迎从 GitHub 下载。
我们把目光集中在用到 Measurement API 的部分上:首先是拖动滑动条的时候,在 label 上显示风速:
func handleWindSpeedChange(slider: UISlider) {
let windSpeed = Measurement(value: Double(slider.value), unit: UnitSpeed.metersPerSecond)
let milesPerHour = windSpeed.converted(to: .milesPerHour)
windSpeedLabel.text = "Wind speed: \(windSpeed) (\(milesPerHour))"
}
label 的显示就像下面这样:
哇哦!只是一个简单的 demo 而已,不需要弄得这么精确。有时候小数点后的位数显示得太多,都看不到后面的单位 m/s 了。要解决这个问题,我们可以使用上面提过的 MeasurementFormatter
。
let windSpeed = Measurement(value: Double(slider.value), unit: UnitSpeed.metersPerSecond)
let measurementFormatter: MeasurementFormatter = {
let formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
let numberFormatter = NumberFormatter()
numberFormatter.minimumIntegerDigits = 1
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 1
formatter.numberFormatter = numberFormatter
return formatter
}()
let metersPerSecond = measurementFormatter.string(from: windSpeed)
let milesPerHour = measurementFormatter.string(from: windSpeed.converted(to: .milesPerHour))
windSpeedLabel.text = "Wind speed: \(metersPerSecond) (\(milesPerHour))"
创建 formatter 的时候,我们要指定它使用 providedUnit
。这是为了防止 formatter 忽略我们指定的单位,用它自己认为合适的格式输出。对我们现在的情况来说,如果不设这个 option,formatter 会对两个结果都用 英里/小时 为单位输出。
MeasurementFormatter 本身包含另一个 formatter(有点像嵌套的 formatter!),内层的 formatter 是用来格式化数字部分的。我们要求数字显示为一位(且仅一位)小数。
最后,我们使用构造好的 formatter,将 米/秒 和 英里/每小时 的两种风速度量分别格式化为 string。
为了有一点视觉反馈,我们希望叶片转动的速度能随着风速改变(要注意,下面用到的这些数值只是为了展示用的,跟风力的物理学基础没有任何关系)。
显示动画的是 TurbineView
,不过我们要把每秒钟叶片转动多少角度的数值传给它。你可能会想到去定义一个属性,类似这样:
/// 叶片每秒转动的角度,用弧度的形式表示
public var bladeRotationPerSecond: Double
这样定义没什么问题,也跟苹果官方的 API 保持一致,角度是用弧度表示的。不过,怎么防止使用者无意中传了角度值而不是弧度值呢?你可能会说:“他们应该好好看看文档”。这句话有一定道理,不过万一这个属性没有文档呢?并且,因为我们平常更习惯用角度值,所以这也是个容易不小心犯下的错误。
那我们能怎么利用上 Measurement 框架,只约束使用者传过来的是表示角度的值,具体单位不限呢?这样使用者想传弧度或者角度都可以,反正都会自动转化成我们需要的单位。听起来很棒,我们试试吧:
// TurbineView 的属性
public var bladeRotationPerSecond: Measurement<UnitAngle> = Measurement(value: 0, unit: UnitAngle.degrees) {
didSet {
rotate()
}
}
在 viewController 里,我们可以用下面这段代码来计算一秒旋转多少角度。
func calculateTurbineRotation() {
// 假设滑动条拖到最快时,转速达到每秒 1 圈
let ratio = windSpeedSlider.value / windSpeedSlider.maximumValue
let fullRotation = Measurement(value: 360, unit: UnitAngle.degrees)
let rotationAnglePerSecond = fullRotation * Double(ratio)
turbine.bladeRotationPerSecond = rotationAnglePerSecond
}
参数想传角度或者弧度都可以——我选择了角度。然后根据当前风速来计算旋转角度(如果滑动条的 value
为 0,ratio
就是 0 / 40 = 0;拖到最快的一端,滑动条的 value
是 40,ratio
就是 40 / 40 = 1),用到了乘法操作符,非常方便。
下面来欣赏我们美丽的风车吧:
扩展阅读
本文中我们用到了苹果提供的几组单位,不过这些只是冰山一角;苹果一共提供了 170 多种不同的单位。你需要用的单位大概率就在其中,不过,如果真的没有,也可以自己创建一个。想知道怎么创建(还有其他内容!),请看这部 WWDC 视频。
原作者:Sam Burnstone @sam_burnstone
ShinobiControls 官网:ShinobiControls.com twitter : @shinobicontrols
译者:戴仓薯,本文翻译时参考了 simpletonking 的同一篇译文,非常感谢~