How can I add a placeholder in a UITextView, similar to the one you can set for UITextField, in Swift?
Updated for Swift 4
UITextView doesn't inherently have a placeholder property so you'd have to create and manipulate one programmatically using UITextViewDelegate methods. I recommend using either solution #1 or #2 below depending on the desired behavior.
Note: For either solution, add UITextViewDelegate to the class and set textView.delegate = self to use the text view’s delegate methods.
Solution #1 - If you want the placeholder to disappear as soon as the user selects the text view:
First set the UITextView to contain the placeholder text and set it to a light gray color to mimic the look of a UITextField's placeholder text. Either do so in the viewDidLoad or upon the text view's creation.
textView.text = "Placeholder"
textView.textColor = UIColor.lightGray
Then when the user begins to edit the text view, if the text view contains a placeholder (i.e. if its text color is light gray) clear the placeholder text and set the text color to black in order to accommodate the user's entry.
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == UIColor.lightGray {
textView.text = nil
textView.textColor = UIColor.black
}
}
Then when the user finishes editing the text view and it's resigned as the first responder, if the text view is empty, reset its placeholder by re-adding the placeholder text and setting its color to light gray.
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = "Placeholder"
textView.textColor = UIColor.lightGray
}
}
Solution #2 - If you want the placeholder to show whenever the text view is empty, even if the text view’s selected:
First set the placeholder in the viewDidLoad:
textView.text = "Placeholder"
textView.textColor = UIColor.lightGray
textView.becomeFirstResponder()
textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
(Note: Since the OP wanted to have the text view selected as soon as the view loads, I incorporated text view selection into the above code. If this is not your desired behavior and you do not want the text view selected upon view load, remove the last two lines from the above code chunk.)
Then utilize the shouldChangeTextInRange UITextViewDelegate method, like so:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Combine the textView text and the replacement text to
// create the updated text string
let currentText:String = textView.text
let updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
// If updated text view will be empty, add the placeholder
// and set the cursor to the beginning of the text view
if updatedText.isEmpty {
textView.text = "Placeholder"
textView.textColor = UIColor.lightGray
textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
}
// Else if the text view's placeholder is showing and the
// length of the replacement string is greater than 0, set
// the text color to black then set its text to the
// replacement string
else if textView.textColor == UIColor.lightGray && !text.isEmpty {
textView.textColor = UIColor.black
textView.text = text
}
// For every other case, the text should change with the usual
// behavior...
else {
return true
}
// ...otherwise return false since the updates have already
// been made
return false
}
And also implement textViewDidChangeSelection to prevent the user from changing the position of the cursor while the placeholder's visible. (Note: textViewDidChangeSelection is called before the view loads so only check the text view's color if the window is visible):
func textViewDidChangeSelection(_ textView: UITextView) {
if self.view.window != nil {
if textView.textColor == UIColor.lightGray {
textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
}
}
}
Floating Placeholder
It's simple, safe and reliable to position a placeholder label above a text view, set its font, color and manage placeholder visibility by tracking changes to the text view's character count.
Update: Incorporated suggestions made by #RakshithaMurangaRodrigo in Feb 10, '23 comment
Swift 5:
class NotesViewController : UIViewController {
#IBOutlet var textView : UITextView!
var placeholderLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
placeholderLabel = UILabel()
placeholderLabel.text = "Enter some text..."
placeholderLabel.font = .italicSystemFont(ofSize: (textView.font?.pointSize)!)
placeholderLabel.sizeToFit()
textView.addSubview(placeholderLabel)
placeholderLabel.frame.origin = CGPoint(x: 5, y: (textView.font?.pointSize)! / 2)
placeholderLabel.textColor = .tertiaryLabel
placeholderLabel.isHidden = !textView.text.isEmpty
}
}
extension NotesViewController : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
placeholderLabel?.isHidden = !textView.text.isEmpty
}
func textViewDidEndEditing(_ textView: UITextView) {
placeholderLabel?.isHidden = !textView.text.isEmpty
}
func textViewDidBeginEditing(_ textView: UITextView) {
placeholderLabel?.isHidden = true
}
}
Swift:
Add your text view programmatically or via Interface Builder, if the last, create the outlet:
#IBOutlet weak var yourTextView: UITextView!
Please add the delegate (UITextViewDelegate):
class ViewController: UIViewController, UITextViewDelegate {
In the viewDidLoad method, do add the following:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
yourTextView.delegate = self
yourTextView.text = "Placeholder text goes right here..."
yourTextView.textColor = UIColor.lightGray
Now let me introduce the magic part, add this function:
func textViewDidBeginEditing(_ textView: UITextView) {
if yourTextView.textColor == UIColor.lightGray {
yourTextView.text = ""
yourTextView.textColor = UIColor.black
}
}
Do note that this will execute whenever editing starts, there we will check conditions to tell the state, using the color property.
Setting text to nil i do not recommend. Right after that, we set the text color to desired, in this case, black.
Now add this function too:
func textViewDidEndEditing(_ textView: UITextView) {
if yourTextView.text == "" {
yourTextView.text = "Placeholder text ..."
yourTextView.textColor = UIColor.lightGray
}
}
Let me insist, do not compare to nil, i have already tried that and it would not work. We then set the values back to placeholder style, and set the color back to placeholder color because it is a condition to check in textViewDidBeginEditing.
I am surprised that no one mentioned NSTextStorageDelegate. UITextViewDelegate's methods will only be triggered by user interaction, but not programmatically. E.g. when you set a text view's text property programmatically, you'll have to set the placeholder's visibility yourself, because the delegate methods will not be called.
However, with NSTextStorageDelegate's textStorage(_:didProcessEditing:range:changeInLength:) method, you'll be notified of any change to the text, even if it's done programmatically. Just assign it like this:
textView.textStorage.delegate = self
(In UITextView, this delegate property is nil by default, so it won't affect any default behaviour.)
Combine it with the UILabel technique #clearlight demonstrates, one can easily wrap the whole UITextView's placeholder implementation into an extension.
extension UITextView {
private class PlaceholderLabel: UILabel { }
private var placeholderLabel: PlaceholderLabel {
if let label = subviews.compactMap( { $0 as? PlaceholderLabel }).first {
return label
} else {
let label = PlaceholderLabel(frame: .zero)
label.font = font
addSubview(label)
return label
}
}
#IBInspectable
var placeholder: String {
get {
return subviews.compactMap( { $0 as? PlaceholderLabel }).first?.text ?? ""
}
set {
let placeholderLabel = self.placeholderLabel
placeholderLabel.text = newValue
placeholderLabel.numberOfLines = 0
let width = frame.width - textContainer.lineFragmentPadding * 2
let size = placeholderLabel.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
placeholderLabel.frame.size.height = size.height
placeholderLabel.frame.size.width = width
placeholderLabel.frame.origin = CGPoint(x: textContainer.lineFragmentPadding, y: textContainerInset.top)
textStorage.delegate = self
}
}
}
extension UITextView: NSTextStorageDelegate {
public func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {
if editedMask.contains(.editedCharacters) {
placeholderLabel.isHidden = !text.isEmpty
}
}
}
Note that the use of a private (nested) class called PlaceholderLabel. It has no implementation at all, but it provides us a way to identify the placeholder label, which is far more 'swifty' than using the tag property.
With this approach, you can still assign the delegate of the UITextView to someone else.
You don't even have to change your text views' classes. Just add the extension(s) and you will be able to assign a placeholder string to every UITextView in your project, even in the Interface Builder.
I left out the implementation of a placeholderColor property for clarity reasons, but it can be implemented for just a few more lines with a similar computed variable to placeholder.
Based on some of the great suggestions here already, I was able to put together the following lightweight, Interface-Builder-compatible subclass of UITextView, which:
Includes configurable placeholder text, styled just like that of UITextField.
Doesn't require any additional subviews or constraints.
Doesn't require any delegation or other behaviour from the ViewController.
Doesn't require any notifications.
Keeps that placeholder text fully separated from any outside classes looking at the field's text property.
Any improvement suggestions are welcome, especially if there's any way to pull iOS's placeholder color programatically, rather than hard-coding it.
Swift v5:
import UIKit
#IBDesignable class TextViewWithPlaceholder: UITextView {
override var text: String! { // Ensures that the placeholder text is never returned as the field's text
get {
if showingPlaceholder {
return "" // When showing the placeholder, there's no real text to return
} else { return super.text }
}
set { super.text = newValue }
}
#IBInspectable var placeholderText: String = ""
#IBInspectable var placeholderTextColor: UIColor = UIColor(red: 0.78, green: 0.78, blue: 0.80, alpha: 1.0) // Standard iOS placeholder color (#C7C7CD). See https://stackoverflow.com/questions/31057746/whats-the-default-color-for-placeholder-text-in-uitextfield
private var showingPlaceholder: Bool = true // Keeps track of whether the field is currently showing a placeholder
override func didMoveToWindow() {
super.didMoveToWindow()
if text.isEmpty {
showPlaceholderText() // Load up the placeholder text when first appearing, but not if coming back to a view where text was already entered
}
}
override func becomeFirstResponder() -> Bool {
// If the current text is the placeholder, remove it
if showingPlaceholder {
text = nil
textColor = nil // Put the text back to the default, unmodified color
showingPlaceholder = false
}
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
// If there's no text, put the placeholder back
if text.isEmpty {
showPlaceholderText()
}
return super.resignFirstResponder()
}
private func showPlaceholderText() {
showingPlaceholder = true
textColor = placeholderTextColor
text = placeholderText
}
}
I did this by using two different text views:
One in the background that is used as a placeholder.
One in the foreground (with a transparent background) that the user actually types in.
The idea is that once the user starts typing stuff in the foreground view, the placeholder in the background disappears (and reappears if the user deletes everything). So it behaves exactly like a placeholder for the single line text field.
Here's the code I used for it. Note that descriptionField is the field the user types in and descriptionPlaceholder is the one in the background.
func textViewDidChange(descriptionField: UITextView) {
if descriptionField.text.isEmpty == false {
descriptionPlaceholder.text = ""
} else {
descriptionPlaceholder.text = descriptionPlaceholderText
}
}
I tried to make code convenient from clearlight's answer.
extension UITextView{
func setPlaceholder() {
let placeholderLabel = UILabel()
placeholderLabel.text = "Enter some text..."
placeholderLabel.font = UIFont.italicSystemFont(ofSize: (self.font?.pointSize)!)
placeholderLabel.sizeToFit()
placeholderLabel.tag = 222
placeholderLabel.frame.origin = CGPoint(x: 5, y: (self.font?.pointSize)! / 2)
placeholderLabel.textColor = UIColor.lightGray
placeholderLabel.isHidden = !self.text.isEmpty
self.addSubview(placeholderLabel)
}
func checkPlaceholder() {
let placeholderLabel = self.viewWithTag(222) as! UILabel
placeholderLabel.isHidden = !self.text.isEmpty
}
}
usage
override func viewDidLoad() {
textView.delegate = self
textView.setPlaceholder()
}
func textViewDidChange(_ textView: UITextView) {
textView.checkPlaceholder()
}
Here is what I'm using for getting this job done.
#IBDesignable class UIPlaceholderTextView: UITextView {
var placeholderLabel: UILabel?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
override func prepareForInterfaceBuilder() {
sharedInit()
}
func sharedInit() {
refreshPlaceholder()
NotificationCenter.default.addObserver(self, selector: #selector(textChanged), name: UITextView.textDidChangeNotification, object: nil)
}
#IBInspectable var placeholder: String? {
didSet {
refreshPlaceholder()
}
}
#IBInspectable var placeholderColor: UIColor? = .darkGray {
didSet {
refreshPlaceholder()
}
}
#IBInspectable var placeholderFontSize: CGFloat = 14 {
didSet {
refreshPlaceholder()
}
}
func refreshPlaceholder() {
if placeholderLabel == nil {
placeholderLabel = UILabel()
let contentView = self.subviews.first ?? self
contentView.addSubview(placeholderLabel!)
placeholderLabel?.translatesAutoresizingMaskIntoConstraints = false
placeholderLabel?.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: textContainerInset.left + 4).isActive = true
placeholderLabel?.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: textContainerInset.right + 4).isActive = true
placeholderLabel?.topAnchor.constraint(equalTo: contentView.topAnchor, constant: textContainerInset.top).isActive = true
placeholderLabel?.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: textContainerInset.bottom).isActive = true
}
placeholderLabel?.text = placeholder
placeholderLabel?.textColor = placeholderColor
placeholderLabel?.font = UIFont.systemFont(ofSize: placeholderFontSize)
}
#objc func textChanged() {
if self.placeholder?.isEmpty ?? true {
return
}
UIView.animate(withDuration: 0.25) {
if self.text.isEmpty {
self.placeholderLabel?.alpha = 1.0
} else {
self.placeholderLabel?.alpha = 0.0
}
}
}
override var text: String! {
didSet {
textChanged()
}
}
}
I know there're several approaches similar to this but the benefits from this one are that it can:
Set placeholder text, font size and color in IB.
No longer shows the warning of "Scroll View has ambiguous scrollable content" in IB.
Add animation to show/hide of placeholder.
Swift:
Add your TextView #IBOutlet:
#IBOutlet weak var txtViewMessage: UITextView!
In the viewWillAppear method, do add the following :
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
txtViewMessage.delegate = self // Give TextViewMessage delegate Method
txtViewMessage.text = "Place Holder Name"
txtViewMessage.textColor = UIColor.lightGray
}
Please add the Delegate Using extension (UITextViewDelegate):
// MARK: - UITextViewDelegate
extension ViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
if !txtViewMessage.text!.isEmpty && txtViewMessage.text! == "Place Holder Name" {
txtViewMessage.text = ""
txtViewMessage.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if txtViewMessage.text.isEmpty {
txtViewMessage.text = "Place Holder Name"
txtViewMessage.textColor = UIColor.lightGray
}
}
}
SET value in view load
txtVw!.autocorrectionType = UITextAutocorrectionType.No
txtVw!.text = "Write your Placeholder"
txtVw!.textColor = UIColor.lightGrayColor()
func textViewDidBeginEditing(textView: UITextView) {
if (txtVw?.text == "Write your Placeholder")
{
txtVw!.text = nil
txtVw!.textColor = UIColor.blackColor()
}
}
func textViewDidEndEditing(textView: UITextView) {
if txtVw!.text.isEmpty
{
txtVw!.text = "Write your Placeholder"
txtVw!.textColor = UIColor.lightGrayColor()
}
textView.resignFirstResponder()
}
One more solution (Swift 3):
import UIKit
protocol PlaceholderTextViewDelegate {
func placeholderTextViewDidChangeText(_ text:String)
func placeholderTextViewDidEndEditing(_ text:String)
}
final class PlaceholderTextView: UITextView {
var notifier:PlaceholderTextViewDelegate?
var placeholder: String? {
didSet {
placeholderLabel?.text = placeholder
}
}
var placeholderColor = UIColor.lightGray
var placeholderFont = UIFont.appMainFontForSize(14.0) {
didSet {
placeholderLabel?.font = placeholderFont
}
}
fileprivate var placeholderLabel: UILabel?
// MARK: - LifeCycle
init() {
super.init(frame: CGRect.zero, textContainer: nil)
awakeFromNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(PlaceholderTextView.textDidChangeHandler(notification:)), name: .UITextViewTextDidChange, object: nil)
placeholderLabel = UILabel()
placeholderLabel?.textColor = placeholderColor
placeholderLabel?.text = placeholder
placeholderLabel?.textAlignment = .left
placeholderLabel?.numberOfLines = 0
}
override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel?.font = placeholderFont
var height:CGFloat = placeholderFont.lineHeight
if let data = placeholderLabel?.text {
let expectedDefaultWidth:CGFloat = bounds.size.width
let fontSize:CGFloat = placeholderFont.pointSize
let textView = UITextView()
textView.text = data
textView.font = UIFont.appMainFontForSize(fontSize)
let sizeForTextView = textView.sizeThatFits(CGSize(width: expectedDefaultWidth,
height: CGFloat.greatestFiniteMagnitude))
let expectedTextViewHeight = sizeForTextView.height
if expectedTextViewHeight > height {
height = expectedTextViewHeight
}
}
placeholderLabel?.frame = CGRect(x: 5, y: 0, width: bounds.size.width - 16, height: height)
if text.isEmpty {
addSubview(placeholderLabel!)
bringSubview(toFront: placeholderLabel!)
} else {
placeholderLabel?.removeFromSuperview()
}
}
func textDidChangeHandler(notification: Notification) {
layoutSubviews()
}
}
extension PlaceholderTextView : UITextViewDelegate {
// MARK: - UITextViewDelegate
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(text == "\n") {
textView.resignFirstResponder()
return false
}
return true
}
func textViewDidChange(_ textView: UITextView) {
notifier?.placeholderTextViewDidChangeText(textView.text)
}
func textViewDidEndEditing(_ textView: UITextView) {
notifier?.placeholderTextViewDidEndEditing(textView.text)
}
}
result
A simple and quick solution that works for me is:
#IBDesignable
class PlaceHolderTextView: UITextView {
#IBInspectable var placeholder: String = "" {
didSet{
updatePlaceHolder()
}
}
#IBInspectable var placeholderColor: UIColor = UIColor.gray {
didSet {
updatePlaceHolder()
}
}
private var originalTextColor = UIColor.darkText
private var originalText: String = ""
private func updatePlaceHolder() {
if self.text == "" || self.text == placeholder {
self.text = placeholder
self.textColor = placeholderColor
if let color = self.textColor {
self.originalTextColor = color
}
self.originalText = ""
} else {
self.textColor = self.originalTextColor
self.originalText = self.text
}
}
override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()
self.text = self.originalText
self.textColor = self.originalTextColor
return result
}
override func resignFirstResponder() -> Bool {
let result = super.resignFirstResponder()
updatePlaceHolder()
return result
}
}
We can implement textview PlaceHolder quite easily if we are Using pod IQKeyboardManagerSwift in our project just have to follow 4 steps
We have to assign the class IQTextView to our TextView class.
We have to import the IQKeyboardManagerSwift in our Controller page
Last but not least make the outlet of the textView on the Controller page // if you want :)
Give the textView some place holder text through the storyboard inspectable
Swift 3.2
extension EditProfileVC:UITextViewDelegate{
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == UIColor.lightGray {
textView.text = nil
textView.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = "Placeholder"
textView.textColor = UIColor.lightGray
}
}
}
First when user start editing textViewDidBeginEditing call and then check the if colour of text grey means user didn't write anything then set as textview nil and change the colour to black for user texting.
When user end editing textViewDidEndEditing is call and check if user doesn't write anything in textview then text set as grey colour with text "PlaceHolder"
Here is my way of solving this problem (Swift 4):
The idea was to make the simplest possible solution which allows to use placeholders of different colors, resizes to placeholders size, will not overwrite a delegate meanwhile keeping all UITextView functions work as expected.
import UIKit
class PlaceholderTextView: UITextView {
var placeholderColor: UIColor = .lightGray
var defaultTextColor: UIColor = .black
private var isShowingPlaceholder = false {
didSet {
if isShowingPlaceholder {
text = placeholder
textColor = placeholderColor
} else {
textColor = defaultTextColor
}
}
}
var placeholder: String? {
didSet {
isShowingPlaceholder = !hasText
}
}
#objc private func textViewDidBeginEditing(notification: Notification) {
textColor = defaultTextColor
if isShowingPlaceholder { text = nil }
}
#objc private func textViewDidEndEditing(notification: Notification) {
isShowingPlaceholder = !hasText
}
// MARK: - Construction -
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(textViewDidBeginEditing(notification:)), name: UITextView.textDidBeginEditingNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(textViewDidEndEditing(notification:)), name: UITextView.textDidEndEditingNotification, object: nil)
}
// MARK: - Destruction -
deinit { NotificationCenter.default.removeObserver(self) }
}
Swift Answer
Here is the custom class, that animates placeholder.
class CustomTextView: UITextView {
// MARK: - public
public var placeHolderText: String? = "Enter Reason.."
public lazy var placeHolderLabel: UILabel! = {
let placeHolderLabel = UILabel(frame: .zero)
placeHolderLabel.numberOfLines = 0
placeHolderLabel.backgroundColor = .clear
placeHolderLabel.alpha = 0.5
return placeHolderLabel
}()
// MARK: - Init
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
enableNotifications()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
enableNotifications()
}
func setup() {
placeHolderLabel.frame = CGRect(x: 8, y: 8, width: self.bounds.size.width - 16, height: 15)
placeHolderLabel.sizeToFit()
}
// MARK: - Cycle
override func awakeFromNib() {
super.awakeFromNib()
textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 8)
returnKeyType = .done
addSubview(placeHolderLabel)
placeHolderLabel.frame = CGRect(x: 8, y: 8, width: self.bounds.size.width - 16, height: 15)
placeHolderLabel.textColor = textColor
placeHolderLabel.font = font
placeHolderLabel.text = placeHolderText
bringSubviewToFront(placeHolderLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
setup()
}
// MARK: - Notifications
private func enableNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(textDidChangeNotification(_:)), name: UITextView.textDidChangeNotification , object: nil)
}
#objc func textDidChangeNotification(_ notify: Notification) {
guard self == notify.object as? UITextView else { return }
guard placeHolderText != nil else { return }
UIView.animate(withDuration: 0.25, animations: {
self.placeHolderLabel.alpha = (self.text.count == 0) ? 0.5 : 0
}, completion: nil)
}
}
I don't know why people over complicate this issue so much.... It's fairly straight forward and simple. Here's a subclass of UITextView that provides the requested functionality.
- (void)customInit
{
self.contentMode = UIViewContentModeRedraw;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
}
- (void)textChanged:(NSNotification *)notification
{
if (notification.object == self) {
if(self.textStorage.length != 0 || !self.textStorage.length) {
[self setNeedsDisplay];
}
}
}
#pragma mark - Setters
- (void)setPlaceholderText:(NSString *)placeholderText withFont:(UIFont *)font
{
self.placeholderText = placeholderText;
self.placeholderTextFont = font;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[[UIColor lightGrayColor] setFill];
if (self.textStorage.length != 0) {
return;
}
CGRect inset = CGRectInset(rect, 8, 8);//Default rect insets for textView
NSDictionary *attributes = #{NSFontAttributeName: self.placeholderTextFont, NSForegroundColorAttributeName: [UIColor grayColor]};
[self.placeholderText drawInRect:inset withAttributes:attributes];
}`
This is my ready to use solution if you are working with multiple text views
func textViewShouldBeginEditing(textView: UITextView) -> Bool {
// Set cursor to the beginning if placeholder is set
if textView.textColor == UIColor.lightGrayColor() {
textView.selectedTextRange = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)
}
return true
}
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
// Remove placeholder
if textView.textColor == UIColor.lightGrayColor() && text.characters.count > 0 {
textView.text = ""
textView.textColor = UIColor.blackColor()
}
if text == "\n" {
textView.resignFirstResponder()
return false
}
return true
}
func textViewDidChange(textView: UITextView) {
// Set placeholder if text is empty
if textView.text.isEmpty {
textView.text = NSLocalizedString("Hint", comment: "hint")
textView.textColor = UIColor.lightGrayColor()
textView.selectedTextRange = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)
}
}
func textViewDidChangeSelection(textView: UITextView) {
// Set cursor to the beginning if placeholder is set
let firstPosition = textView.textRangeFromPosition(textView.beginningOfDocument, toPosition: textView.beginningOfDocument)
// Do not change position recursively
if textView.textColor == UIColor.lightGrayColor() && textView.selectedTextRange != firstPosition {
textView.selectedTextRange = firstPosition
}
}
Swift - I wrote a class that inherited UITextView and I added a UILabel as a subview to act as a placeholder.
import UIKit
#IBDesignable
class HintedTextView: UITextView {
#IBInspectable var hintText: String = "hintText" {
didSet{
hintLabel.text = hintText
}
}
private lazy var hintLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFontOfSize(16)
label.textColor = UIColor.lightGrayColor()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setupView()
}
private func setupView() {
translatesAutoresizingMaskIntoConstraints = false
delegate = self
font = UIFont.systemFontOfSize(16)
addSubview(hintLabel)
NSLayoutConstraint.activateConstraints([
hintLabel.leftAnchor.constraintEqualToAnchor(leftAnchor, constant: 4),
hintLabel.rightAnchor.constraintEqualToAnchor(rightAnchor, constant: 8),
hintLabel.topAnchor.constraintEqualToAnchor(topAnchor, constant: 4),
hintLabel.heightAnchor.constraintEqualToConstant(30)
])
}
override func layoutSubviews() {
super.layoutSubviews()
setupView()
}
}
Swift 3.1
This extension worked well for me: https://github.com/devxoul/UITextView-Placeholder
Here is a code snippet:
Install it via pod:
pod 'UITextView+Placeholder', '~> 1.2'
Import it to your class
import UITextView_Placeholder
And add placeholder property to your already created UITextView
textView.placeholder = "Put some detail"
Thats it...
Here how it looks (Third box is a UITextView)
I had to dispatch queue to get my placeholder text to reappear once editing was completed.
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.text == "Description" {
textView.text = nil
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
DispatchQueue.main.async {
textView.text = "Description"
}
}
}
Swift 4, 4.2 and 5
[![#IBOutlet var detailTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
detailTextView.delegate = self
}
extension ContactUsViewController : UITextViewDelegate {
public func textViewDidBeginEditing(_ textView: UITextView) {
if textView.text == "Write your message here..." {
detailTextView.text = ""
detailTextView.textColor = UIColor.init(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.86)
}
textView.becomeFirstResponder()
}
public func textViewDidEndEditing(_ textView: UITextView) {
if textView.text == "" {
detailTextView.text = "Write your message here..."
detailTextView.textColor = UIColor.init(red: 0/255, green: 0/255, blue: 0/255, alpha: 0.30)
}
textView.resignFirstResponder()
}
[![}][1]][1]
Contrary to just about every answer on this post, UITextView does have a placeholder property. For reasons beyond my comprehension, it is only exposed in IB, as such:
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="My Placeholder"/>
</userDefinedRuntimeAttributes>
So if you are using storyboards and a static placeholder will suffice, just set the property on the inspector.
You can also set this property in code like this:
textView.setValue("My Placeholder", forKeyPath: "placeholder")
Its cloudy as to weather this is accessed via private API, as the property is exposed.
I haven't tried submitting with this method. But I will be submitting this way shortly and will update this answer accordingly.
UPDATE:
I have shipped this code in multiple releases with no issues from Apple.
UPDATE:
This will only work in Xcode pre 11.2
Swift 5.2
Standalone class
Use this if you want a class which you can use anywhere as it is self contained
import UIKit
class PlaceHolderTextView:UITextView, UITextViewDelegate{
var placeholderText = "placeholderText"
override func willMove(toSuperview newSuperview: UIView?) {
textColor = .lightText
delegate = self
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.text == placeholderText{
placeholderText = textView.text
textView.text = ""
textView.textColor = .darkText
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text == ""{
textView.text = placeholderText
textColor = .lightText
}
}
}
The key here is the willMove(toSuperView:) function as it allows you to setup the view before being added to another view's hierarchy (similar to viewDidLoad/viewWillAppear in ViewControllers)
No need to add any third party library. Just use below code...
class SubmitReviewVC : UIViewController, UITextViewDelegate {
#IBOutlet var txtMessage : UITextView!
var lblPlaceHolder : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
txtMessage.delegate = self
lblPlaceHolder = UILabel()
lblPlaceHolder.text = "Enter message..."
lblPlaceHolder.font = UIFont.systemFont(ofSize: txtMessage.font!.pointSize)
lblPlaceHolder.sizeToFit()
txtMessage.addSubview(lblPlaceHolder)
lblPlaceHolder.frame.origin = CGPoint(x: 5, y: (txtMessage.font?.pointSize)! / 2)
lblPlaceHolder.textColor = UIColor.lightGray
lblPlaceHolder.isHidden = !txtMessage.text.isEmpty
}
func textViewDidChange(_ textView: UITextView) {
lblPlaceHolder.isHidden = !textView.text.isEmpty
}
}
There is no such property in ios to add placeholder directly in TextView rather you can add a label and show/hide on the change in textView. SWIFT 2.0 and make sure to implement the textviewdelegate
func textViewDidChange(TextView: UITextView)
{
if txtShortDescription.text == ""
{
self.lblShortDescription.hidden = false
}
else
{
self.lblShortDescription.hidden = true
}
}
I like #nerdist's solution. Based on that, I created an extension to UITextView:
import Foundation
import UIKit
extension UITextView
{
private func add(_ placeholder: UILabel) {
for view in self.subviews {
if let lbl = view as? UILabel {
if lbl.text == placeholder.text {
lbl.removeFromSuperview()
}
}
}
self.addSubview(placeholder)
}
func addPlaceholder(_ placeholder: UILabel?) {
if let ph = placeholder {
ph.numberOfLines = 0 // support for multiple lines
ph.font = UIFont.italicSystemFont(ofSize: (self.font?.pointSize)!)
ph.sizeToFit()
self.add(ph)
ph.frame.origin = CGPoint(x: 5, y: (self.font?.pointSize)! / 2)
ph.textColor = UIColor(white: 0, alpha: 0.3)
updateVisibility(ph)
}
}
func updateVisibility(_ placeHolder: UILabel?) {
if let ph = placeHolder {
ph.isHidden = !self.text.isEmpty
}
}
}
In a ViewController class, for example, this is how I use it:
class MyViewController: UIViewController, UITextViewDelegate {
private var notePlaceholder: UILabel!
#IBOutlet weak var txtNote: UITextView!
...
// UIViewController
override func viewDidLoad() {
notePlaceholder = UILabel()
notePlaceholder.text = "title\nsubtitle\nmore..."
txtNote.addPlaceholder(notePlaceholder)
...
}
// UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
txtNote.updateVisbility(notePlaceholder)
...
}
Placeholder on UITextview!
UPDATE:
In case you change textview's text in code, remember to call updateVisibitly method to hide placeholder:
txtNote.text = "something in code"
txtNote.updateVisibility(self.notePlaceholder) // hide placeholder if text is not empty.
To prevent the placeholder being added more than once, a private add() function is added in extension.
In swift2.2:
public class CustomTextView: UITextView {
private struct Constants {
static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
}
private let placeholderLabel: UILabel = UILabel()
private var placeholderLabelConstraints = [NSLayoutConstraint]()
#IBInspectable public var placeholder: String = "" {
didSet {
placeholderLabel.text = placeholder
}
}
#IBInspectable public var placeholderColor: UIColor = CustomTextView.Constants.defaultiOSPlaceholderColor {
didSet {
placeholderLabel.textColor = placeholderColor
}
}
override public var font: UIFont! {
didSet {
placeholderLabel.font = font
}
}
override public var textAlignment: NSTextAlignment {
didSet {
placeholderLabel.textAlignment = textAlignment
}
}
override public var text: String! {
didSet {
textDidChange()
}
}
override public var attributedText: NSAttributedString! {
didSet {
textDidChange()
}
}
override public var textContainerInset: UIEdgeInsets {
didSet {
updateConstraintsForPlaceholderLabel()
}
}
override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(textDidChange),
name: UITextViewTextDidChangeNotification,
object: nil)
placeholderLabel.font = font
placeholderLabel.textColor = placeholderColor
placeholderLabel.textAlignment = textAlignment
placeholderLabel.text = placeholder
placeholderLabel.numberOfLines = 0
placeholderLabel.backgroundColor = UIColor.clearColor()
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholderLabel)
updateConstraintsForPlaceholderLabel()
}
private func updateConstraintsForPlaceholderLabel() {
var newConstraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]",
options: [],
metrics: nil,
views: ["placeholder": placeholderLabel])
newConstraints += NSLayoutConstraint.constraintsWithVisualFormat("V:|-(\(textContainerInset.top))-[placeholder]",
options: [],
metrics: nil,
views: ["placeholder": placeholderLabel])
newConstraints.append(NSLayoutConstraint(
item: placeholderLabel,
attribute: .Width,
relatedBy: .Equal,
toItem: self,
attribute: .Width,
multiplier: 1.0,
constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0)
))
removeConstraints(placeholderLabelConstraints)
addConstraints(newConstraints)
placeholderLabelConstraints = newConstraints
}
#objc private func textDidChange() {
placeholderLabel.hidden = !text.isEmpty
}
public override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self,
name: UITextViewTextDidChangeNotification,
object: nil)
}
}
In swift3:
import UIKit
class CustomTextView: UITextView {
private struct Constants {
static let defaultiOSPlaceholderColor = UIColor(red: 0.0, green: 0.0, blue: 0.0980392, alpha: 0.22)
}
private let placeholderLabel: UILabel = UILabel()
private var placeholderLabelConstraints = [NSLayoutConstraint]()
#IBInspectable public var placeholder: String = "" {
didSet {
placeholderLabel.text = placeholder
}
}
#IBInspectable public var placeholderColor: UIColor = CustomTextView.Constants.defaultiOSPlaceholderColor {
didSet {
placeholderLabel.textColor = placeholderColor
}
}
override public var font: UIFont! {
didSet {
placeholderLabel.font = font
}
}
override public var textAlignment: NSTextAlignment {
didSet {
placeholderLabel.textAlignment = textAlignment
}
}
override public var text: String! {
didSet {
textDidChange()
}
}
override public var attributedText: NSAttributedString! {
didSet {
textDidChange()
}
}
override public var textContainerInset: UIEdgeInsets {
didSet {
updateConstraintsForPlaceholderLabel()
}
}
override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
NotificationCenter.default.addObserver(self,
selector: #selector(textDidChange),
name: NSNotification.Name.UITextViewTextDidChange,
object: nil)
placeholderLabel.font = font
placeholderLabel.textColor = placeholderColor
placeholderLabel.textAlignment = textAlignment
placeholderLabel.text = placeholder
placeholderLabel.numberOfLines = 0
placeholderLabel.backgroundColor = UIColor.clear
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholderLabel)
updateConstraintsForPlaceholderLabel()
}
private func updateConstraintsForPlaceholderLabel() {
var newConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]",
options: [],
metrics: nil,
views: ["placeholder": placeholderLabel])
newConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-(\(textContainerInset.top))-[placeholder]",
options: [],
metrics: nil,
views: ["placeholder": placeholderLabel])
newConstraints.append(NSLayoutConstraint(
item: placeholderLabel,
attribute: .width,
relatedBy: .equal,
toItem: self,
attribute: .width,
multiplier: 1.0,
constant: -(textContainerInset.left + textContainerInset.right + textContainer.lineFragmentPadding * 2.0)
))
removeConstraints(placeholderLabelConstraints)
addConstraints(newConstraints)
placeholderLabelConstraints = newConstraints
}
#objc private func textDidChange() {
placeholderLabel.isHidden = !text.isEmpty
}
public override func layoutSubviews() {
super.layoutSubviews()
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0
}
deinit {
NotificationCenter.default.removeObserver(self,
name: NSNotification.Name.UITextViewTextDidChange,
object: nil)
}
}
I wrote a class in swift. You need to import this class whenever required.
I can't add comment because of reputation. add one more delegate need in #clearlight answer.
func textViewDidBeginEditing(_ textView: UITextView) {
cell.placeholderLabel.isHidden = !textView.text.isEmpty
}
is need
because textViewDidChange is not called first time
no there is not any placeholder available for textview. you have to put label above it when user enter in textview then hide it or set by default value when user enters remove all values.
Related
I'm trying to build and integrate to a custom UIViewController with a tapRecognizer and an UITextView.
The issue I face is that the UITextView display part of the text off screen and I don't know what is causing this! I expect something simple or maybe including more classes but I haven't found anything pointing me in the right direction yet.
The size / position of a UITextView is working well when using UIViewRepresentable and a UITextView but not with a UIViewController / UIViewControllerRepresentable and a UITextView inside it for some reasons (see example bellow with both for comparison).
Here is the code of the test app I have:
import SwiftUI
import UIKit
struct TestingView: View {
var body: some View {
VStack {
WrappedUIViewController().padding()
Spacer()
WrappedUITextView(myText: "This is a long text to see if the word wrap work in this case better I hope so but I don't know. Is it? I hope it is, ! Do you? I do! Hope you do too, do you?").padding()
Spacer()
}
}
}
struct WrappedUITextView: UIViewRepresentable {
let myText: String
func makeUIView(context: Context) -> UITextViewPlus {
let view = UITextViewPlus()
view.isScrollEnabled = true
view.isEditable = false
view.isUserInteractionEnabled = true
view.font = UIFont(name: "Time New Roman", size: 20)
view.isOpaque = false
view.text = "WrappedUITextView \(myText)"
return view
}
func updateUIView(_ uiView: UITextViewPlus, context: Context) {}
}
struct WrappedUIViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = CustomUIViewController
func makeUIViewController(context: Context) -> CustomUIViewController {
return CustomUIViewController()
}
func updateUIViewController(_ uiViewController: CustomUIViewController, context: Context) {}
}
class CustomUIViewController: UIViewController {
var textView: UITextView = UITextView()
var tapGesture: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
self.textView.addGestureRecognizer(tapGesture)
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.clipsToBounds = true
self.textView.text = """
This is some text that will need to be displayed on multiple lines as it's longer than the screen size, will it wrapp correctly?
This is a great testing app
This is the end of this textView content
Here is a Potato
"""
self.textView.backgroundColor = UIColor.green
self.textView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.textView.textContainer.lineBreakMode = .byWordWrapping
view.addSubview(self.textView)
}
#objc func handleTap(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: textView)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition = textView.closestPosition(to: position)!
guard let textRange: UITextRange = textView.tokenizer.rangeEnclosingPosition(tapPosition, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1)) else { return }
let tappedWord: String = textView.text(in: textRange) ?? ""
print("tapped word -> \(tappedWord)")
}
override func viewWillLayoutSubviews() {
textView.sizeThatFits(self.view.bounds.size)
textView.sizeToFit()
}
}
The green textView is not wrapping the text properly or has a size that goes off screen for some reasons:
I'm new to swift and IOS so I would not be surprised if I do something wrong, any help is welcome!
EDIT: in case the code of the other file would help resolve this (I doubt it though):
import SwiftUI
#main
struct testAppApp: App {
var body: some Scene {
WindowGroup {
Text("Test App")
TestingView()
}
}
}
I have made changes to you CustomViewController class and added constraints to your textview to allow it to align properly. Methods and lines of code i have added are commented
You have to tell Auto Layout how you want your view to be aligned and positioned otherwise everything will be chaotic.
class CustomUIViewController: UIViewController {
var textView: UITextView = UITextView()
var tapGesture: UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
self.textView.addGestureRecognizer(tapGesture)
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.clipsToBounds = true
self.textView.text = """
This is some text that will need to be displayed on multiple lines as it's longer than the screen size, will it wrapp correctly?
This is a great testing app
This is the end of this textView content
Here is a Potato
"""
self.textView.backgroundColor = UIColor.green
self.textView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.textView.textContainer.lineBreakMode = .byWordWrapping
self.textView.translatesAutoresizingMaskIntoConstraints = false //ADDED: allows view to obey Auto Layout constraints.
view.addSubview(self.textView)
addConstraints() //ADDED: method for invoking constraints
}
func addConstraints(){
let margins = view.layoutMarginsGuide //safe area layout - esp for devices with notch
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: margins.topAnchor), //textview's top anchor = that of parent view
textView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
textView.heightAnchor.constraint(equalToConstant: 200), //gave textview a constant height
textView.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
])
}
#objc func handleTap(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: textView)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition = textView.closestPosition(to: position)!
guard let textRange: UITextRange = textView.tokenizer.rangeEnclosingPosition(tapPosition, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1)) else { return }
let tappedWord: String = textView.text(in: textRange) ?? ""
print("tapped word -> \(tappedWord)")
}
override func viewWillLayoutSubviews() {
textView.sizeThatFits(self.view.bounds.size)
textView.sizeToFit()
}
}
After multiple iterations I managed to get something that would work, actually 2 options to solve this:
Changing "view.addSubview(self.textView)" to "view. = self.textView" in viewDidLoad. I'm not sure what this change but with this I have the UITextview bound ajusted by the system.
override func viewDidLoad() {
super.viewDidLoad()
...
view = self.textView
}
the second and most likely "better" solution: set a frame to my UITextView inside viewDidLoad. When "textView.sizeToFit()" the height is then addapted while the width is maintained to what I wanted:
override func viewDidLoad() {
super.viewDidLoad()
...
self.textView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 10000)
...
view.addSubview(self.textView)
}
I want to enable login button once all the condition satisfied for text field. I have added editchange event to two text fields through starboard.By default i have adding one label to text field with red background color to indicate(wrong) once user enters correct values in textfield then i need to change label color to green color. It is not working as except.
My entire code:
import UIKit
import IQKeyboardManagerSwift
class SignInFormViewController: UIViewController , UITextFieldDelegate {
#IBOutlet weak var forgotPasswordButton: UIButton!
#IBOutlet weak var signInButton: UIButton!
// Existing User
#IBOutlet weak var existingEmailAddressTextField: UITextField!
#IBOutlet weak var existingPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
//Setting text fileds effect
self.makeTextFiledEffect(textField: existingEmailAddressTextField, backColor: UIColor.red, borderColor: UIColor.lightGray)
self.makeTextFiledEffect(textField: existingPasswordTextField, backColor: UIColor.red, borderColor: UIColor.lightGray)
//Disable sing button by default
signInButton.isEnabled = false
}
func makeTextFiledEffect(textField : UITextField, backColor : UIColor, borderColor : UIColor) {
for viewPrevious in textField.subviews{
if viewPrevious.tag == 1000{
viewPrevious.removeFromSuperview()
}
}
let arrowView = UILabel(frame: CGRect(x:0, y: 0, width: 5, height: textField.frame.size.height))
arrowView.tag = 1000
arrowView.backgroundColor = backColor
textField.changeDynamicBorderColor(borderColor: borderColor)
textField.addSubview(arrowView)
}
#IBAction func validateTextField(_ sender: UITextField) {
if sender.text?.count == 1 {
if sender.text?.first == " " {
sender.text = ""
return
}
}
guard
let emaiTextField = existingEmailAddressTextField.text, !emaiTextField.isEmpty && self.validateEmail(emaiTextField)==true ,
let password = existingPasswordTextField.text, !password.isEmpty
else {
self.makeTextFiledEffect(textField: sender, backColor: UIColor.red, borderColor: UIColor.lightGray)
self.signInButton.isEnabled = false
return
}
self.makeTextFiledEffect(textField: sender, backColor: UIColor.green, borderColor: UIColor.lightGray)
signInButton.isEnabled = true
}
#IBAction func signInButtonTapped(_ sender: UIButton) {
print("Tapped")
}
func validateEmail(_ candidate: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
return NSPredicate(format: "SELF MATCHES %#", emailRegex).evaluate(with: candidate)
}
}
extension UITextField
{
func changeDynamicBorderColor(borderColor : UIColor){
self.layer.borderColor = borderColor.cgColor
}
open override func draw(_ rect: CGRect) {
self.layer.cornerRadius = 10.0
self.layer.borderWidth = 1.0
// self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.masksToBounds = true
}
}
You can find a similar question : Enable a button in Swift only if all text fields have been filled out
Thanks in advance..
Follow the below basic steps to achieve your goal:-
Create a function which will perform all the validations(UI Level or API level or any validation). If any validation fails, it will return false, otherwise return true
loginButton.isEnabled = returned value of above function
Try this:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
self.validateField()
return true
}
// MARK: - Validations
func validateField() -> Bool
{
if (self.isValidEmail(testStr: txtEmail.text!) && strPassword.characters.count > 0 )
{
btn.isUserInteractionEnabled = .true
btn.backgroundColor = .green
return true
}
else
{
btn.isUserInteractionEnabled = .false
btn.backgroundColor = .red
return false
}
}
func isValidEmail(testStr:String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
if (emailTest.evaluate(with: testStr) == false)
{
print("Invalid Email")
return false
}
else
{
return true
}
}
I want to change the color of a link within a UILabel. I've found loads of past questions on how to do this for a UITextView, and past questions with answers in Obj-C (but can't translate these to Swift as properties that did exist in Obj-c no longer do such as NSMutableAttributedString.linkTextAttribtues for example).
But I cannot find how to do this for a UILabel and in Swift 4.
For default NSAttributedString.Key.link color will be blue.
If you need custom colors for links you can set the attribute as NSAttributedString.Key.attachment instead of .link and set the foreground and underline colors like this:
let linkCustomAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14),
NSAttributedString.Key.foregroundColor: UIColor.red,
NSAttributedString.Key.underlineColor: UIColor.magenta,
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
NSAttributedString.Key.attachment: URL(string: "https://www.google.com")] as [NSAttributedString.Key : Any]
If you need to handle touches on links you can use this custom label class:
import UIKit
public protocol UILabelTapableLinksDelegate: NSObjectProtocol {
func tapableLabel(_ label: UILabelTapableLinks, didTapUrl url: String, atRange range: NSRange)
}
public class UILabelTapableLinks: UILabel {
private var links: [String: NSRange] = [:]
private(set) var layoutManager = NSLayoutManager()
private(set) var textContainer = NSTextContainer(size: CGSize.zero)
private(set) var textStorage = NSTextStorage() {
didSet {
textStorage.addLayoutManager(layoutManager)
}
}
public weak var delegate: UILabelTapableLinksDelegate?
public override var attributedText: NSAttributedString? {
didSet {
if let attributedText = attributedText {
textStorage = NSTextStorage(attributedString: attributedText)
findLinksAndRange(attributeString: attributedText)
} else {
textStorage = NSTextStorage()
links = [:]
}
}
}
public override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
public override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
isUserInteractionEnabled = true
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
private func findLinksAndRange(attributeString: NSAttributedString) {
links = [:]
let enumerationBlock: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Void = { [weak self] value, range, isStop in
guard let strongSelf = self else { return }
if let value = value {
let stringValue = "\(value)"
strongSelf.links[stringValue] = range
}
}
attributeString.enumerateAttribute(.link, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
attributeString.enumerateAttribute(.attachment, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let locationOfTouch = touches.first?.location(in: self) else {
return
}
textContainer.size = bounds.size
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
for (urlString, range) in links where NSLocationInRange(indexOfCharacter, range) {
delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
return
}
}
}
Setup label in your code:
customLabel.attributedText = <<Your attributed text with custom links>>
customLabel.delegate = self
Implement delegate:
extension YourClass: UILabelTapableLinksDelegate {
func tapableLabel(_ label: UILabelTapableLinks, didTapUrl url: String, atRange range: NSRange) {
print("didTapUrl: ", url)
}
}
It is easier to use UITextView instead of UILabel and write something like:
textView.linkTextAttributes = [
.foregroundColor: UIColor.red,
.underlineColor: UIColor.red,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
The answers above are correct but setting .attachment as the url doesn't open the url, at least not for me (using iOS 13).
The color of .link is not affected by the .foregroundColor in NSAttributedString, but from the tintColor of your UITextView
let urlAttributes: [NSAttributedString.Key: Any] = [
.link: URL(string:"https://google.com"),
.foregroundColor: textColor,
.underlineColor: textColor,
.underlineStyle: NSUnderlineStyle.single.rawValue
.underlinColor: UIColor.green
]
textView.tintColor = UIColor.green
textView.attributed = urlAttributes
should set the text and the underline of the link to green
I have a custom UILabel based class, that does not display properly in the storyboard.
All it displays is a blank label. It happens with both XCode 6 and 7 beta.
import UIKit
#IBDesignable
class CCLabel: UILabel {
#IBInspectable var spacingChar: CGFloat = 0 {
didSet {
self.updateAttributed(self.text)
}
}
#IBInspectable var extraLineSpacing: CGFloat = 0 {
didSet {
self.updateAttributed(self.text)
}
}
override var bounds: CGRect {
didSet {
if (bounds.size.width != self.bounds.size.width) {
self.setNeedsUpdateConstraints()
}
super.bounds = bounds
}
}
override func updateConstraints() {
if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
super.updateConstraints()
}
override var text: String? {
didSet {
if let txt = text {
self.updateAttributed(txt)
}
}
}
private func updateAttributed(text: String?) {
if (text == nil) {
return
}
if (extraLineSpacing > 0 || spacingChar > 0) {
let attrString = NSMutableAttributedString(string: text!)
let rng = NSMakeRange(0, attrString.length)
if (extraLineSpacing > 0) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = extraLineSpacing
paragraphStyle.alignment = self.textAlignment
attrString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:rng)
}
if (spacingChar > 0) {
attrString.addAttribute(NSKernAttributeName, value:spacingChar, range:rng)
}
attrString.addAttribute(NSFontAttributeName, value: self.font, range: rng)
self.attributedText = attrString
}
}
}
That's the custom UILabel class in IB:
When I delete the name of the custom class label, it displays properly.
When I add it again, it does not disappear. However, the next time it's white again.
When you run the project, it's perfect. I suppose XCode is not rendering it properly, but cannot tell why.
I think you need to call this:
[super drawRect:rect];
When you subclass any other than UIView, you SHOULD CALL super.draw( _ ...) in your implementation. (https://developer.apple.com/documentation/uikit/uiview/1622529-drawrect)
I am new on swift and on my project I've been trying to use custom label such as clickable '#' and '#' sub strings on the related label. I found a library that called as FFLabel on github. I've made some tests for FFLabel. Everything is good, but I want to use different colors for '#' and '#' sub strings. I' ve tried to make some changes on source code. However, it did not work correctly. Also, '#' sub strings is not working(it is not clickable). I guess that there is a regex issue in source code.
Here is the source code of FFLabel:
import UIKit
#objc
public protocol FFLabelDelegate: NSObjectProtocol {
optional func labelDidSelectedLinkText(label: FFLabel, text: String)
}
public class FFLabel: UILabel {
public var linkTextColor = UIColor(red: 0, green: 63.0/255.0, blue: 121.0/255.0, alpha: 1.0)
public weak var labelDelegate: FFLabelDelegate?
// MARK: - override properties
override public var text: String? {
didSet {
updateTextStorage()
}
}
override public var attributedText: NSAttributedString? {
didSet {
updateTextStorage()
}
}
override public var font: UIFont! {
didSet {
updateTextStorage()
}
}
override public var textColor: UIColor! {
didSet {
updateTextStorage()
}
}
// MARK: - upadte text storage and redraw text
private func updateTextStorage() {
if attributedText == nil {
return
}
let attrStringM = addLineBreak(attributedText!)
regexLinkRanges(attrStringM)
addLinkAttribute(attrStringM)
textStorage.setAttributedString(attrStringM)
setNeedsDisplay()
}
/// add link attribute
private func addLinkAttribute(attrStringM: NSMutableAttributedString) {
var range = NSRange(location: 0, length: 0)
var attributes = attrStringM.attributesAtIndex(0, effectiveRange: &range)
attributes[NSFontAttributeName] = font!
attributes[NSForegroundColorAttributeName] = textColor
attrStringM.addAttributes(attributes, range: range)
attributes[NSForegroundColorAttributeName] = linkTextColor
for r in linkRanges {
attrStringM.setAttributes(attributes, range: r)
}
}
/// use regex check all link ranges
private let patterns = ["[a-zA-Z]*://[a-zA-Z0-9/\\.]*", "#.*?#", "#[\\u4e00-\\u9fa5a-zA-Z0-9_-]*"]
private func regexLinkRanges(attrString: NSAttributedString) {
linkRanges.removeAll()
let regexRange = NSRange(location: 0, length: count(attrString.string))
for pattern in patterns {
let regex = NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions.DotMatchesLineSeparators, error: nil)
let results = regex?.matchesInString(attrString.string, options: NSMatchingOptions(rawValue: 0), range: regexRange)
if let results = results {
for r in results {
linkRanges.append(r.rangeAtIndex(0))
}
}
}
}
/// add line break mode
private func addLineBreak(attrString: NSAttributedString) -> NSMutableAttributedString {
let attrStringM = NSMutableAttributedString(attributedString: attrString)
var range = NSRange(location: 0, length: 0)
var attributes = attrStringM.attributesAtIndex(0, effectiveRange: &range)
var paragraphStyle = attributes[NSParagraphStyleAttributeName] as? NSMutableParagraphStyle
if paragraphStyle != nil {
paragraphStyle!.lineBreakMode = NSLineBreakMode.ByWordWrapping
} else {
// iOS 8.0 can not get the paragraphStyle directly
paragraphStyle = NSMutableParagraphStyle()
paragraphStyle!.lineBreakMode = NSLineBreakMode.ByWordWrapping
attributes[NSParagraphStyleAttributeName] = paragraphStyle
attrStringM.setAttributes(attributes, range: range)
}
return attrStringM
}
public override func drawTextInRect(rect: CGRect) {
let range = glyphsRange()
let offset = glyphsOffset(range)
layoutManager.drawBackgroundForGlyphRange(range, atPoint: offset)
layoutManager.drawGlyphsForGlyphRange(range, atPoint: CGPointZero)
}
private func glyphsRange() -> NSRange {
return NSRange(location: 0, length: textStorage.length)
}
private func glyphsOffset(range: NSRange) -> CGPoint {
let rect = layoutManager.boundingRectForGlyphRange(range, inTextContainer: textContainer)
let height = (bounds.height - rect.height) * 0.5
return CGPoint(x: 0, y: height)
}
// MARK: - touch events
public override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let location = (touches.first as! UITouch).locationInView(self)
selectedRange = linkRangeAtLocation(location)
modifySelectedAttribute(true)
}
public override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let location = (touches.first as! UITouch).locationInView(self)
if let range = linkRangeAtLocation(location) {
if !(range.location == selectedRange?.location && range.length == selectedRange?.length) {
modifySelectedAttribute(false)
selectedRange = range
modifySelectedAttribute(true)
}
} else {
modifySelectedAttribute(false)
}
}
public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if selectedRange != nil {
let text = (textStorage.string as NSString).substringWithRange(selectedRange!)
labelDelegate?.labelDidSelectedLinkText!(self, text: text)
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue()) {
self.modifySelectedAttribute(false)
}
}
}
public override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
modifySelectedAttribute(false)
}
private func modifySelectedAttribute(isSet: Bool) {
if selectedRange == nil {
return
}
var attributes = textStorage.attributesAtIndex(0, effectiveRange: nil)
attributes[NSForegroundColorAttributeName] = linkTextColor
let range = selectedRange!
textStorage.addAttributes(attributes, range: range)
setNeedsDisplay()
}
private func linkRangeAtLocation(location: CGPoint) -> NSRange? {
if textStorage.length == 0 {
return nil
}
let offset = glyphsOffset(glyphsRange())
let point = CGPoint(x: offset.x + location.x, y: offset.y + location.y)
let index = layoutManager.glyphIndexForPoint(point, inTextContainer: textContainer)
for r in linkRanges {
if index >= r.location && index <= r.location + r.length {
return r
}
}
return nil
}
// MARK: - init functions
override public init(frame: CGRect) {
super.init(frame: frame)
prepareLabel()
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepareLabel()
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
private func prepareLabel() {
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
userInteractionEnabled = true
}
// MARK: lazy properties
private lazy var linkRanges = [NSRange]()
private var selectedRange: NSRange?
private lazy var textStorage = NSTextStorage()
private lazy var layoutManager = NSLayoutManager()
private lazy var textContainer = NSTextContainer()
}
Thank you for your answers
King regards
Note: I solved hashtag clickable issue(regular expression issue) by changing the hashtag expression as #[\\u4e00-\\u9fa5a-zA-Z0-9_-]*. But the rest of the issues still going on.
You could also try ActiveLabel.swift which is an UILabel drop-in replacement supporting Hashtags (#), Mentions (#) and URLs (http://) written in Swift.
Maybe that's exactly what you're looking for. Giving usernames, hashtags and links different colors is as simple as this:
label.textColor = .blackColor()
label.hashtagColor = .blueColor()
label.mentionColor = .greenColor()
label.URLColor = .redColor()
Disclaimer: I'm the author of the library.