【Apple発の新ローカライズ方法】iOS10から導入されたMeasurements & Unitsのすべて #単位

# この記事について

せっかく開発したアプリはより多くの人に使ってほしいですよね。

ということで、ローカライズという言語や地域ごとに表示を変える仕組みを紹介したいと思います。
そして、今回は特にiOS10から新しく導入された数量や単位を扱う仕組みについて紹介します。

---

## 紹介

Measurements and Unitsを書くときの記述量を少なくするライブラリを作りました。

* [ken0nek/SwiftMeasurement](https://github.com/ken0nek/SwiftMeasurement)

# Measurements & Units とは

iOS10からFoundationフレームワークに追加された、数量と単位を扱う新しい仕組みです。
この仕組みを使うことによって、国や地域ごとの単位の表示を気にすることなくアプリの開発ができるようになります。

* [Pokemon GO、アメリカ人にメートル法を教える｜ギズモード・ジャパン](http://www.gizmodo.jp/2016/07/pokemon-go-is-secretly-teaching-americans-the-metric-system.html)

という記事も一時期話題になりましたが、
この仕組を使えばアメリカではマイル、日本ではキロメートル、また違う国ではフィートのように簡単に対応することができます👀

ライフログ系、ワークアウト系のアプリには特に相性の良い機能です。

# Measurementについて

Measurementは以下のように宣言され、数量と`単位`を持ちます。

```swift

/// A `Measurement` is a model type that holds a `Double` value associated with a `Unit`.
///
/// Measurements support a large set of operators, including `+`, `-`, `*`, `/`, and a full set of comparison operators.
public struct Measurement<UnitType : Unit> : ReferenceConvertible, Comparable, Equatable {

    public typealias ReferenceType = NSMeasurement

    /// The unit component of the `Measurement`.
    public let unit: UnitType

    /// The value component of the `Measurement`.
    public var value: Double

    /// Create a `Measurement` given a specified value and unit.
    public init(value: Double, unit: UnitType)

    /// The hash value.
    ///
    /// Hash values are not guaranteed to be equal across different executions of
    /// your program. Do not save hash values to use during a future execution.
    public var hashValue: Int { get }
}
```

---

たとえば、20kmを表現したいときは、valueを20に、unitにキロメートル（`UnitLength.kilometers`）を指定します。

`Measurement型`の変数同士は（`次元`が同じなら）足し算、引き算が可能で、
`Double型`の変数とは、掛け算、割り算もすることができます。

```swift

let distanceTraveled = Measurement(value: 20, unit: UnitLength.kilometers)
// 20.0 km

let distanceToGo = Measurement(value: 1.975, unit: UnitLength.kilometers)
// 1.975 km

let totalDistance = distanceTraveled + distanceToGo // 21.975 km
let tripleDistance = 3 * distanceToGo // 5.925 km
let halfDistance = distanceToGo / 2 // 0.9875 km
```

次からは上で出てきた、単位と次元について説明します。

# Unitについて

まずは単位（`Unit`）についてです。

単位の特徴をまとめます

- 全ての単位は記号を持つ（メートルだったら`m`）
- 単位は次元を持つことがある（持たないこともある）
- 同一次元内なら等価な変換を行なうことができる（`m ⇔ ft`） 

![160630-wwdc2016-wantedly-new.007.jpeg](https://qiita-image-store.s3.amazonaws.com/0/58141/49065a64-419e-ddfc-4660-9c8344cefd0c.jpeg)

よって、`Unit`の定義を見てみると`String型`の`symbol`（記号）だけが宣言されています。


```swift

open class Unit : NSObject, NSCopying, NSSecureCoding {

    
    open var symbol: String { get }

    
    public init(symbol: String)
}
```

# Dimention

次に、次元の話題に移ります。

- 次元は単位の種類を表す（長さ、重さ、速さなど）
- 異なる単位で表すことができる（`km, m, ft, mi`など）
- 基本単位を必ず持つ（長さ -> メートル、時間 -> 秒など）
- 同一次元内で単位変換を行なうことができる

![160630-wwdc2016-wantedly-new.009.jpeg](https://qiita-image-store.s3.amazonaws.com/0/58141/39137eb8-dfd9-bf6b-3cec-412be9e4f574.jpeg)

`Dimension`の定義を見てみると、
単位変換を行なうための`UnitConverter型`の`converter`と、
次元内で基本となる単位である`baseUnit`を持ちます

```swift

open class Dimension : Unit, NSSecureCoding {

    
    @NSCopying open var converter: UnitConverter { get }

    
    public init(symbol: String, converter: UnitConverter)

    
    /*
     This class method returns an instance of the dimension class that represents the base unit of that dimension.
     e.g.
        NSUnitSpeed *metersPerSecond = [NSUnitSpeed baseUnit];
     */
    open class func baseUnit() -> Self
}

```

`Dimension`を継承した単位は既に170個以上定義されていて、逐一単位を定義する必要はありません。

面積、加速度、電圧、角度、周波数など国際単位系に準拠した単位をすぐに利用することができます。

![160630-wwdc2016-wantedly-new.011.jpeg](https://qiita-image-store.s3.amazonaws.com/0/58141/80886818-3ad8-fb7c-d5ff-dc4fc2eee7af.jpeg)

## Dimensionの良いところ

Dimensionの良いところは同一次元ないなら単位を気にすることなく計算できる点です。

---

例として先程の変数の一方の単位をフィートに変えてみます。
フィートとメートルは同じ次元なので、自分で変換式を定義することなく計算でき、
結果は長さの基本単位であるメートルで出力されます。

```swift

let distanceTraveled = Measurement(value: 2, unit: UnitLength.kilometers)
// 2.0 km

//let distanceToGo = Measurement(value: 1.975, unit: UnitLength.kilometers)
//let totalDistance = distanceTraveled + distanceToGo

let distanceToGo = Measurement(value: 11.3, unit: UnitLength.feet)
// 11.3 ft

let totalDistance = distanceTraveled + distanceToGo
// 2003.44424 m
```

# MeasurementFormatter

最後にこれらの値を表示する仕組みについて説明します。

![160630-wwdc2016-wantedly-new.020.jpeg](https://qiita-image-store.s3.amazonaws.com/0/58141/4236a6fc-df08-74b3-2ce0-947084d9ce5e.jpeg)

いままでもFormatterといえば、NumberFormatter、DateFormatterなどがありましたが、そこに新たに仲間入りしたのがMeasurementFormatterです。

```swift

public class MeasurementFormatter : Formatter {

    public var unitOptions: MeasurementFormatter.UnitOptions

    public var unitStyle: Formatter.UnitStyle

    @NSCopying public var locale: Locale!

    @NSCopying public var numberFormatter: NumberFormatter!
    
    public func string(from measurement: Measurement<Unit>) -> String
    
    public func string(from unit: Unit) -> String
}

```

MeasurementFommatterはデフォルトで現在の地域の単位が優先されるようになっています。

たとえば、valueを5、単位をマイルで設定したdistanceは、現在の設定地域が日本のとき8.047 kmと自動的に変換されて表示されます！！

```swift

let formatter = MeasurementFormatter()
let distance = Measurement(value: 5, unit: UnitLength.miles)

formatter.string(from: distance) // 8.047 km
```

`unitOptions`や`unitStyle`を用いればより詳細な指定をすることもできます。

## UnitStyle

`short`、`medium`、`long`の三種類があり、単位の表示が変わります

```swift

/*
* There are 3 widths: long, medium, and short.
* For example, for English, when formatting "3 pounds"
* Long is "3 pounds"; medium is "3 lb"; short is "3#";
*/
@available(iOS 8.0, *)
public enum UnitStyle : Int {
        
    case short
    case medium
    case long
}
```

## UnitOptions

```swift

public struct UnitOptions : OptionSet {

    public init(rawValue: UInt)
        
    public static var providedUnit: MeasurementFormatter.UnitOptions { get }

    public static var naturalScale: MeasurementFormatter.UnitOptions { get }

    public static var temperatureWithoutUnit: MeasurementFormatter.UnitOptions { get }
}
```

- `.providedUnit`は指定された単位で表示したいときに使います
- `.naturalScale`は"自然な表示"が行なわれます（AppleWatchなどの小さい画面に表示されるときに便利）
- `.temperatureWithoutUnit`は`C`や`F`などの単位なしに温度を表示するときに使います

![160630-wwdc2016-wantedly-new.024.jpeg](https://qiita-image-store.s3.amazonaws.com/0/58141/0c6cdb8e-dbc7-d5bf-38be-d98ccd3208b9.jpeg)

# まとめ

このiOS10から導入されたMeasurements & Unitsという仕組みを使えば、開発者が単位変換に煩わされることなく数量の表示、変換、ローカライズをすることができ、よりユーザーにやさしいアプリにすることができると思います。

ぜひ参考にしてみてください！

---

# 発展編

- Measurement
- Unit
- Dimension
- MeasurementFormatter

の4つで基本的な実装は可能なのですが、「アプリ内で独自の単位を作りたい！」などの要求が出てきたときに対応するために発展編を用意しました。

## 発展編 - カスタム単位を作る

メートルやフィートなどが出てきましたが日本古来の単位がない、、と思ったので作ってみましょう。

例として、今回は単位「尺」を作ってみたいと思います。

* [尺貫法 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%B0%BA%E8%B2%AB%E6%B3%95)

によると、「尺」は33分の10メートルらしいです。

なので、「尺」は以下のように定義できます。

```swift

extension UnitLength {
    
    class var syaku: UnitLength {
        return UnitLength(symbol: "尺",
                          converter: UnitConverterLinear(coefficient: 10/33))
    }
}
```

上のコードを説明すると、
「尺」の次元は長さ(`[L]`)なので、`UnitLength`内で宣言します。
そして、記号は"尺"なので、symbolには"尺"を指定します。
さらに、`1尺 ⇔ 10/33 m`なのでconverterには`UnitConverterLinear(coefficient: 10/33))`を指定します。

このように宣言するだけで、

```swift

let lengthInShayku = Measurement(value: 10, unit: UnitLength.syaku)
// 10.0 尺

let lengthInYards = lengthInShayku.converted(to: UnitLength.yards)
// 3.31397969193245 yd

let lengthInFeet = Measurement(value: 5.3, unit: UnitLength.feet)
// 5.3 ft

lengthInShayku + lengthInFeet
// 4.64574303030303 m
```

単位「尺」を持つ変数を宣言したり、
その値をヤードに変換したり、
他の`UnitLength`の単位を持つ変数との演算が可能になります！👀

さきほどから登場している`UnitConverter`について少し説明したいと思います。

### UnitConverter

UnitConverterは同一次元内でbaseUnitと他の単位の変換を担うものです。

UnitConverterとそれを継承したUnitConverterLinearの2種類があります。

単位の変換はほとんど線形なので後者を使うことが多いと思います。

さきほどの「尺」の例だと、定数はなく

[mの値] = 10/33 * [尺の値]

なので、coefficient（係数）に 10/33を指定しました。

また、華氏[F]と摂氏[C]の変換だと、

F = 9/5 * C + 32

となるので、係数に9/5、定数に32を指定すればよいということになります。

y = ax + bという１次関数を思い出して、以下の式に当てはめる。
[baseUnitValue] = coefficient * [value] + constant

## 発展編 - カスタム次元を作る

カスタム単位の作り方の次はカスタム次元を作ってみましょう。
カスタム次元の例として、ある出来事の感激度、印象度を考えてみました。

ポイントはDimensionを継承し、baseUnitを定義することです。

```swift

class UnitImpression : Dimension {

    class var good: UnitImpression {
        return UnitImpression(symbol: "👍", converter: UnitConverterLinear(coefficient: 1))
    }

    class var great: UnitImpression {
        return UnitImpression(symbol: "👏", converter: UnitConverterLinear(coefficient: 10))
    }

    class var wonderful: UnitImpression {
        return UnitImpression(symbol: "😘", converter: UnitConverterLinear(coefficient: 50))
    }

    class var lifeChanging: UnitImpression {
        return UnitImpression(symbol: "😍", converter: UnitConverterLinear(coefficient: 2000))
    }

    override class func baseUnit() -> UnitImpression {
        return good
    }
}
```

## 発展編 - アプリに組み込む

たとえば、プレゼンのまとめアプリを作るとしましょう。
それに必要な`Presentation`型を定義してみます。
発表者の名前、タイトル、プレゼン中に動いた距離、発表時間、印象度を入れてみました。
それに加えて、プレゼン中にどれだけ動いたかを測る指標も加えてみました。
これを組み込むのもMeasuremetの仕組みを使えば元の単位を気にすることなく簡単に定義することができます。

```swift

struct Presentation {
    let speaker    : String
    let title      : String
    let movement   : Measurement<UnitLength>
    let duration   : Measurement<UnitDuration>
    let impression : Measurement<UnitImpression>

    var movementRate: Measurement<UnitSpeed> {
        let distanceInMeters = movement.converted(to: .meters)
        let durationInSeconds = duration.converted(to: .seconds)
        return Measurement(
            value: distanceInMeters.value / durationInSeconds.value,
            unit: UnitSpeed.metersPerSecond)
    }
}
```

使ってみましょう。

```swift

let presentation =
    Presentation(
        speaker:    "ken0nek",
        title:      "Measurements & Units",
        movement:   Measurement(value:  60, unit: UnitLength.centimeters),
        duration:   Measurement(value:  10, unit: UnitDuration.minutes),
        impression: Measurement(value: 100, unit: UnitImpression.lifeChanging))

presentation.movementRate // 000.1 m/s

presentation.impression // 100.0 😍
presentation.impression.converted(to: UnitImpression.baseUnit()) // 50000.0 👍

presentation.movement.converted(to: UnitLength.syaku) // 1.98 尺
```

以上のように、単位を含めた値の取得、単位の変換が簡単にできました。

# 発展編 - まとめ

発展編では、カスタム単位やカスタム次元の作り方、それのアプリへの組み込み方を紹介しました。
少し長く難しい説明になってしまったかと思いますが参考になれば幸いです。

[iOSアプリケーション開発コースメンター: とみー]

---

※ 本記事はWWDC2016で公開されている情報をもとに掲載しております。

※ 本記事は以下の発表をQiitaにまとめたものです。
* [Measurements and Units](http://www.slideshare.net/KenTominaga/measurements-and-units-63696729)
* [WWDC16. Meetup @Wantedly with 日本経済新聞社 - connpass](https://wantedly.connpass.com/event/33967/)

# 参考

* [Measurement - Apple Developer Documentation](https://developer.apple.com/reference/foundation/measurement)
* [Unit - Apple Developer Documentation](https://developer.apple.com/reference/foundation/nsunit)
* [Dimension - Apple Developer Documentation](https://developer.apple.com/reference/foundation/nsdimension)
* [UnitConverter - Apple Developer Documentation](https://developer.apple.com/reference/foundation/unitconverter)
* [UnitConverterLinear - Apple Developer Documentation](https://developer.apple.com/reference/foundation/unitconverterlinear)
* [MeasurementFormatter - Apple Developer Documentation](https://developer.apple.com/reference/foundation/nsmeasurementformatter)
* [About Internationalization and Localization](https://developer.apple.com/library/prerelease/content/documentation/MacOSX/Conceptual/BPInternational/Introduction/Introduction.html)
* [Measurements and Units - WWDC 2016 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2016/238/)
* [238_measurements_and_units.pdf](http://devstreaming.apple.com/videos/wwdc/2016/238eemv232k9lp2px30/238/238_measurements_and_units.pdf)