Get IndexPath for - UITextView should interact with URL at indexPath - url

I have a collection view. Inside the user name will be displayed, along with that if he checked in to a place and friends that are with him. I am using the username as a TextView, and I am assigning URL's for 2 types of text (friends and checkIn). It works as it should with only one problem.
I cannot figure out a way to get the IndexPath when the user taps a link. It will send to the link and triggers the function but I can't get the indexPath. I looked everywhere but no documentation about it. Forever in your debt for a bit of help cos, I've been struggling a lot with this.
This is the function that makes username show the 2 links:
//GET THE INDEX PATH FOR ASSIGNMENT TO THE LINKS ??
func assignNameFriendsAndCheckIn(name: String, checkIn: String, friends: String, cellName: UITextView) {
let nameSurname = name
let checkIn = checkIn
var string = name
let friendsString = friends
string = "\(nameSurname)\(checkIn)\(friendsString)"
let attributedString = NSMutableAttributedString(string: string)
attributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.boldSystemFont(ofSize: 14), range: (string as NSString).range(of: nameSurname))
attributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 11), range: (string as NSString).range(of: checkIn))
attributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 11), range: (string as NSString).range(of: friendsString))
attributedString.addAttribute(NSAttributedString.Key.link, value: "checkIn", range: (string as NSString).range(of: checkIn))
cellName.linkTextAttributes = [NSAttributedString.Key.foregroundColor:UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 11)]
attributedString.addAttribute(NSAttributedString.Key.link, value: "friends", range: (string as NSString).range(of: friendsString))
cellName.linkTextAttributes = [NSAttributedString.Key.foregroundColor:UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 11)]
cellName.attributedText = attributedString
}
And this is how I catch the links:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "checkIn" {
print("Check In")
return true
} else if URL.absoluteString == "friends" {
print("Friends")
return true
} else {
print("No Urls set")
return false
}
}

Based on the suggestion Larme gave me I came up with this, and it works.
In didSelectRow, I am assigning a tap gesture and then I make the NSAttributedString with the links. In the second part, I am retrieving the point of gesture in collectionView and get the indexPath. Now the actions will be assigned. Because the first click will be on the String and not the Label the indexPath will be assigned with a delay. So I am delaying the assignment with DispatchQueue :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath) as! CollectionCell
cell.nameSurname.delegate = self
................................................
cell.nameSurname.isUserInteractionEnabled = true
cell.nameSurname.tag = (indexPath.section * 100) + indexPath.item
let tapName : UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapName))
cell.nameSurname.addGestureRecognizer(tapName)
tapName.delegate = self
assignNameLocationAndCheckIn(name: nameSurname, checkIn: checkIn, city: city, date: date, friends: friendsString, cellName: cell.nameSurname, postID: cell.postID)
}
This is the function to calculate the indexPath (urlIndexPath is just a varaible):
#objc func didTapName(sender: UITapGestureRecognizer) {
let pointInCollectionView = sender.location(in: collectionView)
let indexPath = collectionView?.indexPathForItem(at: pointInCollectionView)
urlIndexPath = indexPath!.item
print("Name was tapped: \(indexPath!.item) : \(posts[(indexPath?.row)!].postID!)")
}
And finally I am using the indexPath:
//MARK:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "checkIn" {
print("Check In")
checkInText()
return true
} else if URL.absoluteString == "friends" {
print("Friends")
tagFriends()
return true
} else {
print("No Urls set")
return false
}
}
//MARK:
func checkInText() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("Send to: \(self.urlIndexPath)")
}
}
//MARK:
func tagFriends() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print("Send to Friends: \(self.urlIndexPath)")
}
}

Related

ios - Replace/delete characters [• ] in UITextView on Enter

