5

I would like to blend a UIView with my app's background, using a special blend mode (in my case, the Overlay mode). However, the view to blend is contained in a complex hierarchy of views.

Blending a view with its direct siblings can be achieved using view.layer.compositingFilter = "overlayBlendMode", but the view won't blend with non-siblings views, like the app background.

To recreate the problem, I made the following playground:

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    override func loadView() {
        let parentView = UIView()
        parentView.backgroundColor = .purple

        // Child view
        let childView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        childView.layer.borderColor = UIColor.orange.cgColor
        childView.layer.borderWidth = 3
        parentView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
        childChildView.backgroundColor = .white
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childView.addSubview(childChildView)

        self.view = parentView
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

We can see here that the child child view, in white, is not blended:

Result

Whereas the view should appear blended like this (the border should not change color):

What it should look like

To create the second picture, I applied the compositing filter on the childView instead of the childChildView, which will blend all the other subviews — therefore it's not what I want. I just want this specific view to be blended.

Note: this view is supposed to move, because it's inside a UIScrollView.

EDIT: More complex example with image background and scrollviews

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {

    override func loadView() {
        let parentView = UIView()

        // Background image
        let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
        backgroundImageView.frame = UIScreen.main.bounds
        parentView.addSubview(backgroundImageView)

        // Page view (horizontal scrollview)
        let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        pageView.contentSize = CGSize(width: 600, height: 200)
        pageView.flashScrollIndicators()
        pageView.layer.borderColor = UIColor.orange.cgColor
        pageView.layer.borderWidth = 3
        parentView.addSubview(pageView)

        // Child view (vertical scrollview)
        let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
        childView.contentSize = CGSize(width: 100, height: 300)
        childView.layer.borderColor = UIColor.green.cgColor
        childView.layer.borderWidth = 3
        pageView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
        childChildView.backgroundColor = .white
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childView.addSubview(childChildView)

        self.view = parentView
    }

}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Morpheus
  • 1,189
  • 2
  • 11
  • 33

2 Answers2

1

UPDATE 2:

I've tried several ways including adding layers or creating custom image filters that use the background image as input image but none of these solutions got the desired result. The main problem was always the view hierarchy.

I may have found a solution by using a generated image of the views or the actual background image as the content background of the childView once the childChildView is being created but before being displayed. I've changed your example code a bit to add a scroll view and background image in the parentView. See if this works for you / is your desired result:

    import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {

    override func loadView() {
        let parentView = UIView()
        parentView.backgroundColor = .purple

        let imageName = "image.jpg"
        let image = UIImage(named: imageName)
        let imageWidth = Int((image?.size.width)!)
        let imageheight = Int((image?.size.height)!)
        let imageView = UIImageView(image: image!)
        imageView.frame = CGRect(x: 50, y: 50, width: imageWidth , height: imageheight)
        parentView.addSubview(imageView)

        // Child view as UIScrollView
        let childView = UIScrollView(frame: CGRect(x: 55, y: 55, width: imageWidth - 10, height: imageheight - 10 ))
        childView.contentSize = CGSize(width: imageWidth - 10, height: 5000)
        childView.layer.borderColor = UIColor.orange.cgColor
        childView.flashScrollIndicators()
        childView.layer.borderWidth = 10
        parentView.addSubview(childView)

        // ChildChild view
        let childChildView = UIView(frame: CGRect(x: 15, y: 100, width: 85, height: imageheight - 180))
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childChildView.backgroundColor = .white

        //Creating a static image of the background views BEFORE adding the childChildView.
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        format.preferredRange = .standard ///color profile
        ///Change the imageView to the parentView size of the app. Not available if not set in the playground.
        let renderer = UIGraphicsImageRenderer(size: imageView.bounds.size, format: format)
        let imageBG = renderer.image { context in
            ///This draws all subviews of the parentView one after the other.
            ///Because the background image is not a parent of our current view, otherwise childView.drawHierachy would have been enough
            for subview in parentView.subviews {
                    ///Skip specific views or view classes you don't want to be added to the image. or if you only need the parentView itself rendered remove the for in loop.
                    subview.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
            }
        }
        //Adding the static background image. This could simply also be the actual image: UIImage if no other views are supposed to be used.
        childView.layer.contents = imageBG.cgImage

        childView.addSubview(childChildView)

        self.view = parentView
    }

}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

It results in the following:

enter image description here

UPDATE:

The colors in the images are misleading, as you could assume a normal transparency effect would be the same. But the overlayBlendMode is quite different as Coconuts has pointed out. I assume the issue is that the compositingFilter only works with the view below, even if this view is transparent. I tried finding a workaround by using a mask that cuts out a square of the size of the childchild from the childview. But this also didn't work as the mask is also applied to all subviews. The only way I got it to work is by making the childchildview a sibling of childview instead, or a direct subview of the background view. But not sure if this will be possible in the complex view hierarchy mentioned by Coconuts.

    // Sibling view with adjusted x and y
    let childChildView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
    childChildView.backgroundColor = .white
    childChildView.layer.compositingFilter = "overlayBlendMode"

    parentView.addSubview(childChildView)

MISC:

To only get the visual result of the sample images, not actually using the overlayBlendMode filter as asked by Coconut.

If you only need to blend the color you could change the alpha value of the color.

    // Child child view
    let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
    childChildView.backgroundColor = UIColor(white: 1, alpha: 0.5)
    //childChildView.layer.compositingFilter = "overlayBlendMode"
    childView.addSubview(childChildView)

Or try this:

    // Child child view
    let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
    childChildView.backgroundColor = .white
    childChildView.layer.opacity = 0.5
    childView.addSubview(childChildView)

ADDITIONAL ATTEMPTS WHEN HAVING SEVERAL SCROLL VIEWS:

This is an attempt to solve the from Coconut added more complicated view hierarchy with multiple scroll views. The performance needs to be improved or the part that adjusts the background image of the background image layer needs to run in sync when the app is updating (redrawing) its views. At the moment it's lagging behind a bit.

    import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {

    override func loadView() {

        let parentView = UIView()

        // Background image
        let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
        backgroundImageView.frame = UIScreen.main.bounds
        parentView.addSubview(backgroundImageView)

        // Page view (horizontal scrollview)
        let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        pageView.contentSize = CGSize(width: 600, height: 200)
        pageView.flashScrollIndicators()
        pageView.layer.borderColor = UIColor.yellow.cgColor
        pageView.layer.borderWidth = 3
        pageView.clipsToBounds = true
        parentView.addSubview(pageView)

        // Child view (vertical scrollview)
        let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
        childView.contentSize = CGSize(width: 100, height: 300)
        childView.layer.borderColor = UIColor.red.cgColor
        childView.layer.borderWidth = 3
        pageView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50))

        //Child child view foreground sublayer
        let childChildFrontLayer = CALayer()
        childChildFrontLayer.frame = childChildView.frame.offsetBy(dx: -75, dy: -50)
        childChildFrontLayer.backgroundColor = UIColor.white.cgColor
        childChildFrontLayer.compositingFilter = "overlayBlendMode"

        //Child child view background sublayer
        let childChildBackLayer = CALayer()
        childChildBackLayer.contents = UIImage(named: "image.jpg")?.cgImage
        var absolutFrame = parentView.convert(childChildView.frame, from: childView)
        childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)

        childChildView.layer.addSublayer(childChildBackLayer)
        childChildView.layer.addSublayer(childChildFrontLayer)
        childView.addSubview(childChildView)

        //Checking for any scrolling. Is slightly faster then the scollview delegate methods but might cause main thread checker warning.
        DispatchQueue.global(qos: .userInteractive).async {
            while true {
                if pageView.isDragging || pageView.isTracking || pageView.isDecelerating || childView.isDragging || childView.isTracking || childView.isDecelerating {
                    absolutFrame = parentView.convert(childChildView.frame, from: childView)
                    DispatchQueue.main.async {
                        childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)
                    }
                }
            }
        }
        self.view = parentView
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Marco Boerner
  • 1,243
  • 1
  • 11
  • 34
  • No the whole point is being able to blend using a special blending mode (Overlay in my case) – Morpheus May 19 '20 at 12:36
  • Do I understand that correctly, you want to use the overlay blend mode (which I guess does have a different effect if you not only use a purple background?) but it doesn't seem to be working since the childchildView is basically trying to apply the filter to the childview (even though this view only has a border but no background color?) – Marco Boerner May 19 '20 at 18:01
  • Yes, the childChildView is only blended with its direct siblings, while I want it to be blended with all the views behind it, up to the app background. Regarding your update, it's indeed not possible for me to move the view up in the hierarchy, because it's contained in a scrollview to be scrollable — plus, this would the whole app view hierarchy a huge mess :/ – Morpheus May 20 '20 at 06:55
  • Okay, hopefully this will work for you. Otherwise I'm out of ideas. ;) – Marco Boerner May 20 '20 at 19:11
  • This is a step forward! Still, this raises two points: 1) how do you do when childView is smaller than the image view (when the scrollview does't occupy the whole page), and 2) how do you do when the childView is itself in another scrollview? In my case, the childView equivalent (vertical scrollView) is itself contained in a horizontal scrollView to make "pages". Is there really no way to tell the view to blend with ANY view that is behind it, regardless of the view hierarchy? – Morpheus May 21 '20 at 23:16
  • 1.) Let me check that. A question to number 2.) You mean one or more childViews as scrollViews are inside another view which is a horizontal paging scrollView? --and I was trying hard to find anything that would allow to use the effect on all layers but I couldn't find anything in that regard. I must say Apples documentation to the anything in regards with compositingFilter or CIFilters etc. is very incomplete and partially outdated. – Marco Boerner May 22 '20 at 00:50
  • Yes that's what I meant for 2), I added a more complex code in the question please see above. Maybe it's possible to recompute the `drawHierarchy` at each frame change? – Morpheus May 22 '20 at 05:57
  • I tried the recalculation using the scrollViewDidScroll delegate method but even for the most simple adjustments it's lagging behind very noticeably. I'll see if I can find something else. – Marco Boerner May 23 '20 at 00:16
  • To number 1.) You could solve that with the clipsToBounds = false option of the super view and set the starting coordinates of the view to a negative number. 2.) It's quite difficult, the solution I added is more a workaround but maybe with some adjustments you can make it work. It basically needs to redraw the background in sync with the redrawing of the childchildview. Otherwise, I'd look into using metal or writing custom CI filters. But this is quite the task and beyond my skills at this moment. : ) – Marco Boerner May 23 '20 at 22:37
0

Is the issue related to the child view background being clear and therefore 'blending' to give the white colour. Could set the child view background colour to be equal to the app background and then then just blend the childView within the childChildView?

  • My app background can actually be a gradient or an image, so I don't think I could do something like this :/ – Morpheus May 11 '20 at 15:28