String を扱うときには、単純に使うことができますが、それ以外のデータを扱うときには工夫が必要です。String 以外のデータを扱う方法について説明します。
Xcode13.3/Swift5.6/iOS15.4 で試すと、以下の2点で問題が発生します。
– Formatter#getObjectValue の sigunature が変更されたことによるコンパイルエラー
– getObjectValue で返した値が反映されない
1つ目への対応は難しくありませんが、2つ目への対応方法は不明です。
LocalBinding を使用して変換するほうが安定する気がします。
[SwiftUI] Binding のおさらいと Local Binding
Sponsor Link
基本:String の入力
TextField は、引数として、String へ Binding されたものを受け取ることができ、テキストフィールドに入力された文字列をセットしてくれます。
Binding で渡していますので、親 View 側の変数も更新されます。
//
// ContentView.swift
// TextFieldWithFormatter
//
// Created by Tomoaki Yagishita on 2020/10/13.
//
import SwiftUI
struct ContentView: View {
@State private var strValue:String = ""
var body: some View {
VStack {
Group {
TextField("input String", text: $strValue)
Text("input value : \(strValue)")
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
以下のように動きます。
すこし応用:String 以外への対応時に使用する TextField
UI でやりとりされる情報は、String 以外にもあります。
そのような情報も、TextField を使って入出力することができます。Formatter を指定することで、そのような情報も処理することができるようになります。
TextField を使用するときに、formatter を指定します。
init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) where S : StringProtocol
Apple のドキュメントは、こちら。
用意されている Formatter
Formatter は、以下のような Formatter が用意されています。
- ByteCountFormatter
- DateFormatter
- DateComponentsFormatter
- DateIntervalFormatter
- EnergyFormatter
- LengthFormatter
- MassFormatter
- NumberFormatter
- PersonNameComponentsFormatter
Formatter は、SwiftUI に合わせて導入されたものではなく、以前から存在します。例えば、日付情報を地域に合わせて表示するときには、以前から DateFormatter が使用されていました。
各 Formatter については、Apple のドキュメントを参照してください。
以下では、NumberFormatter と DateFormatter を使います。
NumberFormatter 例
NumberFormatter は、数値に関しての Formatter です。
//
// ContentView.swift
// TextFieldWithFormatter
//
// Created by Tomoaki Yagishita on 2020/10/13.
//
import SwiftUI
struct ContentView: View {
@State private var strValue:String = ""
@State private var intValue:Int = 0
var intFormatter: Formatter = NumberFormatter() // NumberFormatter を素のままで使用
var body: some View {
VStack {
Group {
TextField("input String", text: $strValue)
Text("input value : \(strValue)")
}
.padding()
Group {
TextField("input Int", value: $intValue, formatter: intFormatter)
Text("Int value: \(intValue)")
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DateFormatter 例
DateFormatter を使うことで、日付情報を変換できます。
//
// ContentView.swift
// TextFieldWithFormatter
//
// Created by Tomoaki Yagishita on 2020/10/13.
//
import SwiftUI
struct ContentView: View {
@State private var strValue:String = ""
@State private var intValue:Int = 0
var intFormatter: Formatter = NumberFormatter()
@State private var dateValue: Date = Date()
var dateFormatter: DateFormatter
@State private var moneyValue: Decimal = 0
var priceFormatter: PriceFormatter = PriceFormatter()
init() {
self.dateFormatter = DateFormatter()
self.dateFormatter.dateStyle = .short // 日付は、短い表示形式
self.dateFormatter.timeStyle = .none // 時刻は、表示しない
}
var body: some View {
VStack {
Group {
TextField("input String", text: $strValue)
Text("input value : \(strValue)")
}
.padding()
Group {
TextField("input Int", value: $intValue, formatter: intFormatter)
Text("Int value: \(intValue)")
}
.padding()
Group {
TextField("date Int", value: $dateValue, formatter: dateFormatter)
Text("Date value: \(dateValue)")
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DateFormatter は、日付の表示フォーマットや時刻の表示フォーマットなど たくさんの設定項目があります。
ここでは、日付だけを扱う設定で使っています。
けっこう応用:カスタム Formatter を使う
NumberFormatter や DateFormatter のように 既存の Formatter を使うのはそこまで難しくありませんが、対応する入力形式に限りがあります。
ユーザーから入力される様々なフォーマットに対応するために、カスタマイズした Formatter が必要となるケースがあります。
既存の Formatter も様々な設定が行えるようになっています。Apple のドキュメントでもカスタム Formatter を作る前に、既存の Formatter でできないか考えなさい と書いています。
金額 Formatter を作る
家計簿ソフトを開発していることもあり、金額を表す ¥ マークが入力されても扱えるような Formatter を作ってみます。
今回は、「表示するときには円記号を付与する」 と 「入力として扱うときには不要な文字列を削除して評価する」 というような Formatter を実装しました。
カスタム Formatter を作るときのポイント
カスタマイズするときにポイントとなる関数は、以下の2つです。
func string(for obj: Any?) -> String?
「対象から、表示用の文字列を作る」ときにコールされます。
Apple のドキュメントは、こちら。
func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool
「入力された文字列から対象を作る」ときにコールされます。
Apple のドキュメントは、こちら。
以下のような形で実装します。
class PriceFormatter: Formatter {
let digits = "0123456789"
override func string(for obj: Any?) -> String? {
guard let decimal = obj as? Decimal else { return nil } // Decimal が渡されなかったら処理しない
return String(format: "¥%.0f", Double(truncating: decimal as NSNumber)) // ¥マークを付与して表示
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool {
let digitString = string.filter{digits.contains($0)} // 渡された文字列から数字以外を外す
obj?.pointee = Decimal(string: digitString)! as AnyObject // 文字列から Decimal を作成して返す
return true // 処理できたので、true を返す
}
}
動かしてみる
//
// ContentView.swift
// TextFieldWithFormatter
//
// Created by Tomoaki Yagishita on 2020/10/13.
//
import SwiftUI
class PriceFormatter: Formatter {
let digits = "0123456789"
override func string(for obj: Any?) -> String? {
guard let decimal = obj as? Decimal else { return nil }
return String(format: "¥%.0f", Double(truncating: decimal as NSNumber))
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool {
let digitString = string.filter{digits.contains($0)}
obj?.pointee = Decimal(string: digitString)! as AnyObject
return true
}
}
struct ContentView: View {
@State private var strValue:String = ""
@State private var intValue:Int = 0
var intFormatter: Formatter = NumberFormatter()
@State private var dateValue: Date = Date()
var dateFormatter: DateFormatter
@State private var moneyValue: Decimal = 0
var priceFormatter: PriceFormatter = PriceFormatter()
init() {
self.dateFormatter = DateFormatter()
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeStyle = .none
}
var body: some View {
VStack {
Group {
TextField("input String", text: $strValue)
Text("input value : \(strValue)")
}
.padding()
Group {
TextField("input Int", value: $intValue, formatter: intFormatter)
Text("Int value: \(intValue)")
}
.padding()
Group {
TextField("date Int", value: $dateValue, formatter: dateFormatter)
Text("Date value: \(dateValue)")
}
.padding()
Group {
TextField("price value", value: $moneyValue, formatter: priceFormatter)
Text("price value: \(Double(truncating: NSDecimalNumber(decimal:moneyValue)), specifier: "%.f")")
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
まとめ
- String は、そのまま TextField で扱うことができる
- String 以外については、Formatter と TextField を組み合わせて扱う
- カスタム Formatter を使うことで、表示するときと値として取り込むときの振る舞いをカスタマイズできる
説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。
Sponsor Link