I have a UITextView with a custom NSTextStorage, where I add a list bullet, after each Enter, if the prior line starts with a list bullet.
When a user Enter and there is only a list bullet in the beginning of the line where the cursor is, I remove the list bullet and stay on that line.
The first function works as expected. But I have a hard time figuring out how to remove the list bullet.
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: "", selectedRangeLocationMove: text.last!.count)
}
It is the code above that does not work. I probably do not use the correct Range.
This is all of my code.
class TextView: UITextView {
internal var storage: TextStorage!
var defaultAttributes: [NSAttributedString.Key: AnyObject] = [:]
override init(frame: CGRect, textContainer: NSTextContainer?) {
let container = (textContainer == nil) ? NSTextContainer() : textContainer!
container.widthTracksTextView = true
container.heightTracksTextView = true
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(container)
self.storage = TextStorage()
self.storage.addLayoutManager(layoutManager)
super.init(frame: .zero, textContainer: container)
self.textContainerInset = .init(top: 16, left: 16, bottom: 16, right: 16)
self.isScrollEnabled = true
self.storage.textView = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
TextStorage
class TextStorage: NSTextStorage {
var backingStore: NSMutableAttributedString = NSMutableAttributedString()
var textView: UITextView!
override var string: String {
return self.backingStore.string
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
func replaceCharactersInRange(_ replaceRange: NSRange, withString str: String, selectedRangeLocationMove: Int) {
if textView.undoManager!.isUndoing {
textView.selectedRange = NSMakeRange(textView.selectedRange.location - selectedRangeLocationMove, 0)
replaceCharactersInRange(NSMakeRange(replaceRange.location, str.count), withString: "")
} else {
replaceCharactersInRange(replaceRange, withString: str)
textView.selectedRange = NSMakeRange(textView.selectedRange.location + selectedRangeLocationMove, 0)
}
}
}
extension NSMutableAttributedString {
func replaceCharactersInRange(_ range: NSRange, withString str: String) {
if isSafeRange(range) {
replaceCharacters(in: range, with: str)
}
}
func isSafeRange(_ range: NSRange) -> Bool {
if range.location < 0 {
return false
}
let maxLength = range.location + range.length
return maxLength <= string.count
}
}
class TextUtils {
class func isReturn(str: String) -> Bool {
return str == "\n"
}
class func isBackspace(str: String) -> Bool {
return str == ""
}
class func startOffset(_ string: String, location: Int) -> (String, Int) {
var offset: Int = 0
var word = NSString(string: string).substring(to: location)
let lines = string.components(separatedBy: "\n")
if lines.count > 0 {
let last = lines.last!
offset = word.count - last.count
word = last
}
return (word, offset)
}
}
I used also used self.deleteCharacters(in:) instead of the following function. It did not work either.
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
To sum up, I just need to remove a word where the cursor is when a user Enter and stay on that line.
I will be very grateful if you help me fix this issue.
Update
How to test this code?
On the first line, add a bullet + one space [• ], write a few words and enter.
From this point on, it adds a bullet since the prior line starts with a bullet.
Find below modified functions. Tested with Xcode 11.4 / iOS 13.4
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) - 1, text.last!.count + 1)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: 0)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
guard range.upperBound <= string.count else { return }
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}

How to make the alert have the correct word?

I have text in a TextView, there are some words in the text, in the case when the user clicks on it, an alert should appear with a word, which for example can mean a word translation into some language.
I tried to use a dictionary, where the key is the word in the text, and the word in the alert is a value. But it does not work. There is an alert with the wrong word.
import UIKit
class ViewController: UIViewController , UITextViewDelegate {
private let kURLString = "https://www.mywebsite.com"
let dictionary = ["website" : "Johny" , "visit" : "Bilbo"]
var keyOne : String?
var valueOne : String?
#IBOutlet weak var text: UITextView! {
didSet{
text.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
let originalText = "Please visit the website for more information."
let attributedOriginalText = NSMutableAttributedString(string: originalText)
for (key , value) in dictionary {
keyOne = key
valueOne = value
let linkRange = attributedOriginalText.mutableString.range(of: keyOne!)
attributedOriginalText.addAttribute(.link, value: kURLString, range: linkRange)
}
text.attributedText = attributedOriginalText
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: valueOne!)
}
return false
}
func alert (value : String) {
let alert = UIAlertController (title: nil, message: value, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Ок", style: .default , handler : { (UIAlertAction) in
self.viewDidLoad()
})
alert.addAction(restartAction)
present(alert, animated: true, completion: nil)
}
}
You can do this by creating an extension for reusable next time as follow code below:
extension UITextView{
func textRangeFromNSRange(range:NSRange)->String{
let myNSString = self.text as NSString
return myNSString.substring(with: range)
}
}
Usage:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: textView.textRangeFromNSRange(range: characterRange))
}
return false
}
If you want to get value from your dictionary you can do like below:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if (URL.absoluteString == kURLString) {
alert(value: dictionary[textView.textRangeFromNSRange(range: characterRange)]!)
}
return false
}
Noted: Make sure links in textViews are selectable but not editable.
Links in text views are interactive only if the text view is selectable but noneditable.
set editable false & selectable true:
text.isEditable = false
text.isSelectable = true
To get selected text from textview which is key in your case use below function:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
if let key = textView.text.substring(with: characterRange){
if let value = dictionary[String(key)]{
print("text :",value)
alert(value: value)
}
}
return false
}
Using extension for string to get Substring with range:
extension String {
func substring(with nsrange: NSRange) -> Substring? {
guard let range = Range(nsrange, in: self) else { return nil }
return self[range]
}
}

