Placeholder Text for UITextView mixed w/ First Responder - swift

I currently have a text view with placeholder text that dissapears whenever a user taps on the text view and the text in the textview reappears whenever the first responder is resigned, if the text view is empty. (Heres the code I use for that in case anyone wants to use it)
*Note, first set the text color of the textview to light gray and set the placeholder text. Then use these methods:
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
//If it begins editing, then set the color to black
if (textView.tag == 0){
textView.text = ""
textView.textColor = .black
textView.tag = 1
}
return true
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = "Example: I started my career as a street wear model based in Maryland. After 3 years of working with some of the top companies there, I moved to LA, where I currently reside. I’ve been featured in shows, 12 magazines, commercials, and a number of music videos. Now, Im currently looking to continue working with clothing companies and campaigns."
textView.textColor = .lightGray
textView.tag = 0
}
}
I wanted to step things up a notch. Right now, the text disappears whenever the text view becomes the first responder. I want the text to disappear whenever the user actually starts typing, not just when the text view is selected. When the screen appears, I automatically set the first responder to the text view and I plan on keeping it that way. But because its automatically set, you're not able to see the placeholder text. I only want the text view to disappear whenever the user presses a key, not because its selected.

Lets assume you have a placeholder text as this,
let placeholderText = "Example: I started my career as a street wear model based in Maryland. After 3 years of working with some of the top companies there, I moved to LA, where I currently reside. I’ve been featured in shows, 12 magazines, commercials, and a number of music videos. Now, Im currently looking to continue working with clothing companies and campaigns."
And you are setting this text to textView in storyboard or in viewDidLoad before calling becomeFirstResponder of textView.
Then in these two delegate methods you can achieve this behavior as below,
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if textView.text == placeholderText {
textView.text = ""
}
return true
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = placeholderText
}
}
Currently you are clearing the text in textViewShouldBeginEditing so, that is the main reason you are not able to see that text. You should remove clearing the text there but you can keep changing the colors etc as it is.

Here's the code
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
let placeholder = "Example: I started my career as a street wear model based in Maryland. After 3 years of working with some of the top companies there, I moved to LA, where I currently reside. I’ve been featured in shows, 12 magazines, commercials, and a number of music videos. Now, Im currently looking to continue working with clothing companies and campaigns."
let start = NSRange(location: 0, length: 0)
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
textView.text = placeholder
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChangeSelection(_ textView: UITextView) {
// Moves cursor to start when tapped on textView with placeholder
if textView.text == placeholder {
textView.selectedRange = start
}
}
func textViewDidChange(_ textView: UITextView) {
// Manages state of text when changed
if textView.text.isEmpty {
textView.text = placeholder
textView.textColor = .lightGray
} else if textView.text != placeholder {
textView.textColor = .black
}
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Called when you're trying to enter a character (to replace the placeholder)
if textView.text == placeholder {
textView.text = ""
}
return true
}
}
Updated #1: Maximum length
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Called when you're trying to enter a character (to replace the placeholder)
if textView.text == placeholder {
textView.text = ""
} else if textView.text.count >= 175 && text.count > 0 {
// Now it can delete symbols but can't enter new
return false
}
return true
}
There's slight bug when trying to clear textView by holding erase button. Placeholder is not showing. Method textViewDidChange somehow is not called (goes out of sync with shouldChangeTextIn). Possible workaround https://stackoverflow.com/a/9169556/5228431

Related

why the textfield delegate not works with a setting text programmatically on textfield?

