TextView Counter On Typing And Count New Line Swift IOS - ios

I want to count newlines ("\n") in a textview.
Or, more specifically, I want to make textview box to count the characters when typing ( +1 ) in label. Also count ( +2 ) when the text have new line and counter ( Label ) must be continuous.
Here is my code:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let allowedChars = 70
let charsInTextView = -txtmessage.text.count
let remainingChars = allowedChars + charsInTextView
if (text == "\n") {
let remainingChars = 70 - (txtmessage.text.count * 2 )
countlabel.text = String(remainingChars)
}
if (text != "\n"){
countlabel.text = String(remainingChars)
}
return true
}

import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let textView = UITextView(frame: CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(textView)
textView.delegate = self
textView.backgroundColor = .white
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
guard let text = textView.text else {
return
}
let totalLength = text.count
let newlineCount = text.filter {$0 == "\n"}.count
print("Total characters are \(totalLength) of which \(newlineCount) are newLines total of all characters counting newlines twice is \(totalLength + newlineCount)")
}
}
let v = ViewController()
v.preferredContentSize = CGSize(width: 1024, height: 768)
PlaygroundPage.current.liveView = v

Related

NSMutableAttributedString to UITextField in Swift

I need to make extra characters in textfield red, so I used NSMutableAttributedString, could anyone please show me how can I transfer a NSMutableAttributedString to UITextField
#IBOutlet weak var inputLimitField: UITextField!
#IBOutlet weak var characterCountLabel: UILabel!
private let allowedChars = 10
override func viewDidLoad() {
super.viewDidLoad()
characterCountLabel.text = "10/10"
inputLimitField.delegate = self
}
func checkRemainingChars() {
let charsInTextView = -(inputLimitField.text?.count ?? 0)
let remainingChars = allowedChars + charsInTextView
characterCountLabel.textColor = .black
inputLimitField.textColor = .black
inputLimitField.layer.borderColor = UIColor.systemGray6.cgColor
if remainingChars < 0 {
getColoredText(text: inputLimitField.text ?? "") // how to implement this NSMutableAttributedString in inputLimitField.text
characterCountLabel.textColor = .red
inputLimitField.layer.borderColor = UIColor.red.cgColor
inputLimitField.layer.cornerRadius = 6.0
inputLimitField.layer.borderWidth = 1.0
}
characterCountLabel.text = ("\(String(remainingChars))/10")
}
func textFieldDidChangeSelection(_ textField: UITextField) {
checkRemainingChars()
}
func getColoredText(text:String) -> NSMutableAttributedString{
let string:NSMutableAttributedString = NSMutableAttributedString(string: text)
string.addAttribute(.foregroundColor, value: UIColor.red, range: NSRange(location: allowedChars, length: string.length))
return string
}
getColoredText returns the attributed string. Just assign it to the attributedText property of the text field
inputLimitField.attributedText = getColoredText(text: inputLimitField.text!)
Note: the text property of UITextField is never nil. You can forced unwrap it.

Detect clicked mutable attributed string

