Created
January 20, 2016 17:41
-
-
Save MosheBerman/1a990d15863737047968 to your computer and use it in GitHub Desktop.
A textview that can interpolating text with input ranges.
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
// | |
// AgreementInput.swift | |
// Intake | |
// | |
// Created by Moshe Berman on 1/14/16. | |
// Copyright © 2016 Moshe Berman. All rights reserved. | |
// | |
import Foundation | |
func == (left : AgreementInput, right : AgreementInput) -> Bool { | |
return left.identifier.isEqual(right.identifier) | |
} | |
func != (left : AgreementInput, right : AgreementInput) -> Bool { | |
return !(left == right) | |
} | |
class AgreementInput : CustomDebugStringConvertible, Equatable | |
{ | |
var identifier : NSUUID | |
var range : NSRange | |
let field : FormField | |
var debugDescription : String { | |
get { | |
return "(\(identifier.UUIDString)) : \(range), \(field)" | |
} | |
} | |
init(withRange range: NSRange, andField field: FormField) { | |
self.range = range | |
self.field = field | |
self.identifier = NSUUID() | |
} | |
} | |
// | |
// AgreementView.swift | |
// Intake | |
// | |
// Created by Moshe Berman on 12/11/15. | |
// Copyright © 2015 Moshe Berman. All rights reserved. | |
// | |
import UIKit | |
class AgreementView: UITextView, UITextViewDelegate { | |
// MARK: - Input | |
override var inputAccessoryView : UIView? { | |
get { | |
let accessory = InputAccessoryView(frame: CGRectZero) | |
accessory.agreementView = self | |
return accessory | |
} | |
set (view) { | |
} | |
} | |
// MARK: - Placeholders | |
var placeholderToken = "__x__" | |
var inputs : Array<AgreementInput> = [] // Initial positioning of the delimiters | |
// MARK: - Editing State | |
var stringLengthBeforeChange : Int = 0 | |
var lastInput : AgreementInput? = nil | |
var isEditing : Bool = false | |
// MARK: - Form Submission | |
var submission : FormSubmission? | |
var formfields : [FormField] = [] | |
var form : Form? { | |
didSet { | |
if let agreement = self.form, let text = agreement.agreementText { | |
self.submission = FormSubmission(form: agreement) | |
self.formfields = agreement.allFields() | |
// Generate input objects first. | |
self.inputs = self.inputsFromText(text: text) | |
let strippedText : String = stringByRemovingTokensFromString(string: text, token: self.placeholderToken) | |
let textWithPlaceholders : String = self.stringByInsertingPlaceholdersIntoText(text: strippedText) | |
self.text = textWithPlaceholders | |
self.applyAttributes() | |
} | |
} | |
} | |
// MARK: - Initializers | |
override init(frame: CGRect, textContainer: NSTextContainer?) { | |
super.init(frame: frame, textContainer: textContainer) | |
self.commonInit() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
self.commonInit() | |
} | |
/* | |
Perform initialization common to all initialization paths. | |
*/ | |
private func commonInit() { | |
self.delegate = self | |
self.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody), size: 0) | |
self.subscribeToFontChangeNotifications() | |
self.subscribeToKeyboardNotifications() | |
} | |
// MARK: - UITextViewDelegate | |
func textViewShouldBeginEditing(textView: UITextView) -> Bool { | |
return self.currentInput() != nil | |
} | |
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool { | |
var shouldEdit = true | |
self.isEditing = true | |
// Corner case | |
let isDelete = (range.length > 0 && text.isEmpty) | |
guard let currentInput : AgreementInput = self.currentInput() else { | |
return shouldEdit // TODO: Consider if this should be changed to 'false' | |
} | |
let currentRange = currentInput.range | |
self.stringLengthBeforeChange = textView.text.characters.count | |
/** | |
There are two cases where we care about deletes: | |
1. If the user has deleted all of the text. | |
2. If the user tries to delete before the start of the current input range. | |
*/ | |
if isDelete | |
{ | |
// Case 1, deny editing. | |
if range.location == currentRange.location - 1 | |
{ | |
shouldEdit = false | |
} | |
// Case 2: We want to replace the last character with the plaeholder text. | |
else if range.location == currentRange.location | |
{ | |
// Get the placeholder for the current input | |
let placeholder = self.placeholderTextForInput(currentInput) | |
// Perform the replacement. | |
self.textStorage.replaceCharactersInRange(currentRange, withAttributedString: NSAttributedString(string: placeholder)) | |
// Move the selection to the start of the current input range. | |
self.selectedRange = NSMakeRange(currentInput.range.location, 0) | |
// Calculate the delta and then update. | |
let delta = deltaValueForTextChangeInTextView(textView: textView) | |
self.updateDeltasAndFormData(delta, input: currentInput) | |
shouldEdit = false | |
} | |
} | |
else | |
{ | |
/** | |
When "forward-editing" we care about: | |
1. Newlines, which we don't want to allow. | |
2. The first insertion when there's placeholder - we need to replace the placeholder and correctly position the carat. | |
3. Every other case, where we just allow the insertion. | |
*/ | |
let rangeOfNewline : NSRange = (text as NSString).rangeOfCharacterFromSet(NSCharacterSet.newlineCharacterSet()) | |
let containsNewline = rangeOfNewline.location != NSNotFound | |
// Case 1. Deny editing. | |
if containsNewline | |
{ | |
shouldEdit = false | |
} | |
// Case 2. We want to replace the placeholder with the text. | |
else if self.currentInputContainsPlaceholderText() | |
{ | |
// Replace the text | |
self.textStorage.replaceCharactersInRange(currentRange, withAttributedString: NSAttributedString(string: text)) | |
// Calculate a new range which will begin after the characters | |
// we just inserted, so that we position the carat correctly. | |
let newRange = NSMakeRange(currentRange.location+text.characters.count, 0) | |
// Now, update the ranges and form values | |
let delta = deltaValueForTextChangeInTextView(textView: textView) | |
self.updateDeltasAndFormData(delta, input: currentInput) | |
// Position the carat correctly. | |
self.selectedRange = newRange | |
shouldEdit = false | |
} | |
// Case 3. | |
else | |
{ | |
shouldEdit = true | |
} | |
} | |
// If we are done editing, we need to change global state | |
// Else, change it in textViewDidChange. | |
self.isEditing = shouldEdit | |
return shouldEdit | |
} | |
func textViewDidEndEditing(textView: UITextView) { | |
self.lastInput = nil | |
} | |
func textViewDidChangeSelection(textView: UITextView) { | |
/** | |
We don't fire if we just made a manual input change. | |
This allows to not have to worry about being out of bounds after | |
character insertions before we adjust the input ranges. | |
This is handled by the isEditing flag. | |
--- | |
We want to distinguish between two kinds of selections: | |
1. Carat | |
2. Selection | |
*/ | |
let isCarat = textView.selectedRange.length == 0 | |
/** | |
When the selection changes **to a valid field**, there are three cases: | |
1. We could be currently editing nothing. | |
2. We could be currently editing the field containing the new selection. | |
3. We could be currently editing a different field from the one containing the new selection. | |
We need to update state based on this, to handle editing just beyond the bound of our input range. | |
*/ | |
// The new range is inside of a field. | |
if let currentInput = self.currentInput() | |
{ | |
// Case 1: New selection | |
if self.lastInput == nil | |
{ | |
self.lastInput = currentInput | |
} | |
// Case 2: Selected the current field | |
else if currentInput == self.lastInput | |
{ | |
// No need to change. | |
} | |
// Case 3: Selected a different field | |
else | |
{ | |
self.lastInput = currentInput | |
let caretRect = self.caretRectForPosition(self.selectedTextRange!.start) | |
self.scrollRectToVisible(caretRect, animated: true) | |
} | |
// If we're inside of a field, and it has placeholder text, jump to start. | |
if self.currentInputContainsPlaceholderText() | |
{ | |
self.selectedRange = NSMakeRange(currentInput.range.location, 0) | |
} | |
else if !isCarat | |
{ | |
/** | |
Three cases for selection of multiple character: | |
1. The selection is contained entirely in the active range. Don't need to do anything. | |
2. The range extends beyond the front (location) of the current range. | |
3. The range extends beyond the back (location + length) of the current range. | |
In cases 2 & 3, we want to clamp the selection. For case 1, do nothing. | |
*/ | |
// Case 2 | |
if currentInput.range.containsTailOfRange(range: self.selectedRange) { | |
print("Contains the end of a selection, clamping.") | |
let differenceBetweenTheTwoLocations = currentInput.range.overlapLengthWithRange(range: self.selectedRange) | |
print("Difference: \(differenceBetweenTheTwoLocations)") | |
let newRange = NSMakeRange(currentInput.range.location + differenceBetweenTheTwoLocations, self.selectedRange.length - differenceBetweenTheTwoLocations) | |
self.selectedRange = newRange | |
} | |
// Case 3 | |
else if currentInput.range.containsHeadOfRange(range: self.selectedRange) | |
{ | |
print("Contains the start of a selection, clamping.") | |
let differenceBetweenTheTwoLocations = abs(currentInput.range.location - self.selectedRange.location) | |
let newLength = currentInput.range.length - differenceBetweenTheTwoLocations | |
print("New Length: \(newLength) Difference: \(differenceBetweenTheTwoLocations)") | |
let newRange = NSMakeRange(self.selectedRange.location, newLength) | |
self.selectedRange = newRange | |
} | |
} | |
} | |
// Selection is outside a field, end editing. | |
// Only fire if we aren't mid-editing transaction | |
else if !isEditing | |
{ | |
// print("Tapped outside of a field. Force end editing.") | |
textView.endEditing(true) | |
} | |
} | |
func textViewDidChange(textView: UITextView) { | |
self.isEditing = false | |
guard let lastInput = self.lastInput else { | |
return | |
} | |
// Calculate the change in text length | |
let delta = self.deltaValueForTextChangeInTextView(textView: textView) | |
self.updateDeltasAndFormData(delta, input: lastInput) | |
} | |
// MARK: - Updating State | |
/** | |
Update the form submission and the deltas. | |
*/ | |
func updateDeltasAndFormData(delta:Int, input lastInput: AgreementInput) | |
{ | |
self.updateRangesWithDelta(delta, beginningWithInput: lastInput) | |
self.updateSubmissionsFromRelevantRanges() | |
self.applyAttributes() // Must occur after updating submissions because this relies on placeholder text. | |
} | |
// MARK: - Displaying a Form | |
/** | |
Collect all of the placeholder indices into an instance variable. The String that we iterated, without any delimiters in it. | |
- parameter text : A string containing placeholders which we'll use to create our form. | |
- returns: A string without the placeholders. | |
*/ | |
private func inputsFromText(let text text:String) -> [AgreementInput] { | |
// Reset the indices | |
var inputs : [AgreementInput] = [] | |
var castedText : NSString = text | |
var range = castedText.rangeOfString(self.placeholderToken) | |
var cursor = 0 | |
while range.location != NSNotFound && cursor < self.formfields.count { | |
let input = AgreementInput(withRange: range, andField: self.formfields[cursor]) | |
inputs.append(input) | |
castedText = castedText.stringByReplacingOccurrencesOfString(self.placeholderToken, withString: "", options: [], range: range) | |
range = castedText.rangeOfString(self.placeholderToken) | |
cursor = cursor + 1 | |
} | |
return inputs | |
} | |
/** | |
Gets a string that is the result of the input string, without any placeholder tokens. | |
- returns : The original string with all occurrences of the token removed. | |
*/ | |
private func stringByRemovingTokensFromString(string string : String, token: String) -> String { | |
return string.stringByReplacingOccurrencesOfString(token, withString: "") | |
} | |
/** | |
Insert the initial placeholders into the form. | |
- parameter text : A String to insert placeholders into. We'll use the instance's delimiterIndices for information on where to place them. | |
- returns : A String containing the original text and the insertions. | |
*/ | |
private func stringByInsertingPlaceholdersIntoText(text inputString: String) -> String { | |
var offset = 0 | |
let text : NSMutableString = NSMutableString(string: inputString) | |
for input in self.inputs { | |
let range = input.range | |
// Get the string to display | |
let displayText = self.textToDisplayForInput(input: input) | |
text.insertString(displayText, atIndex: range.location + offset) | |
// Update the target range in the our array of ranges | |
let count = displayText.characters.count | |
let newRange = NSMakeRange(range.location+offset, count) | |
input.range = newRange | |
// Offset the subsequent ranges based on the length of the string we just inserted | |
offset += newRange.length | |
// print("Inserted placeholder '\(displayText)' as placeholder into range \(newRange) with count of \(count)") | |
} | |
return text as String | |
} | |
/** | |
Applies the appropriate style to our text. | |
*/ | |
func applyAttributes() | |
{ | |
self.applyAttributes(highlightInvalidFields: false) | |
} | |
func applyAttributes(highlightInvalidFields highlightInvalidFields: Bool) | |
{ | |
var attributes : [String : AnyObject] = [:] | |
attributes = [ | |
NSFontAttributeName : UIFont(descriptor: UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody), size: 0) | |
] | |
let entireRange = NSRange(0..<self.text.characters.count) | |
self.textStorage.setAttributes(attributes, range: entireRange) | |
// print("Entire range: \(entireRange)") | |
for input in self.inputs { | |
let range = input.range | |
if let attributes = self.attributesForInput(input, highlightInvalidInput: highlightInvalidFields) | |
{ | |
self.textStorage.addAttributes(attributes, range: range) | |
// print("Adding attributes for range \(input)") | |
} | |
else | |
{ | |
// print("No attributes for range \(input).") | |
} | |
} | |
} | |
/** | |
Iterate the submissions ranges and update the values. | |
If range matching the field contains the placeholder, | |
remove the value from the submission object. Otherwise, | |
assume the value is useful and collect it. | |
*/ | |
private func updateSubmissionsFromRelevantRanges() { | |
for input in self.inputs { | |
let field = input.field | |
let range = input.range | |
let entireText : NSString = (self.textStorage.string as NSString) | |
let rangeOfEntireText = NSMakeRange(0, entireText.length) | |
if rangeOfEntireText.contains(range: range) | |
{ | |
let text : NSString = (self.textStorage.string as NSString).substringWithRange(range) | |
if text.isEqualToString(self.placeholderTextForInput(input)) { | |
self.submission?.removeSubmissionValueForField(field) | |
} | |
else | |
{ | |
self.submission?.setSubmissionValueForField(text, field: field) | |
} | |
} | |
else | |
{ | |
print("Found a range that doesn't overlap correctly: \(input)") | |
} | |
} | |
if let submission = self.submission | |
{ | |
print("\(submission)") | |
} | |
else | |
{ | |
print("No submission.") | |
} | |
} | |
// MARK: - Input Values and Placeholders | |
/** | |
This can be the placeholder, the submission value, or the field description. | |
- parameter input : An AgreementInput object from our form. | |
*/ | |
private func textToDisplayForInput(input input : AgreementInput) -> String { | |
var text = self.placeholderTextForInput(input) | |
// Now try to overwrite the default with a submission value. | |
if let submission = self.submission, let object : String = submission.submissionValueForField(field: input.field) as? String { | |
text = object | |
} | |
return text | |
} | |
/** | |
Get the placeholder text for a field. If the field has no name and no description, | |
a localized version of the word "Field" will be returned. | |
- returns : A string to display inside of the form as a placeholder for the given field. | |
*/ | |
private func placeholderTextForInput(input : AgreementInput) -> String { | |
let field = input.field | |
/** | |
* Try in order: | |
* - Display name | |
* - Description | |
* - Generic "Field" | |
*/ | |
return ((field.displayName ?? field.fieldDescription ?? NSLocalizedString("Field", comment: "")) as String) | |
} | |
// MARK: - Display Attributes | |
/** | |
Calculates and returns the attributes for a given range by looking up the field and calling displayAttributesForField() | |
- returns : If the range corresponds to a field, returns a dictionary with text display attributes. Otherwise, nil. | |
*/ | |
private func attributesForInput(input: AgreementInput, highlightInvalidInput : Bool)-> [String : AnyObject]? { | |
let description = self.textToDisplayForInput(input: input) | |
var color = UIColor.blackColor() | |
if description == input.field.fieldDescription || description == input.field.displayName | |
{ | |
color = UIColor.lightGrayColor() | |
} | |
if highlightInvalidInput == true | |
{ | |
if let submission = self.submission | |
{ | |
if !submission.fieldIsValid(input.field) | |
{ | |
color = UIColor(red: 0.8, green: 0, blue: 0, alpha: 1.0) | |
} | |
else if input.field.required == false && submission.submissionValueForField(field: input.field) == nil { | |
color = UIColor(red: 0.8, green: 0.8, blue: 0.0, alpha: 1.0) | |
} | |
else | |
{ | |
print("Valid input") | |
} | |
} | |
} | |
let attributes : [String : AnyObject] = [ | |
NSForegroundColorAttributeName : color, | |
NSUnderlineColorAttributeName : UIColor.blackColor(), | |
NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleDouble.rawValue | |
] | |
return attributes | |
} | |
// MARK: - Ranges | |
/** | |
Returns a range representing an editable piece of the text, assuming | |
the target range overlaps the supplied range. | |
- parameter range : A range to check against our collection of placeholder delimiter ranges. | |
- returns : An NSRange containing the first or last index in the range, if it exists. Otherwise returns nil. | |
*/ | |
private func inputContaining(range testRange : NSRange) -> NSRange? { | |
var rangeToReturn : NSRange? | |
for input in self.inputs { | |
let editableRange = input.range | |
if editableRange.contains(range: testRange) { | |
rangeToReturn = editableRange | |
break | |
} | |
} | |
return rangeToReturn | |
} | |
/** | |
Gets the currently edited input. | |
- returns : An input that matches the range that we're editing | |
*/ | |
private func currentInput() -> AgreementInput? { | |
var currentInput : AgreementInput? = nil | |
for input in self.inputs { | |
if input.range.contains(range: self.selectedRange) { | |
currentInput = input | |
break | |
} | |
} | |
return currentInput | |
} | |
/** | |
Determines if the current range contains placeholder text. | |
- returns : true if the current range's text is a placeholder. | |
*/ | |
func currentInputContainsPlaceholderText() -> Bool { | |
guard let input = self.currentInput() else { | |
return false | |
} | |
return self.textToDisplayForInput(input: input) == self.placeholderTextForInput(input) | |
} | |
/** | |
Moves all editable ranges that follow a given range by the supplied delta. | |
- parameter delta : The offset to adjust the ranges | |
- parameter modifiedRange : The range to begin with. All ranges including and after the range at the supplied index are modified. | |
*/ | |
private func updateRangesWithDelta(delta: Int, beginningWithInput input: AgreementInput) { | |
let startIndex : Int = (self.inputs as NSArray).indexOfObject(input) | |
var index = startIndex | |
if index == NSNotFound { | |
print("Can't find start input...") | |
} | |
// print("Adjusting ranges from index \(index)\nDelta: \(delta)") | |
while index < self.inputs.count { | |
let input : AgreementInput = self.inputs[index] | |
let originalRange : NSRange = input.range | |
var newRange : NSRange | |
// If we are modifying the first range | |
// Update the length. | |
if index == startIndex { | |
newRange = NSRange(location: originalRange.location, length: originalRange.length + delta) | |
} | |
else // update the location | |
{ | |
newRange = NSRange(location: originalRange.location + delta, length: originalRange.length) | |
} | |
// print("Replacing \(self.inputs[index].range) with \(newRange).") | |
self.inputs[index].range = newRange | |
index = index + 1 | |
} | |
} | |
// MARK: - Text Manipulation | |
/** | |
Calculates how many characters the ranges should be offset by the text change. | |
- parameter textView : The UITextView who's delegate is calling | |
- returns A signed character offset which is the number of characters deleted or replaced. | |
*/ | |
func deltaValueForTextChangeInTextView(textView textView:UITextView) -> Int { | |
let delta : Int = textView.text.characters.count - self.stringLengthBeforeChange | |
// print("\nDelta: \(delta)"); | |
return delta | |
} | |
/** | |
Checks if newly inserted text is the second whitespace in a row, which triggers conversion of both spaces to a period/full stop. | |
- parameter newText : The text being inserted | |
- parameter text : The original text | |
- parameter range: The range describing where the modification is occurring | |
- returns : true if inserting this space would cause two spaces in a row to be inserted, otherwise false. | |
*/ | |
func isNewlyInsertedText(text newText: String, secondConsecutiveSpaceWhenAddedToText text:String, atRange range: NSRange) -> Bool { | |
var isSecondSpace : Bool = false | |
if newText.characters.count > 0 { | |
let whitespaceCharacterSet : NSCharacterSet = NSCharacterSet.whitespaceCharacterSet() | |
let previousCharacter = (text as NSString).characterAtIndex(range.location-1) | |
let isNewTextSingleCharacter = newText.characters.count == 1 // Verify that we are checking one character (edge case: paste text starting with space) | |
let isNewCharacterWhitespace = whitespaceCharacterSet.characterIsMember((newText as NSString).characterAtIndex(0)) // Is the new | |
let isPreviousCharacterWhitespace = whitespaceCharacterSet.characterIsMember(previousCharacter) | |
if isNewTextSingleCharacter && isPreviousCharacterWhitespace && isNewCharacterWhitespace { | |
isSecondSpace = true | |
} | |
} | |
return isSecondSpace | |
} | |
// MARK: - Subscribing to Notifications | |
/** | |
Subscribe to content size category changes. | |
*/ | |
private func subscribeToFontChangeNotifications() { | |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "contentCategorySizeDidChange:", name: UIContentSizeCategoryDidChangeNotification, object: nil) | |
} | |
/** | |
Subscribe to keyboard change notifications. | |
*/ | |
private func subscribeToKeyboardNotifications() { | |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardChanged:", name: UIKeyboardDidShowNotification, object: nil) | |
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardChanged:", name: UIKeyboardDidHideNotification, object: nil) | |
} | |
// MARK: - Handling Notifications | |
/** | |
Handle keyboard change notifications. | |
- parameter notification : An NSNotification delivered by NSNotificationCenter | |
*/ | |
func keyboardChanged(notification : NSNotification) { | |
guard let userInfo : NSDictionary = notification.userInfo else { | |
print("There's no keyboard notification userinfo. That's a bug.") | |
return | |
} | |
let key : NSString = UIKeyboardFrameEndUserInfoKey | |
let boundsValue : NSValue = userInfo[key] as! NSValue | |
let bounds = boundsValue.CGRectValue() | |
let intersection = CGRectIntersection(bounds, self.frame) | |
let oldTop = self.contentInset.top | |
self.contentInset = UIEdgeInsetsMake(oldTop, 0, intersection.size.height, 0) | |
self.scrollIndicatorInsets = self.contentInset | |
} | |
/** | |
Handle Content Category Size change notifications. | |
- parameter notification : An NSNotification delivered by NSNotificationCenter | |
*/ | |
func contentCategorySizeDidChange(notification : NSNotification) { | |
self.applyAttributes() | |
} | |
// MARK: - Modifying Selection | |
/** | |
If there's a current input, find the next one. | |
- returns : An AgreementInput if there's a "next" one. If not, returns nil. | |
*/ | |
func nextInput() -> AgreementInput? { | |
var input : AgreementInput? = nil | |
if let current = self.currentInput(), let index = self.inputs.indexOf(current) { | |
let newIndex = index.successor() | |
if newIndex < self.inputs.count && newIndex >= 0 | |
{ | |
input = self.inputs[newIndex] | |
} | |
} | |
return input | |
} | |
/** | |
If there's a current input, find the next one. | |
- returns : An AgreementInput if there's a "next" one. If not, returns nil. | |
*/ | |
func previousInput() -> AgreementInput? { | |
var input : AgreementInput? = nil | |
if let current = self.currentInput(), let index = self.inputs.indexOf(current) { | |
let newIndex = index.predecessor() | |
if newIndex < self.inputs.count && newIndex >= 0 | |
{ | |
input = self.inputs[newIndex] | |
} | |
} | |
return input | |
} | |
func hasNextInput() -> Bool { | |
var hasNext : Bool = false | |
if let _ = nextInput() { | |
hasNext = true | |
} | |
return hasNext | |
} | |
func hasPreviousInput() -> Bool { | |
var hasPrev : Bool = false | |
if let _ = self.previousInput() { | |
hasPrev = true | |
} | |
return hasPrev | |
} | |
// MARK: - Activating A Different Input | |
func activateNext() { | |
if let next = self.nextInput() | |
{ | |
self.selectedRange = next.range | |
} | |
} | |
func activatePrevious() { | |
if let previous = self.previousInput() | |
{ | |
self.selectedRange = previous.range | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment