Skip to content

Instantly share code, notes, and snippets.

@Infinity-James
Last active May 3, 2018 11:55
Show Gist options
  • Save Infinity-James/02e9f12501f104c12fc66bd8a2493d2c to your computer and use it in GitHub Desktop.
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.
//
// 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