Swift 5.7 / iOS 16になって、正規表現の機能が大幅に強化されました。
それまで用意されていたNSRegularExpressionはひたすら使いづらく、悪夢のような代物でしたが、RegexやRegex Literalsの導入で、Swiftの正規表現は他の言語とほぼ遜色ない使い勝手になりました。
しかしSwiftはそこにとどまらず、RegexBuilderなるDSLを用意してきました。
SwiftUIのように正規表現を記述できるというものです。
呪文のような正規表現を直接書かなくても良くなり、構造的に記述できるので間違いが少なくなる。
少し冗長にはなりますが可読性が高く、コメントを差し込めたりでわかりやすいなど、これはなかなか良いアイディアだと思います。
そもそも正規表現は強力なものですし、これだけ使いやすくなれば今後使う場面も増えてくると思いますので、RegexBuilderを触ってみたいと思います。
そんなわけで、RegexBuilderでカラーコードをUIColorとColorに変換するスクリプトを作成してみました。
下記コードを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に適合したものになります。
現在String
もRegexComponent
に適合しているため、"#"
などの文字列も利用できています。
RegexComponent Implementations | Apple Developer Documentation
RegexComponent
に適合したstructを用意することで、メルアドなど、もっと特殊な要素も用意することができそうです。
**
.hexDigit
はRegexComponent
の定数です。
いわゆるメタ文字が定数として用意されています。
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を使ったが、最小マッチとかもできそう
など、まだまだ調べることはありそうです。