I'm setting programmatically text in textfield, the text is a birthdate, but the delegate not works, if I use a native keyboard the delegate works, but if I set a text programmatically on a textfield, the delegates not works
I'm using swift 4.2
import UIKit
class userBirthdateViewController: UIViewController, UITextFieldDelegate, NumberCalculable {
#IBOutlet weak var messageBirthdate: UILabel!
#IBOutlet weak var inputBirthdate: UITextField!
#IBOutlet weak var keyboardView: keyboardView!
override func viewDidLoad() {
super.viewDidLoad()
messageBirthdate.text = "Debes tener 18 años cumplidos para\npoder abrir una cuenta Flink"
inputBirthdate.attributedPlaceholder = NSAttributedString(string: "DD-MM-AAAA", attributes: [NSAttributedString.Key.foregroundColor: UIColor.gray])
self.inputBirthdate.inputView = UIView()
self.inputBirthdate.becomeFirstResponder()
keyboardView.delegate = self
inputBirthdate.delegate = self
// Do any additional setup after loading the view.
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == inputBirthdate {
// check the chars length dd -->2 at the same time calculate the dd-MM --> 5
if (inputBirthdate?.text?.count == 2) || (inputBirthdate?.text?.count == 5) {
//Handle backspace being pressed
if !(string == "") {
// append the text
inputBirthdate?.text = (inputBirthdate?.text)! + "-"
}
}
// check the condition not exceed 9 chars
return !(textField.text!.count > 9 && (string.count ) > range.length)
}
else {
return true
}
}
#IBAction func probe(_ sender: Any) {
}
func addNumber(_ number: Int) {
if number != -1
{
inputBirthdate.insertText("\(number)")
}
else
{
if !inputBirthdate.text!.isEmpty
{
inputBirthdate.text?.removeLast()
}
}
}
}
I expect that the textfield responds like a native keyboard, the only difference is that I set programmatically the text on textfield.
I use this line self.inputBirthdate.inputView = UIView() because I don't want that the native keyboard appears, but only the cursor
how I can resolve this?
I tried with set insertText but not works :(
Delegate methods responds to user input only. you need to manually call the delegate methods after setting the text.
As I see in your question you don't want to display the default keyboard for inputting the date of birth. I would suggest you to use a UIDatePicker in place of Keyboard, this way you don't need to do the validations yourself. Moreover you can also specify minimum and maximum age limit with the help of UIDatePicker. On how to implement this. https://stackoverflow.com/a/32153107/2299040 is a great post

How to detect dismissed TextView in TableView?

I have a tableView with dynamic cells with multiple TextViews. I am dismissing a keyboard with a "Cancel" and trying to determine which TextView is being dismissed to "undo" the changes made by user.
Based on this similar question: How to determine which textfield is active swift I have adapted one of the answers for the following extension:
extension UIView {
var textViewsInView: [UITextView] {
return subviews
.filter ({ !($0 is UITextView) })
.reduce (( subviews.compactMap { $0 as? UITextView }), { summ, current in
return summ + current.textViewsInView
})
}
var selectedTextView: UITextView? {
return textViewsInView.filter { $0.isFirstResponder }.first
}
}
This is working and I am presently testing in the following code:
#objc func cancelButtonAction() {
if let test = tableView.selectedTextView {
print("View Found")
}
tableView.beginUpdates()
tableView.endUpdates()
}
Doing a break at print("View Found") I can inspect "test". The following is the result.
This appears to only identify the view Test by a memory address. My question is how do I interpret this to identify the view that was being edited?
Update: There seems to be some issue in understanding. Assume I have a table with two cells and in each cell two textViews (dynamic cells). Assume the table loads by saying in the 4 textViews. "Hi John", "Hi Sam", "Bye John", "Bye Sam". Suppose the user starts editing a cell and changes one cell to read "Nachos". Then the user decides to cancel. I want to then replace with the value that was there before (from my model). I can find the textView but it now reads "Nachos". Therefore I do not know which textView to reload with the appropriate Hi and Bye.
Implement a placeholder for your textviews so that when their text is empty, it will have a default value. Therefore, when a user presses cancel while in focus of a textview, we can set the textview's text to its default value. See link to implement a textview placeholder.. Text View Placeholder Swift
I did solve this problem by adding the .tag property to the textView objects. I also dropped the extension approach and used textView delegate. The solution required me to first assign the tag and delegate = self for each textView in the tableView: cellForRowAt. The following shows one TextView of many. Notice the tag is setup so I may determine the section and the row it came from and the specific item.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
...
cell.directionsTextView.delegate = self
cell.directionsTextView.tag = indexPath.section*1000 + indexPath.row+1
return cell
}
Two global variables are defined in my tableView class:
var activeTextView = UITextView()
var activeTextViewPresentText = String()
The textViewDidBeginEditing captures the original state of the textView text before the user starts editing.
// Assign the newly active textview to store original value and original text
func textViewDidBeginEditing(_ textView: UITextView) {
print("textView.tag: \(textView.tag)")
self.activeTextView = textView
self.activeTextViewPresentText = textView.text
}
Lastly, if the user cancels the editing, the original text is reloaded.
#objc func cancelButtonAction() {
if activeTextView.text != nil {
activeTextView.text = activeTextViewPresentText
}
self.view.endEditing(true)
tableUpdate()
}

How to Add Multiple Styles to One UITextView - like the iOS Notes App

I'd like to have the user style a UITextView with multiple styles by selecting the range and the style. To keep it simple right now, I have a UITextView and 2 UIButtons (bold and italic), but I'd love to add multiple styles (colors, size, font weight etc).
var selectedRange: NSRange?
#IBOutlet weak var textBox: UITextView!
let boldAttribute = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 22, weight: .bold)]
let italicAttribute = [NSAttributedStringKey.font: UIFont.italicSystemFont(ofSize: 15)]
#IBAction func pressedItalic(_ sender: UIButton) {
if selectedRange != nil {
let string = NSMutableAttributedString(string: textBox.text)
string.addAttributes(italicAttribute, range: selectedRange!)
textBox.attributedText = string
}
}
#IBAction func pressedBold(_ sender: UIButton) {
if selectedRange != nil {
let string = NSMutableAttributedString(string: textBox.text)
string.addAttributes(boldAttribute, range: selectedRange!)
textBox.attributedText = string
}
}
override func viewDidLoad() {
super.viewDidLoad()
textBox.delegate = self
}
func textViewDidChangeSelection(_ textView: UITextView) {
selectedRange = textView.selectedRange
print(selectedRange)
}
The problem: when selecting a new style, the previous selection and selected style goes back to the default. How can I add multiple different styles to one text view without refreshing it every time? My end goal is to build something similar to the prebuilt iOS notes app, where the user can fully customize the styles and layout of the text.
UPDATE:
I'd like the show the controls (buttons) to the user at all time. Is that also possible with a UIMenuController?
EDIT:
I'd love to be able to control all styles (color, font weight, size etc)
Thanks!!!
Rather than mutating the string yourself you can use the methods in the UIResponderStandardEditActions protocol. This protocol is already attached to UITextView and provides access to all the text editing functions available to the user, including toggleBoldFace() and toggleItalics(). These allow multiple different styles to manipulate the text. You can just call them on the textview:
#IBAction func pressedBold(_ sender: UIButton) {
textBox.toggleBoldface(self)
}
See: Apple's Developer Documentation on UIResponderStandardEditActions
I ended up solving it be declaring the NSMutableAttributedString at ViewDidLoad
var string: NSMutableAttributedString?
override func viewDidLoad() {
super.viewDidLoad()
string = NSMutableAttributedString(string: textBox.text)
}
And then using the beginEditing() and endEditing() methods on that string with each action.
#IBAction func pressedItalic(_ sender: UIButton) {
if selectedRange != nil {
string?.beginEditing()
string?.addAttributes(italicAttribute, range: selectedRange!)
textBox.attributedText = string
string?.endEditing()
}
}
This way I can add endless style attributes (size, color, weight etc) to the string sequentially.

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`

UITextView paste method override

I really want to be able to detect a paste event in a UITextView, however it appears this cannot be done.
I originally tried subclassing a UITextView and overriding the paste: method, but it never gets called on a paste event.
Has anyone been able to do this? A previous question on the same ilk didn't have an answer back in August...
The text view doesn't catch the paste: event because it wasn't the actual responder is not the text view, but the private web view (UIWebDocumentView) that powers the text view.
However, on paste, the web view will call the text view's (private) -[UITextView keyboardInput:shouldInsertText:isMarkedText:], and in turn, the text view's delegate's -textView:shouldChangeTextInRange:replacementText:.
Therefore, you just need to implement -textView:shouldChangeTextInRange:replacementText: in the text view's delegate.
(Of course, normal keyboard input will trigger this method too. There's no perfect way to distinguish them.)
#KennyTM what I did for one of my applications was keep up with the current text length and the previous text length. If the (currentTextLength - previousTextLength) was greater than 1, then the user must have pasted something
With iOS 14 you have to do this in two parts to avoid showing the user notification that you are checking the UIPasteboard. In my case I did not want to do anything bad with the user data but I did want to do some special formating when the user did paste into the UITextView.
Step 1: Create a custom UITextView and override paste()
import UIKit
protocol TouchableTextViewDelegate : class{
func touchesDidBegin()
func pasting()
}
class TouchableTextView: UITextView {
weak var touchableDelegate : TouchableTextViewDelegate?
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.isFirstResponder{
return true
}
touchableDelegate?.touchesDidBegin()
return false
}
override func paste(_ sender: Any?) {
touchableDelegate?.pasting()
super.paste(sender)
}
}
Step 2: In the file location where you handle the shouldChangeTextIn create a variable and be sure to set the delegate for the TouchableTextView. In my case
//top of the view
var isPasting : Bool = false
//also when creating UITextView use both delegates
textView.touchableDelegate = self
//add the normal delegate
textView.delegate = self
extension SliderTextView : TouchableTextViewDelegate{
func pasting() {
self.isPaste = true
}
func touchesDidBegin() {
sliderEditingDelegate?.touchesDidBegin(sliderTextView: self)
}
}
Step 3: Inside shouldChangeTextIn I handle the action like this
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let isPaste = self.isPaste
//be sure to set this to false
self.isPaste = false
if isPaste,
let pt = UIPasteboard.general.string,
text.contains(pt){
//you will see the paste notification and that is good for the user
// but only when the user pastes
// do whatever special thing or formatting you want to do
}
return true
}
The good is that you will not trigger the notification unless the user is pasting in the UITextView.
To detect if a user is parsing a text in a textView, compare the replacementText in the shouldChangeTextInRange delegate with the text the user is currently holding in the UIPasteboard. Then take action depending on requirements.
for code, see my answer in the following question:
how to know when text is pasted into UITextView