CAGradientLayer, ne pas resize joliment, déchirer en rotation

J'essaie d'get mon CAGradientLayers, que j'utilise pour créer de beaux arrière-plans dégradés, pour resize joliment la rotation et la présentation modale, mais ils ne joueront pas à la balle.

Voici une video que je viens de créer montrant mon problème: Notez le déchirement en rotation.

Veuillez également noter que cette video a été créée en filmant l'iPhone Simulator sur OS X. J'ai ralenti les animations dans la video pour mettre en évidence mon problème.

Vidéo du problème …

Voici un projet Xcode que je viens de créer (qui est la source de l'application montrée dans la video), essentiellement comme illustré le problème se produit à la rotation et surtout lorsque les vues sont présentées de manière modale:

Projet Xcode, présentant de manière modale des vues avec des arrière-plans CAGradientLayer …

Pour ce que ça vaut, je comprends que l'utilisation de:

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

fait un travail raisonnable de rendre les transitions un peu plus transparentes et less discordantes, mais si vous regardez la video quand, alors que vous êtes en mode paysage, vous voyez que le code ci-dessus n'aidera pas.

Des idées que je peux faire pour régler cela?

John

Lorsque vous créez une couche (comme votre couche de dégradé), aucune vue ne gère la couche (même si vous l'ajoutez en tant que sous-couche de la couche d'une vue). Une couche autonome comme celle-ci ne participe pas au système d'animation UIView .

Ainsi, lorsque vous mettez à jour l'image de la couche de dégradé, la couche anime la modification avec ses propres parameters d'animation par défaut. (Ceci est appelé "animation implicite".) Ces parameters par défaut ne correspondent pas aux parameters d'animation utilisés pour la rotation de l'interface, de sorte que vous obtenez un résultat bizarre.

Je n'ai pas regardé votre projet mais il est sortingvial de reproduire votre problème avec ce 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 

Voici à quoi cela ressemble, avec un ralenti activé dans le simulateur:

mauvaise rotation

Heureusement, c'est un problème facile à résoudre. Vous devez faire en sorte que votre couche de dégradé soit gérée par une vue. Pour ce faire, créez une sous-class UIView qui utilise CAGradientLayer comme couche. Le code est minuscule:

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

Ensuite, vous devez changer votre code pour utiliser GradientView au lieu de CAGradientLayer . Puisque vous utilisez une vue maintenant à la place d'une couche, vous pouvez définir le masque d'autoresizing pour conserver la taille du dégradé dans sa vue d'set, afin que vous n'ayez rien à faire ultérieurement pour gérer 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 

Voici le résultat:

bonne rotation

La meilleure partie de la réponse de @ rob est que la vue contrôle la couche pour vous. Voici le code Swift qui remplace correctement la class de calque et définit le dégradé.

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

Vous pouvez ensuite append la vue en deux lignes où vous voulez.

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

Ma version rapide:

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

Notez que j'ai également dû utiliser l'échelle de l'appareil pour calculer la taille de l'image – pour get un calibrage automatique correct pendant les changements d'orientation (avec la layout automatique).

  1. Dans Interface Builder, j'ai ajouté un UIView et changé sa class en GradientView (la class montrée ci-dessus).
  2. J'ai alors créé un débouché pour cela (myGradientView).
  3. Enfin, dans le controller de vue, j'ai ajouté:

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

Notez que la vue en dégradé est créée dans une méthode "layoutSubviews", car nous avons besoin d'un cadre finalisé pour créer la couche de dégradé.

Cela apparaîtra mieux lorsque vous insertez cette partie du code et supprimerez willAnimateRotationToInterfaceOrientation:duration: implementation.

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

Ce n'est cependant pas très élégant. Dans une application réelle, vous devez sous-classr UIView pour créer une vue en dégradé. Dans cette vue personnalisée, vous pouvez replace layerClass pour qu'il soit protégé par une couche de dégradé:

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

layoutSubviews également layoutSubviews pour gérer lorsque les limites de la vue changent.

Lors de la création de cette vue en arrière-plan, utilisez des masques autoréglables pour que les limites s'ajustent automatiquement aux rotations d'interface.

Version complète de Swift Définissez viewFrame partir de viewController qui possède cette vue dans 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] } } 

Personnellement, je préfère garder tout contenu autonome dans la sous-class vue.

Voici ma mise en œuvre 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 } } 

Une autre version rapide – qui n'utilise pas 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 }) } } 

Dans le controller, je viens d'appeler:

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

Info

  • Utiliser comme une solution de ligne
  • Remplacer le dégradé lorsque vous l'ajoutez à la vue (à utiliser dans les éléments réutilisables)
  • Transiter automatiquement
  • Suppression automatique

Détails

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

Utiliser 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" firstAtsortingbute="top" secondItem="uii-31-sl9" secondAtsortingbute="bottom" constant="50" id="a7J-Hq-IIq"/> <constraint firstAtsortingbute="trailingMargin" secondItem="uii-31-sl9" secondAtsortingbute="trailing" constant="50" id="i9v-hq-4tD"/> <constraint firstItem="uii-31-sl9" firstAtsortingbute="top" secondItem="y3c-jy-aDJ" secondAtsortingbute="bottom" constant="50" id="wlO-83-8FY"/> <constraint firstItem="uii-31-sl9" firstAtsortingbute="leading" secondItem="8bC-Xf-vdC" secondAtsortingbute="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> 

Par programme

 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, atsortingbute: .leading, relatedBy: .equal, toItem: view, atsortingbute: .leadingMargin, multiplier: 1.0, constant: constant).isActive = true NSLayoutConstraint(item: viewWithGradient, atsortingbute: .trailing, relatedBy: .equal, toItem: view, atsortingbute: .trailingMargin , multiplier: 1.0, constant: -1*constant).isActive = true NSLayoutConstraint(item: viewWithGradient, atsortingbute: .bottom, relatedBy: .equal, toItem: view, atsortingbute: .bottomMargin , multiplier: 1.0, constant: -1*constant).isActive = true NSLayoutConstraint(item: viewWithGradient, atsortingbute: .top, relatedBy: .equal, toItem: view, atsortingbute: .topMargin , multiplier: 1.0, constant: constant).isActive = true viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0]) } }