Last active
May 3, 2018 11:55
-
-
Save Infinity-James/02e9f12501f104c12fc66bd8a2493d2c to your computer and use it in GitHub Desktop.
An RxSwift subclass of UITextView which allows for observation of taps on an arbitrary portion of text.
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
// | |
// InteractiveTextView.swift | |
// Views | |
// | |
// Created by James Valaitis on 05/03/2018. | |
// Copyright © 2018 VIPR Digital. All rights reserved. | |
// | |
import RxSwift | |
import UIKit | |
// MARK: Interactive Text View | |
/** | |
A text view with support for tapping on certain words. | |
*/ | |
open class InteractiveTextView: UITextView { | |
// MARK: Properties | |
private let interactiveAttribute = NSAttributedStringKey("com.fixr.interactiveAttribute") | |
private typealias Callback = () -> () | |
private var callbacks = [String: Callback]() | |
// MARK: Initialization | |
public override init(frame: CGRect, textContainer: NSTextContainer?) { | |
super.init(frame: frame, textContainer: textContainer) | |
setup() | |
} | |
public required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setup() | |
} | |
} | |
// MARK: Configuration | |
private extension InteractiveTextView { | |
func setup() { | |
isUserInteractionEnabled = true | |
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) | |
addGestureRecognizer(tapRecognizer) | |
} | |
} | |
// MARK: Interactions | |
public extension InteractiveTextView { | |
/** | |
Observes any tap made on the first instance of the provided text found within the `attributedText`. | |
- Parameter text: The portion of text to be monitored. | |
- Returns: An observable for any tap made on the text. An error will be returned if the text could not be found within `attributedText`. | |
*/ | |
func tap(onString text: String) -> Observable<Void> { | |
guard let range = attributedText.string.range(of: text) else { return .error(InteractiveTextViewError.wordNotFound) } | |
let updatedText = NSMutableAttributedString(attributedString: attributedText) | |
updatedText.addAttributes([interactiveAttribute: text], range: NSRange(range, in: attributedText.string)) | |
attributedText = updatedText | |
return .create { [weak self] observer in | |
guard let `self` = self else { return Disposables.create() } | |
self.callbacks[text] = { observer.onNext(()) } | |
return Disposables.create { self.callbacks[text] = nil } | |
} | |
} | |
} | |
private extension InteractiveTextView { | |
@objc func handleTap(_ recognizer: UITapGestureRecognizer) { | |
// location of tap in myTextView coordinates and taking the inset into account | |
var location = recognizer.location(in: self) | |
location.x -= textContainerInset.left | |
location.y -= textContainerInset.top | |
// character index at tap location | |
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) | |
// if index is valid then do something | |
guard characterIndex < textStorage.length else { return } | |
// check if the tap location has a certain attribute | |
guard let observedText = attributedText.attribute(interactiveAttribute, at: characterIndex, effectiveRange: nil) as? String, | |
let callback = callbacks[observedText] else { return } | |
callback() | |
} | |
} | |
// MARK: Interactive Text View Error | |
public enum InteractiveTextViewError: Error { | |
/// The provided word could not be found within the text. | |
case wordNotFound | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment