clearlight
clearlight

Reputation: 12615

Can't get CALayer or UIView from SwiftSVG to render to UIImage with UIGraphicsImageRenderer()

I'm trying to render a CALayer from SwiftSVG package (on github) as a UIImage. It isn't working.

Source .svg file defines a viewBox 1169 x 1000.

If I add CALayer to UIView and add UIView (not UIImage) to view hierarchy, the SVG graphics are perfect, so CALayer is fine.

I added renderer.cgContext based drawing commands in the closure, to draw an outline around the rendering area. It worked. The resulting UIImage had an outline drawn, proving the image renderer block itself works. Thus...

svgLayer.render() does not render visible graphics in the produced image.

svgView.drawHierarchy() doesn't work either (for example if I add the SVG layer as a sublayer of a view and try to render the view instead).


WORKS AND PRODUCES VALID CALAYER WITH PERFECT SVG GRAPHICS:
    let viewBox : CGRect? = CGRectMake(0, 0, 116.9, 100)
    let filename =  (fieldKey + ".svg").lowercased()
    let svgURL = gbl_svgDataUrl.appendingPathComponent(filename)

    let svgLayer = CALayer(SVGURL: svgURL) { (svgLayer) in
        svgLayer.fillColor = color.cgColor
        if let viewBox {
            svgLayer.resizeToFit(viewBox)
        }
    }

CAN GET DRAWN GRAPHICS INTO IMAGE BUT NOT RENDERED CALAYER OR VIEW:

    let format = UIGraphicsImageRendererFormat.preferred()
    format.opaque = false
    let image = UIGraphicsImageRenderer(size: viewBox.size, format: format).image { renderer in
        svgLayer.render(in: renderer.cgContext)  // <-- DOESN'T WORK

        //svgView.drawHierarchy(in: viewBox,     // <-- DOESN'T WORK
        //   afterScreenUpdates: false)           
    }

Upvotes: 0

Views: 347

Answers (1)

clearlight
clearlight

Reputation: 12615

I figured out a resolution. Tricky situation, so I'm answering my own question so this may come up in searches for related terms and help someone in the future

  1. @Sweeper's comment to the question right, when trying to create a CALayer with SwiftSVG CALayer(SVGURL), it is async, and the layer can only be added to the view inside the closure. What confused me is that's different than how SwiftSVG docs (at github project main page) shows the UIView(SVGURL:) API, which does complete synchronously, and the view is ready when the main call completes. Therefore, I switched from using CALayer(SVGURL:) to UIView(SVGURL:) to avoid async problems. And that necessitated to using view.drawHierarchy() instead of layer.render()

  2. Then I hit a Catch-22 with this line:

    svgView.drawHierarchy(in: viewBox, afterScreenUpdates: ??)
    
  3. If I set afterScreenUpdates:false, I got this warning in the console:

"Rendering a view (0x117405440, UIView) that has not been rendered at least once requires afterScreenUpdates:YES."
  1. But if I set afterScreenUpdates:true, I got this error:
"View (0x10a159e00, UIView) drawing with afterScreenUpdates:YES inside CoreAnimation commit is not supported."

And with either error the SVG View would not render into the image.

I tried forcing the view to display with setNeedsLayout() and layoutIfNeeded() before image rendering, which didn't work. Neither did moving code into viewDidLayoutSubviews().

Solution: afterScreenUpdates:true and add a delay to avoid the 2nd error.

let viewBox = CGRectMake(0, 0, 116.9, 100)
let filename =  (fieldKey + ".svg").lowercased()
let svgURL = gbl_svgDataUrl.appendingPathComponent(filename)

let svgView = UIView(SVGURL: svgURL) { (svgLayer) in
    svgLayer.fillColor = color.cgColor
    svgLayer.resizeToFit(viewBox)
}

let format = UIGraphicsImageRendererFormat.preferred()
format.opaque = false
var image: UIImage?

// Add a slight delay to ensure the view has been rendered at least once

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {

    image = UIGraphicsImageRenderer(size: viewBox.size, format: format).image { renderer in
        svgView.drawHierarchy(in: viewBox, afterScreenUpdates: true)
    }

    if let image = image {

        let imageView = UIImageView(image: image)
        imageView.frame = viewBox
        imageView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(imageView)

        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
            imageView.heightAnchor.constraint(equalToConstant: viewBox.size.height),
            imageView.widthAnchor.constraint(equalToConstant: viewBox.size.width),
        ])
    }
}

Upvotes: 0

Related Questions