[iOS 10 day by day] Day 7:单位换算

《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 的显示就像下面这样:

未经格式化的 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。

格式化后的 label

为了有一点视觉反馈,我们希望叶片转动的速度能随着风速改变(要注意,下面用到的这些数值只是为了展示用的,跟风力的物理学基础没有任何关系)。

显示动画的是 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 视频

原文地址:iOS 10 Day by Day :: Day 7 :: Measurement

原作者:Sam Burnstone @sam_burnstone

ShinobiControls 官网:ShinobiControls.com twitter : @shinobicontrols

文集地址:iOS 10 day by day 仓薯翻译

译者:戴仓薯,本文翻译时参考了 simpletonking同一篇译文,非常感谢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容