[iOS 10 day by day] Day 7:單位換算

發表於2016-11-19

本文介紹了 iOS 10 新出的 Measurement API,主要用於諸如英里/公里、角度/弧度之類的單位轉換。

《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 這個單位空間。只能在同一個單位空間之內互相換算,這種限制是顯而易見的——不然,把公升換算成英里怎麼換算?

運算子

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 的顯示就像下面這樣:

[iOS 10 day by day] Day 7:單位換算
未經格式化的 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。

[iOS 10 day by day] Day 7:單位換算
格式化後的 label

為了有一點視覺反饋,我們希望葉片轉動的速度能隨著風速改變(要注意,下面用到的這些數值只是為了展示用的,跟風力的物理學基礎沒有任何關係)。

顯示動畫的是 TurbineView,不過我們要把每秒鐘葉片轉動多少角度的數值傳給它。你可能會想到去定義一個屬性,類似這樣:

/// 葉片每秒轉動的角度,用弧度的形式表示
public var bladeRotationPerSecond: Double複製程式碼

這樣定義沒什麼問題,也跟蘋果官方的 API 保持一致,角度是用弧度表示的。不過,怎麼防止使用者無意中傳了角度值而不是弧度值呢?你可能會說:“他們應該好好看看文件”。這句話有一定道理,不過萬一這個屬性沒有文件呢?並且,因為我們平常更習慣用角度值,所以這也是個容易不小心犯下的錯誤。

那我們能怎麼利用上 Measurement 框架,只約束使用者傳過來的是表示角度的值,具體單位不限呢?這樣使用者想傳弧度或者角度都可以,反正都會自動轉化成我們需要的單位。聽起來很棒,我們試試吧:

// TurbineView 的屬性
public var bladeRotationPerSecond: Measurement = 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),用到了乘法操作符,非常方便。

下面來欣賞我們美麗的風車吧:

[iOS 10 day by day] Day 7:單位換算
根據風速旋轉的風車

擴充套件閱讀

本文中我們用到了蘋果提供的幾組單位,不過這些只是冰山一角;蘋果一共提供了 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同一篇譯文,非常感謝~