I have this code to write a paragraph like book with numbering for each sentence , the problem I'm facing is i can't find how to color one sentence when the user clicks in any word from it
import UIKit
let descender: CGFloat = UIFont.systemFont(ofSize: 25).descender
class ViewController: UIViewController , UITextViewDelegate, UIGestureRecognizerDelegate {
var all = [NSMutableAttributedString]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let style = NSMutableParagraphStyle()
style.alignment = NSTextAlignment.justified
style.baseWritingDirection = .rightToLeft
style.lineBreakMode = .byWordWrapping
let myAttribute = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 25)] // ,
// NSAttributedString.Key.paragraphStyle: style ,
// NSAttributedString.Key.baselineOffset: NSNumber(value: 0)]
let textView = UITextView(frame:CGRect(x: 20, y: 100, width: UIScreen.main.bounds.width - 40 , height: UIScreen.main.bounds.height))
let attributedString = NSMutableAttributedString()
Array(1..<50).forEach {
let small = $0 % 2 == 0 ? " long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one " : "long text part two long text part twolong text part twolong text part twolong text part twolong text part twolong text part twolong text part two "
let attributedString2 = NSMutableAttributedString(string: small,attributes: myAttribute)
attributedString.append(attributedString2)
let textAttachment11 = SubTextAttachment()
textAttachment11.image = generateImageWithText(text: "\($0)")
let attrStringWithImage11 = NSAttributedString(attachment: textAttachment11)
attributedString.append(attrStringWithImage11)
}
textView.attributedText = attributedString;
self.view.addSubview(textView)
textView.isEditable = false
textView.isSelectable = true
textView.delegate = self
let tap = UITapGestureRecognizer(target: self, action: #selector(self.textTapped(_:)))
tap.delegate = self
textView.isUserInteractionEnabled = true
textView.addGestureRecognizer(tap)
}
func generateImageWithText(text: String) -> UIImage? {
let image = UIImage(named: "qqq")!
print(text," ",image.size)
let imageView = UIImageView(image: image)
imageView.backgroundColor = UIColor.clear
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
label.font = UIFont.systemFont(ofSize: 50)
label.backgroundColor = UIColor.clear
label.textAlignment = .center
label.textColor = UIColor.black
label.text = text
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let imageWithText = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageWithText
}
#objc func textTapped(_ sender:UITapGestureRecognizer) {
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
return true
}
}
class SubTextAttachment:NSTextAttachment {
override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
let height = lineFrag.size.height
var scale: CGFloat = 1.0;
let imageSize = image!.size
if (height < imageSize.height) {
scale = height / imageSize.height
}
let value = CGRect(x: 0, y: descender, width: imageSize.width * scale, height: imageSize.height * scale)
return value
}
}
I know how to change the foreground color of any sub attributed string , but how i can know that the clicked part belong to the one to be colored ?
Also is there any better way to build this UI (in terms of performance ) as with tableView/CollectionView there is a dequeueing but here there isn't ?
So any hep is greatly appreciated
With NSAttributedString , you can use CoreText to render.
Convert NSAttributedString to CTFrame, then render it.
The key part
when you click a word in paragraph,
with override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
you can get a CGPoint
with that CGPoint & CTFrame, you can know the text range clicked in the text.
then rebuild the NSAttributedString 、CTFrame & rerender
here is the code you can refer
import UIKit
import CoreText
class TextRenderView: UIView {
let frameRef:CTFrame
let theSize: CGSize
let keyOne = //...
let keyTwo = //...
let rawTxt: String
let contentPage: NSAttributedString
let keyRanges: [Range<String.Index>]
override init(frame: CGRect){
rawTxt = //...
var tempRanges = [Range<String.Index>]()
if let rangeOne = rawTxt.range(of: keyOne){
tempRanges.append(rangeOne)
}
if let rangeTwo = rawTxt.range(of: keyTwo){
tempRanges.append(rangeTwo)
}
keyRanges = tempRanges
contentPage = NSAttributedString(string: rawTxt, attributes: [NSAttributedString.Key.font: UIFont.regular(ofSize: 15), NSAttributedString.Key.foregroundColor: UIColor.black])
let calculatedSize = contentPage.boundingRect(with: CGSize(width: UI.std.width - CGFloat(15 * 2), height: UI.std.height), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size
let padding: CGFloat = 10
theSize = CGSize(width: calculatedSize.width, height: calculatedSize.height + padding)
let framesetter = CTFramesetterCreateWithAttributedString(contentPage)
let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: theSize), transform: nil)
frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
super.init(frame: frame)
backgroundColor = UIColor.white
}
required init?(coder: NSCoder) {
fatalError()
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else{
return
}
ctx.textMatrix = CGAffineTransform.identity
ctx.translateBy(x: 0, y: bounds.size.height)
ctx.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(frameRef, ctx)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else{
return
}
let pt = touch.location(in: self)
guard let offset = parserRect(with: pt, frame: frameRef), let pos = rawTxt.index(rawTxt.startIndex, offsetBy: offset, limitedBy: rawTxt.endIndex) else{
return
}
if keyRanges[0].contains(pos){
print(0)
}
else if keyRanges[1].contains(pos){
print(1)
}
}
func parserRect(with point: CGPoint, frame textFrame: CTFrame) -> Int?{
var result: Int? = nil
let path: CGPath = CTFrameGetPath(textFrame)
let bounds = path.boundingBox
guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else{
return result
}
let lineCount = lines.count
guard lineCount > 0 else {
return result
}
var origins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), &origins)
for i in 0..<lineCount{
let baselineOrigin = origins[i]
let line = lines[i]
var ascent: CGFloat = 0
var descent: CGFloat = 0
var linegap: CGFloat = 0
let lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap)
let lineFrame = CGRect(x: baselineOrigin.x, y: bounds.height-baselineOrigin.y-ascent, width: CGFloat(lineWidth), height: ascent+descent+linegap + 10)
if lineFrame.contains(point){
result = CTLineGetStringIndexForPosition(line, point)
break
}
}
return result
}
}
helper method:
extension String {
func range(ns inner: String) -> NSRange{
return (self as NSString).range(of: inner)
}
}
here is the github code you can refer

