忘備録も兼ねて、Measurement の使い方について改めて説明します。
Sponsor Link
Step1: Measurementを使って表現
Measurementを使うことで、Celsius/摂氏とFahrenheit/華氏の変換が簡単になることを試したいので、使います。
SwiftUIを使うので、複数のTextFieldで共有することを考えて、@Stateを使い定義します。
@State private var temperature = Measurement(value: 25.6, unit: UnitTemperature.celsius)
Measurementを使うことで、例えば、華氏での値が欲しければ、
temperature.converted(to: UnitTemperature.fahrenheit)
とすることで、華氏での値を取得することができます。
ここまでは、非常にわかりやすいです。
Step2: UIレイアウト
UIは、タイトルを一番上に配置して、その後、TextFieldを縦に並べることとしました。(摂氏、華氏だけでなく、ケルビン/Kでも表示してみました)
struct ContentView: View {
@State private var temp = Measurement(value: 25.6, unit: UnitTemperature.celsius)
var body: some View {
VStack(alignment: .center) {
Text("Temperature Converter")
TextField("Temp in Celsius", value: $temp, formatter: ????)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Temp in Celsius", value: $temp, formatter: ????)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Temp in kelvin", value: $temp, formatter: ????)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(50)
}
}
TextFieldを配置するところに難しさはありません。
ただ、TextFieldで値を表示するためのformatterでハマりました。
MeasurementFormatterを使うのが定石だと思うのですが、単位を指定して表示させるMeasurementFormatterがありません・・・😓
Localeに合わせて自動で表示してくれるのは非常に便利ですが、こういう時に困ることがわかりました。
Step3: MeasurementFormatterを拡張
なければ、作ってしまえということで以下のようなMeasurementForamtterの拡張版を作りました。表示する単位を指定することができます。
# 温度に使用するUnitTemperatureをうまく、Generics的に扱えませんでした・・・orz 今後の課題とします。
以下のようなGerericsを使ったクラスを作成しました。
# さらに、元々のUnitを使ったMeasurementを返すように改善しました。
class MeasurementFormatterWithDisplayUnit : MeasurementFormatter{
let originalUnit: T
let displayUnit: T
init(originalUnit: T, displayUnit:T) {
self.originalUnit = originalUnit
self.displayUnit = displayUnit
super.init()
}
required init?(coder: NSCoder) {
// get local unit, argument does not have any meaning
self.originalUnit = T.init(coder: coder)!
self.displayUnit = T.init(coder: coder)!
super.init(coder: coder)
}
override func string(for obj: Any?) -> String? {
guard let measurement = obj as? Measurement else { return nil }
let anotherFormatter = MeasurementFormatter()
anotherFormatter.unitOptions = .providedUnit
return anotherFormatter.string(from: measurement.converted(to: self.displayUnit))
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool {
guard let opt1 = obj else { return false }
// remove symbol (i.e. pick up only numbers
let numberExpressionCharacterSet = CharacterSet(charactersIn: "0123456789,.")
let numberString = string.components(separatedBy: numberExpressionCharacterSet.inverted).joined()
guard let number = self.numberFormatter.number(from: numberString) else { return false }
let displayMeasurement = Measurement(value: number.doubleValue, unit: self.displayUnit)
opt1.pointee = (Measurement(value: displayMeasurement.converted(to: self.originalUnit).value, unit: self.originalUnit) as AnyObject)
return true
}
}
記録のために、以下のGenericsでないバージョンも残しておきます。対応Dimensionごとに作る必要があります。
# クイックハック感が強いですが、その通りです。
class MeasurementFormatterForTemperature : MeasurementFormatter{
static let NumberExpressionCharacterSet = CharacterSet(charactersIn: "0123456789,.")
let displayUnit: UnitTemperature
init(displayUnit:UnitTemperature) {
self.displayUnit = displayUnit
super.init()
}
required init?(coder: NSCoder) {
// get local unit, argument does not make any meaning
self.displayUnit = Measurement(value: 0.0, unit: .celsius).unit
super.init(coder: coder)
}
override func string(for obj: Any?) -> String? {
guard let measurement = obj as? Measurement else { return nil }
let anotherFormatter = MeasurementFormatter()
anotherFormatter.unitOptions = .providedUnit
return anotherFormatter.string(from: measurement.converted(to: self.displayUnit))
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool {
guard let opt1 = obj else { return false }
// remove symbol (i.e. pick up only numbers
let numberString = string.components(separatedBy: MeasurementFormatterForTemperature.NumberExpressionCharacterSet.inverted).joined()
guard let number = self.numberFormatter.number(from: numberString) else { return false }
opt1.pointee = (Measurement(value: number.doubleValue, unit: UnitTemperature.celsius) as AnyObject)
return true
}
}
Step4: 完成したUnitConverter
struct ContentView: View {
@State private var temp = Measurement(value: 25.6, unit: UnitTemperature.celsius)
var body: some View {
VStack(alignment: .center) {
Text("Temp. Converter")
.font(.title)
TextField("Temp in Celsius", value: $temp, formatter: MeasurementFormatterWithDisplayUnit(originalUnit: UnitTemperature.celsius, displayUnit: UnitTemperature.celsius))
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Temp in Celsius", value: $temp, formatter: MeasurementFormatterWithDisplayUnit(originalUnit: UnitTemperature.celsius, displayUnit: UnitTemperature.fahrenheit))
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("Temp in kelvin", value: $temp, formatter: MeasurementFormatterWithDisplayUnit(originalUnit: UnitTemperature.celsius, displayUnit: UnitTemperature.kelvin))
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(50)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Step5: まとめ
当初、UnitConverterが10行くらいで作れます とかになると思いましたが、変換そのものではなく、表示でハマりました。
ただ、このアプリがあくまで練習用のアプリということでの特殊なシチュエーションな気もします。
説明は以上です。
Sponsor Link