Created
April 3, 2021 16:29
-
-
Save art-divin/eea7acf7e35a2cf148af4492b7d794be to your computer and use it in GitHub Desktop.
SwiftUI String tokenizer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct TokenColor { | |
var text : Color | |
var background : Color | |
} | |
extension String : Identifiable { | |
public var id: Self { | |
UUID().uuidString | |
} | |
} | |
extension String : View { | |
static let digitColors : [TokenColor] = [.init(text: .black, background: .yellow)] | |
static let wordColors : [TokenColor] = [.init(text: .white, background: .blue)] | |
static let urlColors : [TokenColor] = [.init(text: .blue, background: .white)] | |
func randomColor(from colors: [TokenColor]) -> TokenColor { | |
colors.randomElement()! | |
} | |
public var body: some View { | |
switch Token(rawValue: self) { | |
case .digit(let string): | |
Text(string) | |
.foregroundColor(self.randomColor(from: Self.digitColors).text) | |
.background( | |
RoundedRectangle(cornerRadius: 5) | |
.foregroundColor(self.randomColor(from: Self.digitColors).background) | |
) | |
case .word(let string): | |
Text(string) | |
.foregroundColor(self.randomColor(from: Self.wordColors).text) | |
.background( | |
RoundedRectangle(cornerRadius: 5) | |
.foregroundColor(self.randomColor(from: Self.wordColors).background) | |
) | |
case .url(let string): | |
Text(string) | |
.foregroundColor(self.randomColor(from: Self.urlColors).text) | |
.background( | |
RoundedRectangle(cornerRadius: 5) | |
.foregroundColor(self.randomColor(from: Self.urlColors).background) | |
) | |
default: | |
Text(self) | |
} | |
} | |
} | |
enum Token { | |
case digit(String) | |
case word(String) | |
case url(String) | |
init?(rawValue: String) { | |
if let digits = rawValue.matches(regex: #"[0-9].+"#) { | |
if let digitRange = digits.map(\.range).first, let digits = rawValue.string(from: digitRange) { | |
self = .digit(digits) | |
} else { | |
self = .word(rawValue) | |
} | |
} else if let url = rawValue.urlMatches() { | |
if let linkRange = url.map(\.range).first, let url = rawValue.string(from: linkRange) { | |
self = .url(url) | |
} else { | |
self = .word(rawValue) | |
} | |
} else { | |
self = .word(rawValue) | |
} | |
} | |
} | |
extension String { | |
func urlMatches() -> [NSTextCheckingResult]? { | |
guard let regex = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { | |
return nil | |
} | |
let totalRange = NSRange(location: 0, length: self.count) | |
let matches = regex.matches(in: self, options: [], range: totalRange) | |
return matches.isEmpty ? nil : matches | |
} | |
func matches(regex: String) -> [NSTextCheckingResult]? { | |
guard let percentRegex = try? NSRegularExpression(pattern: regex) else { | |
return nil | |
} | |
let totalRange = NSRange(location: 0, length: self.count) | |
let matches = percentRegex.matches(in: self, options: [], range: totalRange) | |
return matches.isEmpty ? nil : matches | |
} | |
func firstMatch(regex: String) -> NSTextCheckingResult? { | |
let found = self.matches(regex: regex) | |
guard let first = found?.first else { | |
return nil | |
} | |
return first | |
} | |
func groupMatch(regex: String, group: String) -> Range<String.Index>? { | |
guard let found = self.firstMatch(regex: regex) else { | |
return nil | |
} | |
return Range(found.range(withName: group), in: self) | |
} | |
func string(from range: NSRange) -> String? { | |
let startIndex = self.startIndex | |
let offsetIndex = self.index(startIndex, offsetBy: range.location) | |
let endIndex = self.index(offsetIndex, offsetBy: range.length - 1) | |
return String(self[offsetIndex ... endIndex]) | |
} | |
} | |
struct ContentView: View { | |
@State var text : String | |
@State private var strings : [String] = [] | |
var body: some View { | |
VStack { | |
ScrollView(.horizontal) { | |
ScrollViewReader { proxy in | |
HStack { | |
ForEach(self.strings) { | |
$0.id($0) | |
} | |
} | |
.onChange(of: self.strings) { | |
proxy.scrollTo($0.last, anchor: .trailing) | |
} | |
} | |
} | |
TextEditor(text: self.$text) | |
.border(Color.black, width: 1) | |
} | |
.onChange(of: self.text) { | |
self.strings = $0.split(separator: " ").map(String.init) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView(text: "") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment