A text view that supports selection and expansion

Overview

The Problem

UILabel and UITextView offer unsatisfying support for text selection.

Existing solutions like TTTAttributedLabel are great but offer a somewhat limited API for text selection.

Features

Installation

CocoaPods

Add the following to your Podfile

pod 'SelectableTextView', '~> 1.0.2'

Carthage

Add the following to your Cartfile

github "jhurray/SelectableTextView" ~> 1.0.2

Add to project Manually

Clone the repo and manually add the Files in /SelectableTextView

Usage

import SelectableTextView

let textView = SelectableTextView()
textView.text = "Hello World!"
textView.truncationMode = .truncateTail
textView.alignment = .center
textView.numberOfLines = 1

let greetingValidator = MatchesTextValidator(text: "hello")
textView.registerValidator(_ validator: greetingValidator) { (validText, validator) in
	// Handle selection of "Hello"
}

let exclamationValidator = SuffixValidator(suffix: "!")
textView.registerValidator(_ validator: exclamationValidator) { (validText, validator) in
	// Handle selection of "World!"
}

Text Selection

To create selectable text, you have to create and register a validator. The validator must conform to the TextSelectionValidator protocol.

let hashtagValidator = PrefixValidator(prefix: "#")
textView.registerValidator(validator: hashtagValidator) { (validText, validator) in
	// Handle selection of hashtag
}

You can unregister a validator at any time.

textView.removeValidator(validator: hashtagValidator)

Custom Validators

Here is a resource for creating custom validators using the TextSelectionValidator protocol.

There are other more specific protocols that make customization easier like ContainerTextSelectionValidator and CompositeTextSelectionValidator.

Prewritten Validators

There are a few prewritten validators supplied. These can be used as they are, as building blocks for other more complex validators, and as examples on how to build custom validators.

Text Validators
MatchesTextValidator(text: String, caseSensitive: Bool = false)

ContainsTextValidator(text: String, caseSensitive: Bool = false)

PrefixValidator(text: String, caseSensitive: Bool = false)

SuffixValidator(text: String, caseSensitive: Bool = false)

HashtagTextValidator()

AtSymbolTagTextValidator()

QuotationsTextValidator()

HandlebarsValidator(searchableText: String, replacementText: String)
Abstract Validators
ReverseValidator(validator: TextSelectionValidator)

ContainerValidator(validator: TextSelectionValidator, selectionAttributes: [String: Any]? = nil)

CompositeValidator(validators: [TextSelectionValidator], selectionAttributes: [String: Any]? = nil)
Link Validators
LinkValidator() // Validates any link (HTTP, HTTPS, file, etc...)

HTTPLinkValidator() // Validates HTTP and HTTPS links

UnsafeLinkValidator() // Validates HTTP links

HTTPSLinkValidator()

CustomLinkValidator(urlString: String!, replacementText: String? = nil) 

Customization is possible using the LinkValidatorAttributes protocol. Example here.

Regex Validators
RegexValidator(pattern: String, options: NSRegularExpression.Options = .caseInsensitive)

EmailValidator()

PhoneNumberValidator()

Text Expansion

You can add a text expansion button with the following method:


public func addExpansionButton(collapsedState: (text: String, lines: Int), expandedState: (text: String, lines: Int), attributes: [String: Any]? = nil)

You can remove the expansion button using the following method:

public func removeExpansionButton(numberOfLines: Int = 1)

Example:

let attributes = [NSForegroundColorAttributeName: purple]
textView.addExpansionButton(collapsedState: ("More...", 2),
                             expandedState: ("Less", 0),
                                attributes: attributes)
                                
...

textView.removeExpansionButton(numberOfLines: 2)

You can customize the background color of the expansion button using the SelectedBackgroundColorAttribute property HighlightedTextSelectionAttributes struct as an attribute key.

let attributes: [String: Any] = [HighlightedTextSelectionAttributes.SelectedBackgroundColorAttribute : UIColor.purple]

Customization

text

  • Sets the content of the text view
  • Type: String?

font

  • Sets the font of the text view
  • Type: UIFont
  • Defaults to UIFont.systemFont(ofSize: 17)

textColor

  • Sets the default text color
  • Type: UIColor
  • Defaults to UIColor.darkText

attributedText

  • Overrides the text and textColor with the attributed text
  • Type: NSAttributedString?
  • Defaults to nil

textAlignment

  • Alignment of text in the text view
  • Type: TextAlignment
  • Supports 3 types: .left, .right, .center
  • Defaults to .left

lineBreakMode

  • Determines how the text view handles new lines
  • Type: LineBreakMode
  • Supports 1 type: .wordWrap
    • Defaults to . wordWrap
  • See Goals

truncationMode

  • Determines the bahavior of the last word in the last line of the text view
  • Type: TruncationMode
  • Supports 2 types: .clipping, .truncateTail
  • Defaults to .clipping
  • See Goals

numberOfLines

  • Determines the number of lines in the text view
  • Type: Int
  • Defaults to 0
  • 0 lines means unbounded, similar to UILabel

lineSpacing

  • Determines the spacing between lines
  • Type: CGFloat
  • Defaults to 0
  • Supports negative values

textContainerInsets

  • Sets the content inset of the text view
  • Type: UIEdgeInsets
  • Defaults to UIEdgeInsets.zero

selectionAttributes

  • Sets the default selection attributes for selectable text
  • Type: [String : AnyObject]?
  • Defaults: color = tintColor, font = boldSystemFont(ofSize: font.pointSize + 2)

isExpanded

  • Tracks the state of the expansion button
  • Type: Bool?
  • Defaults to nil. Will only return a value if the expansion button is added
  • If the expansion button is added, this property will toggle the state

textContentSize

  • Readonly, returns the size of the text content
  • Type: CGSize

isSelectionEnabled

  • Determines if selection is enabled for the text view
  • Type: Bool
  • Defaults to true

isScrollEnabled

  • Determines if scrolling is enabled for the text view
  • Type: Bool
  • Defaults to false

scrollDelegate

  • Forwards scrolling events fron the text view
  • Type: SelectableTextViewDelegate?

delegate

  • Delegates work for the text view
  • Type: SelectableTextViewScrollDelegate?

Supported Escape Characters

  • New Line \n
  • Tab \t
  • Null Terminator \0

If you want to have text next to to a selectabe portion of text but still validate the text correctly, use the null terminator.

let text = "The period next to the #Hashtag\0. Will not be highlighted if I use a hashtag validator."

Miscelaneous

framesOfWordsMatchingValidator

You can get the relative frames of words within the text view with the method below. This is how I set up the stars effect in the first example gif.

public func framesOfWordsMatchingValidator(_ validator: TextSelectionValidator) -> [CGRect]
Tab Length

You can adjust the number of spaces a tab character creates using TabTextModelConfig.numberOfSpaces. The default value is 4.

TabTextModelConfig.numberOfSpaces = 2

Interface Builder

You can set most customization properties via interface builder. SelectableTextView is marked as @IBDesignable.

  • numberOfLines: Int
  • text: String
  • textColor: UIColor
  • lineSpacing: Float
  • isSelectionEnabled: Bool
  • isScrollEnabled: Bool
  • fontSize: Float
  • truncateTail: Bool
  • topTextInsets: Float
  • bottomTextInsets: Float
  • leftTextInsets: Float
  • rightTextInsets: Float

Delegate

Default implementations are provided for all SelectableTextViewDelegate methods.

public protocol SelectableTextViewDelegate: class {
    
    /// Resolves conflict between multiple validates that return `true` from their `validate:` method
    //
    // i.e. PrefixTextValidator for `#` and `#my` will both return true for `#myCoolHashtag`,
    // but the actions they are registered for may differ
    //
    /// Default behavior is to choose the first validator in the composite validator's `validators` array
    func resolveValidationConflictsForSelectableTextView(textView: SelectableTextView, conflictingValidators: [TextSelectionValidator]) -> TextSelectionValidator
    
    /// Defaults to `false`
    func animateExpansionButtonForSelectableTextView(textView: SelectableTextView) -> Bool
    
    /// Defaults to `.truncateTail`
    func truncationModeForWordsThatDontFitForSelectableTextView(textView: SelectableTextView) -> TruncationMode
    
    /// Optional, Default empty implementation provideed
    func selectableTextViewContentHeightDidChange(textView: SelectableTextView, oldHeight: CGFloat, newHeight: CGFloat)
}

Scrolling

SelectableTextView supports scrolling and forwards scroll events through SelectableTextViewScrollDelegate.

public protocol SelectableTextViewScrollDelegate: class {
    
    func selectableTextViewDidScroll(_ scrollView: UIScrollView)
    func selectableTextViewWillBeginDragging(_ scrollView: UIScrollView)
    func selectableTextViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
    func selectableTextViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)
    func selectableTextViewWillBeginDecelerating(_ scrollView: UIScrollView)
    func selectableTextViewDidEndDecelerating(_ scrollView: UIScrollView)
    func selectableTextViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
}

You can also scroll to specific words or the first word that passes a validator.

/// Scrolls to the first instance of the word
/// Attempts to match the text and display text of a word
public func scrollToWord(_ word: String, position: ScrollPosition, animated: Bool)
    
   /// Scrolls to the first instance of a word that passes the provided TextSelectionValidator
public func scrollToWordPassingValidator(_ validator: TextSelectionValidator, position: ScrollPosition, animated: Bool)

Goals

  • Character wrapping
  • More truncation styles: .head, .center

Contact Info && Contributing

Feel free to email me at [email protected]. I'd love to hear your thoughts on this, or see examples where this has been used.

MIT License

You might also like...
RichTextKit is a Swift-based library for working with rich text in UIKit, AppKit and SwiftUI.
RichTextKit is a Swift-based library for working with rich text in UIKit, AppKit and SwiftUI.

