Swiftの文字列補間をカスタマイズする

以下の3つのProtocolを用いることで、Swiftの文字列補間(式展開, 変数展開などとも言う)をカスタマイズすることができる。

文字列リテラルで任意の型を初期化する

ExpressibleByStringLiteralは、準拠した型を文字列リテラルで初期化することを可能にするProtocolで、StringもこのProtocolに準拠している。

このProtocolを任意の型に準拠させることで、String同様に文字列リテラルで初期化することができる。ただ、このProtocolに準拠するのみだと文字列補間を扱うことはできない。

struct Note {
    let value: String
}

// 文字列リテラルでNoteをinitできるようになる
extension Note: ExpressibleByStringLiteral {
    init(stringLiteral value: StringLiteralType) {
        self.init(value: value)
    }
}

let note1: Note = "NOTE1"
print(note1.value)
// → NOTE

// 文字列補完は扱えない
let note2: Note = "NOTE\(2)"
// → error: cannot convert value of type 'String' to specified type 'Note'

文字列補間を含む文字列リテラルで任意の型を初期化する

文字列補間を可能にするためには、ExpressibleByStringInterpolationに準拠する必要がある。StringはこのProtocolに準拠しているため文字列補間を含む文字列リテラルで初期化できるようになっている。

上記のコードにおいて、NoteはこのProtocolに準拠しておらず文字列補間が行えないため、文字列リテラル "NOTE\(2)" はStringとして認識されて error: cannot convert value of type 'String' to specified type 'Note' というエラーが出力されている。

ExpressibleByStringInterpolationを任意の型に準拠させると、String同様に文字列補間を含む文字列リテラルで初期化できるようになる。

ちなみに、ExpressibleByStringInterpolationはExpressibleByStringLiteralを継承している。

struct Note {
    let value: String
}

// String同様の文字列補間をNoteのinitで使えるようになる
extension Note: ExpressibleByStringInterpolation {
    // MARK: - ExpressibleByStringLiteral
    init(stringLiteral value: StringLiteralType) {
        self.init(value: value)
    }
}

let note2: Note = "NOTE\(2)"
print(note2.value)
// → NOTE2

文字列補間をカスタマイズする

ExpressibleByStringInterpolationは以下のStringInterpolationというStringInterpolationProtocolに準拠したassociatedtypeを持ち、このStringInterpolationを拡張することで文字列補間をカスタマイズすることができる。

associatedtype StringInterpolation : StringInterpolationProtocol = DefaultStringInterpolation where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType

例えば、StringのStringInterpolationを拡張することで、Stringの文字列リテラル内で下記のような文字列補間を有効にできる。具体的には、以下のようにappendInterpolationメソッドをoverloadすることで行う。overloadのため、任意の引数をもつメソッドを任意の数だけ追加でき、利用時にはコード補完もされる。

extension String.StringInterpolation {
    mutating func appendInterpolation(if bool: Bool, _ str: String) {
        if bool { appendLiteral(str) }
    }
}
print("\(if: true, "TRUE")")
// → TRUE

code completion
コード補完もされる

StringのStringInterpolationはDefaultStringInterpolationであるため、DefaultStringInterpolationが適用されている他の型でも同様の文字列補間を利用できる。

extension String.StringInterpolation {
    mutating func appendInterpolation(if bool: Bool, _ str: String) {
        if bool { appendLiteral(str) }
    }
}

struct Note {
    let value: String
}

extension Note: ExpressibleByStringInterpolation {
    // MARK: - ExpressibleByStringLiteral
    init(stringLiteral value: StringLiteralType) {
        self.init(value: value)
    }
}

let note: Note = "NOTE \(if: true, "TRUE")"
print(note.value)
// → NOTE TRUE

また、StringInterpolationを自作することで独自の文字列補間のみを利用することもできる。この場合、DefaultStringInterpolationで実装されている文字列補間は利用できなくなる。

struct Note {
    let value: String
}

extension Note: ExpressibleByStringInterpolation {
    // MARK: - ExpressibleByStringLiteral
    init(stringLiteral value: StringLiteralType) {
        self.init(value: value)
    }

    init(stringInterpolation: StringInterpolation) {
        self.init(stringLiteral: stringInterpolation.output)
    }
    
    struct StringInterpolation: StringInterpolationProtocol {
        var output = ""
        
        init(literalCapacity: Int, interpolationCount: Int) {
            output.reserveCapacity(literalCapacity)
        }
        
        mutating func appendLiteral(_ literal: String) {
            output.append(literal)
        }
        
        mutating func appendInterpolation(ifNot bool: Bool, _ str: String) {
            if !bool { appendLiteral(str) }
        }
    }
}

let note: Note = "NOTE \(ifNot: false, "FALSE")"
print(note.value)
// → NOTE FALSE