UISearchbar using UItextfield in swift 4

I am working on JSON. My Json data print into the tableview. I want to filtered that data with searchbar. So I put Textfield for using the Searchbar. I use reference from this website
http://findnerd.com/list/view/How-to-create-your-own-search-bar-in-Swift-Not-using-UISearchBar/20577/
My Search bar is working but not properly. I want to filter data after I write 3 Words in searchbar.If I write "Ku" then my tableview remain hide. If I write "kus" in searchbar then searchbar started searching and show me filtered data in tableview started from "kus". my searchbar related code are these
struct PatientData:Decodable {
var ID : String
var dt_bod : String
var e_gender : String
var int_glcode : String
var var_email : String
var var_fname : String
var var_phoneno : String
var var_uname : String
init(userdata : [String:Any]) {
self.ID = userdata["ID"] as! String
self.dt_bod = userdata["dt_bod"] as! String
self.e_gender = userdata["e_gender"] as! String
self.int_glcode = userdata["int_glcode"] as! String
self.var_email = userdata["var_email"] as! String
self.var_fname = userdata["var_fname"] as! String
self.var_phoneno = userdata["var_phoneno"] as! String
self.var_uname = userdata["var_uname"] as! String
}
var tabledata = [String]()
var tableFilterData = [String]()
var patientDetails = [PatientData]()
#IBAction func textfieldchanged(_ sender: Any) {
tableview.isHidden = true
}
my textfield change character function
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
let searchText = textField.text! + string
if searchText.count >= 3 {
tableview.isHidden = false
tableFilterData = tabledata.filter({ (result) -> Bool in
return result.range(of: searchText, options: .caseInsensitive) != nil
})
print(tableFilterData) // I got filtered data here but how to show this data into the tableview
tableview.reloadData()
}
else{
tableFilterData = []
}
return true
}
tableview part is
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return patientDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell") as UITableViewCell!
let aa = patientDetails[indexPath.row].var_fname + " , " + patientDetails[indexPath.row].dt_bod + " , " + patientDetails[indexPath.row].var_phoneno
self.tabledata.append(aa)
cell.textLabel?.text = aa
cell.textLabel?.font = searchTextfield.font
return cell
}
Try this:
#objc func textFieldActive() {
tableView.isHidden = tableFilterData.count > 0 ? false : true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
let searchText = textField.text! + string
if searchText.count >= 3 {
tableView.isHidden = false
tableFilterData = tabledata.filter({ (result) -> Bool in
return result.range(of: searchText, options: .caseInsensitive) != nil
})
tableView.reloadData()
}
else{
tableFilterData = []
}
return true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
return tableFilterData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = tableFilterData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = data
return cell
}
In Swift 4 or Swift 5, you can use like bellow..
Your tableview like bellow
Create a project
Create add textfield, tableView connect to viewcontroller
add bellow code..
class ViewController: UIViewController ,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate{
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var txtName: UITextField!
var originalArr = [[String:Any]]();
var searchArrRes = [[String:Any]]()
var searching:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
//Assign delegate don't forget
txtName.delegate = self
tableView.delegate = self
tableView.dataSource = self
//my array
originalArr = [
["name": "abdul", "number": "+8800000001"],
["name": "abdin", "number": "+8800000002"],
["name": "Enamul", "number": "+8800000003"],
["name": "enam", "number": "+8800000004"],
["name": "Rafi", "number": "+8800000005"],
["name": "Ehaque", "number": "+8800000006"],
["name": "ab", "number": "+8800000007"],
["name": "Emon", "number": "+8800000008"],
["name": "enamu1", "number": "+8800000009"],
["name": "rafi", "number": "+88000000010"],
["name": "karim", "number": "+88000000011"],
["name": "radev", "number": "+88000000012"],
["name": "da", "number": "+88000000013"],
["name": "aa", "number": "+88000000014"],
["name": "rafi", "number": "+88000000010"],
["name": "karim", "number": "+88000000011"],
["name": "radev", "number": "+88000000012"],
["name": "da", "number": "+88000000013"],
["name": "aa", "number": "+88000000014"]
]
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
//input text
let searchText = textField.text! + string
//add matching text to arrya
searchArrRes = self.originalArr.filter({(($0["name"] as! String).localizedCaseInsensitiveContains(searchText))})
if(searchArrRes.count == 0){
searching = false
}else{
searching = true
}
self.tableView.reloadData();
return true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//check search text & original text
if( searching == true){
return searchArrRes.count
}else{
return originalArr.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//custom cell Custom_cell
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! Custom_cell
//check search text & original text
if( searching == true){
var dict = searchArrRes[indexPath.row]
cell.label_name.text = dict["name"] as? String
cell.label_number.text = dict["number"] as? String
}else{
var dict = originalArr[indexPath.row]
cell.label_name.text = dict["name"] as? String
cell.label_number.text = dict["number"] as? String
}
return cell
}
}
You can download full source from GitHub Link: https://github.com/enamul95/TableView_Search
You can check size of text before filter:
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool{
var searchText = textField.text! + string
if string == "" {
searchText = (searchText as String).substring(to: searchText.index(before: searchText.endIndex))
}
if searchText == "" {
isSearch = false
tableview.reloadData()
}
else{
if searchText.count > 2 {
getSearchArrayContains(searchText)
}
}
return true
}
Please use this code:-
func getSearchArrayContains(_ text : String) {
tableFilterData = tableData.filter({$0.lowercased().contains(text)})
isSearch = true
tableview.reloadData()
}
For should three character use this linkenter link description here:-
Thanks
All of the above answers try to reverse engineer the UITextField string by concatenating the change to the previous string. This needed to be done because the delegate method shouldChangeCharactersIn is called before the change is made on the UITextField string.
These implementations are wrong and do not work when the user scrolls the cursor to the left and continue typing or selects and replaces text (as the answers ignore the NSRange delegate variable).
A better implementation is to not use the delegate method at all and instead add a target to the UITextField. This works because UITextField inherits from UIControl.
override func viewDidLoad() {
super.viewDidLoad()
searchTextField.addTarget(self, action: #selector(searchTextChanged(_:)), for: .editingChanged)
}
#objc func searchTextChanged(_ sender: UITextField) {
let search = sender.text ?? ""
filterContentForSearchText(search)
}
func filterContentForSearchText(_ searchText: String) {
print("Filterin with:", searchText)
filtered.removeAll()
filtered = original.filter { thing in
return "\(thing.value.lowercased())".contains(searchText.lowercased())
}
tableView.reloadData()
}
Note: This action can also be created in the storyboard by creating an #IBAction and dragging the Editing Changed connection from the UITextField to the #IBAction

