Skip to content

Instantly share code, notes, and snippets.

@DanielCardonaRojas
Last active March 8, 2019 14:01
Show Gist options
  • Save DanielCardonaRojas/5e268a6787d0c6ae72116f3f96ce3b37 to your computer and use it in GitHub Desktop.
Save DanielCardonaRojas/5e268a6787d0c6ae72116f3f96ce3b37 to your computer and use it in GitHub Desktop.
A class simplifies handling NSAttributedString properties.
//
// TextViewFormatter.swift
// TextViewFormatter
//
// Created by Daniel Esteban Cardona Rojas on 3/6/19.
// Copyright © 2019 Daniel Esteban Cardona Rojas. All rights reserved.
//
import UIKit
/*
Note when using on textviews. Setting the attributed string will move the cursor.
Use something as the following to preserve cursor position:
let range = editingTextView.selectedRange
editingTextView.attributedText = attr
editingTextView.selectedRange = range
*/
struct FormatRule {
typealias WordPredicate = (String) -> Bool
typealias Style = [NSAttributedString.Key: Any]
typealias Rule = (String) -> (Style, [NSRange])
let rule: Rule
init(predicate: @escaping WordPredicate, style: Style) {
self.rule = { str in
let words = Set(str.words()).sorted()
let ranges = words.filter({ predicate($0) }).flatMap({ str.ranges(of: $0) }).map({ NSRange($0, in: str) })
return (style, ranges)
}
}
init(regex: NSRegularExpression, style: Style) {
self.rule = { string in
let range = NSRange(location: 0, length: string.count)
let results: [NSTextCheckingResult] = regex.matches(in: string, options: [], range: range)
let ranges: [NSRange] = results.map({ checkingResult in
let rangeIndices = Array(0..<checkingResult.numberOfRanges)
let rngs = rangeIndices.map({ checkingResult.range(at: $0) })
return rngs
}).flattened()
return (style, ranges)
}
}
func rangeMap(for string: String) -> [NSRange: Style] {
let (style, ranges) = rule(string)
var dict = [NSRange: Style]()
for r in ranges {
dict[r] = style
}
return dict
}
static func combineStyles(_ style1: Style, _ style2: Style) -> Style {
return style1.merging(style2, uniquingKeysWith: { fst, snd in snd })
}
static func styleMap(for string: String, rules: [FormatRule]) -> [NSRange: Style] {
var map = [NSRange: Style]()
for formatRule in rules {
let dict = formatRule.rangeMap(for: string)
map.merge(dict, uniquingKeysWith: { FormatRule.combineStyles($0, $1) })
}
return map
}
}
class TextFormatter {
private var rules = [FormatRule]()
private var defaultStyling: FormatRule.Style
init(defaultStyling: FormatRule.Style) {
self.defaultStyling = defaultStyling
}
// MARK: - Public API
func format(string: String) -> NSAttributedString {
let styleMap = FormatRule.styleMap(for: string, rules: rules)
let attributed = NSMutableAttributedString(attributedString: NSAttributedString(string: string))
let wholeRange = NSRange(location: 0, length: string.count)
attributed.addAttributes(defaultStyling, range: wholeRange)
for (r, style) in styleMap {
attributed.addAttributes(style, range: r)
}
return attributed
}
func addFormatRules(_ formattingRules: FormatRule...) {
rules += formattingRules
}
}
extension String {
func words() -> [String] {
let space = CharacterSet(charactersIn: " ")
return self.lines().flatMap({ $0.components(separatedBy: space) })
}
func lines() -> [String] {
let separators = CharacterSet(charactersIn: "\n")
return self.components(separatedBy: separators)
}
func ranges(of searchString: String) -> [Range<String.Index>] {
let _indices = indices(of: searchString)
let count = searchString.count
return _indices.map({ index(startIndex, offsetBy: $0)..<index(startIndex, offsetBy: $0+count) })
}
func indices(of occurrence: String) -> [Int] {
var indices = [Int]()
var position = startIndex
while let range = range(of: occurrence, range: position..<endIndex) {
let i = distance(from: startIndex,
to: range.lowerBound)
indices.append(i)
let offset = occurrence.distance(from: occurrence.startIndex,
to: occurrence.endIndex) - 1
guard let after = index(range.lowerBound,
offsetBy: offset,
limitedBy: endIndex) else {
break
}
position = index(after: after)
}
return indices
}
}
extension Array {
func unwrapped<T>() -> [T] where Element == T? {
return self.filter { $0 != nil }.map { $0! }
}
func flattened<T>() -> [T] where Element == [T] {
return self.flatMap({ $0 })
}
func filterMap<T>(_ trans: ((Element) -> T?)) -> [T] {
return self.map(trans).unwrapped()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment