问题
I have different images of different foods that I add to a UIView
(I choose to use an UIView
instead of an UIImageView
). The original color of the images are black and I change them to .lightGray
using .alwaysTemplate
.
// the imageWithColor function on the end turns it .lightGray: [https://stackoverflow.com/a/24545102/4833705][1]
let pizzaImage = UIImage(named: "pizzaImage")?.withRenderingMode(.alwaysTemplate).imageWithColor(color1: UIColor.lightGray)
foodImages.append(pizzaImage)
I add the food images to the UIView in cellForRow
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell
cell.myView.layer.contents = foodImages[indexPath.item].cgImage
return cell
}
The UIView is inside a cell and in the cell's layoutSubviews
I add a gradientLayer with an animation that gives a shimmer effect but when the cells appear on screen the animation doesn't occur.
What's the issue?
class FoodCell: UICollectionViewCell {
let myView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 7
view.layer.masksToBounds = true
view.layer.contentsGravity = CALayerContentsGravity.center
view.tintColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setAnchors()
}
override func layoutSubviews() {
super.layoutSubviews()
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0, 0.5, 1]
gradientLayer.frame = myView.frame
let angle = 45 * CGFloat.pi / 180
gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.duration = 2
animation.fromValue = -self.frame.width
animation.toValue = self.frame.width
animation.repeatCount = .infinity
gradientLayer.add(animation, forKey: "...")
}
fileprivate func setAnchors() {
addSubview(myView)
myView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
myView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
myView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
myView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
}
回答1:
You could maybe try this, put this code inside it's own function:
func setUpGradient() {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
...
gradientLayer.add(animation, forKey: "...")
}
Then in your init function call it
override init(frame: CGRect) {
super.init(frame: frame)
setUpGradient()
}
It seems like your problem might be the layoutSubviews can be called a lot, but the init function will only be called when the view is initialized with a frame. Also putting the setup code in its own function will make it easier to do other things like update the gradient layer's frame if the frame changes.
回答2:
I got it working.
I took @Matt's advice in the comments under the question and added myView to the cell's contentView
property instead of the cell directly. I can't find the post but I just read that for animations to work in a cell, whichever views the animations are on needs to be added to the cell's contentView
I moved the gradientLayer from layoutSubviews and instead made it a lazy property.
I also moved the animation into it's own lazy property.
I used this answer and set the gradientLayer's frame to the cell's bounds
property (I initially had it set to the cell's frame
property)
I added a function that adds the gradientLayer to myView's layer's insertSublayer
property and call that function in cellForRow
. Also as per @Matt's comments under my answer to prevent the gradientLayer from constantly getting added over again I add a check to see if the gradient is in the UIView's layer's hierarchy (I got the idea from here even though it's used for a different reason). If it isn't there I add and if is I don't add it.
// I added both the animation and the gradientLayer here
func addAnimationAndGradientLayer() {
if let _ = (myView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("it's already in here so don't readd it")
} else {
gradientLayer.add(animation, forKey: "...") // 1. added animation
myView.layer.insertSublayer(gradientLayer, at: 0) // 2. added the gradientLayer
print("it's not in here so add it")
}
}
To call the function to add the gradientLayer to the cell it's called in cellForRow
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell
cell.removeGradientLayer() // remove the gradientLayer due to going to the background and back issues
cell.myView.layer.contents = foodImages[indexPath.item].cgImage
cell.addAnimationAndGradientLayer() // I call it here
return cell
}
Updated code for the cell
class FoodCell: UICollectionViewCell {
let myView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 7
view.layer.masksToBounds = true
view.layer.contentsGravity = CALayerContentsGravity.center
view.tintColor = .lightGray
return view
}()
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0, 0.5, 1]
gradientLayer.frame = self.bounds
let angle = 45 * CGFloat.pi / 180
gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
return gradientLayer
}()
lazy var animation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.duration = 2
animation.fromValue = -self.frame.width
animation.toValue = self.frame.width
animation.repeatCount = .infinity
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
return animation
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setAnchors()
}
func addAnimationAndGradientLayer() {
// make sure the gradientLayer isn't already in myView's hierarchy before adding it
if let _ = (myView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("it's already in here so don't readd it")
} else {
gradientLayer.add(animation, forKey: "...") // 1. add animation
myView.layer.insertSublayer(gradientLayer, at: 0) // 2. add gradientLayer
print("it's not in here so add it")
}
}
// this function is explained at the bottom of my answer and is necessary if you want the animation to not pause when coming from the background
func removeGradientLayer() {
myView.layer.sublayers?.removeAll()
gradientLayer.removeFromSuperlayer()
setNeedsDisplay() // these 2 might not be necessary but i called them anyway
layoutIfNeeded()
if let _ = (iconImageView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("no man the gradientLayer is not removed")
} else {
print("yay the gradientLayer is removed")
}
}
fileprivate func setAnchors() {
self.contentView.addSubview(myView)
myView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true
myView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
myView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true
myView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0).isActive = true
}
}
As a side note this BELOW works great if the users CAN’T scroll the cells (placeholder cells) but if they CAN make sure to test before adding because it's buggy
Another problem I ran into was when I would go to the background and come back the animation wouldn't move. I followed this answer (code below on how to use it) which works although in that same thread I amended that answer to use this answer to start the animation from the beginning which works BUT there are issues.
I noticed even though I came back from the foreground and the animation worked sometimes when I scrolled the animation got stuck. To get around it I called cell.removeGradientLayer()
in cellForRow
and then again as explained below. However it still got stuck when scrolling but by calling the above it got unstuck. It works for what I need it for because I only show these cells while the actual cells are loading. I'm disabling scrolling when the animation occurs anyway so I don't have to worry about it. FYI this stuck issue only seems to happen when coming back from the background and then scrolling.
I also had to remove the gradientLayer from the cell by calling cell.removeGradientLayer()
when the app went to the background and then when it came back to the foreground I had to call cell.addAnimationAndGradientLayer()
to add it again. I did that by adding background/foreground Notifications in the class that has the collectionView. In the accompanying Notification functions I just scroll through the visible cells and call the cell's functions that are necessary (code is also below).
class PersistAnimationView: UIView {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.willEnterForegroundNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func didBecomeActive() {
self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
self.persistentAnimations.removeAll()
if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
self.layer.resume()
}
}
func willResignActive() {
self.persistentSpeed = self.layer.speed
self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
self.persistAnimations(withKeys: self.layer.animationKeys())
self.layer.speed = self.persistentSpeed //restore original speed
self.layer.pause()
}
func persistAnimations(withKeys: [String]?) {
withKeys?.forEach({ (key) in
if let animation = self.layer.animation(forKey: key) {
self.persistentAnimations[key] = animation
}
})
}
func restoreAnimations(withKeys: [String]?) {
withKeys?.forEach { key in
if let persistentAnimation = self.persistentAnimations[key] {
self.layer.add(persistentAnimation, forKey: key)
}
}
}
}
extension CALayer {
func pause() {
if self.isPaused() == false {
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
}
func isPaused() -> Bool {
return self.speed == 0.0
}
func resume() {
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
// as per the amended answer comment these 2 lines out to start the animation from the beginning when coming back from the background
// let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
// self.beginTime = timeSincePause
}
}
And in the cell class instead of making MyView and instance of UIView
I instead made it an instance of PersistAnimationView
like this:
class FoodCell: UICollectionViewCell {
let MyView: PersistAnimationView = {
let persistAnimationView = PersistAnimationView()
persistAnimationView.translatesAutoresizingMaskIntoConstraints = false
persistAnimationView.layer.cornerRadius = 7
persistAnimationView.layer.masksToBounds = true
persistAnimationView.layer.contentsGravity = CALayerContentsGravity.center
persistAnimationView.tintColor = .lightGray
return persistAnimationView
}()
// everything else in the cell class is the same
Here are the Notifications for the class with the collectionView. The animations also stop when the view disappears or reappears so you’ll have to manage this in viewWillAppear and viewDidDisappear too.
class MyClass: UIViewController, UICollectionViewDatasource, UICollectionViewDelegateFlowLayout {
var collectionView: UICollectionView!
// MARK:- View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appHasEnteredBackground), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addAnimationAndGradientLayerInFoodCell()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
removeGradientLayerInFoodCell()
}
// MARK:- Functions for Notifications
@objc func appHasEnteredBackground() {
removeGradientLayerInFoodCell()
}
@objc func appWillEnterForeground() {
addAnimationAndGradientLayerInFoodCell()
}
// MARK:- Supporting Functions
func removeGradientLayerInFoodCell() {
// if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
if (self.view.window != nil) {
collectionView.visibleCells.forEach { (cell) in
if let cell = cell as? FoodCell {
cell.removeGradientLayer()
}
}
}
}
func addAnimationAndGradientLayerInFoodCell() {
// if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
if (self.view.window != nil) {
collectionView.visibleCells.forEach { (cell) in
if let cell = cell as? FoodCell {
cell.addAnimationAndGradientLayer()
}
}
}
}
}
来源:https://stackoverflow.com/questions/55881944/swift-animation-in-gradientlayer-not-appearing-in-cell