Check in cell (in TextView) text or link (Swift)

I have a table with ImageView and TextView
How can i check if in cell i have text then (in image view = Image "Text"), else if in cell i have link (in image view = Image "Link")
var myData: [String] = []
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if (URL.scheme?.contains("http"))! || (URL.scheme?.contains("https"))! {
cell.ImageV?.image = UIImage(named:"Link")
print("Link")
} else {
cell.ImageV?.image = UIImage(named:"Text")
print("Text")
}
return false
}
Example:
But it not work
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.nameText?.text = myData[indexPath.row]
//
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if (URL.scheme?.contains("http"))! || (URL.scheme?.contains("https"))! {
// Handle links
cell.ImageV?.image = UIImage(named:"Link")
print("Link")
} else {
// Handle anything else that has slipped through.
cell.ImageV?.image = UIImage(named:"Text")
print("Text")
}
return false
}
return cell
}
Save and Load:
func save() {
UserDefaults.standard.set(myData, forKey: "notes")
//UserDefaults.standard.set(Data, forKey: "ImageDate")
UserDefaults.standard.synchronize()
}
//
func load(){
if let loadData = UserDefaults.standard.stringArray(forKey: "notes") {
myData = loadData
table.reloadData()
}
}
UPDATED
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let dict = myData[indexPath.row]
if dict.contains("http") || dict.contains("https")
{
print("Link")
}
else{
print("Text")
}
}
Hope this helps you
You can check if text is URL or not with following code
guard let url = URL(string: self.text!) else { return false }
if UIApplication.shared.canOpenURL(url) {
// url
}else {
//not an url
}

How to add followed text to uitextfield