Tap gesture only for specific ranges of a UILabel in Swift not working when the base string contains double quotation

I did the implementation in this in my ios app.but when base text given by, guard let text = self.lblTermsAndConditions.text else { return } contains double quotation func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool returns false.
For example when text given by lblTermsAndConditions is, By clicking \"Signup\",I agree to the Terms and Conditions & Privacy Policy and privacyPolicyRange = Terms and Conditions & Privacy Policy didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool returns false.
my code
#IBOutlet weak var privacyPolicyLabel: HuqLabel!{
didSet{
privacyPolicyLabel.text = "By clicking \"Signup\",I agree to the Terms and Conditions & Privacy Policy"
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.privacyPolicyLabel.isUserInteractionEnabled = true
let tapgesture = UITapGestureRecognizer(target: self, action: #selector(tappedOnLabel(_ :)))
tapgesture.numberOfTapsRequired = 1
tapgesture.numberOfTouchesRequired = 1
self.privacyPolicyLabel.addGestureRecognizer(tapgesture)
self.privacyPolicyLabel.isUserInteractionEnabled = true }
extension at the bottom of the viewcontroller
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y);
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
I am using it like this
guard let text = self.privacyPolicyLabel.text else { return }
let privacyPolicyRange = (text as NSString).range(of: "Terms and Conditions & Privacy Policy")
if gesture.didTapAttributedTextInLabel(label: self.privacyPolicyLabel, inRange: privacyPolicyRange) {
print("clicked on terms and conditions")
}else{
print("false")
}
This prints false.
How can I fix this?
Edit:
my custom label
import UIKit
class HuqLabel: UILabel {
private var attributes = HuqStringAttributes.attributes(for: .bigTitle)
private var typograpyStyle: HuqTypographyStyle!
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
adjustsFontForContentSizeCategory = true
numberOfLines = 0
}
// Must be called after setting the attributed text
func setNumberOfLines(_ numOfLines: Int, breakMode: NSLineBreakMode = .byTruncatingTail) {
numberOfLines = numOfLines
lineBreakMode = breakMode
}
func setText(_ text: String) {
self.text = text
setLetterSpacing()
}
func set(_ text: String = "", typographyStyle: HuqTypographyStyle, alignment: NSTextAlignment = .left) {
typograpyStyle = typographyStyle
self.text = text
accessibilityLabel = text
self.attributes = HuqStringAttributes.attributes(for: typographyStyle, alignment: alignment)
setLetterSpacing()
}
func setAttributesForStrikeThrough(_ text: String, typographyStyle: HuqTypographyStyle, alignment: NSTextAlignment = .left) {
self.text = text
accessibilityLabel = text
self.attributes = HuqStringAttributes.attributes(for: typographyStyle, alignment: alignment)
let attributedString = NSMutableAttributedString(string: text, attributes: self.attributes)
attributedString.addAttribute(NSAttributedString.Key.kern, value: letterSpacingForTypographyStyle(for: typographyStyle), range: NSRange(location: 0, length: text.count))
attributedString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 1, range: NSMakeRange(0, attributedString.length))
attributedText = attributedString
}
func setLetterSpacing() {
let attributedString = NSMutableAttributedString(string: self.text ?? "", attributes: self.attributes)
attributedString.addAttribute(NSAttributedString.Key.kern, value: letterSpacingForTypographyStyle(for: typograpyStyle), range: NSRange(location: 0, length: self.text?.count ?? 0))
attributedText = attributedString
}
private func letterSpacingForTypographyStyle(for typographyStyle: HuqTypographyStyle) -> CGFloat {
switch typographyStyle {
case .bigTitle, .title, .bodyLight, .bodyWhite, .caption, .bodyBoldWhite, .bodyBoldBlue, .bodyBalck, .bodySmall, .titleBlue, .captionBlack, .bigTitleOrange, .captionMediumBlue:
return 0
}
}
func setText(_ text: String,
withBoldTextSections boldSections: [String], font: UIFont = FontFamily.Ubuntu.bold.font(size: 16),color:UIColor = ColorName.brownGrey.color) {
let attributes = HuqStringAttributes.attributes(for: .bodyLight, alignment: .center)
let attributedFullString = NSMutableAttributedString(string: text,
attributes: attributes)
boldSections.forEach { section in
let rangeOfSection = attributedFullString.mutableString.range(of: section)
attributedFullString.addAttributes([
NSAttributedString.Key.foregroundColor: color,
NSAttributedString.Key.font:font], range: rangeOfSection)
}
attributedText = attributedFullString
}
-
import Foundation
enum HuqTypographyStyle {
case bigTitle
case bigTitleOrange
case title
case titleBlue
case bodyLight
case bodyWhite
case bodyBalck
case caption
case captionBlack
case bodyBoldBlue
case bodyBoldWhite
case bodySmall
case captionMediumBlue
-
import Foundation
import UIKit
struct HuqFonts {
static func font(for typographyStyle: HuqTypographyStyle) -> UIFont {
switch typographyStyle {
case .bigTitle, .bigTitleOrange:
return ubuntuMedium(size: 30)
case .title, .titleBlue:
return ubuntuMedium(size: 18)
case .bodyLight, .bodyBalck:
return ubuntuRegular(size: 16)
case .bodyWhite:
return ubuntuRegular(size: 14)
case .caption, .captionBlack:
return ubuntuRegular(size: 12)
case .bodyBoldBlue, .bodyBoldWhite:
return ubuntuBold(size: 16)
case .bodySmall:
return ubuntuRegular(size: 10)
case .captionMediumBlue:
return ubuntuMedium(size: 12)
}
}
private static func ubuntuMedium(size: CGFloat) -> UIFont {
return FontFamily.Ubuntu.medium.font(size: size)
}
private static func ubuntuRegular(size: CGFloat) -> UIFont {
return FontFamily.Ubuntu.regular.font(size: size)
}
private static func ubuntuBold(size: CGFloat) -> UIFont {
return FontFamily.Ubuntu.bold.font(size: size)
}
}
import UIKit
struct HuqStringAttributes {
static func attributes(for typographyStyle: HuqTypographyStyle,
alignment: NSTextAlignment = .left) -> [NSAttributedString.Key: Any] {
var attributes: [NSAttributedString.Key: Any] = [:]
attributes[.font] = HuqFonts.font(for: typographyStyle).scaled
attributes[.foregroundColor] = colorsForTypographyStyles(for: typographyStyle).color
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = alignment
attributes[.paragraphStyle] = paragraphStyle
return attributes
}
private static func colorsForTypographyStyles(for typographyStyle: HuqTypographyStyle) -> ColorName {
switch typographyStyle {
case .bigTitle, .title, .bodyBalck, .captionBlack:
return ColorName.black
case .bodyLight ,.caption, .bodySmall:
return ColorName.brownGrey
case .bodyWhite, .bodyBoldWhite:
return ColorName.white
case .bodyBoldBlue, .titleBlue, .captionMediumBlue:
return ColorName.turquoiseBlue
case .bigTitleOrange:
return ColorName.bloodOrange
}
}
}
Add this line in viewDidLoad():
self.privacyPolicyLabel.lineBreakMode = .byWordWrapping
Quick testing seems that solves the issue.
EDIT
Give this a try. It uses a standard UILabel - all via code, no #IBOutlet connections - so just assign the custom class of a view controller to LabelLinkViewController. It's also using the UITapGestureRecognizer extension you posted in your question.
class LabelLinkViewController: UIViewController {
var privacyPolicyLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
privacyPolicyLabel.translatesAutoresizingMaskIntoConstraints = false
privacyPolicyLabel.backgroundColor = .yellow
view.addSubview(privacyPolicyLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
privacyPolicyLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
privacyPolicyLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
privacyPolicyLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
])
self.privacyPolicyLabel.lineBreakMode = .byWordWrapping
self.privacyPolicyLabel.isUserInteractionEnabled = true
let tapgesture = UITapGestureRecognizer(target: self, action: #selector(tappedOnLabel(_ :)))
tapgesture.numberOfTapsRequired = 1
tapgesture.numberOfTouchesRequired = 1
self.privacyPolicyLabel.addGestureRecognizer(tapgesture)
self.privacyPolicyLabel.isUserInteractionEnabled = true
self.privacyPolicyLabel.text = "By clicking \"Signup\", I agree to the Terms and Conditions & Privacy Policy"
//self.privacyPolicyLabel.text = "By clicking Signup, I agree to the Terms and Conditions & Privacy Policy"
}
//MARK:- tappedOnLabel
#objc func tappedOnLabel(_ gesture: UITapGestureRecognizer) {
guard let text = self.privacyPolicyLabel.text else { return }
let privacyPolicyRange = (text as NSString).range(of: "Terms and Conditions & Privacy Policy")
if gesture.didTapAttributedTextInLabel(label: self.privacyPolicyLabel, inRange: privacyPolicyRange) {
print("clicked on terms and conditions")
}else{
print("false")
}
}
}
If that works for you, but it does not work when you use your HuqLabel class, then that's where the problem lies.

