Slow NSAttributed String Rendering - swift

I'm working on an app for text editing, I when I enter more than 10 000 the app is very slow. I run some for loops during typing, but it's should be a big deal. Is it possible to load the text as you scroll down, in chunks, or something else? Like Dispatch etc? I'm working with NSAttributed String
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool
{
textView.textStorage.addAttribute(NSForegroundColorAttributeName, value: styles.titleTextColor().withAlphaComponent(0.10), range: NSMakeRange(0, textView.attributedText.length))
let textRange: UITextRange? = textView.tokenizer.rangeEnclosingPosition((textView.selectedTextRange?.start)!, with: UITextGranularity.sentence, inDirection: 1)
if textRange != nil
{
let sentenceRange = textView.rangeFromTextRange(textRange: textRange!)
textView.textStorage.addAttribute(NSForegroundColorAttributeName, value: styles.titleTextColor().withAlphaComponent(1), range: sentenceRange)
}
}
extension UITextView
{
func rangeFromTextRange(textRange: UITextRange) -> NSRange
{
let location : Int = self.offset(from: self.beginningOfDocument, to: textRange.start)
let length : Int = self.offset(from: textRange.start, to: textRange.end)
return NSMakeRange(location, length)
}

Related

NSTextView cursor doesn't appear when typing text on macOS 10.14

I'm observing a strange issue on macOS 10.12 Mojave with NSTextView.
.
I'm changing the textStorage attributes in didChangeText() like this :
self.textStorage?.beginEditing()
ARTokenManager.getToken(text: text, language: language) { (tokens) in
// This line reset the attributes
// If I remove it, the cursor appear properly
// But the attributes are conserved
self.textStorage?.setAttributes([NSAttributedString.Key.font: self.font!,
NSAttributedString.Key.foregroundColor: self.defaultTextColor], range: range)
for token in tokens {
let attributeRange = NSRange(location: token.range.location + range.location, length: token.range.length)
self.textStorage?.addAttributes(token.attributes, range: attributeRange)
}
}
self.textStorage?.endEditing()
When I remove the setAttributes method, everything works as expected, but I can't explain why. I'm possibly resetting the attributes wrong. This issue only works with Mojave.
Does someone have the same issue or can explain me what I'm doing wrong ?
Thank you.
After some research, I discovered that my question was more about syntax highlighting with NSTextView. I know this is a question that a lot of macOS developers are asking about and there are a lot of solutions for that. This is not probably the best one, but this is how I’ve solved this problem.
NSTextStorage
To achieve that, I’ve used a subclass of NSTextStorage. This is where all the syntax work will be done. NSTextStorage is not protocol oriented so you have to override method by yourself as the Apple documentation suggest :
class SyntaxTextStorage: NSTextStorage {
private var storage: NSTextStorage
override var string: String {
return storage.string
}
override init() {
self.storage = NSTextStorage(string: "")
super.init()
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
return storage.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
storage.replaceCharacters(in: range, with: str)
edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
beginEditing()
storage.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
This is the basic to create your text storage.
NSTextStorage + NSTextView
The next step is to set your text storage into your textView. To do so, you can use the replaceTextStorage() method accessible in the textView layoutManager.
class SyntaxTextView: NSTextView {
var storage: SyntaxTextStorage!
override func awakeFromNib() {
super.awakeFromNib()
configureTextStorage()
}
private func configureTextStorage() {
storage = SyntaxTextStorage()
layoutManager?.replaceTextStorage(storage)
}
}
Syntaxing
The final step is to do your syntax job. The CPU cost of this process is very hight. There is a lot of way to do it to have the best performances. I suggest you to implement a class that will returns you a list of NSAttributedString and NSRange. The job of the text storage should only be applying the style to your text.
Personally, I've used the processEditing method to perform my text analyze :
override func processEditing() {
super.processEditing()
syntaxCurrentParagraph()
}
I recommend you to do you syntax analyze in background, then, if there is no text change since your last analyze, apply the change to your text. Always in my text storage, I've implemented a syntax method that apply the style to the text :
private func syntax(range: NSRange, completion: ((_ succeed: Bool) -> Void)? = nil) {
guard range.length > 0 else {
completion?(true)
return
}
// Save your data to do the job in background
let currentString = self.string
let substring = currentString.substring(range: range)
let mutableAttributedString = NSMutableAttributedString(string: substring, attributes: NSAttributedString.defaultAttributes as [NSAttributedString.Key : Any])
DispatchQueue.global(qos: .background).async {
ARSyntaxManager.tokens(text: substring, language: self.language) { (tokens) in
// Fill you attributed string
for token in tokens {
mutableAttributedString.addAttributes(token.attributes, range: token.range)
}
DispatchQueue.main.async {
// Check if there is no change
guard self.string.count == currentString.count else {
completion?(false)
return
}
completion?(true)
// Apply your change
self.storage.replaceCharacters(in: range, with: mutableAttributedString)
self.displayVisibleRect()
}
}
}
}
That's it. Hope it will help some of you.
I found the solution. I have to use the didProcessEditing method in NSTextStorageDelegate instead of didChangeText.

Set password protection in UITextView

Can i hide password in UITextView by * or any other symbol? I need to use UITextView instead of UITextField. I want to hide all characters of textView.
Using an UITextView leaves the whole job of masking the text yourself. You also need to make sure you disable copying for security reasons. Set your delegate property and handle this something on these lines:
var originalText: String?
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
originalText = ((originalText ?? "") as NSString).replacingCharacters(in: range, with: text)
return true
}
func textViewDidChange(_ textView: UITextView) {
textView.text = String(repeating: "*", count: (textView.text ?? "").count)
}
If you need to retrieve the value of the actual text that was input use the originalText property.
Create a global variable for password string.
var passwordString = ""
Then set delegates of UITextView like:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
passwordString = ((passwordString ?? "") as NSString).replacingCharacters(in: range, with: text)
return true
}
func textViewDidChange(_ textView: UITextView) {
//replace character with * or anyother character
yourtextView.text = String(repeating: "*", count: (textView.text ?? "").count)
}
and dont forget to do this:
yourTextview.delegate = self
I like to share my own implementation after using the previous answers for a while in a chat-like app, where the UITextView is constantly filled and emptied.
My UITextView works as an entry of text for different kind of data types (phones, e-mails, etc.) and I did not want to create other UITextView specifically for handling this scenario, so I decided to subclass it and restructure a little bit the code because I faced with circumstances that the logic breaks when using an external keyboard or changing the text property programatically (doing the last one does not call the delegate method).
So first subclassing...
UITextView subclass
class MyTextView: UITextView {
var isProtected = false // `true` for activate the password mode
var plainText: String! = String() // Variable to save the text when `isProtected`
override var text: String! {
get { return isProtected ? plainText : super.text }
set {
if !isProtected {
plainText = newValue
}
super.text = newValue
}
}
}
PS: The overriding of the text property helps us to get always the plain text in the UITextView without calling other variables.
Then, in the view controller where the delegate is implemented...
UITextViewDelegate
extension MyViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if myTextView.isProtected {
myTextView.plainText = (myTextView.plainText as NSString).replacingCharacters(in: range, with: text) // Basically: when is in password mode, saves all written characters in our auxiliar variable
}
return true
}
func textViewDidChange(_ textView: UITextView) {
if myTextView.isProtected {
textView.text = String(repeating: "•", count: textView.text.count) // Change every letter written with the character "•"
}
}
}
Finally, you only need to toggle the isProtected flag somewhere in MyViewController and that`s it:
myTextView.isProtected = true //or `false`

How to prevent username textfield for not using whitespace or special characters swift 3.0 [duplicate]

I am creating a trivia application that asks for a username on start up. I'd like to make it impossible to use characters such as #$#!^& etc (also including "space"). I took a look at this post here but it is written entirely in Objective-C. Thanks in advance.
Swift 4 iOS 11.2.x based on using an extension, tests to see if a string is a valid hex number in this example.
extension String {
var containsValidCharacter: Bool {
guard self != "" else { return true }
let hexSet = CharacterSet(charactersIn: "1234567890ABCDEFabcdef")
let newSet = CharacterSet(charactersIn: self)
return hexSet.isSuperset(of: newSet)
}
}
You use it like with the UITextFieldDelegate.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return (string.containsValidCharacter)
}
Although I read in an earlier post that CharacterSets do not support characters that are composed of more than one Unicode.Scalar; so use with caution I guess.
Since you're explicitly asking for Swift, I've translated the top asnwer in the linked question.
let notAllowedCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.";
func textField(
textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String)
-> Bool
{
let set = NSCharacterSet(charactersInString: notAllowedCharacters);
let inverted = set.invertedSet;
let filtered = string
.componentsSeparatedByCharactersInSet(inverted)
.joinWithSeparator("");
return filtered != string;
}
internal func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if let text = string{
if text == "#" || text == "$" || text == "!"{ \\and so on
return false
}
}
return true
}
Swift 2.3
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let characters = ["#", "$", "!", "&","#"]
for character in characters{
if string == character{
print("This characters are not allowed")
return false
}
}
}
So this is probably the most robust way to restrict Spaces. Using this user won't be able to Paste/Type Whitespaces
This is how you can Implement using Swift 3.
Add below mentioned extension snippet to a Swift file;
extension String {
var containsWhitespace: Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x20:
return true
default:
continue
}
}
return false
}
}
In your ViewController Swift file drag out your Editing Changed Instance and a Referencing Outlet of UITextField from Storyboard, the one mentioned in picture below:
Use the dragged Instances as mentioned below:
Referencing Outlet as:
#IBOutlet weak var textField: UITextField!
and Editing Changed as:
#IBAction func textChanged(_ sender: UITextField) {
if (textField.text!.containsWhitespace) == true {
print("Restrict/Delete Whitespace")
emailField.deleteBackward()
} else {
print("If Its not Whitespace, Its allowed.")
}
}
This will detect and remove whitespace as soon as user tries to type/paste it.
Swift 4 iOS 11.2.x based on using an extension, tests to see if a string is a valid hex number in this example.
extension String {
var containsValidCharacter: Bool {
guard self != "" else { return true }
let hexSet = CharacterSet(charactersIn: "1234567890ABCDEFabcdef")
let newSet = CharacterSet(charactersIn: self)
return hexSet.isSuperset(of: newSet)
}
}
You use it like with the UITextFieldDelegate.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return (string.containsValidCharacter)
}
Swift : 3 and a different approach:
Add a target function for the text field change in your viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(ViewController.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
}
in the target function, simply detect the entered char and replace it with blank. I have tested it and it prevents the user from entering any non desirable characters in the text field.
func textFieldDidChange(textField: UITextField) {
if let textInField = textField.text{
if let lastChar = textInField.characters.last{
//here include more characters which you don't want user to put in the text field
if(lastChar == "*")
{
textField.text = textInField.substring(to: textInField.index(before: textInField.endIndex))
}
}
}
}
Adding on to what #Evdzhan Mustafa said. You want to add a return statement in case the string is empty. Without it you won't be able to delete your text. Modified Code Below:
Swift 3 Version
let notAllowedCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.";
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isEmpty{
return true
}
print("String: \(string)")
let set = NSCharacterSet(charactersIn: notAllowedCharacters);
let inverted = set.inverted;
let filtered = string.components(separatedBy: inverted).joined(separator: "")
print("String Filtered: \(filtered)")
return filtered != string;
}

Uneditable prefix inside a UITextField using Swift

I'm having a problem regarding the creation of a prefix inside a UITextField using the new Swift language. Currently I have created the UITextField using the Interface Builder and I have assigned an IBOutlet to it, named usernameField, then using the textFieldDidBeginEditing function I write a NSMutableAttributedString inside it, named usernamePrefix, containing only the word "C-TAD-" and finally I limited the UITextField max characters number to 13, like so:
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet var usernameField : UITextField!
private var usernamePrefix = NSMutableAttributedString(string: "C-TAD-")
func textFieldDidBeginEditing(textField: UITextField) {
if textField == usernameField {
if usernameField.text == "" {
usernameField.attributedText = usernamePrefix
}
}
usernameField.addTarget(self, action: "textFieldDidChangeText:", forControlEvents:UIControlEvents.EditingChanged)
}
func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {
let maxUsernameLength = countElements(usernameField.text!) + countElements(string!) - range.length
return maxUsernameLength <= 13
}
override func viewDidLoad() {
super.viewDidLoad()
usernameField.delegate = self
passwordField.delegate = self
}
}
Now, how can I assign new parameters to the usernamePrefix in order to have to give 2 different colors to the text written in the UITextField? I would like to have the prefix in .lightGreyColor() and the rest in .blackColor(). Also how can I make the usernamePrefix un-editable and un-deletable by the user?
Thanks for the help
Simpler option would be to set leftView of the UITextField and customise it how you like it:
let prefix = UILabel()
prefix.text = "C-TAD-"
// set font, color etc.
prefix.sizeToFit()
usernameField.leftView = prefix
usernameField.leftViewMode = .whileEditing // or .always
It is un-editable and un-deletable and you don't need to do any calculations to check the length of the input.
For the first part, you can refactor your delegate method as follow.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
//This makes the new text black.
textField.typingAttributes = [NSForegroundColorAttributeName:UIColor.blackColor()]
let protectedRange = NSMakeRange(0, 6)
let intersection = NSIntersectionRange(protectedRange, range)
if intersection.length > 0 {
return false
}
if range.location == 12 {
return true
}
if range.location + range.length > 12 {
return false
}
return true
}
This will lock down both the length at 13 and the prefix can not be deleted. Everything typed will be UIColor.blackColor()
Then you can a method like the following in your viewDidLoad, to set the prefix.
func makePrefix() {
let attributedString = NSMutableAttributedString(string: "C-TAD-")
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.lightGrayColor(), range: NSMakeRange(0,6))
textField.attributedText = attributedString
}
I've adopted the solution from Jeremy and make a little bit improvement to make it a bit more swifty, and also handle the case when user pastes multiple characters into the text field.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let protectedRange = NSRange(location: 0, length: usernamePrefix.length)
let intersection = protectedRange.intersection(range)
// prevent deleting prefix
if intersection != nil {
return false
}
// limit max character count
if (textField.text ?? "").count + string.count > 13 {
return false
}
return true
}

UITextView length issue

I've a UITextView with scroll enabled, I can change font and size of the text inside. So, How can I set a text limit? I've already tried to set max row and max number of character but it doesn't working because I need to set a size limit of the text, How can I do this?
You have to implement the UITextViewDelegate:
class ViewController: UIViewController, UITextViewDelegate {
and then set the delegate of the textView in the viewDidLoad-method:
yourTextView.delegate = self
After that you can use the shouldChangeTextInRange method to check the letters:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
let maxtext: Int = 140
//If the text is larger than the maxtext, the return is false
// Swift 2.0
return textView.text.characters.count + (text.characters.count - range.length) <= maxtext
// Swift 1.1
// return countElements(textView.text) + (countElements(text.length) - range.length) <= maxtext
}
The nice part in this solution is, that you can even control cut/copy and paste. So the user can't trick the field to accept more letters by copying.
1) class ViewController: UIViewController, UITextViewDelegate {
2) textView.delegate = self
3)
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
let maxCharacter: Int = 1000
return (textView.text?.utf16.count ?? 0) + text.utf16.count - range.length <= maxCharacter
}
Usually you'd conform to the UITextFieldDelegate protocol, specify your view controller as the delegate for the UITextField (specifying this either in IB or in code) and then implement a shouldChangeCharactersInRange that considers the length of the string:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let oldString = textField.text ?? ""
let startIndex = oldString.startIndex.advancedBy(range.location)
let endIndex = startIndex.advancedBy(range.length)
let newString = oldString.stringByReplacingCharactersInRange(startIndex ..< endIndex, withString: string)
return newString.characters.count <= kMaxLength
}
If, on the other hand, you want to control the amount of text entered to conform to the size of the control, you could use the aforementioned shouldChangeTextInRange, but use sizeWithAttributes to dictate whether that method returns true or false rather than the number of characters.
This isn't quite right, but it illustrates the basic idea:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let oldString = textField.text ?? ""
let startIndex = oldString.startIndex.advancedBy(range.location)
let endIndex = startIndex.advancedBy(range.length)
let newString = oldString.stringByReplacingCharactersInRange(startIndex ..< endIndex, withString: string)
let stringSize = newString.sizeWithAttributes([NSFontAttributeName : textField.font ?? UIFont.systemFontSize()])
return stringSize.width < textField.editingRectForBounds(textField.bounds).size.width
}