📏

Measurement.FormatStyleのあれこれ

2023/05/05に公開

Measurement (数値&単位のデータ)を表示する時、Measurement.FormatStyle を使ったやり方があります。この記事ではこの Measurement.FormatStyle について書きます。

おさらい

Measurement

  • 単位

を保持するもの。

作成するときは数値と単位を指定する。

Measurement(value: 100, unit: UnitTemperature.fahrenheit) 華氏の100度

Measurement.formatted()

Measurement には formatted() というメソッドがある。String を返す。

var hundred = Measurement(value: 100, unit: UnitTemperature.fahrenheit)

print(hundred.formatted()) //出力はOSの設定による

出力はOSの設定によるので、華氏に設定してあれば100°Fだし、摂氏に設定してあれば37.777778°Cと表示します。
以下はVenturaの設定画面の例。


Venturaの設定画面

Measurement.FormatStyle

formatted() の出力結果がOSの設定によるのは使いにくいでかんわという場合は、形式を指定する次のものがあります。

func formatted<S>(_ style: S) -> S.FormatOutput where S : FormatStyle, S.FormatInput == Measurement<UnitType>

この引数に与えるものが FormatStyle で、本日の主役になります。
プロトコルの FormatStyle があり、それを実装したstructの Measurement.FormatStyle があります。同じ名前なのでややこしいです。FormatStyle はSwiftのみで、iOS15から。

formatted(引) の話に戻ります。引数の FormatStyle のインスタンスを作るときは使用頻度の高い設定を抜粋した次のメソッドが便利です。

static func measurement<UnitType>(
    width: Measurement<UnitType>.FormatStyle.UnitWidth,
    usage: MeasurementFormatUnitUsage<UnitType> = .general,
    numberFormatStyle: FloatingPointFormatStyle<Double>? = nil
) -> Measurement<UnitType>.FormatStyle where UnitType : Dimension

定義の見た目はゴツいですが、使い方は次のようになります。

var hundred = Measurement(value: 100, unit: UnitLength.meters)
hundred.formatted(.measurement(
                    width: .narrow, 
		    usage: .general))

引数で設定した2つの意味は

  • width 表示の文字の長さ metersやmなどを切り替えます
  • usage 何の単位を使うかの判断方式

です。この他に数値の形式を指定する引数もありますが、世の中に情報が多いと思うのでこの記事では触れません。
サンプルを見てください。まずはwidthを切り替えた様子です。

var hundred1 = Measurement(
                value: 100, unit: UnitLength.meters)
hundred1.formatted(.measurement(
                        width: .narrow, 
			usage: .general)) //100m
hundred1.formatted(.measurement(
                        width: .abbreviated, 
			usage: .general)) //100 m
hundred1.formatted(.measurement(
                        width: .wide, 
			usage: .general)) //100 meters

widthを切り替えると単位の文字列の長さが変わります。abbreviatedとは省略されたという意味です。abbreviatedよりもnarrowの方が短い。言語の仕様で3パターン用意しておいて、そこに何を当てはめるかは各単位ごとに決めるという感じのようです。

次にusageを切り替えます。

var hundred2 = Measurement(
                value: 100, unit: UnitLength.millimeters)
hundred2.formatted(.measurement(
                        width: .narrow, 
			usage: .asProvided)) //100mm
hundred2.formatted(.measurement(
                        width: .narrow, 
			usage: .general)) //10cm

usageを .asProvided にすると設定された通りの単位(ミリメートル)を選択しました。
.general はより自然にcmを選択しました。
usageの違いを今度は温度で確認します。

var hundred3 = Measurement(
                value: 100, unit: UnitTemperature.fahrenheit)
hundred3.formatted(.measurement(
                        width: .abbreviated, 
			usage: .asProvided)) //100℉
hundred3.formatted(.measurement(
                        width: .abbreviated, 
			usage: .general)) //37.777778℃

華氏でデータを作成し、.asProvided で表示すると華氏になります。.generalにすると、実行環境の設定が摂氏なので摂氏になりました。

usageは単位ごとにいろいろあります。長さだと、人、道、レンズの焦点などがあります。

ローカライズ

locale() というメソッドがあります。このように使います。

var hundred4 = Measurement(value: 100, unit: UnitTemperature.fahrenheit)
hundred4.formatted(.measurement(width: .wide, usage: .asProvided)
                       .locale(Locale(identifier: "ja_JP")))

measurement の生成物に対して locale を投げる形です。
ローカライズには

  • 地域 (単位もこちらに含まれる)
  • 言語

の2要素があります。locale の挙動を見る時はこれを意識する必要があります。

usage: .asProvided のとき

言語の方だけに効果があるようです。つまり単位は .measurement() で決定し、その表示言語を .locale() で切り替えました。

var hundred4 = Measurement(value: 100, unit: UnitTemperature.fahrenheit)
hundred4.formatted(.measurement(width: .wide, usage: .asProvided)
                       .locale(Locale(identifier: "ja_JP"))) //華氏100度
hundred4.formatted(.measurement(width: .wide, usage: .asProvided)
                       .locale(Locale(identifier: "en_US"))) //100 degrees Fahrenheit


var hundred5 = Measurement(value: 100, unit: UnitTemperature.celsius)
hundred5.formatted(.measurement(width: .wide, usage: .asProvided)
                       .locale(Locale(identifier: "ja_JP"))) //摂氏100度
hundred5.formatted(.measurement(width: .wide, usage: .asProvided)
                       .locale(Locale(identifier: "en_US"))) //100 degrees Celsius

usage: .general のとき

地域と言語両方に適用されました。

var hundred4 = Measurement(value: 100, unit: UnitTemperature.fahrenheit)
hundred4.formatted(.measurement(width: .wide, usage: .general)
                       .locale(Locale(identifier: "ja_JP"))) //摂氏38度
hundred4.formatted(.measurement(width: .wide, usage: .general)
                       .locale(Locale(identifier: "en_US"))) //100 degrees Fahrenheit


var hundred5 = Measurement(value: 100, unit: UnitTemperature.celsius)
hundred5.formatted(.measurement(width: .wide, usage: .general)
                       .locale(Locale(identifier: "ja_JP"))) //摂氏100度
hundred5.formatted(.measurement(width: .wide, usage: .general)
                       .locale(Locale(identifier: "en_US"))) //212 degrees Fahrenheit

温度だけにアクセスを許されたものがある

  • 100 degrees
  • 100 degrees Fahrenheit

のように摂氏/華氏の情報を表示するかどうかを決める設定があります。要はFahrenheitという言葉を表示しないように出来ます。

hidesScaleName trueにすると摂氏/華氏の情報を表示しない。

ただし、usageが asProvided のときのみ有効になります。つまり、環境により単位が変更される可能性があるときは、この摂氏/華氏の情報を表示しない機能は無効です。

参考

↓プロトコルのFormatStyle
https://developer.apple.com/documentation/foundation/formatstyle/

↓structのFormatStyle
https://developer.apple.com/documentation/foundation/measurement/formatstyle/

https://fuckingformatstyle.com/measurement-style/

https://ampersandsoftworks.com/posts/measurements-and-their-formatting/

読む必要がないあとがき

iOSアプリ開発の世界では昔からデータと出力形式は分けるものだった。有名なところで、

  • Date
  • DateFormatter

というのがある。Dateは数直線上のある一点を表すもの。DateFormatterは出力形式。このように分割してDateをシンプルにするやり方だった。

しかし最近は、データに出力形式を含むやり方になってきた。このやり方は データ.出力せよ() と書けるので、正直こっちの方がコードは書きやすい。

Discussion