IOS 13 UITextView with Html Attributed String embedded in UITableViewCell scrolling lag

In IOS 12 the scrolling of UITableView is smooth without lag even when html string contains image but in IOS 13 UITableView scrolling becomes laggy. Already try shouldresterizing = true but doesn't solve the problem. Simulator and Device are the same results.
class ProductTextViewCell:UITableViewCell {
#IBOutlet weak var textView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
textView.textContainer.lineFragmentPadding = 0
textView.text = "-"
textView.dataDetectorTypes = .all
}
func set(str:String) {
print(str)
if let attributed = str.html2AttributedString {
let mutable = NSMutableAttributedString(attributedString: attributed)
self.textView.attributedText = mutable
mutable.enumerateAttribute(NSAttributedStringKey.attachment, in: NSMakeRange(0, attributed.length), options: .init(rawValue: 0), using: { (value, range, stop) in
if let attachement = value as? NSTextAttachment {
if let image = attachement.image(forBounds: attachement.bounds, textContainer: NSTextContainer(), characterIndex: range.location) {
let screenSize: CGRect = UIScreen.main.bounds
let max = screenSize.width - 20
print(max)
print(image.size.width)
if image.size.width > max {
let scale = image.size.height / image.size.width
attachement.bounds = CGRect(x: 0, y: 0, width: max, height: max * scale)
}
}
}
})
} else {
self.textView.attributedText = nil
}
}
}

