LoginSignup
2
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-11-05

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

それまで用意されていたNSRegularExpressionはひたすら使いづらく、悪夢のような代物でしたが、RegexRegex Literalsの導入で、Swiftの正規表現は他の言語とほぼ遜色ない使い勝手になりました。

しかしSwiftはそこにとどまらず、RegexBuilderなるDSLを用意してきました。

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

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

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

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


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

スクリーンショット 2022-11-05 16.35.58.png

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

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の部分を具体的に見てみましょう。

    /// 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のページに詳細は記載されていますが、代表的なものは以上のような対応になります。

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

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

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

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

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

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

**

Regexは正規表現を構成する本体の部分になりますが、内部で使う要素はRegexComponentに適合したものになります。

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

RegexComponent Implementations | Apple Developer Documentation

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

**

.hexDigitRegexComponentの定数です。

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

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

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


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

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

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

        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でなく、正規表現リテラルも当然動作します。

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など、正規表現から別の正規表現を生成できそう
  • 今回はwholeMatchを使ったが、最小マッチとかもできそう

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0