2017-01-23 1 views
1

J'utilise le très pratique UIColor(patternImage:) pour créer quelques CAShapeLayer s avec des motifs en mosaïque dans une application iOS 10 avec Xcode 8.2. La mosaïque commence toujours à l'origine de la vue, ce qui peut être gênant si vous voulez qu'elle commence ailleurs. Pour illustrer, voici une capture d'écran du simulateur (le code ci-dessous):Comment changer la phase de UIColor patternImage dans Swift?

Tiling problem

Le CAShapeLayer à gauche commence à (0,0), donc tout va bien. Celui de droite est à (110,50), donc il est divisé en deux. Voici le code:

let firstBox = CAShapeLayer() 
firstBox.fillColor = UIColor(patternImage: UIImage(named: "test-image")!).cgColor 
view.layer.addSublayer(firstBox) 
firstBox.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100)).cgPath 

let secondBox = CAShapeLayer() 
secondBox.fillColor = UIColor(patternImage: UIImage(named: "test-image")!).cgColor 
view.layer.addSublayer(secondBox) 
secondBox.path = UIBezierPath(rect: CGRect(x: 110, y: 50, width: 100, height: 100)).cgPath 

Je veux régler la phase du motif de la CAShapeLayer droite de sorte que les deux tuiles montrent un visage plein. Apple documentation pour UIColor (patternImage :) fait référence à une fonction obligeamment à cet effet:

Pour modifier la phase, rendre la couleur la couleur actuelle puis utilisez la fonction setPatternPhase(_:) pour changer la phase.

Sons simples! Mais j'ai du mal à le mettre en œuvre. Je ne suis pas vraiment sûr de ce que signifie «faire la couleur de la couleur actuelle». J'ai essayé d'obtenir le contexte actuel et appelant setPatternPhase sur, à la fois avant et après l'attribution de la couleur de remplissage à la couche:

UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 25, height: 25)) 

Aucun effet notable. J'ai essayé de sous-classer le UIView contenant et en plaçant la phase dans sa méthode drawRect:, comme suggéré dans this answer. Mais drawRect: n'existe pas dans Swift, donc j'ai essayé à la fois draw(_ rect:) et draw(_ layer:, in:). Les deux fonctions sont appelées, mais il n'y a pas d'effet notable.

class PatternView: UIView { 
    override func draw(_ rect: CGRect) { 
     UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 25, height: 25)) 
     super.draw(rect) 
    } 
    override func draw(_ layer: CALayer, in ctx: CGContext) { 
     ctx.setPatternPhase(CGSize(width: 25, height: 25)) 
     super.draw(layer, in: ctx) 
    } 
} 

À la suggestion de Dave Weston, j'ai utilisé UIImage « s .set() pour régler la course en cours et remplir le contexte actuel avant d'appeler setPatternPhase. Malheureusement, la sortie n'est pas affectée. Voici le code que j'ai essayé:

let secondBoxColor = UIColor(patternImage: UIImage(named: "test-image")!) 
secondBoxColor.set() 
UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 50, height: 50)) 

let secondBox = CAShapeLayer() 
secondBox.fillColor = secondBoxColor.cgColor 
view.layer.addSublayer(secondBox) 
secondBox.path = UIBezierPath(rect: CGRect(x: 110, y: 50, width: 100, height: 100)).cgPath 

Comment puis-je changer la phase du motif qui est dessiné dans un CAShapeLayer?

Répondre

0

Pour que votre motif soit la couleur actuelle, vous devez appeler la méthode d'instance set() sur l'instance UIColor qui contient votre motif. Cela configure la couleur en tant que trait actuel et couleur de remplissage pour le contexte actuel. Ensuite, selon les documents d'Apple, setPatternPhase devrait fonctionner.

+0

Merci pour votre aide. J'ai essayé d'utiliser 'set()' avant 'setPatternPhase()', mais cela ne fait pas de différence dans la sortie. J'ai édité ma question pour inclure le nouveau code (à la fin). Qu'est-ce que je fais mal? – Robert

