iOS TextView is saved blurry when scaled

343 views Asked by At

I tried save textview as image with not device scale. I implemented a method to save an image by adding an arbitrary textview according to the UI value. Because when I tried save image using drawHierarchy method in up scale, image was blurry.

Condition when textview is saved blurry

  • not device scale (up scale)
  • 1-1. isScrollEnabled = false and height of textview is more than 128.
  • 1-2. isScrollEnabled = true (always text is blurry)

here is my code

func drawQuoteImage() {
    var campusSize = view.frame.size 
    var scale = UIScreen.main.scale + 2 

    // 1. Create View 
    let quoteView = UIView(frame: CGRect(x: 0, y: 0, width: campusSize.width, height: campusSize.height))
    let textview = UITextView()
    textview.attributedText = NSAttributedString(string: quoteLabel.text, attributes: textAttributes as [NSAttributedString.Key : Any])
    textview.frame = transfromFrame(originalFrame: quoteLabel.frame, campusSize: campusSize)
    quoteView.addSubview(textview)

    // 2. Render image
    UIGraphicsBeginImageContextWithOptions(quoteView.frame.size, false, scale)
    let context = UIGraphicsGetCurrentContext()!
    context.setRenderingIntent(.relativeColorimetric)
    context.interpolationQuality = .high
    quoteView.drawHierarchy(in: quoteView.frame, afterScreenUpdates: true)
    quoteView.layer.render(in: context)
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    quoteImage = image 
}

private func transfromFrame(originalFrame: CGRect, campusSize: CGSize) -> CGRect
{
    if UIDevice.current.screenType == .iPhones_X_XS {
        return CGRect(x: round(originalFrame.origin.x), y: round(originalFrame.origin.y), width: round(originalFrame.width), height: round(originalFrame.height))
    }
    else {
        var frame = CGRect()
        let ratioBasedOnWidth = campusSize.width / editView.frame.width
        let ratioBasedOnHeight = campusSize.height / editView.frame.height
        frame.size.width = round(originalFrame.width * ratioBasedOnWidth)
        frame.size.height = round(originalFrame.height * ratioBasedOnHeight)
        frame.origin.x = round(originalFrame.origin.x * ratioBasedOnWidth)
        frame.origin.y = round(originalFrame.origin.y * ratioBasedOnHeight)
        return frame
    }
}

Wired Point

when height of textview is more than 128, textview is save blurry. I found related value when I put textview default height is 128.

enter image description here

Height is 128 or less (when isScrollEnabled is false), textview is saved always clear. But when height is more than 128, it looks blurry.

Height 128

enter image description here

Height 129

enter image description here

I'd like to know how to clearly draw image with textview at @5x scale. (textview height is bigger than 128)

1

There are 1 answers

1
DonMag On BEST ANSWER

Here's a quick example using a UIView extension from this accepted answer: https://stackoverflow.com/a/51944513/6257435

We'll create a UITextView with a size of 240 x 129. Then add 4 buttons to capture the text view at 1x, 2x, 5x and 10x scale.

It looks like this when running:

enter image description here

and the result...

At 1x scale - 240 x 129 pixels:

enter image description here

At 2x scale - 480 x 258 pixels:

enter image description here

At 5x scale - 1200 x 645 pixels (just showing a portion):

enter image description here

At 10x scale - 2400 x 1290 pixels (just showing a portion):

enter image description here

The extension:

extension UIView {
    func scale(by scale: CGFloat) {
        self.contentScaleFactor = scale
        for subview in self.subviews {
            subview.scale(by: scale)
        }
    }
    
    func getImage(scale: CGFloat? = nil) -> UIImage {
        let newScale = scale ?? UIScreen.main.scale
        self.scale(by: newScale)
        
        let format = UIGraphicsImageRendererFormat()
        format.scale = newScale
        
        let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
        
        let image = renderer.image { rendererContext in
            self.layer.render(in: rendererContext.cgContext)
        }
        
        return image
    }
}

Sample controller code:

class TextViewCapVC: UIViewController {
    let textView = UITextView()
    let resultLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // add a stack view with buttons
        let stack = UIStackView()
        stack.axis = .vertical
        stack.spacing = 12
        
        [1, 2, 5, 10].forEach { i in
            let btn = UIButton()
            btn.setTitle("Create Image at \(i)x scale", for: [])
            btn.setTitleColor(.white, for: .normal)
            btn.setTitleColor(.lightGray, for: .highlighted)
            btn.backgroundColor = .systemBlue
            btn.tag = i
            btn.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
            stack.addArrangedSubview(btn)
        }
        
        [textView, stack, resultLabel].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // text view 280x240, 20-points from top, centered horizontally
            textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            textView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            textView.widthAnchor.constraint(equalToConstant: 240.0),
            textView.heightAnchor.constraint(equalToConstant: 129.0),
            
            // stack view, 20-points from text view, same width, centered horizontally
            stack.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20.0),
            stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            stack.widthAnchor.constraint(equalTo: textView.widthAnchor),
            
            // result label, 20-points from stack view
            //  20-points from leading/trailing
            resultLabel.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
            resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
        ])
        
        let string = "Test"
        
        let attributes: [NSAttributedString.Key: Any] = [
            .foregroundColor: UIColor.blue,
            .font: UIFont.italicSystemFont(ofSize: 104.0),
        ]
        
        let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
        textView.attributedText = attributedString
        
        resultLabel.font = .systemFont(ofSize: 14, weight: .light)
        resultLabel.numberOfLines = 0
        resultLabel.text = "Results:"
        
        // so we can see the view frames
        textView.backgroundColor = .yellow
        resultLabel.backgroundColor = .cyan
        
    }
    
    @objc func gotTap(_ sender: Any?) {
        guard let btn = sender as? UIButton else { return }
        
        let scaleFactor = CGFloat(btn.tag)
        
        let img = textView.getImage(scale: scaleFactor)
        
        var s: String = "Results:\n\n"
        
        let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fName: String = "\(btn.tag)xScale-\(img.size.width * img.scale)x\(img.size.height * img.scale).png"
        let url = documents.appendingPathComponent(fName)
        if let data = img.pngData() {
            do {
                try data.write(to: url)
            } catch {
                s += "Unable to Write Image Data to Disk"
                resultLabel.text = s
                return
            }
        } else {
            s += "Could not get png data"
            resultLabel.text = s
            return
        }
        s += "Logical Size: \(img.size)\n\n"
        s += "Scale: \(img.scale)\n\n"
        s += "Pixel Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n\n"
        s += "File \"\(fName)\"\n\nsaved to Documents folder\n"
        resultLabel.text = s
        
        // print the path to documents in debug console
        //  so we can copy/paste into Finder to get to the files
        print(documents.path)
    }

}