About RichTextKit RichTextKit is a Swift-based library that lets you work with rich text in UIKit, AppKit and SwiftUI. RichTextKit is under developmen

AEOTPTextField - A beautiful iOS OTP Text Field library, written in Swift with full access customization in UI.
AEOTPTextField - A beautiful iOS OTP Text Field library, written in Swift with full access customization in UI.

AEOTPTextField - A beautiful iOS OTP Text Field library, written in Swift with full access customization in UI.

A SwiftUI TextField with a prompt (or placeholder) that floats above the text field when active or not empty. Requires iOS 15.
A SwiftUI TextField with a prompt (or placeholder) that floats above the text field when active or not empty. Requires iOS 15.

FloatingPromptTextField A prompt is the label in a text field that informs the user about the kind of content the text field expects. In a default Tex

Render Markdown text in SwiftUI
Render Markdown text in SwiftUI

MarkdownUI MarkdownUI is a library for rendering Markdown in SwiftUI, fully compliant with the CommonMark Spec. Supported Platforms You can use the Ma

Autocomplete for a text field in SwiftUI using async/await
Autocomplete for a text field in SwiftUI using async/await

Autocomplete for a text field in SwiftUI using async/await

ExpandableText 😎 (SwiftUI) Expand the text with the
ExpandableText 😎 (SwiftUI) Expand the text with the "more" button

ExpandableText 😎 (SwiftUI) Expand the text with the "more" button Get Started import SwiftUI import ExpandableText struct ExpandableText_Test: View

Handles some of the base configuration to make creating a UIAlertController with text entry even easier. Plus validation!
Handles some of the base configuration to make creating a UIAlertController with text entry even easier. Plus validation!

🍅 FancyTextEntryController A simpler/easier API for adding text fields to UIAlertControllers. Not a custom view, just uses good ol' UIAlertController

ARAutocompleteTextView is a subclass of UITextView that automatically displays text suggestions in real-time
ARAutocompleteTextView is a subclass of UITextView that automatically displays text suggestions in real-time

ARAutocompleteTextView is a subclass of UITextView that automatically displays text suggestions in real-time. This is perfect for automatically suggesting the domain as a user types an email address, #hashtag or @alexruperez.

UITextField that automatically formats text to display in the currency format
UITextField that automatically formats text to display in the currency format

CurrencyTextField The numbers that the user enters in the field are automatically formatted to display in the dollar amount format. For example, if th

Owner
Jeff Hurray
Jeff Hurray
Transition from any SwiftUI Text view into an inline navigation bar title when the view is scrolled off-screen, as seen in Apple's TV & TestFlight iOS apps.

SwiftUI Matched Inline Title Transition from any SwiftUI Text view into an inline navigation bar title when the view is scrolled off-screen, as seen i

Seb Jachec 19 Oct 9, 2022
DGPlaceholderTextView - A light-weight UITextView that supports for placeholder

DGPlaceholderTextView Requirements Installation Usage Properties DGPlaceholderTe

donggyu 5 Jan 26, 2022
SwiftUI TextEdit View - A proof-of-concept text edit component in SwiftUI & CoreText.

A proof-of-concept text edit component in SwiftUI & CoreText. No UIKit, No AppKit, no UITextView/NSTextView/UITextField involved.

Marcin Krzyzanowski 80 Dec 1, 2022
A beautiful and flexible text field control implementation of "Float Label Pattern". Written in Swift.

SkyFloatingLabelTextField SkyFloatingLabelTextField is a beautiful, flexible and customizable implementation of the space saving "Float Label Pattern"

Skyscanner 4k Jan 1, 2023
Focus text field in SwiftUI dynamically and progress through form using iOS keyboard.

Focuser Focuser allows to focus SwiftUI text fields dynamically and implements ability move go through the form using Keyboard for iOS 13 and iOS 14.

Art Technologies 118 Dec 25, 2022
Currency text field formatter available for UIKit and SwiftUI 💶✏️

CurrencyText provides lightweight libraries for formating text field text as currency, available for both UIKit and SwiftUI. Its main core, the Curren

Felipe Lefèvre Marino 183 Dec 15, 2022
Changes the color of the label text when the button is pressed and also prints hello to the console

MY FIRST APP App Description This app changes the color of the label text when the button is pressed and also prints "hello" to the console. App Walk-

null 0 Nov 29, 2021
UITextField and UITextView subclasses with placeholders that change into floating labels when the fields are populated with text.

Deprecated Please use JVFloatLabeledTextField instead or feel free to chime in on an issue if you'd like to take over the repo. RPFloatingPlaceholders

rob phillips 1.1k Jan 5, 2023
Declarative text styles and streamlined Dynamic Type support for iOS

StyledText StyledText is a library that simplifies styling dynamic text in iOS applications. Instead of having to use attributed strings every time yo

Blue Apron 233 Oct 18, 2022
Lightweight set of text fields with nice animation and functionality

TweeTextField This is lightweight library that provides different types of Text Fields based on your needs. I was inspired by Jan Henneberg. Features

Oleg 471 Nov 10, 2022