0

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

sheko
  • 516
  • 4
  • 15

1 Answers1

0

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 NSAttributedStringCTFrame & 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

dengApro
  • 3,848
  • 2
  • 27
  • 41