Last active
July 28, 2020 02:58
-
-
Save CapnSpellcheck/317de353688777879dc323738ec1a223 to your computer and use it in GitHub Desktop.
Extending UITextFieldDelegate and UITextViewDelegate to enable modifying edit replacements; multi-delegates provide the ability to compose delegates (filters). Note: Multi-Delegates necessarily strongref their constituents.
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
import Foundation | |
@objc protocol ExtendedUITextFieldDelegate : UITextFieldDelegate { | |
@objc optional func extendedTextField(_ textField: UITextField, | |
shouldChangeCharactersIn range: NSRange, | |
replacementString string: String) -> String? | |
} | |
class TextFieldMultiDelegate : NSObject, UITextFieldDelegate { | |
private var delegates: Array<ExtendedUITextFieldDelegate> = [] | |
func add(delegate: ExtendedUITextFieldDelegate) { | |
delegates.append(delegate) | |
} | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
var ret = true | |
var replacement = string | |
for d in delegates { | |
if d.responds(to: #selector(ExtendedUITextFieldDelegate.extendedTextField(_:shouldChangeCharactersIn:replacementString:))) { | |
if let new = d.extendedTextField?(textField, shouldChangeCharactersIn: range, replacementString: replacement) { | |
ret = false | |
replacement = new | |
} | |
} else if d.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false { | |
return false | |
} | |
} | |
if !ret { | |
textField.text = (textField.text! as NSString).replacingCharacters(in: range, with: replacement) | |
} | |
return ret | |
} | |
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { | |
for d in delegates { | |
if d.textFieldShouldBeginEditing?(textField) == false { | |
return false | |
} | |
} | |
return true | |
} | |
func textFieldDidBeginEditing(_ textField: UITextField) { | |
for d in delegates { | |
d.textFieldDidBeginEditing?(textField) | |
} | |
} | |
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { | |
for d in delegates { | |
if d.textFieldShouldEndEditing?(textField) == false { | |
return false | |
} | |
} | |
return true | |
} | |
func textFieldDidEndEditing(_ textField: UITextField) { | |
for d in delegates { | |
d.textFieldDidEndEditing?(textField) | |
} | |
} | |
} | |
@objc protocol ExtendedUITextViewDelegate : UITextViewDelegate { | |
@objc optional func extendedTextView(_ textView: UITextView, | |
shouldChangeCharactersIn range: NSRange, | |
replacementString string: String) -> String? | |
} | |
class TextViewMultiDelegate : NSObject, UITextViewDelegate { | |
private var delegates: Array<ExtendedUITextViewDelegate> = [] | |
func add(delegate: ExtendedUITextViewDelegate) { | |
delegates.append(delegate) | |
} | |
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText string: String) -> Bool { | |
var ret = true | |
var replacement = string | |
for d in delegates { | |
if d.responds(to: #selector(ExtendedUITextViewDelegate.extendedTextView(_:shouldChangeCharactersIn:replacementString:))) { | |
if let new = d.extendedTextView?(textView, shouldChangeCharactersIn: range, replacementString: replacement) { | |
ret = false | |
replacement = new | |
} | |
} else if d.textView?(textView, shouldChangeTextIn: range, replacementText: replacement) == false { | |
return false | |
} | |
} | |
if !ret { | |
textView.text = (textView.text! as NSString).replacingCharacters(in: range, with: replacement) | |
} | |
return ret | |
} | |
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { | |
for d in delegates { | |
if d.textViewShouldBeginEditing?(textView) == false { | |
return false | |
} | |
} | |
return true | |
} | |
func textViewDidBeginEditing(_ textView: UITextView) { | |
for d in delegates { | |
d.textViewDidBeginEditing?(textView) | |
} | |
} | |
func textViewShouldEndEditing(_ textView: UITextView) -> Bool { | |
for d in delegates { | |
if d.textViewShouldEndEditing?(textView) == false { | |
return false | |
} | |
} | |
return true | |
} | |
func textViewDidEndEditing(_ textView: UITextView) { | |
for d in delegates { | |
d.textViewDidEndEditing?(textView) | |
} | |
} | |
} | |
class NewlineLimitTextViewFilter : NSObject, ExtendedUITextViewDelegate { | |
private var newlines: Int16 = 0 | |
private let limit: Int16 | |
private let charset = CharacterSet.newlines | |
init(newlineLimit: Int16) { | |
limit = newlineLimit | |
} | |
func extendedTextView(_ textView: UITextView, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> String? { | |
NSLog("NewlineLimitFilter: limit=%d current=%d", limit, newlines) | |
(textView.text as NSString).substring(with: range) | |
.forEach { char in | |
if charset.contains(char.unicodeScalars.first!) { | |
newlines -= 1 | |
NSLog("NewlineLimitFilter: decr") | |
} | |
} | |
var index = string.startIndex | |
while index != string.endIndex { | |
let char = string[index] | |
if charset.contains(char.unicodeScalars.first!) { | |
newlines += 1 | |
NSLog("NewlineLimitFilter: decr") | |
if newlines >= limit { | |
NotificationCenter.default.post(name: .NewlineLimitExceeded, object: textView) | |
newlines = limit | |
return String(string[string.startIndex ..< index]) | |
} | |
} | |
index = string.index(after: index) | |
} | |
return nil | |
} | |
func textViewDidBeginEditing(_ textView: UITextView) { | |
newlines = 0 | |
textView.text.forEach { char in | |
if charset.contains(char.unicodeScalars.first!) { | |
newlines += 1 | |
} | |
} | |
} | |
} | |
// length limit is of utf16 view | |
class LengthLimitTextFilter : NSObject, ExtendedUITextFieldDelegate, ExtendedUITextViewDelegate { | |
private let limit: Int | |
init(lengthLimit: Int) { | |
limit = lengthLimit | |
} | |
func extendedTextView(_ textView: UITextView, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> String? { | |
let utf16 = textView.text.utf16 | |
let nsstring = string as NSString | |
let length = utf16.count - range.length + nsstring.length | |
NSLog("LengthLimitTextFilter: limit=%d current=%d range=%d replacement=%d", limit, utf16.count, range.length, nsstring.length) | |
if length > limit { | |
return nsstring.substring(to: nsstring.length - (length - limit)) | |
} | |
return nil | |
} | |
func extendedTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> String? { | |
let utf16 = textField.text!.utf16 | |
let nsstring = string as NSString | |
let length = utf16.count - range.length + nsstring.length | |
if length > limit { | |
return nsstring.substring(to: nsstring.length - (length - limit)) | |
} | |
return nil | |
} | |
} | |
class AlphabeticTextFilter : NSObject, ExtendedUITextFieldDelegate { | |
private let charset: CharacterSet | |
init(extended: Bool) { | |
if extended { | |
let others = CharacterSet(charactersIn: " '\u{8217}\u{2D}\u{2010}\u{2011}\u{058A}\u{1806}\u{1B60}\u{30FB}\u{FE63}\u{FF0D}\u{FF65}") | |
charset = CharacterSet.letters.union(others) | |
} else { | |
charset = CharacterSet.letters | |
} | |
} | |
func extendedTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> String? { | |
var index = string.startIndex | |
while index != string.endIndex { | |
let char = string[index] | |
if !char.unicodeScalars.allSatisfy(charset.contains(_:)) { | |
return String(string[string.startIndex ..< index]) | |
} | |
index = string.index(after: index) | |
} | |
return nil | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment