Search code examples
swiftuiwebviewcashapelayerzposition

Is it Possible to Interleave WebView and CALayerViews in Swift


I have a view that acts as a container for multiple CAShapeLayers. These contain bezier that form a composition on the screen that the user can manage in different ways to do with line weight, colour, shape fill, opacity etc.

I want to introduce text via a WebView that will occupy the same container. The trick is that ideally I want to be able to control the zPosition in the container of each element, relative to the others.

Can a WebView be in a container in a way that it shares zPosition range with CAShapeLayers in that container?


Solution

  • Couple options...

    1. Use html Canvas and draw shapes in the page content itself.
    2. Create a clear web view and draw shapes in views under / on-top-of the web view.
    3. Add sublayers to the web view's layer (will still need to implement clear html to put layers under the html content).

    Here's a quick example of Option 3:

    class ViewController: UIViewController {
        
        var webView: WKWebView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            webView = WKWebView()
            webView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(webView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                webView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                webView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                webView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                webView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            ])
            
            // this will allow us to see the shape layers
            //  if we put them *behind* the html content
            webView.isOpaque = false
            webView.backgroundColor = .clear
            webView.scrollView.backgroundColor = .clear
            
            // webView will be clear, so let's give it a border
            // so we can see the frame
            webView.layer.borderWidth = 1
            webView.layer.borderColor = UIColor.black.cgColor
            
            webView.loadHTMLString(html, baseURL: nil)
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            // we'll create 4 CAShapeLayers
            //  with oval paths
            //  each 1/4 the height of the webView
            //  overlapping a little
            var y: CGFloat = 12.0
            let yInc: CGFloat = (webView.bounds.height - 32.0) * 0.25
            var pathRect: CGRect = webView.bounds.insetBy(dx: 20.0, dy: 0.0)
            pathRect.size.height *= 0.25
            
            let colors: [UIColor] = [
                .systemRed, .systemGreen, .systemBlue, .cyan
            ]
            
            for (i, c) in colors.enumerated() {
                
                // oval CAShapeLayer with
                //  solid color thick stroke
                //  50% transparent color fill
                let cLayer = CAShapeLayer()
                cLayer.lineWidth = 8
                cLayer.strokeColor = c.cgColor
                cLayer.fillColor = c.withAlphaComponent(0.5).cgColor
                cLayer.path = UIBezierPath(ovalIn: pathRect.offsetBy(dx: 0.0, dy: y)).cgPath
                
                // add shape layers to the webView's layer
                //  shape layers will NOT scroll
                var targetLayer = webView.layer
                
                // or, add shape layers to the webView's scrollView's layer
                //  shape layers WILL scroll
                //targetLayer = webView.scrollView.layer
                
                if i < 2 {
                    // insert the first two layers *behind* the web page content
                    targetLayer.insertSublayer(cLayer, at: UInt32(i))
                } else {
                    // add the second two layer *on top of* the web page content
                    targetLayer.addSublayer(cLayer)
                }
                
                y += yInc
            }
            
        }
        
        let html: String =
    """
    <html>
    <head>
    <style type=\"text/css\">
    body{
     font-size: 100px;
     font-family:verdana;
     text-align: center;
     color: #ffdd00;
    }
    </style>
    </head>
    <body>
    <p>This is some web page body text in a WKWebView.</p>
    <p>Red and Green shape layers are <i><b>behind</b></i> the html content.</p>
    <p>Blue and Cyan shape layers are <i><b>on top of</b></i> the html content.</p>
    <p>Let's add enough text to exceed the height of the web view so we can see what happens when we scroll.</p>
    </body>
    </html>
    """
        
    }
    

    Looks like this:

    enter image description here enter image description here