I have one text field with place holder called letschat. Now whenever I start typing in my textfield, I want to show my textfield as some #letschat. When my textfield is empty that time my placeholder have to show. That I did. But I want to set whenever I start typing in my textfield. Whatever I am typing with that I want this text also to visible like:
Some #Lletschat
How can I do this?
I created a UITextField subclass that uses the placeholder (if set) as a suffix. As far as I can see everything works as expected. Maybe there are some tweaks needed to suit your needs.
Feel free to ask if anything is unclear:
class SuffixTextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
private func sharedInit() {
addTarget(self, action: #selector(textChanged), for: .editingChanged)
}
override var text: String? {
didSet {
selectedTextRange = maxTextRange
}
}
override var attributedText: NSAttributedString? {
didSet {
selectedTextRange = maxTextRange
}
}
#objc private func textChanged() {
if let currentText = text, let placeholder = placeholder {
if currentText == placeholder {
self.text = nil
} else if !currentText.hasSuffix(placeholder) {
self.text = currentText + placeholder
}
}
}
private var maxCursorPosition: UITextPosition? {
guard let placeholder = placeholder, !placeholder.isEmpty else { return nil }
guard let text = text, !text.isEmpty else { return nil }
return position(from: beginningOfDocument, offset: (text as NSString).range(of: placeholder, options: .backwards).location)
}
private var maxTextRange: UITextRange? {
guard let maxCursorPosition = maxCursorPosition else { return nil }
return textRange(from: maxCursorPosition, to: maxCursorPosition)
}
override var selectedTextRange: UITextRange? {
get { return super.selectedTextRange }
set {
guard let newRange = newValue,
let maxCursorPosition = maxCursorPosition else {
super.selectedTextRange = newValue
return
}
if compare(maxCursorPosition, to: newRange.start) == .orderedAscending {
super.selectedTextRange = textRange(from: maxCursorPosition, to: maxCursorPosition)
} else if compare(maxCursorPosition, to: newRange.end) == .orderedAscending {
super.selectedTextRange = textRange(from: newRange.start, to: maxCursorPosition)
} else {
super.selectedTextRange = newValue
}
}
}
}
here you can see a preview:
https://www.dropbox.com/s/etkbme37wuxbw1q/preview.mov?dl=0
You can take the action of UITextField textFieldDidChange and get the call of every time when the textField text changed.
Just like that:
func textChangedAction(sender:UITextFiled) {
if sender.text.rangeOfString("#Lletschat") != nil{
sender.text = sender.text.replacingOccurrences(of: "#Lletschat", with: "")
}
sender.text = "\(sender.text!) #Lletschat"
}
If you want to changed the color of your specific text you can check here.
Implement textfield delegate like this-
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
textField.text = textField.text?.replacingOccurrences(of: " #\(textField.placeholder!)", with: "", options: .literal, range: nil)
textField.text?.append(string)
textField.text?.append(" #\(textField.placeholder!)")
return false
}
Simple solution:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let newRange = textField.text?.range(from: range), let result = textField.text?.replacingCharacters(in: newRange, with: string) else { return true }
if result.endsWithString("#letschat") {
return true
} else {
textField.text = result + "#letschat"
let position = textField.position(from: textField.beginningOfDocument, offset: result.characters.count)!
textField.selectedTextRange = textField.textRange(from: position, to: position)
return false
}
}
With helper extension:
extension String {
func range(from oldOne: NSRange) -> Range<String.Index>? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: oldOne.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(utf16.startIndex, offsetBy: oldOne.location + oldOne.length, limitedBy: utf16.endIndex),
let from = from16.samePosition(in: self),
let to = to16.samePosition(in: self)
else { return nil }
return from ..< to
}
func endsWithString(_ string: String) -> Bool {
guard characters.count >= string.characters.count else { return false }
let index = self.index(startIndex, offsetBy: characters.count - string.characters.count)
let substring = self.substring(from: index)
return substring == string
}
}
Difficult but clear solution is to create your own UIControl-subclass with UITextField and UILabel children views:
+-----------+ +-----------+
| textfield | -(3px)- | #letschat |
+-----------+ +-----------+
Use autolayout to keep the distance of 3 pixels between it.
Don't forget to configure your class to send all the incoming actions to the textfield. You can use different font colours for these controls so user won't be confused about efforts to change label's value.
Conform your class to UITextfieldDelegate and then assign textfield.delegate = self
Now, add this Delegate Method, if you want your #letschat to be appended after user endtyping.
func textFieldDidEndEditing(textField: UITextField) {
textField.text = "\(textField.text)#letschat"
}
or if you want it at the typing time.
textField.addTarget(self, action: #selector(YourViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingChanged)
func textFieldDidChange(textField: UITextField) {
if textField.containsString("#letschat") {
textField.text = textField.text.stringByReplacingOccurrencesOfString("#letschat", withString: "")
}
textField.text = "\(textField.text)#letschat"
}
Hope this helps.

Resources