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

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.

Related

Custom keyboard and shouldChangeCharactersIn

I have custom keyboard which I added as textfield.inputView. But my shouldChangeCharactersIn at UITextFieldDelegate doesn’t work? Any idea?
You have to implement the call to the delegate method yourself prior to inserting the text into the control.
For example, let’s assume you have some function for handling tap of a button, which calls UIKeyInput method insertText. Just check that the delegate implements the method and that it did not return false:
#objc func didTapButton(_ sender: ...) {
guard let range = target?.selectedRange else { return } // assumes `target` was defined to conform to `UITextInput`, using extension shared below
let string = ...
if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
return
}
if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
return
}
target?.insertText(string) // assumes `target` was defined to conform to `UIKeyInput`
}
Where:
extension UITextInput {
var selectedRange: NSRange? {
guard let textRange = selectedTextRange else { return nil }
let location = offset(from: beginningOfDocument, to: textRange.start)
let length = offset(from: textRange.start, to: textRange.end)
return NSRange(location: location, length: length)
}
}
Clearly, the details are dependent upon your custom keyboard implementation (the above is based upon https://stackoverflow.com/a/57275689/1271826), but hopefully it illustrates the basic idea.

macOS: Take Emoji from CharacterPalette (revised)

(This is a revised question - including answer - following on from macOS: Take emoji from characterPalette which describes the problems encountered in more detail)
Background/use case
I have an app where, instead of creating and maintaining an icon library, I let users type an emoji as a placeholder graphic. This works beautifully within the context of my app, but I am not happy with the input mechanism I use.
Problem
I would like to simplify this so I open the characterPalette, select an emoji, and display it either as the button's StringValue or in a Label (=non-editable NSTextField).
This does not seem possible. Unlike NSColorPanel or NSFontPanel, the characterPanel is not exposed to the Cocoa framework, so I cannot take its selectedValue, set its action, or catch a notification. The documentation for orderFrontCharacterPalette simply says Opens the character palette which ... is not helpful.
Attempted solutions and problems encountered
I tried to work with making my receiver the firstResponder, but unlike NSTextView, NSTextField cannot process emoji. I found a workaround using an NSTextView with an NSBox in front, making it the firstResponder, and using NSApp.orderFrontCharacterPalette(sender)but found that under various circumstances which all seem to involve an extra drawing call – setting the button's title, showing a label in SystemFont Mini size (regular size worked fine) the CharacterPalette will open (=the system menu now offers 'Hide Emoji & Symbols') without being displayed. (This persists until the application closes, even if you try to open the CharacterPalette through the regular menu/shortcut)
For the partial solution involving NSTextInputClient (the no-show seems to be a persistent bug), see answer below.
The emoji picker needs a minimal implementation of NSTextInputClient. For example a button:
class MyButton: NSButton, NSTextInputClient {
override var acceptsFirstResponder: Bool {
get {
return true
}
}
override func becomeFirstResponder() -> Bool {
return true
}
override func resignFirstResponder() -> Bool {
return true
}
func insertText(_ string: Any, replacementRange: NSRange) {
// this method is called when the user selects an emoji
if let string = string as? String {
self.title = string
}
}
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
}
func unmarkText() {
}
func selectedRange() -> NSRange {
return NSMakeRange(0, 0)
}
func markedRange() -> NSRange {
return NSMakeRange(NSNotFound, 0)
}
func hasMarkedText() -> Bool {
return false
}
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
return nil
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return []
}
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
// the emoji picker uses the returned rect to position itself
var rect = self.bounds
rect.origin.x = NSMidX(rect)
rect.size.width = 0
return self.window!.convertToScreen(self.convert(rect, to:nil))
}
func characterIndex(for point: NSPoint) -> Int {
return 0
}
}
NSTextInputClient needs a NSTextInputContext. NSView returns a context from inputContext if the class conforms to NSTextInputClient unless isEditable is implemented and returns false. A label doesn't return a NSTextInputContext, the solution is to override inputContext:
class MyTextField: NSTextField, NSTextInputClient {
var myInputContext : NSTextInputContext?
override var inputContext: NSTextInputContext? {
get {
if myInputContext == nil {
myInputContext = NSTextInputContext(client:self)
}
return myInputContext
}
}
// and the same methods as the button, set self.stringValue instead of self.title in insertText(_:replacementRange:)
}
Willeke pointed me at NSTextInputClient which has provided the best solution so far. Apple's only example is in ObjectiveC, convoluted, and overly complex for what I was trying to do, so I am reproducing my code here.
Caveat: this is not a full implementation of NSTextInputClient, just enough to capture emoji input
I have created an NSButton subclass:
class TextReceiverButton: NSButton, NSTextInputClient {
//specific methods
func setButtonTitle(_ string: String?){
self.title = string ?? 🦊
}
//NSTextInputClient methods
func insertText(_ string: Any, replacementRange: NSRange) {
let receivedText = string as? String
setButtonTitle(receivedText)
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return [.font, .paragraphStyle, .writingDirection]
}
//Omitted: For anything else that wants a value, I return NSMakeRange(0, 0)/NSRect.zero or 0 as well as false for marked text and nil for attributed substring
}
(If you add the protocol to your class, it will offer stubs for the other methods)
The full set for NSAttributedString.Key is
[.font, .foregroundColor, .glyphInfo, .kern, .ligature, .link, .markedClauseSegment, .obliqueness, .paragraphStyle, .shadow, .spellingState, .strikethroughColor, .strikethroughStyle, .strokeColor, .strokeWidth, .superscript, .textAlternatives, .textEffect, .toolTip, .underlineColor, .underlineStyle, .verticalGlyphForm, .writingDirection]
(I have tested the short form with simple and composite emoji and nothing else seems necessary.)
The button's action is
#IBAction func displayEmojiInButton(_ sender: Any) {
NSApp.orderFrontCharacterPalette(self)
view.window?.makeFirstResponder(textReceiverButton)
}
Problems/Bugs
The NSTextInputClient document says 'you can subclass NSView' and Apple's code turns an NSView into a fully functional (receiving and drawing) text view class (I can't built it, but I assume it worked). So theoretically, you should be able to use the same code for NSTextField, which also ultimately inherits from NSView.
However, it turns out that NSTextField displays the 'CharacterPalette allegedly opens but never displays' bug I talked about earlier; though it does work with NSView. (I have not tested this further).
Furthermore, NSTextInputClient is not a complete replacement for NSTextView: it does not receive input from the keyboard viewer. (See Willecke's answer/comment for explanation/solution to these).
Verdict
NSApp.orderFrontCharacterPalette(self) fails 95% of the time when called from a view in the vincinity of a tab view (in splitView next to TabViewController, embedded in TabViewController), so while this code may be correct, it's also useless a lot of the time, at least under 10.13.

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`

Swift checking textfield input live

I've been struggling to find how to check the input of a textfield as a user is typing.
if a user types a word, it should change a label and an image according to some defined rules.
my code is working, but I'm always a step behind. (as it reads the content always before the next character is entered.
If it just to check the length I could use countElements(textfield) + 1, but I want it to also show that a user cannot use certain characters as they are typing, therefore that would not work for checking undesired characters.
I am assuming the function I am using is not the right one "shouldChangeCharacters". So I am a bit lost as to what to use. Is there a way to read a println or NSLog command to return to an outlet?
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let passwordcheck = UserPasswordTextField.text
if UserPasswordTextField.isFirstResponder() {
if isValidPassword(passwordcheck) {
PWimg.image = UIImage(named: "passwordapprovedicon")
} else if passwordcheck.isEmpty {
PWimg.image = UIImage(named: "passwordiconwrong")
} else {
PWimg.image = UIImage(named: "passwordiconwrong")
}
}
func isValidPassword(testStr2:String) -> Bool {
println("validate password: \(testStr2)")
let passwordRegEx = "[A-Z0-9a-z._%+-:/><#]{6,30}"
if let passwordTest = NSPredicate(format: "SELF MATCHES %#", passwordRegEx) {
return passwordTest.evaluateWithObject(testStr2)
}
return false
Listen for UIControlEventEditingChanged events from the text field. Register for them either with
the Interface Builder: drag from the text field to the file, and select the action type "Editing Changed"
the following code:
override func viewDidLoad() {
super.viewDidLoad()
// ...
textField.addTarget(self, action:"edited", forControlEvents:UIControlEvents.EditingChanged)
}
func edited() {
println("Edited \(textField.text)")
}
Updated for swift 3:
override func viewDidLoad() {
super.viewDidLoad()
//...
textField.addTarget(self, action: #selector(textFieldDidChange), for:.editingChanged)
}
func textFieldDidChange(){
print(textField.text)
}
Updated for swift 4.2: just add #objc to func textFieldDidChange()
override func viewDidLoad()
{
super.viewDidLoad()
textField.addTarget(self,
action : #selector(textFieldDidChange),
for : .editingChanged)
}
#objc func textFieldDidChange()
{ print(textField.text ?? "Doh!") }

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
}