[Swift] FormatStyle の作り方

     
⌛️ 2 min.
WWDC21で導入された FormatStyle, ParseStrategy, ParseableFormatStyle を説明してみます。複数回に分けて説明します。今回は FormatStyle を作ってみます。
FormatStyle, ParseableFormatStyle の 使い方/作り方を理解するための記事シリーズです。

[Swift] FormatStyle の使い方
[Swift] ParseableFormatStyle の使い方
[Swift] FormatStyle の作り方
SwiftUI2021 [Swift] ParseableFormatStyle の作り方

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.2.1
  • Xcode 14.3 Beta
  • iOS 16.0

これまでの記事

前回の記事では、FormatStyle の使い方について説明しました。
・FormatStyle を使用すると データ を 適切な表現の String に変換できる
[Swift] FormatStyle の使い方

・ParseableFormatStyle を使用すると適切な表現の String をデータに変換できる
[Swift] ParseableFormatStyle の使い方

FormatStyle を作る

よくある例ですが、PhoneNumber というデータを作って、そのデータをさまざまな表記で表示できるようにしてみます。

電話番号

対象とする電話番号について少し確認しておきます。

東京のような市外局番が 2桁 で始まる電話番号は、 XX – XXXX – XXXX であり、
神奈川/横浜のような市外局番が3桁で始まる電話番号は、XXX – XXX – XXXX となります。
つまり 電話番号は、2桁 – 4桁 – 4桁 フォーマットか、3桁 – 3桁 – 4桁 フォーマットかのどちらかのようです。そして、いずれにしても、10桁 です。

電話番号Q&A / 総務省のページは、こちら

※ 携帯電話の番号は 3桁 – 4桁 – 4桁 で合計 11桁で、少し異なります。携帯番号も対応するのであれば、すこし手を入れる必要があります。

番号を3つに区切られますが、それぞれを 市外局番(areaCode)、市内局番(localCode)、番号(number) と呼ぶことにします。

実現したい表記

以下のフォーマットを使い分けたいとします。(市外局番3桁、市内局番3桁で例示してます)

・(XXX)XXX-XXXX (areaParenthesis)
・XXX(XXX)XXXX (localParenthesis)
・XXX-XXX-XXXX (hyphen)

カッコ内は、それぞれの表記名称です。

準備

FormatStyle を作るための準備として、最初に、PhoneNumber を定義します。

struct PhoneNumber

PhoneNumber は、電話番号データを持つための struct です。

public struct PhoneNumber: Equatable {
    let areaCode: String     // 2-3 digits
    let localCode: String // 3-4 digits
    let number: String   // should be 4 digits
    // note: acceptable format 2-4-4  or 3-3-4
}

順番に、市外局番・市内局番・番号 を”文字列” で定義しています。

FormatStyle 定義

PhoneNumber 向けの FormatStyle を定義していきます。

TDD で進めていきたいので、テストコードを書いてから実装をします。

FormatStyle は、実際に使用されるときは、データ型.formatted(…) のような形で使用されますが、内部実装としては formatStyle.format(データ型) というメソッドが使われていますので、そちらのメソッドのテストから書いていきます。

テストコード .format

ということで、まずは、.format のテストを書きます。

func test_FormatStyle() async throws {
    let phone = PhoneNumber(areaCode: "045", localCode: "123", number: "4567")

    XCTAssertEqual(PhoneNumberFormatStyle(.hyphen).format(phone), "045-123-4567")
    XCTAssertEqual(PhoneNumberFormatStyle(.areaParenthesis).format(phone), "(045)123-4567")
    XCTAssertEqual(PhoneNumberFormatStyle(.localParenthesis).format(phone), "045(123)4567")
}

3種類の表記をテストしています。

PhoneNumberFormatStyle実装

テストが書けたので実装していきます。

extension PhoneNumber {
    public struct PhoneNumberFormatStyle : FormatStyle {
        // (1)
        public typealias FormatInput = PhoneNumber
        public typealias FormatOutput = String

        // (2)
        public enum PhoneNumberStyle: Codable {
            case areaParenthesis   // (XX)XXXX-XXXX
            case localParenthesis  // XX(XXXX)XXXX
            case hyphen            // XX-XXXX-XXXX
        }

        var style: PhoneNumberStyle
        init(_ style: PhoneNumberStyle = .hyphen) {
            self.style = style
        }

        // (3)
        public func format(_ phoneNumber: PhoneNumber) -> String {
            switch style {
            case .areaParenthesis:
                return "(" + phoneNumber.areaCode + ")" + phoneNumber.localCode + "-" + phoneNumber.number
            case .localParenthesis:
                return phoneNumber.areaCode + "(" + phoneNumber.localCode + ")" + phoneNumber.number
            case .hyphen:
                return phoneNumber.areaCode + "-" + phoneNumber.localCode + "-" + phoneNumber.number
            }
        }
    }
}
コード解説
  1. FormatStyle の Input は、PhoneNumber で Output は String と定義します
  2. 実際のスタイルを enum で定義しています
  3. format メソッドの実装です

上記のコードでテストが通りますので、基本的な実装はできたことがわかります。
# デフォルトをハイフン表記にしていますが、深い意味はありません。

テストコード .formatted

実際に使用する時には、データ型.formatted として使いたいので、そのような使い方のテストも書いていきます。

func test_Formatted() async throws {
    let phone = PhoneNumber(areaCode: "045", localCode: "123", number: "4567")

    XCTAssertEqual(phone.formatted(.phone()), "045-123-4567")
    XCTAssertEqual(phone.formatted(.phone(.areaParenthesis)), "(045)123-4567")
    XCTAssertEqual(phone.formatted(.phone(.localParenthesis)), "045(123)4567")
}

ここまでくると、Foundation で定義されている他の FormatStyle と見劣りしなくなります。

PhoneNumber の extension を実装

先ほどのテストをパスさせるために、PhoneNumber の extension を実装していきます。

extension PhoneNumber {
    public func formatted(_ format: S) -> S.FormatOutput where S: FormatStyle, S.FormatInput == PhoneNumber {
        return format.format(self)
    }
}

上記で、FormatInput が PhoneNumber である FormatStyle を使って、PhoneNumber を フォーマットすることができるようになります。

機能的には十分かもしれませんが、ここまでの定義だけだと、以下のような書き方が必要になってしまいます。

phone.formatted(PhoneNumber.PhoneNumberFormatStyle(.localParenthesis))

使えないこともないですが、FormatStyle に static で代表的なフォーマットを定義しておくのが、便利です。

extension FormatStyle where Self == PhoneNumber.PhoneNumberFormatStyle {
    static func phone(_ style: PhoneNumber.PhoneNumberFormatStyle.PhoneNumberStyle = .hyphen) -> PhoneNumber.PhoneNumberFormatStyle { .init(style) }
}

上記のように定義しておくことで、以下のような記述が可能になります。

number.formatted(.phone(.localParenthesis))

ここまで実装すると、先ほどのテストもパスするようになります。

まとめ

独自型に対して、FormatStyle の作り方を説明しました。

FormatStyle の作り方
  • FormatStyle に conform させる
  • FormatInput = 独自型、FormatOutput = String とする
  • format メソッドを定義して、フォーマットした出力を行う
  • データ型の extension で .formatted を定義するのが Swift 流
  • FormatStyle にも extension で static を定義しておくと便利

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

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