I have a code that scrolls through full-screen images forward and backward when tapping the left or right side of the screen. When an image appears on the screen, it performs one of the animation types: top, bottom, left, right, zoomin, zoomout — each of these animations consists of two sub-animations (the first animation is fast, and the second one is looped).
The problem is that sometimes, when switching images, the animation coordinates stack. Under normal circumstances, animations should only affect either X, Y, or Scale. But in my case, it sometimes happens that images move diagonally (X + Y), the animation goes beyond the image boundaries, and I see black areas on the screen. This shouldn't happen. How can I fix this?
I'm using removeAllAnimations
before each animation, but it doesn't help.
full code:
class ReaderController: UIViewController, CAAnimationDelegate {
var pagesData = [PageData]()
var index = Int()
var pageIndex: Int = -1
let pageContainer: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let pageViews: [PageLayout] = {
let view = [PageLayout(), PageLayout()]
view[0].translatesAutoresizingMaskIntoConstraints = false
view[1].translatesAutoresizingMaskIntoConstraints = false
return view
}()
private weak var currentTransitionView: PageLayout?
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupConstraints()
pageViews[0].index = index
pageViews[1].index = index
pageViews[0].pageIndex = pageIndex
pageViews[1].pageIndex = pageIndex
pageTransition(animated: false, direction: "fromRight")
}
func setupViews() {
pageContainer.addSubview(pageViews[0])
pageContainer.addSubview(pageViews[1])
view.addSubview(pageContainer)
}
func setupConstraints() {
pageContainer.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
pageContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true
pageContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
pageContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
pageViews[0].topAnchor.constraint(equalTo: pageContainer.topAnchor).isActive = true
pageViews[0].bottomAnchor.constraint(equalTo: pageContainer.bottomAnchor).isActive = true
pageViews[0].leadingAnchor.constraint(equalTo: pageContainer.leadingAnchor).isActive = true
pageViews[0].trailingAnchor.constraint(equalTo: pageContainer.trailingAnchor).isActive = true
pageViews[1].topAnchor.constraint(equalTo: pageContainer.topAnchor).isActive = true
pageViews[1].bottomAnchor.constraint(equalTo: pageContainer.bottomAnchor).isActive = true
pageViews[1].leadingAnchor.constraint(equalTo: pageContainer.leadingAnchor).isActive = true
pageViews[1].trailingAnchor.constraint(equalTo: pageContainer.trailingAnchor).isActive = true
}
func loadData(fileName: Any) -> PagesData {
var url = NSURL()
url = Bundle.main.url(forResource: "text", withExtension: "json")! as NSURL
let data = try! Data(contentsOf: url as URL)
let person = try! JSONDecoder().decode(PagesData.self, from: data)
return person
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: view.self)
if view.safeAreaInsets.left > 30 {
if (location.x > self.view.frame.size.width - (view.safeAreaInsets.left * 1.5)) {
pageTransition(animated: true, direction: "fromRight")
} else if (location.x < (view.safeAreaInsets.left * 1.5)) {
pageTransition(animated: true, direction: "fromLeft")
}
}
else {
if (location.x > self.view.frame.size.width - 40) {
pageTransition(animated: true, direction: "fromRight")
} else if (location.x < 40) {
pageTransition(animated: true, direction: "fromLeft")
}
}
}
}
func pageTransition(animated: Bool, direction: String) {
let result = loadData(fileName: pagesData)
switch direction {
case "fromRight":
pageIndex += 1
case "fromLeft":
pageIndex -= 1
default: break
}
pageViews[0].pageIndex = pageIndex
pageViews[1].pageIndex = pageIndex
guard pageIndex >= 0 && pageIndex < result.pagesData.count else {
pageIndex = max(0, min(pageIndex, result.pagesData.count - 1))
return
}
let fromView = pageViews[0].isHidden ? pageViews[1] : pageViews[0]
let toView = pageViews[0].isHidden ? pageViews[0] : pageViews[1]
toView.imageView.layer.removeAllAnimations()
toView.imageView.transform = .identity
toView.configure(theData: result.pagesData[pageIndex])
if animated {
fromView.isHidden = true
toView.isHidden = false
} else {
fromView.isHidden = true
toView.isHidden = false
}
}
}
class PageLayout: UIView {
var index = Int()
var pageIndex = Int()
let imageView: UIImageView = {
let image = UIImageView()
image.contentMode = .scaleAspectFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var imageViewTopConstraint = NSLayoutConstraint()
var imageViewBottomConstraint = NSLayoutConstraint()
var imageViewLeadingConstraint = NSLayoutConstraint()
var imageViewTrailingConstraint = NSLayoutConstraint()
var imagePosition = ""
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(imageView)
setupConstraints()
}
func setupConstraints() {
imageView.layer.removeAllAnimations()
imageView.transform = .identity
removeConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint,
imageViewTrailingConstraint])
switch imagePosition {
case "top":
imageView.transform = .identity
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: -40.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(translationX: 0, y: 40.0)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.translatedBy(x: 0, y: -40.0)
}, completion: nil)
})
case "bottom":
imageView.transform = .identity
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 40.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(translationX: 0, y: -40)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.translatedBy(x: 0, y: 40)
}, completion: nil)
})
case "left":
imageView.transform = .identity
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -40.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(translationX: 40, y: 0)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.translatedBy(x: -40, y: 0)
}, completion: nil)
})
case "right":
imageView.transform = .identity
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 40.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(translationX: -40, y: 0)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.translatedBy(x: 40, y: 0)
}, completion: nil)
})
case "zoomin":
imageView.transform = CGAffineTransformScale(.identity, 1.0, 1.0)
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = .identity
}, completion: nil)
})
case "zoomout":
imageView.transform = CGAffineTransformScale(.identity, 1.1, 1.1)
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: { _ in
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.scaledBy(x: 1.1, y: 1.1)
}, completion: nil)
})
default:
imageView.transform = .identity
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
}
}
func configure(theData: PageData) {
imageView.image = UIImage(named: "page\(pageIndex+1)")
imagePosition = theData.imagePosition
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("Not happening")
}
}
struct PagesData: Decodable {
var pagesData: [PageData]
}
struct PageData: Decodable {
let textData, textPosition, textColor, shadowColor, textAlignment, imagePosition: String
}
JSON:
{
"pagesData" : [
{
"textData" : "",
"textPosition" : "topLeft",
"textColor" : "FFFFFF",
"shadowColor" : "000000",
"textAlignment" : "left",
"imagePosition" : "left",
},
{
"textData" : "",
"textPosition" : "bottomLeft",
"textColor" : "FFFFFF",
"shadowColor" : "000000",
"textAlignment" : "left",
"imagePosition" : "bottom",
},
{
"textData" : "",
"textPosition" : "zoomin",
"textColor" : "FFFFFF",
"shadowColor" : "000000",
"textAlignment" : "left",
"imagePosition" : "right",
},
{
"textData" : "",
"textPosition" : "bottomCenter",
"textColor" : "FFFFFF",
"shadowColor" : "000000",
"textAlignment" : "left",
"imagePosition" : "zoomout",
},
{
"textData" : "",
"textPosition" : "topLeft",
"textColor" : "FFFFFF",
"shadowColor" : "000000",
"textAlignment" : "left",
"imagePosition" : "left",
},
]
}
I can't reproduce your "images move diagonally (X + Y)" ... although when tapping quickly I do see "the animation goes beyond the image boundaries".
What might correct the issue is to NOT execute the second part of the animation if the first part has not completed.
Try changing your animation blocks to this:
UIView.animate(withDuration: 2.0, delay: 0, options: [.curveEaseOut, .allowUserInteraction, .beginFromCurrentState], animations: {
self.imageView.transform = CGAffineTransform(translationX: 0, y: 40.0)
}, completion: { bCompleted in
if bCompleted {
UIView.animate(withDuration: 6.0, delay: 0, options: [.curveLinear, .autoreverse, .repeat, .beginFromCurrentState, .allowUserInteraction], animations: {
self.imageView.transform = self.imageView.transform.translatedBy(x: 0, y: -40.0)
}, completion: nil)
}
})