《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 升)。
建立度量值
先從簡單的開始,假設我們有一品脫牛奶,想知道一品脫是多少公升。程式碼如下:
1 2 3 |
let milk = Measurement(value: 1, unit: UnitVolume.imperialPints) milk.converted(to: .liters) // 輸出 0.568261 L |
很簡單吧!定義出了某個單位的度量值之後,就只能把它換算為同一個單位空間的其他單位。converted
這一步會自動進行型別檢查,milk
變數的型別是 Measurement
,換算成的單位也要屬於 UnitVolume
這個單位空間。只能在同一個單位空間之內互相換算,這種限制是顯而易見的——不然,把公升換算成英里怎麼換算?
運算子
Measurement API 支援對度量值使用運算子。
如果我們想要 5 品脫的牛奶,就可以寫:
1 |
let fivePints = milk * 5 |
這樣會建立一個新的度量值,接下來我們就可以把它換算成另一個單位:
1 2 |
fivePints.converted(to: .cups) // 輸出 11.8387708333333 cup |
可以注意到,把程式碼放在 Playground 裡,或者把度量值列印出來,末尾會自動帶上它的單位。
當然,能用的運算子不只有乘號。還有其他的幾種,比如雙等號 ==
:
1 2 3 4 |
let kms = Measurement(value: 5, unit: UnitLength.kilometers) let meters = Measurement(value: 5000, unit: UnitLength.meters) kms == meters // true |
以及加號:
1 |
kms + meters // 10000.0 m |
Formatter
前面提到過,做本地化的時候我們經常需要為不同的 locale 顯示不同的單位。
除了新的 Measurement API,蘋果還提供了 MeasurementFormatter
,它能把度量值轉化成格式化字串。
預設情況下,measurement formatter 使用的是使用者當前的 locale。下面我們手動更改這一點,更改前後分別列印同一段兩個城市之間的距離,看看輸出有什麼變化:
1 2 3 4 5 6 7 8 |
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 上顯示風速:
1 2 3 4 5 6 7 |
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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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
,不過我們要把每秒鐘葉片轉動多少角度的數值傳給它。你可能會想到去定義一個屬性,類似這樣:
1 2 |
/// 葉片每秒轉動的角度,用弧度的形式表示 public var bladeRotationPerSecond: Double |
這樣定義沒什麼問題,也跟蘋果官方的 API 保持一致,角度是用弧度表示的。不過,怎麼防止使用者無意中傳了角度值而不是弧度值呢?你可能會說:“他們應該好好看看文件”。這句話有一定道理,不過萬一這個屬性沒有文件呢?並且,因為我們平常更習慣用角度值,所以這也是個容易不小心犯下的錯誤。
那我們能怎麼利用上 Measurement 框架,只約束使用者傳過來的是表示角度的值,具體單位不限呢?這樣使用者想傳弧度或者角度都可以,反正都會自動轉化成我們需要的單位。聽起來很棒,我們試試吧:
1 2 3 4 5 6 |
// TurbineView 的屬性 public var bladeRotationPerSecond: Measurement = Measurement(value: 0, unit: UnitAngle.degrees) { didSet { rotate() } } |
在 viewController 裡,我們可以用下面這段程式碼來計算一秒旋轉多少角度。
1 2 3 4 5 6 7 8 9 10 |
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
譯者:戴倉薯,本文翻譯時參考了 simpletonking 的同一篇譯文,非常感謝~