CAGradientLayer, no cambia de tamaño, rasgando la rotation

Estoy intentando get mi CAGradientLayers, que estoy usando para crear agradables backgrounds de degradado, para networkingimensionar bien la rotation y la presentación de la vista modal, pero no jugarán pelota.

Aquí hay un video que acabo de crear que muestra mi problema: Observe el desgarro en la rotation.

También tenga en count que este video fue creado al filmar el iPhone Simulator en OS X. He ralentizado las animaciones en el video para resaltar mi problema.

Video del problema …

Aquí hay un proyecto Xcode que acabo de crear (que es la fuente de la aplicación que se muestra en el video), básicamente, como se ilustra, el problema ocurre en la rotation y, especialmente, cuando las vistas se presentan de manera modal:

Xcode Project, presentación modal de vistas con backgrounds CAGradientLayer …

Por lo que vale, entiendo que usando:

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

hace un trabajo razonable para hacer que las transiciones sean un poco más fluidas y less discordantes, pero si miras el video cuando, mientras estoy en el modo horizontal, presento una vista, verás por qué el código anterior no ayudará.

¿Alguna idea de lo que puedo hacer para solucionarlo?

John

Cuando crea una capa (como su capa de degradado), no hay ninguna vista que administre la capa (incluso cuando la agrega como una subcapa de la capa de alguna vista). Una capa independiente como esta no participa en el sistema de animation UIView .

Entonces, cuando actualiza el marco de la capa de gradiente, la capa anima el cambio con sus propios parameters de animation pnetworkingeterminados. (Esto se denomina "animation implícita"). Estos parameters pnetworkingeterminados no coinciden con los parameters de animation utilizados para la rotation de la interfaz, por lo que obtienes un resultado extraño.

No miré su proyecto, pero es trivial reproducir su problema con este código:

 @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 

Esto es lo que parece, con camera lenta habilitada en el simulador:

mala rotación

Afortunadamente, este es un problema fácil de solucionar. Debes hacer que tu capa de gradiente sea administrada por una vista. Para ello, crea una subclass UIView que utiliza un CAGradientLayer como capa. El codigo es minimo:

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

Luego, necesita cambiar su código para usar GradientView lugar de CAGradientLayer . Dado que está utilizando una vista ahora en lugar de una capa, puede configurar la máscara de autorrealización para mantener el tamaño del gradiente a su supervisión, por lo que no tiene que hacer nada más tarde para manejar la 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 

Este es el resultado:

buena rotación

La mejor parte de la respuesta de @ rob es que la vista controla la capa por ti. Aquí está el código Swift que anula apropiadamente la class de capa y establece el gradiente.

 import UIKit class GradientView: UIView { override init(frame: CGRect) { super.init(frame: frame) setupView() } requinetworking 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 } } 

A continuación, puede agregar la vista en dos líneas donde desee.

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

Mi versión rápida:

 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) } } 

Tenga en count que también tuve que usar la escala del dispositivo para calcular el tamaño del cuadro: para get el tamaño automático correcto durante los cambios de orientación (con layout automático).

  1. En Interface Builder, agregué una UIView y cambié su class a GradientView (la class que se muestra arriba).
  2. Luego creé una salida para ello (myGradientView).
  3. Finalmente, en el controller de vista, agregué:

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

Tenga en count que la vista de gradiente se crea en un método "layoutSubviews", ya que necesitamos un marco finalizado para crear la capa de gradiente.

Se verá mejor cuando inserte este trozo de código y eliminará el willAnimateRotationToInterfaceOrientation:duration: implementation.

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

Sin embargo, esto no es muy elegante. En una aplicación real, debe subclass UIView para crear una vista de gradiente. En esta vista personalizada, puede anular layerClass para que esté respaldada por una capa de degradado:

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

También implemente layoutSubviews para manejar cuando layoutSubviews los límites de la vista.

Al crear esta vista de background, use máscaras de autoría para que los límites se ajusten automáticamente en las rotaciones de la interfaz.

Completa versión Swift. Establezca viewFrame desde viewController que posee esta vista en viewDidLayoutSubviews

 import UIKit class MainView: UIView { let topColor = UIColor(networking: 146.0/255.0, green: 141.0/255.0, blue: 171.0/255.0, alpha: 1.0).CGColor let bottomColor = UIColor(networking: 31.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).CGColor requinetworking 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] } } 

Personalmente, prefiero mantener todo contenido dentro de la subclass de vista.

Aquí está mi implementación Swift:

  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 } } 

Otra versión rápida, que no usa 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 }) } } 

En el controller simplemente llamo:

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

Información

  • Usar como una solución de línea
  • Sustitución de gradiente cuando lo agrega a la vista nuevamente (para usar en reutilizables)
  • Automáticamente en tránsito
  • Eliminación automática

Detalles

Swift 3.1, xCode 8.3.3

Solución

 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)) restrationIdentifier = "__ViewWithGradient" for subView in parentView.subviews { if let subView = subView as? ViewWithGradient { if subView.restrationIdentifier == restrationIdentifier { 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 } requinetworking 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() } } 

Uso

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

Usando 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" networking="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> 

Programáticamente

 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]) } }