[SwiftUI] TextField と Formatter の使い方

SwiftUI

TextField は、ユーザーからの入力を受けることが コンポーネントです。
String を扱うときには、単純に使うことができますが、それ以外のデータを扱うときには工夫が必要です。String 以外のデータを扱う方法について説明します。

基本:String の入力

TextField は、引数として、String へ Binding されたものを受け取ることができ、テキストフィールドに入力された文字列をセットしてくれます。

Binding で渡していますので、親 View 側の変数も更新されます。

TextField 例 String

//
//  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()
  }
}

以下のように動きます。

TextField with String

「TextField with String」
これが、基本的な TextField を使った String の受け渡しです。

すこし応用:String 以外への対応時に使用する TextField

UI でやりとりされる情報は、String 以外にもあります。

そのような情報も、TextField を使って入出力することができます。Formatter を指定することで、そのような情報も処理することができるようになります。

TextField を使用するときに、formatter を指定します。

example code

init(_ title: S, value: Binding, 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
MEMO
Formatter は、SwiftUI に合わせて導入されたものではなく、以前から存在します。例えば、日付情報を地域に合わせて表示するときには、以前から DateFormatter が使用されていました。

各 Formatter については、Apple のドキュメントを参照してください。

以下では、NumberFormatter と DateFormatter を使います。

NumberFormatter 例

NumberFormatter は、数値に関しての Formatter です。

NumberFormatter 例

//
//  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()
  }
}

TextField with Int

「TextField with Int」

DateFormatter 例

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 は、日付の表示フォーマットや時刻の表示フォーマットなど たくさんの設定項目があります。

ここでは、日付だけを扱う設定で使っています。

TextField with DateFormatter

「TextField with DateFormatter」


けっこう応用:カスタム Formatter を使う

NumberFormatter や DateFormatter のように 既存の Formatter を使うのはそこまで難しくありませんが、対応する入力形式に限りがあります。
ユーザーから入力される様々なフォーマットに対応するために、カスタマイズした Formatter が必要となるケースがあります。

注意
既存の Formatter も様々な設定が行えるようになっています。Apple のドキュメントでもカスタム Formatter を作る前に、既存の Formatter でできないか考えなさい と書いています。

金額 Formatter を作る

家計簿ソフトを開発していることもあり、金額を表す ¥ マークが入力されても扱えるような Formatter を作ってみます。

今回は、「表示するときには円記号を付与する」 と 「入力として扱うときには不要な文字列を削除して評価する」 というような Formatter を実装しました。

カスタム Formatter を作るときのポイント

カスタマイズするときにポイントとなる関数は、以下の2つです。

example code

func string(for obj: Any?) -> String?

「対象から、表示用の文字列を作る」ときにコールされます。

Apple のドキュメントは、こちら

example code

func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 
                for string: String, 
   errorDescription error: AutoreleasingUnsafeMutablePointer?) -> Bool

「入力された文字列から対象を作る」ときにコールされます。

Apple のドキュメントは、こちら

以下のような形で実装します。

カスタム Formatter

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 を返す
  }
}

動かしてみる

カスタム Formatter 含めた全体コード

//
//  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()
  }
}

TextField with PriceFormatter

「TextField with PriceFormatter」
表示するときには、円マークが表示され、値としては、数値部分が使われていることがわかります。

まとめ

TextField の使い方
  • String は、そのまま TextField で扱うことができる
  • String 以外については、Formatter と TextField を組み合わせて扱う
  • カスタム Formatter を使うことで、表示するときと値として取り込むときの振る舞いをカスタマイズできる

説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です