Last active
March 8, 2019 14:01
-
-
Save DanielCardonaRojas/5e268a6787d0c6ae72116f3f96ce3b37 to your computer and use it in GitHub Desktop.
A class simplifies handling NSAttributedString properties.
This file contains hidden or 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
// | |
// 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