[Swift]UITextView简单的图文混排demo:插入图片、查看图片、富文本持久化

2,252 阅读3分钟

背景需求

**1、图文混排:**基于textView,实现简单图文混排(插入图片)。

2、富文本持久化:为了能够在app退出重新进入后能够恢复之前的图文信息,要求app能够持久化textView.attributedText的所有内容。

**3、点击textView的图片查看详情。**类似微信,点击某图片进入该图片的详细查看界面(这个我直接用了第三方库JXPhotoBrowser来完成)

我为此做了一个小demo,仓库地址:github.com/Norwa9/UITe…

一、图文混排:textView插入图片

从系统相册选取照片插入富文本中去。

extension ViewController:UIImagePickerControllerDelegate,UINavigationControllerDelegate{
    func importPicture() {
        let picker = UIImagePickerController()
        picker.delegate = self
        present(picker, animated: true)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        guard let image = info[.originalImage] as? UIImage else { return }
        insertPictureToTextView(image: image)
        dismiss(animated: true)
    }
    
    func insertPictureToTextView(image:UIImage){
        //创建附件
        let attachment = NSTextAttachment()
        //设置附件的大小
        let imageAspectRatio = image.size.height / image.size.width
        let peddingX:CGFloat =  0
        let imageWidth = textView.frame.width - 2 * peddingX
        let imageHeight = imageWidth * imageAspectRatio
        attachment.image = UIImage(data: image.jpegData(compressionQuality: 0.5)!)
        attachment.bounds = CGRect(x: 0, y: 0,
                                   width: imageWidth,
                                   height: imageHeight)
        //将附件转成NSAttributedString类型的属性化文本
        let attImage = NSAttributedString(attachment: attachment)
        //获取textView的所有文本,转成可变的文本
        let mutableStr = NSMutableAttributedString(attributedString: textView.attributedText)
        //获得目前光标的位置
        let selectedRange = textView.selectedRange
        //插入附件
        mutableStr.insert(attImage, at: selectedRange.location)
        mutableStr.insert(NSAttributedString(string: "\n"), at: selectedRange.location+1)//插入图片后另起一行
        textView.attributedText = mutableStr
    }
}

二、持久化textView的富文本

1、实现存储富文本到用户目录以持久化

2、实现从用户目录读取富文本以恢复数据

注意:读取的富文本需要预处理

extension ViewController{
    //MARK:-存储富文本到用户目录
    func saveAttributedString(id_string:String,aString:NSAttributedString?) {
        do {
            let file = try aString?.fileWrapper (
                from: NSMakeRange(0, aString!.length),
                documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd])
            
            if let dir = FileManager.default.urls (for: .documentDirectory, in: .userDomainMask) .first {
                let path_file_name = dir.appendingPathComponent (id_string)
                do {
                    try file!.write (to: path_file_name, options: .atomic, originalContentsURL: nil)
                } catch {
                    // Error handling
                }
            }
        } catch {
            //Error handling
        }
        
    }


    //MARK:-从用户目录读取富文本
    func loadAttributedString(id_string:String) -> NSAttributedString?{
        if let dir = FileManager.default.urls (for: .documentDirectory, in: .userDomainMask) .first {
            let path_file_name = dir.appendingPathComponent (id_string)
            do{
                let aString = try NSAttributedString(
                    url: path_file_name,
                    options: [.documentType:NSAttributedString.DocumentType.rtfd],
                    documentAttributes: nil)
                return aString
            }catch{
                //
            }
        }
        return nil
    }
    
    //MARK:-预处理富文本,让富文本图片正常地显示
    //prepareTextImages()用于处理从用户目录读出的富文本
    //因为在我之前的测试里,不知道为什么图片读取出来后大小显示异常,于是在网上找了一圈得到这个解决方案
    //就是在传递富文本给textView前,遍历富文本里所有的图片附件,重新设置图片附件的bounds。
    private func prepareTextImages(aString:NSAttributedString) -> NSMutableAttributedString {
        let mutableText = NSMutableAttributedString(attributedString: aString)
        let width  = self.textView.frame.width
        mutableText.enumerateAttribute(NSAttributedString.Key.attachment, in: NSRange(location: 0, length: mutableText.length), options: [], using: { [width] (object, range, pointer) in
            let textViewAsAny: Any = self.textView!
            if let attachment = object as? NSTextAttachment, let img = attachment.image(forBounds: self.textView.bounds, textContainer: textViewAsAny as? NSTextContainer, characterIndex: range.location){
                let aspect = img.size.width / img.size.height
                if img.size.width <= width {
                    attachment.bounds = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
                    return
                }
                let height = width / aspect
                attachment.bounds = CGRect(x: 0, y: 0, width: width, height: height)
            }
            })
        return mutableText
    }
}

三、点击图片进入详情

该功能的核心思路来自stckOverFlow的回答:stackoverflow.com/questions/4…

extension ViewController{
    @objc func tapOnImage(_ sender: UITapGestureRecognizer){
        guard let textView = sender.view as? UITextView else{
            return
        }
        let layoutManager = textView.layoutManager
        var location = sender.location(in: textView)
        location.x -= textView.textContainerInset.left
        location.y -= textView.textContainerInset.top
        
        //推算触摸点处的字符下标
        let characterIndex = layoutManager.characterIndex(
            for: location,
            in: textView.textContainer,
            fractionOfDistanceBetweenInsertionPoints: nil)
        
        if characterIndex < textView.textStorage.length{
            //识别字符下标characterIndex处的富文本信息
            let attachment = textView.attributedText.attribute(NSAttributedString.Key.attachment,
                                                               at: characterIndex,
                                                               effectiveRange: nil) as? NSTextAttachment
            //1.字符下标characterIndex处为图片附件,则展示它
            if let attachment = attachment{
                textView.resignFirstResponder()
                //获取image
                guard let attachImage = attachment.image(forBounds: textView.bounds, textContainer: textView.textContainer, characterIndex: characterIndex)else{
                    print("无法获取image")
                    return
                }
                //展示image
                let browser = JXPhotoBrowser()
                browser.numberOfItems = { 1 }
                browser.reloadCellAtIndex = { context in
                    let browserCell = context.cell as? JXPhotoBrowserImageCell
                    browserCell?.imageView.image = attachImage
                }
                browser.show()
            //2.字符下标characterIndex处为字符,则将光标移到触摸的字符下标
            }else{
                textView.becomeFirstResponder()
                textView.selectedRange = NSMakeRange(characterIndex+1, 0)
            }
        }
    }
}

最后上一下viewDidLoad():

override func viewDidLoad() {
    super.viewDidLoad()
    
    //添加点击手势
    let tap = UITapGestureRecognizer(target: self, action: #selector(tapOnImage(_:)))
    textView.addGestureRecognizer(tap)
    
    //从用户目录读取之前保存的rtfd(带有附件的富文本文件)
    loadAttributedText()
}