RegexBuilderでカラーコードをUIColorに変換するものを作ってみる

Swift 5.7 / iOS 16になって、正規表現の機能が大幅に強化されました。

それまで用意されていた[NSRegularExpression](https://developer.apple.com/documentation/foundation/nsregularexpression)はひたすら使いづらく、悪夢のような代物でしたが、[Regex](https://developer.apple.com/documentation/swift/regex)や[Regex Literals](https://github.com/apple/swift-evolution/blob/644798165cf04084823b8ead8db52f3b3dd03a0d/proposals/0354-regex-literals.md)の導入で、Swiftの正規表現は他の言語とほぼ遜色ない使い勝手になりました。

しかしSwiftはそこにとどまらず、[RegexBuilder](https://developer.apple.com/documentation/regexbuilder)なるDSLを用意してきました。

SwiftUIのように正規表現を記述できるというものです。

呪文のような正規表現を直接書かなくても良くなり、構造的に記述できるので間違いが少なくなる。

少し冗長にはなりますが可読性が高く、コメントを差し込めたりでわかりやすいなど、これはなかなか良いアイディアだと思います。

そもそも正規表現は強力なものですし、これだけ使いやすくなれば今後使う場面も増えてくると思いますので、RegexBuilderを触ってみたいと思います。

-------------

そんなわけで、RegexBuilderでカラーコードをUIColorとColorに変換するスクリプトを作成してみました。

![スクリーンショット 2022-11-05 16.35.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2763088/1226b064-2f67-b207-8ec8-8a6127540b32.png)

下記コードをSwift Playgroundにコピペすると実行できます。

```swift
import SwiftUI
import RegexBuilder
import PlaygroundSupport

enum ColorCode {
    /// 6文字および8文字のカラーコードの正規表現
    /// /^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})?$/
    private static let hexCapture = Capture { Repeat(count: 2) { .hexDigit } }
    private static let pattern8 = Regex {
        Optionally { "#" }
        hexCapture // R
        hexCapture // G
        hexCapture // B
        Optionally { hexCapture } // A
    }

    /// 3文字のカラーコードの正規表現
    /// /^#?([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/
    private static let pattern3 = Regex {
        Optionally { "#" }
        Capture { .hexDigit } // R
        Capture { .hexDigit } // G
        Capture { .hexDigit } // B
    }

    /// カラーコードをRGBA値に変換
    /// - Parameter hexStr: カラーコード
    /// - Returns: RGBA値
    static func convert(from hexStr: String) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)? {
        // 6文字および8文字
        if let match = hexStr.wholeMatch(of: pattern8) {
            let (_, r, g, b, a) = match.output
            return (toDecimal(r), toDecimal(g), toDecimal(b), toDecimal(a ?? "ff"))
        }
        // 3文字
        if let match = hexStr.wholeMatch(of: pattern3) {
            let (_, r, g, b) = match.output
            return (toDecimal(r), toDecimal(g), toDecimal(b), 1.0)
        }
        return nil
    }

    /// 16進をカラー値に変換
    /// - Parameter hex: 16進 (文字列)
    /// - Returns: カラー値
    private static func toDecimal(_ hex: Substring) -> CGFloat {
        let h: String = hex.count == 1 ? .init(repeating: .init(hex), count: 2) : .init(hex)
        return .init(Int(h, radix: 16) ?? 0) / 255.0
    }
}

struct ColorConverter: View {
    /// カラーコード入力
    @State private var colorCode: String = ""

    /// UIColorの出力
    @State private var uiColorString: String = ""

    /// Colorの出力
    @State private var colorString: String = ""

    /// パレットのカラー
    @State private var color: Color?

    /// body
    var body: some View {
        HStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(color ?? .black)
                .frame(width: 80, height: 80)
            VStack {
                TextField("カラーコード", text: $colorCode)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("UIColor", text: .constant(uiColorString), axis: .vertical)
                    .textSelection(.enabled)
                    .font(.caption)
                    .frame(maxHeight: .infinity)
                TextField("Color", text: .constant(colorString), axis: .vertical)
                    .textSelection(.enabled)
                    .font(.caption)
                    .frame(maxHeight: .infinity)
            }
        }
        .frame(width: 400, height: 150)
        .padding()
        .onChange(of: colorCode, perform: convert)
    }

    /// 出力
    private func convert(_ colorCode: String) {
        if let c = ColorCode.convert(from: colorCode) {
            uiColorString = "UIColor(red: \(c.r), green: \(c.g), blue: \(c.b), alpha: \(c.a))"
            colorString = "Color(red: \(c.r), green: \(c.g), blue: \(c.b), opacity: \(c.a))"
            color = .init(red: c.r, green: c.g, blue: c.b, opacity: c.a)
        } else {
            uiColorString = ""
            colorString = ""
            color = nil
        }
    }
}

PlaygroundPage.current.setLiveView(ColorConverter())
```

-------------

RegexBuilderの部分を具体的に見てみましょう。

```swift
    /// 6文字および8文字のカラーコードの正規表現
    /// /^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})?$/
    private static let hexCapture = Capture { Repeat(count: 2) { .hexDigit } }
    private static let pattern8 = Regex {
        Optionally { "#" }
        hexCapture // R
        hexCapture // G
        hexCapture // B
        Optionally { hexCapture } // A
    }
```

`Optionally` `Repeat`などが、正規表現の特殊文字にあたる部分になります。

RegexBuilder | 正規表現
------------ | -------
One | \[.] (括弧内の1文字)
Optionally | ? (0回もしくは1回マッチ)
ZeroOrMore | * (0回以上マッチ)
OneOrMore | + (1回以上マッチ)
Repeat(count: x) | {x} (x回マッチ)

[RegexBuilder](https://developer.apple.com/documentation/regexbuilder)のページに詳細は記載されていますが、代表的なものは以上のような対応になります。

`Optionally { "#" }`が分かりやすいと思いますが、検索対象の文字をブロックで囲む、というのが基本的な使い方になります。

上記の場合、0回もしくは1回#にマッチする、ということになります。

具体的には「#ffffff」と「ffffff」のように#があってもなくてもマッチするという動作になります。

`One`に関してはブロックでなく、`One(.hexDigit)`という使い方になりますが、確実に1文字なのでブロックを使う必要がないということだと思います。

また上記構成要素はstructですので、`private static let hexCapture = Capture { Repeat(count: 2) { .hexDigit } }`のように、正規表現をマクロ的に事前にまとめておく事ができます。

その他、`ChoiceOf`(a|b)などの要素も用意されていますので、正規表現で行えることは問題なくできそうです。

**

`Regex`は正規表現を構成する本体の部分になりますが、内部で使う要素は[RegexComponent](https://developer.apple.com/documentation/swift/regexcomponent)に適合したものになります。

現在`String`も`RegexComponent`に適合しているため、`"#"`などの文字列も利用できています。

[RegexComponent Implementations | Apple Developer Documentation](https://developer.apple.com/documentation/swift/string/regexcomponent-implementations)

`RegexComponent`に適合したstructを用意することで、メルアドなど、もっと特殊な要素も用意することができそうです。

**

`.hexDigit`は`RegexComponent`の定数です。

いわゆるメタ文字が定数として用意されています。

RegexBuilder | 正規表現
------------ | -------
.any | . (全ての文字にマッチ)
.word | \w (アルファベット)
.digit | \d (数字)
.whitespace | \s (スペース)

この他、16進(`.hexDigit`)や日付(`.iso8601`)など、一般的ではないメタ文字も用意されているのが面白いところです。

-------------

```swift
private static let hexCapture = Capture { Repeat(count: 2) { .hexDigit } }
```

`Capture`で囲んだ部分は正規表現的には「()」(サブパターン)になり、`Capture`内のマッチした部分を部分的に取得できるようになります。

具体的には「#aabbcc」は (aa, bb, cc)といった具合に後で取得できるようになります。

```swift
        if let match = hexStr.wholeMatch(of: pattern8) {
            let (_, r, g, b, a) = match.output
```

`match.output`の戻り値の`$0.1`以降がサブパターンになっており、RGBAのそれぞれの値を取得しています。

`a`に関しては`Optionally`で囲んだ部分になっていますので、マッチするかどうかはわかりません。(マッチしない場合もあります)

そのため`a`はOptionalになっています。

ちなみに`$0.0`はマッチした全体が返ってきており、他言語における`$0`と同様になっていて、その意味でも直感的になっています。

------------

ちなみにRegexBuilderでなく、正規表現リテラルも当然動作します。

```swift
private static let pattern8 = /^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})?$/
```

------------

今回は確認していない部分も多いですが、正規表現でできそうなことは問題なくできる印象で、さらにいろいろなことができそうです。

例えば、

- [ignoresCase](https://developer.apple.com/documentation/swift/regex/ignorescase(_:))など、正規表現から別の正規表現を生成できそう
- 今回は[wholeMatch](https://developer.apple.com/documentation/swift/regex/wholematch(in:)-9do8t)を使ったが、最小マッチとかもできそう

など、まだまだ調べることはありそうです。