【Swift】CustomStringConvertibleについて調べてみた


アップルさんのサンプルコードを調べているときに、CustomStringConvertibleというProtocolを使っていました。
見たことあるけどどういうものだったかな？　と疑問に感じたので、調べてみました。

## CustomStringConvertibleとは

> Types that conform to the `CustomStringConvertible` protocol can provide their own representation to be used when converting an instance to a string. The `String(describing:)`initializer is the preferred way to convert an instance of _any_ type to a string. If the passed instance conforms to `CustomStringConvertible`, the `String(describing:)` initializer and the `print(_:)` function use the instance’s custom `description` property.
Accessing a type’s `description` property directly or using `CustomStringConvertible` as a generic constraint is discouraged.

> CustomStringConvertibleプロトコルに準拠したタイプは、インスタンスを文字列に変換するときに使用する独自の表現を提供できます。String(describing:)イニシャライザは、任意の型のインスタンスを文字列に変換する好ましい方法です。渡されたインスタンスがCustomStringConvertibleに準拠している場合、String(describing:)初期化子とprint(_:)関数は、インスタンスのカスタム記述プロパティを使用します。
型の説明プロパティに直接アクセスしたり、CustomStringConvertibleを一般的な制約として使用したりすることはお勧めできません。

（[公式ドキュメント](https://developer.apple.com/documentation/swift/customstringconvertible)より抜粋。日本語訳はmacOSの翻訳機能にお願いしました）

## サンプルコード

Point構造体をprint関数で文字列にした場合にどうなるかみてみましょう。

``` Swift
struct Point {
    let x: Int, y: Int
}

let p = Point(x: 21, y: 30)
print(p) // "Point(x: 21, y: 30)"
```

`Point(x: 21, y: 30)` という文字列に変換されました。
これでも内容はわかりますが、Pointはいらないなということがあるかと思います。

そんな場合に、CustomStringConvertibleの登場です。

``` Swift
struct Point: CustomStringConvertible {
    let x: Int, y: Int

    var description: String {
        return "(\(x), \(y))"
    }
}

let p = Point(x: 21, y: 30)
let s = String(describing: p)
print(s) // (21, 30)
```

Pointが消えて、見事に `(21, 30)` という文字列に変換できました。
名前のとおり、カスタムできるというわけですね。

## 注意点

下記のコードのように直接descriptionプロパティにアクセスすることはできるし、取得する値も  `String(describing:)` を使った場合と同じになります。
しかし、上記の公式ドキュメントによるとお勧めできないそうです。理由が書かれていませんが、将来的に仕様が変わる（プロパティにアクセスできないようになる等）可能性もあるので、素直に `String(describing:)` を使いましょう。

``` Swift
print(p.description) // (21, 30)
```

## 使用例

アップルさんのサンプルコード [Backyard Birds](https://developer.apple.com/documentation/swiftui/backyard-birds-sample) から、CustomStringConvertibleの使用例を見てみましょう。
サブスクリプションの種類を表すenumのPassStatusを宣言して、extensionでCustomStringConvertibleに準拠しています。そして、descriptionでは、caseごとの説明を返すようにしています。

``` Swift
enum PassStatus: Comparable, Hashable {
    case notSubscribed
    case individual
    case family
    case premium
}

extension PassStatus: CustomStringConvertible {
    var description: String {
        switch self {
        case .notSubscribed: String(localized: "Not Subscribed")
        case .individual: String(localized: "Individual")
        case .family: String(localized: "Family")
        case .premium: String(localized: "Premium")
        }
    }
}
```

次に、PassStatusのdescriptionを使用しているところを見てみます。

``` Swift
    @ViewBuilder
    private var manageSubscriptionLabel: some View {
        Label {
            Text("Your Backyard Birds Pass: \(String(describing: passStatus))",
                 comment: "The variable is the type of Backyard Birds Pass (such as Premium, Family, and so on.)")
        } icon: {
            Image(systemName: "wallet.pass")
        }
        #if os(macOS)
        .labelStyle(.titleOnly)
        #endif
    }
```

LabelのTextで使用しています。
`Your Backyard Birds Pass:` の後ろの `\(String(describing: passStatus))` で PassStatus に定義した description を取得して表示しています。

それでは、実際の画面を見てみましょう。

<img width="400" alt="PassStatusのdescription画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38924/896425f4-dbfe-a7e3-c2f7-d95e7360b30b.png">

`お使いのBackyard Birds パス:` の後ろに `個人` と表示されています（あらかじめサブスクリプション画面で `Individual` を選択してあります）。

`CustomStringConvertible` の `description` で `case .individual: String(localized: "Individual")` が選択されて `"Individual"` が返ってきていることがわかります。

余談ですが、  `"Individual"` が `個人` になっているのはLocalizableが設定されているからです。

![スクリーンショット 2024-08-03 10.50.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38924/e27c30a4-b8f2-6ca4-6f12-58b6f5e3b2d4.png)


## 終わりに

自分でコードを書くときには、下記のようにnameを定義していました。

``` Swift
enum PassStatus: Comparable, Hashable {
    case notSubscribed
    case individual
    case family
    case premium
    
    var name: String {
        switch self {
        case .notSubscribed: String(localized: "Not Subscribed")
        case .individual: String(localized: "Individual")
        case .family: String(localized: "Family")
        case .premium: String(localized: "Premium")
        }
    }
}
```

標準のprotocolがあるならそちらを使ったほうがわかりやすくていいなと思うので、これからはCustomStringConvertibleを活用します。

それでは、Have a good Swift life! Have a good CustomStringConvertible iife!