0

Je n'ai pas réussi à résoudre ce problème, mais j'ai pensé partager la solution de contournement que j'utilise au cas où cela serait utile à tout le monde.

Pour autant que je peux dire, CAShapeLayer fait son rendu dans un lieu secret, caché, et ne tient pas compte des display() et draw() normales fonctions qui CALayerDelegate s sont censés utiliser. Par conséquent, vous n'avez jamais accès au CGContext utilisé pour le rendu, il n'y a donc aucun moyen d'appeler setPatternPhase().

Mon cas d'utilisation spécifique pour setPatternPhase() était d'aligner le motif avec le coin supérieur gauche du CAShapeLayer dans lequel il était dessiné, donc j'ai trouvé une autre façon de le faire. Il ne vous permet pas de définir une phase arbitraire.

Ce que j'ai à la place est de créer une nouvelle sous-classe appelée CALayerCAPatternLayer qui prend un UIImage à carreaux et un CGPath pour remplir. Il délègue à une classe CALayerDelegate appelée CAPatternLayerDelegate, qui fournit une fonction draw(layer: in ctx:). Lorsqu'un tirage est demandé, le délégué crée un UIImageView temporaire, le remplit avec l'image en mosaïque, puis le restitue au contexte de CALayer.

Un bon effet secondaire de ceci est que vous pouvez utiliser un UIImage avec des insertions de casquette, ce qui permet une mise à l'échelle de 9 tranches avec la tranche centrale carrelée.

Voici le code pour PatternLayer et PatternLayerDelegate:

class CAPatternLayer: CALayer { 
    var image: UIImage? 
    var path: CGPath? { 
     didSet { 
      if let path = self.path { 
       self.frame = path.boundingBoxOfPath 
       // shift the path to 0,0 since you built position into the frame 
       var translation = CGAffineTransform(translationX: -path.boundingBoxOfPath.origin.x, y: -path.boundingBoxOfPath.origin.y) 
       let shiftedPath = path.copy(using: &translation) 
       // use the shifted version 
       self.path = shiftedPath 
       self.maskLayer.path = shiftedPath 
      } 
     } 
    } 
    let maskLayer: CAShapeLayer = CAShapeLayer() 

    override init() { 
     super.init() 
     self.delegate = CAPatternLayerDelegate.sharedInstance 
     self.setNeedsDisplay() 
     self.mask = self.maskLayer 
    } 
    required init?(coder aDecoder: NSCoder) { 
     fatalError("init(coder:) has not been implemented") 
    } 
    convenience init(path: CGPath, image: UIImage) { 
     self.init() 
     defer { 
      self.image = image 
      self.path = path 
     } 
    } 
} 

class CAPatternLayerDelegate: NSObject, CALayerDelegate { 
    static let sharedInstance = CAPatternLayerDelegate() 
    func draw(_ layer: CALayer, in ctx: CGContext) { 
     // cast layer to a CAPatternLayer so you can access properties 
     if let layer = layer as? CAPatternLayer, let image = layer.image, let path = layer.path { 
      // create a UIImageView to display the image, then render it to the context 
      let imageView = UIImageView() 
      // if a path bounding box was set, use it, otherwise draw over the whole layer 
      imageView.bounds = path.boundingBoxOfPath 
      imageView.image = image 
      imageView.layer.render(in: ctx) 
     } 
    } 
} 

Et voici un exemple utilisé:

// create a path to fill 
let myMaskPath = UIBezierPath(rect: CGRect(x: 50, y: 25, width: 200, height: 100)) 
// pull the image and set it up as a resizeableImage with cap insets 
let patternImage = UIImage(named: "ground")!.resizableImage(withCapInsets: .init(top: 16, left: 16, bottom: 0, right: 16), resizingMode: .tile) 
// create the CAPatternLayer and add it to the view 
let myPatternLayer = CAPatternLayer(path: myMaskPath.cgPath, image: patternImage) 
view.layer.addSublayer(myPatternLayer) 

Sortie:

9-slice tiled pattern