CAGradientLayer, not resizing nicely, tearing on rotation. (video of issue attached)

I’m trying to get my CAGradientLayers, that i’m using to create nice gradient backgrounds, to resize nicely on rotation and modal view presentation, but they will not play ball.

Here is a video I just created showing my problem: Notice the tearing on rotation.

  • how to custom back button in storyboard
  • Attempt to dismiss from view controller while a presentation or dismiss is in progress
  • Is it possible for a UIViewController to present itself?
  • UIKeyboard avoidance and Auto Layout
  • Programmatically changing View Controllers without Navigation Controller Swift
  • Storyboard and custom init
  • Also please note this video was created by filming the iPhone Simulator on OS X. I have slowed down the animations in the video to highlight my issue.

    Video of Problem…

    Here is an Xcode project which I just created (which is the source for the app shown in the video), basically as illustrated the issue occurs on rotation and especially when views are presented modally:

    Xcode Project, modally presenting views with CAGradientLayer backgrounds…

    For what it’s worth I understand that using:

        [[self view] setBackgroundColor:[UIColor blackColor]];
    

    does a reasonable job of making the transitions a bit more seamless and less jarring, but if you look at the video when I, whilst currently in landscape mode, modally present a view, you will see why the above code will not help.

    Any ideas what I can do to sort this out?

    John

    8 Solutions Collect From Internet About “CAGradientLayer, not resizing nicely, tearing on rotation. (video of issue attached)”

    When you create a layer (like your gradient layer), there’s no view managing the layer (even when you add it as a sublayer of some view’s layer). A standalone layer like this doesn’t participate in the UIView animation system.

    So when you update the frame of the gradient layer, the layer animates the change with its own default animation parameters. (This is called “implicit animation”.) These default parameters don’t match the animation parameters used for interface rotation, so you get a weird result.

    I didn’t look at your project but it’s trivial to reproduce your problem with this code:

    @interface ViewController ()
    
    @property (nonatomic, strong) CAGradientLayer *gradientLayer;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.gradientLayer = [CAGradientLayer layer];
        self.gradientLayer.colors = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
        [self.view.layer addSublayer:self.gradientLayer];
    }
    
    - (void)viewDidLayoutSubviews {
        [super viewDidLayoutSubviews];
        self.gradientLayer.frame = self.view.bounds;
    }
    
    @end
    

    Here’s what that looks like, with slow motion enabled in the simulator:

    bad rotation

    Fortunately, this is an easy problem to fix. You need to make your gradient layer be managed by a view. You do that by creating a UIView subclass that uses a CAGradientLayer as its layer. The code is tiny:

    // GradientView.h
    
    @interface GradientView : UIView
    
    @property (nonatomic, strong, readonly) CAGradientLayer *layer;
    
    @end
    
    // GradientView.m
    
    @implementation GradientView
    
    @dynamic layer;
    
    + (Class)layerClass {
        return [CAGradientLayer class];
    }
    
    @end
    

    Then you need to change your code to use GradientView instead of CAGradientLayer. Since you’re using a view now instead of a layer, you can set the autoresizing mask to keep the gradient sized to its superview, so you don’t have to do anything later to handle rotation:

    @interface ViewController ()
    
    @property (nonatomic, strong) GradientView *gradientView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.gradientView = [[GradientView alloc] initWithFrame:self.view.bounds];
        self.gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        self.gradientView.layer.colors = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
        [self.view addSubview:self.gradientView];
    }
    
    @end
    

    Here’s the result:

    good rotation

    The best part about @rob’s answer is that the view controls the layer for you. Here is the Swift code that properly overrides the layer class and sets the gradient.

    import UIKit
    
    class GradientView: UIView {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupView()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupView()
        }
    
        private func setupView() {
            autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    
            guard let theLayer = self.layer as? CAGradientLayer else {
                return;
            }
    
            theLayer.colors = [UIColor.whiteColor().CGColor, UIColor.lightGrayColor().CGColor]
            theLayer.locations = [0.0, 1.0]
            theLayer.frame = self.bounds
        }
    
        override class func layerClass() -> AnyClass {
            return CAGradientLayer.self
        }
    }
    

    You can then add the view in two lines wherever you want.

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let gradientView = GradientView(frame: self.view.bounds)
        self.view.insertSubview(gradientView, atIndex: 0)
    }
    

    My swift version:

    import UIKit
    
    class GradientView: UIView {
    
        override class func layerClass() -> AnyClass {
            return CAGradientLayer.self
        }
    
        func gradientWithColors(firstColor : UIColor, _ secondColor : UIColor) {
    
            let deviceScale = UIScreen.mainScreen().scale
            let gradientLayer = CAGradientLayer()
            gradientLayer.frame = CGRectMake(0.0, 0.0, self.frame.size.width * deviceScale, self.frame.size.height * deviceScale)
            gradientLayer.colors = [ firstColor.CGColor, secondColor.CGColor ]
    
            self.layer.insertSublayer(gradientLayer, atIndex: 0)
        }
    }
    

    Note that I also had to use the device scale to calculate the frame size – to get correct auto-sizing during orientation changes (with auto-layout).

    1. In Interface Builder, I added a UIView and changed its class to GradientView (the class shown above).
    2. I then created an outlet for it (myGradientView).
    3. Finally, In the view controller I added:

      override func viewDidLayoutSubviews() {
          self.myGradientView.gradientWithColors(UIColor.whiteColor(), UIColor.blueColor())
      }
      

    Note that the gradient view is created in a “layoutSubviews” method, since we need a finalized frame to create the gradient layer.

    It will look better when you insert this piece of code and remove the willAnimateRotationToInterfaceOrientation:duration: implementation.

    - (void)viewWillLayoutSubviews
    {
        [[[self.view.layer sublayers] objectAtIndex:0] setFrame:self.view.bounds];    
    }
    

    This is however not very elegant. In a real application you should subclass UIView to create a gradient view. In this custom view you can override layerClass so that it is backed by a gradient layer:

    + (Class)layerClass
    {
      return [CAGradientLayer class];
    }
    

    Also implement layoutSubviews to handle when the bounds of the view changes.

    When creating this background view use autoresizing masks so that the bounds automatically adjust on interface rotations.

    Complete Swift version. Set viewFrame from the viewController that owns this view in viewDidLayoutSubviews

    import UIKit
    
    class MainView: UIView {
    
        let topColor = UIColor(red: 146.0/255.0, green: 141.0/255.0, blue: 171.0/255.0, alpha: 1.0).CGColor
        let bottomColor = UIColor(red: 31.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).CGColor
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupGradient()
        }
    
        override class func layerClass() -> AnyClass {
            return CAGradientLayer.self
        }
    
        var gradientLayer: CAGradientLayer {
            return layer as! CAGradientLayer
        }
    
        var viewFrame: CGRect! {
            didSet {
                self.bounds = viewFrame
            }
        }
    
        private func setupGradient() {
            gradientLayer.colors = [topColor, bottomColor]
        }
    }
    

    Personally, I prefer to keep everything self-contained within the view subclass.

    Here’s my Swift implementation:

                import UIKit
    
                @IBDesignable
                class GradientBackdropView: UIView {
    
                    @IBInspectable var startColor: UIColor=UIColor.whiteColor()
                    @IBInspectable var endColor: UIColor=UIColor.whiteColor()
                    @IBInspectable var intermediateColor: UIColor=UIColor.whiteColor()
    
                    var gradientLayer: CAGradientLayer?
    
                    // Only override drawRect: if you perform custom drawing.
                    // An empty implementation adversely affects performance during animation.
                    override func drawRect(rect: CGRect) {
                        // Drawing code
                        super.drawRect(rect)
    
                        if gradientLayer == nil {
                            self.addGradientLayer(rect: rect)
                        } else {
                            gradientLayer?.removeFromSuperlayer()
                            gradientLayer=nil
                            self.addGradientLayer(rect: rect)
                        }
                    }
    
    
                    override func layoutSubviews() {
                        super.layoutSubviews()
    
                        if gradientLayer == nil {
                            self.addGradientLayer(rect: self.bounds)
                        } else {
                            gradientLayer?.removeFromSuperlayer()
                            gradientLayer=nil
                            self.addGradientLayer(rect: self.bounds)
                        }
                    }
    
    
                    func addGradientLayer(rect rect:CGRect) {
                        gradientLayer=CAGradientLayer()
    
                        gradientLayer?.frame=self.bounds
    
                        gradientLayer?.colors=[startColor.CGColor,intermediateColor.CGColor,endColor.CGColor]
    
                        gradientLayer?.startPoint=CGPointMake(0.0, 1.0)
                        gradientLayer?.endPoint=CGPointMake(0.0, 0.0)
    
                        gradientLayer?.locations=[NSNumber(float: 0.1),NSNumber(float: 0.5),NSNumber(float: 1.0)]
    
                        self.layer.insertSublayer(gradientLayer!, atIndex: 0)
    
                        gradientLayer?.transform=self.layer.transform
                    }
                }
    

    Another swift version – which is not using drawRect.

    class UIGradientView: UIView {
        override class func layerClass() -> AnyClass {
            return CAGradientLayer.self
        }
    
        var gradientLayer: CAGradientLayer {
            return layer as! CAGradientLayer
        }
    
        func setGradientBackground(colors: [UIColor], startPoint: CGPoint = CGPoint(x: 0.5, y: 0), endPoint: CGPoint = CGPoint(x: 0.5, y: 1)) {
            gradientLayer.startPoint = startPoint
            gradientLayer.endPoint = endPoint
            gradientLayer.colors = colors.map({ (color) -> CGColor in return color.CGColor })
        }
    }
    

    In controller I just call:

    gradientView.setGradientBackground([UIColor.grayColor(), UIColor.whiteColor()])
    

    Info

    • Use as one line solution
    • Replacing gradient when you add it to the view again (to use in reusables)
    • Automatically transiting
    • Automatically removing

    Details

    Swift 3.1, xCode 8.3.3

    Solution

    import UIKit
    
    extension UIView {
    
        func addGradient(colors: [UIColor], locations: [NSNumber]) {
            addSubview(ViewWithGradient(addTo: self, colors: colors, locations: locations))
        }
    }
    
    class ViewWithGradient: UIView {
    
        private var gradient = CAGradientLayer()
    
        init(addTo parentView: UIView, colors: [UIColor], locations: [NSNumber]){
    
            super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 2))
            restorationIdentifier = "__ViewWithGradient"
    
            for subView in parentView.subviews {
                if let subView = subView as? ViewWithGradient {
                    if subView.restorationIdentifier == restorationIdentifier {
                        subView.removeFromSuperview()
                        break
                    }
                }
            }
    
            let cgColors = colors.map { (color) -> CGColor in
                return color.cgColor
            }
    
            gradient.frame = parentView.frame
            gradient.colors = cgColors
            gradient.locations = locations
            backgroundColor = .clear
    
            parentView.addSubview(self)
            parentView.layer.insertSublayer(gradient, at: 0)
            parentView.backgroundColor = .clear
            autoresizingMask = [.flexibleWidth, .flexibleHeight]
    
            clipsToBounds = true
            parentView.layer.masksToBounds = true
    
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            if let parentView = superview {
                gradient.frame = parentView.bounds
            }
        }
    
        override func removeFromSuperview() {
            super.removeFromSuperview()
            gradient.removeFromSuperlayer()
        }
    }
    

    Usage

    viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
    

    Using StoryBoard

    ViewController

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var viewWithGradient: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
        }
    }
    

    StoryBoard

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
        <device id="retina4_7" orientation="portrait">
            <adaptation id="fullscreen"/>
        </device>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
            <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <scenes>
            <!--View Controller-->
            <scene sceneID="tne-QT-ifu">
                <objects>
                    <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_17555986" customModuleProvider="target" sceneMemberID="viewController">
                        <layoutGuides>
                            <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                            <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                        </layoutGuides>
                        <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                            <subviews>
                                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uii-31-sl9">
                                    <rect key="frame" x="66" y="70" width="243" height="547"/>
                                    <color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
                                </view>
                            </subviews>
                            <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                            <constraints>
                                <constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uii-31-sl9" secondAttribute="bottom" constant="50" id="a7J-Hq-IIq"/>
                                <constraint firstAttribute="trailingMargin" secondItem="uii-31-sl9" secondAttribute="trailing" constant="50" id="i9v-hq-4tD"/>
                                <constraint firstItem="uii-31-sl9" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="50" id="wlO-83-8FY"/>
                                <constraint firstItem="uii-31-sl9" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="50" id="zb6-EH-j6p"/>
                            </constraints>
                        </view>
                        <connections>
                            <outlet property="viewWithGradient" destination="uii-31-sl9" id="FWB-7A-MaH"/>
                        </connections>
                    </viewController>
                    <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
                </objects>
            </scene>
        </scenes>
    </document>
    

    Programmatically

    import UIKit
    
    class ViewController2: UIViewController {
    
        @IBOutlet weak var viewWithGradient: UIView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let viewWithGradient = UIView(frame: CGRect(x: 10, y: 20, width: 30, height: 40))
            view.addSubview(viewWithGradient)
    
    
            viewWithGradient.translatesAutoresizingMaskIntoConstraints = false
            let constant:CGFloat = 50.0
    
            NSLayoutConstraint(item: viewWithGradient, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: constant).isActive = true
            NSLayoutConstraint(item: viewWithGradient, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin
                        , multiplier: 1.0, constant: -1*constant).isActive = true
            NSLayoutConstraint(item: viewWithGradient, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin
                , multiplier: 1.0, constant: -1*constant).isActive = true
            NSLayoutConstraint(item: viewWithGradient, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin
                , multiplier: 1.0, constant: constant).isActive = true
    
            viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
        }
    }