Showing Text in UITextView with upppercase and getting new text from shouldChangeTextIn keeping case of original and entered text

I have a UITextView that I need to sometimes show in all caps and sometimes in original case. This part is easy but when the user starts editing text I need to update the original string with the changes while keeping the original case of the string. For this reason I have not been able to use textDidChange and instead I am using shouldChangeTextIn and changing the original string. Mostly everything is working as expected except if multiple words are selected and you use a tap on a predictive word in the keyboard. There may be other things that break. What is the best way to fix this to keep the original string while showing a mutated string. Here is a minimal example.
import UIKit
class ViewController: UIViewController {
lazy var textView : UITextView = {
let txtv = UITextView(frame: CGRect(x: 0, y:40, width: self.view.frame.width, height: 200))
txtv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
txtv.delegate = self
txtv.font = UIFont.systemFont(ofSize: 22)
return txtv
}()
lazy var button : UIButton = {
let btn = UIButton(frame: CGRect(x: 0, y:250, width: self.view.frame.width - 40, height: 50))
btn.autoresizingMask = [.flexibleWidth,.flexibleHeight]
btn.setTitleColor(UIColor.blue, for: .normal)
btn.setTitle("SHOW TEXT", for: .normal)
btn.addTarget(self, action: #selector(changeState), for: .touchUpInside)
return btn
}()
var textString = "This is a test to see how we are doing"
var shouldCapitalize : Bool = true
private var hack_shouldIgnorePredictiveInput = false
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textView)
self.view.addSubview(button)
//get things going
updateTextView()
}
func updateTextView(){
//we could change font size here or size of textview
var textToShow = self.textString
if shouldCapitalize == true{
textToShow = textToShow.uppercased()
}
textView.text = textToShow
print("the text string we really care about is::::: \(textString)")
}
#objc func changeState(){
shouldCapitalize = !shouldCapitalize
updateTextView()
}
}
extension ViewController : UITextViewDelegate{
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if hack_shouldIgnorePredictiveInput {
hack_shouldIgnorePredictiveInput = false
return false
}
hack_shouldIgnorePredictiveInput = true
//how do i replace the characters or text changging without changing the case of the original string
print("the textview text is \(textView.text)")
print("the textview text is \((textView.text as NSString).length)")
print("the range is \(range)")
print("the text is \(text)")
let selectedRange = self.textView.selectedRange
if let str = textView.text as? NSString{
if range.length == 0 && text == ""{
print("the ext is empty")
textString = ""
updateTextView()
}
if let tracker = textString as? NSString{
if range.location == tracker.length{
print("adding")
print("the lenght of the tracker is \(tracker.length)")
let newRange = NSMakeRange(tracker.length, range.length)
print("the new range is \(newRange)")
let newString = tracker.replacingCharacters(in: newRange, with: text)
print("the new text is \(newString)")
textString = newString
updateTextView()
}else if range.location < tracker.length{
let newString = tracker.replacingCharacters(in: range, with: text)
print("the new text is \(newString)")
textString = newString
updateTextView()
if (newString as NSString).length > tracker.length{
print("setting cursor \(NSMakeRange(range.location + range.length, 0))")
self.textView.selectedRange = NSMakeRange(range.location + range.length + 1, 0)
}else{
self.textView.selectedRange = NSMakeRange(range.location, 0)
}
}else{
//the problem seems to be in here
print("maybe adding")
print("the lenght of the tracker is \(tracker.length)")
let newRange = NSMakeRange(tracker.length, range.length)
print("the new range is \(newRange)")
let newString = str.replacingCharacters(in: newRange, with: text)
print("the new text is \(newString)")
textString = newString
updateTextView()
}
}
}
hack_shouldIgnorePredictiveInput = false
return false
}
}
So it seems my bottom else was being called because the text was set to empty all driven by the fact that the delegate is called sometimes twice to input a space. The comment from jimmyg helped me see that he was right and the last else should never be hit which was my thought when I wrote the function. Luckily this answer helped explain why empty text was happening. If you are here I recommend upvoting that answer. I also recommend upvoting this answer that helps stop some of the unnecessary calls with predictive text and can be seen in my code. Finally all the logic seems to be in place and much more simple and switching between intended text and transformed text inside the UITextView is working.
import UIKit
class ViewController: UIViewController {
lazy var textView : UITextView = {
let txtv = UITextView(frame: CGRect(x: 0, y:40, width: self.view.frame.width, height: 200))
txtv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
txtv.delegate = self
txtv.font = UIFont.systemFont(ofSize: 22)
return txtv
}()
lazy var button : UIButton = {
let btn = UIButton(frame: CGRect(x: 0, y:250, width: self.view.frame.width - 40, height: 50))
btn.autoresizingMask = [.flexibleWidth,.flexibleHeight]
btn.setTitleColor(UIColor.blue, for: .normal)
btn.setTitle("SHOW TEXT", for: .normal)
btn.addTarget(self, action: #selector(changeState), for: .touchUpInside)
return btn
}()
var textString = "This is a test to see how we are doing"
var shouldCapitalize : Bool = true
private var hack_shouldIgnorePredictiveInput = false
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textView)
self.view.addSubview(button)
//get things going
updateTextView()
}
func updateTextView(){
//we could change font size here or size of textview
var textToShow = self.textString
if shouldCapitalize == true{
textToShow = textToShow.uppercased()
}
textView.text = textToShow
print("realString:: \(textString)")
print("textView showing:: \(textView.text)")
}
#objc func changeState(){
shouldCapitalize = !shouldCapitalize
updateTextView()
}
}
extension ViewController : UITextViewDelegate{
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if hack_shouldIgnorePredictiveInput {
hack_shouldIgnorePredictiveInput = false
return false
}
hack_shouldIgnorePredictiveInput = true
if range.length == 0 && text == "" && self.textString.count > 0 && range.length == self.textString.count{
textString = ""
updateTextView()
}
let tracker = (textString as NSString)
if range.location == tracker.length{
let newRange = NSMakeRange(tracker.length, range.length)
let newString = tracker.replacingCharacters(in: newRange, with: text)
textString = newString
updateTextView()
}else if range.location < tracker.length{
if text.isEmpty && range.length == 0 && range.location > 0{
let newString = tracker.replacingCharacters(in:range, with: text)
textString = newString
updateTextView()
self.textView.selectedRange = NSMakeRange(range.upperBound, 0)
}else{
let newString = tracker.replacingCharacters(in: range, with: text)
textString = newString
updateTextView()
self.textView.selectedRange = NSMakeRange(range.upperBound - tracker.length + (newString as NSString).length , 0)
}
}
hack_shouldIgnorePredictiveInput = false
return false
}
}

Resources