Search code examples
iosswift

How to fix imageview X move animation?


I have an app like a reader. When I touch the right edge of the screen, I go to the next page. When I touch the left edge of the screen, I go to the previous page. My pages contain images. The image is full screen size and 20pt more from one of the edges of the screen. And when I show the page, I want the image move to the right from 0pt to 20pt or left from 0pt to -20pt depending on imageDirection property from json file. I use this code to do this:

reader container:

class ReaderController: UIViewController {
        
    var pagesData = [PageData]()
    var imageArray: [UIImage] = []
    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
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        loadImages()
        setupViews()
        setupConstraints()

        pageViews[0].index = index
        pageViews[0].imageArray = imageArray
        pageViews[1].index = index
        pageViews[1].imageArray = imageArray
        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
    }
    
    func loadImages() {
        for imageIndex in 1...18 { imageArray.append(UIImage(named: "page\(imageIndex)")!) }
    }
        
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            if(touch.view == pageViews[0] || touch.view == pageViews[1]) {
                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

        if pageIndex <= -1 {
            pageIndex = 0
        } else if pageIndex >= result.pagesData.count {
            pageIndex = result.pagesData.count - 1
            finishReading()
        } else {

            let fromView = pageViews[0].isHidden ? pageViews[1] : pageViews[0]
            let toView = pageViews[0].isHidden ? pageViews[0] : pageViews[1]
            toView.configure(theData: result.pagesData[pageIndex])

            if animated {
                toView.isHidden = false
                let animation = CATransition()
                animation.duration = 0
                animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
                animation.isRemovedOnCompletion = true
                animation.subtype = direction

                let tapAnimationType = "fade"
                switch tapAnimationType {
                case "pageCurl": animation.type = "pageCurl"
                case "fade": animation.type = "fade"
                case "moveIn": animation.type = "moveIn"
                case "push": animation.type = "push"
                case "reveal": animation.type = "reveal"
                default: animation.type = "fade" }

                self.pageContainer.layer.add(animation, forKey: "pageAnimation")
                self.pageContainer.bringSubview(toFront: toView)
            }
            else {
                fromView.isHidden = true
                toView.isHidden = false
            }
        }
    }
    
}

page layout:

class PageLayout: UIView {
        
    var imageArray: [UIImage] = []
    var index = Int()
    var pageIndex = Int()
    
    var imageDirection = ""
    
    var imageViewTopConstraint = NSLayoutConstraint()
    var imageViewBottomConstraint = NSLayoutConstraint()
    var imageViewLeadingConstraint = NSLayoutConstraint()
    var imageViewTrailingConstraint = NSLayoutConstraint()
    
    private let imageView: UIImageView = {
        let image = UIImageView()
        image.contentMode = .scaleAspectFill
        image.translatesAutoresizingMaskIntoConstraints = false
        return image
    }()
    
    private let textLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let screenWidth = UIScreen.main.bounds.size.width
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("Not happening")
    }
    
    func setupViews() {
        addSubview(imageView)
        addSubview(textLabel)
    }
    
    func setupConstraints() {
        
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        let window = windowScene?.windows.first
        let safeArea = window?.safeAreaInsets
        
        textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: safeArea!.left+16.0).isActive = true
        textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -safeArea!.right-16.0).isActive = true
        textLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        textLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        
        removeConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint,
                           imageViewTrailingConstraint])
        
        switch imageDirection {
            
        case "left":
            
            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: -20.0)
            imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
            
            addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
            
            imageView.layer.removeAllAnimations()
            imageView.transform = CGAffineTransformTranslate(.identity, 0, 0)
            UIView.animate(withDuration: 4.0, delay: 0, animations: {
                self.imageView.transform = CGAffineTransformTranslate(.identity, 20, 0)
            }, completion: nil)
            
        case "right":
            
            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: 20.0)
            
            addConstraints([imageViewTopConstraint, imageViewBottomConstraint, imageViewLeadingConstraint, imageViewTrailingConstraint])
            
            imageView.layer.removeAllAnimations()
            imageView.transform = CGAffineTransformTranslate(.identity, 0, 0)
            UIView.animate(withDuration: 4.0, delay: 0, animations: {
                self.imageView.transform = CGAffineTransformTranslate(.identity, -20, 0)
            }, completion: nil)
            
        default: break
        }
    }
        
    func configure(theData: PageData) {
        textLabel.text = theData.textData
        imageDirection = theData.imageDirection
        imageView.image = imageArray[pageIndex]
        setupConstraints()
    }
    
}

other:

struct PagesData: Decodable {
    var pagesData: [PageData]
}

struct PageData: Decodable {
    let textData, imageDirection: String
}

JSON:

{
    "pagesData" : [
        
        {
            "textData" : "1",
            "imagePosition" : "left",
        },
        
        {
            "textData" : "2",
            "imagePosition" : "left",
        },

        {
            "textData" : "3",
            "imagePosition" : "right",
        },
        
        {
            "textData" : "4",
            "imagePosition" : "right",
        },
    ]
}

What I have:

All works fine when I turn the page and wait for the animation finished. But when I not wait for the animation finished and go to next page I see that current page with image jumps quickly to 0 and next I see animation for next page.

What I want:

when I not wait for the animation of current page finished and go to next page I can't see the current page jumps to 0. I want to see smoothly flip

Video to reproduce the problem:

At this video I always go to next page not wait for the animation of current page finished. For first, second and third pages all works fine but for next images I have a problems

https://drive.google.com/file/d/1cms3G_o8W8bdGj9t5gAnItPvzFN7Z3mG/view?usp=sharing

Git:

https://github.com/user234567890354678/testImage.git


Solution

  • Thank you for providing a sample project!

    The answer was quite simple, but a lot easier to see (and test) with the project (:

    In your pageTransition(args) you forgot to set fromView.isHidden = true at the end of the animation. E.g.:

    if animated {
        //Animation setup (your code) including unhiding 'toView'
        
        fromView.isHidden = true // here
    } else {
        toView.isHidden = false
        fromView.isHidden = true
    }
    

    This was causing you to display the same view over and over and removing the residual animation caused the snapping effect.