Search code examples
swiftuiimageviewuicollectionviewoffsetparallax

UICollectionView Parallax effect, offset of image start point is incorrect. Swift


Im trying to create an effect similar to that in TastemadeCity for the iPad. Landscape only. A collectionView made up of thin columns (7.5 across the screen) for a dynamic number of images. The idea being that as the collectionView is scrolled horizontally, the image itself will barely seem to move, but the cell will move across it and show a little more of the image.

enter image description here

Ive got it working almost perfectly. However when the cell is created it is centring the image in the cell and not the overall screen (offset by the number of the cell, or collectionView content offset). So when the collection is scrolled, eery image starts too far over to the right and it eventually runs out of image before the edge of the display. I have tried playing with Aspect fit and aspect fill if I change it, it just shrinks the image to the width of the cell. It has to keep the height/width of the screen.

enter image description here

To make things simpler I have tried to auto crop all the images in the array to the same shape as the device screen(I think that code is ok?)

So basically, how do I set the ImageView to the size of the screen, but with only a sliver of it visible through the cell, but with an offset of the collectionView content?

 import UIKit

let parallaxCellIdentifier = "parallaxCell"

class HomeViewController: UICollectionViewController {
    var images = [UIImage]()
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }

    func setup() {
        for i in 1...14 {
            let img = cropToBounds(UIImage(named:"\(i)@2x")!, width: Double(UIScreen.mainScreen().bounds.size.width), height: Double(UIScreen.mainScreen().bounds.size.height))
            images.append(img)
        }
    }
    func cropToBounds(image: UIImage, width: Double, height: Double) -> UIImage {
        let contextImage: UIImage = UIImage(CGImage: image.CGImage!)
        let contextSize: CGSize =  UIScreen.mainScreen().bounds.size
        var posX: CGFloat = 0.0
        var posY: CGFloat = 0.0
        var cgwidth: Double = width
        var cgheight: Double = height
        if Double(image.size.width/image.size.height) > 1  {
            posX = ((contextSize.height - contextSize.width) / 2)
            posY = 0
            cgwidth = Double(contextSize.width)
            cgheight = Double(contextSize.height)
        } else if Double(image.size.width/image.size.height) <= 1 {
            posX = 0
            posY = ((contextSize.width - contextSize.height) / 2)
            cgwidth = Double(contextSize.width)
            cgheight = Double(contextSize.height)
        }
        let rect: CGRect = CGRectMake(posX, posY, CGFloat(cgwidth), CGFloat(cgheight))

        // Create bitmap image from context using the rect
        let imageRef: CGImageRef = CGImageCreateWithImageInRect(contextImage.CGImage, rect)!

        // Create a new image based on the imageRef and rotate back to the original orientation
        let image: UIImage = UIImage(CGImage: imageRef, scale: image.scale, orientation: image.imageOrientation)

        return image
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return images.count
    }
    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let parallaxCell = collectionView.dequeueReusableCellWithReuseIdentifier(parallaxCellIdentifier, forIndexPath: indexPath) as! ParallaxCollectionViewCell
        parallaxCell.image = images[indexPath.row]
        parallaxCell.imageView.contentMode = .ScaleAspectFill
        let xOffset = (collectionView.contentOffset.x )
        parallaxCell.offset(CGPointMake(xOffset, 0.0)) // this doesnt make any difference at this point, i can remove it. I have tried moving the constraints which works here, but breaks them when scrolling

        return parallaxCell
    }


    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
        return 0
    }
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
        return 0
    }
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        let widthOfPoster = collectionView.frame.size.width / 7.5
        let heightOfPoster = collectionView.frame.size.height
        return CGSizeMake(widthOfPoster, heightOfPoster)
    }
    override func scrollViewDidScroll(scrollView: UIScrollView) {
        if let visibleCells = collectionView!.visibleCells() as? [ParallaxCollectionViewCell] {
            for parallaxCell in visibleCells {
                let xOffset = (collectionView!.contentOffset.x )
                parallaxCell.offset(CGPointMake(xOffset, 0.0)) // this doesnt seem to do anything here, but works in the sr
            }
        }
    }
}

This is the function within the cell, it just changes the offset during the scroll

 func offset(offset: CGPoint) {
        imageView.frame = CGRectOffset(self.imageView.bounds, offset.x, offset.y)
    }

Solution

  • So I worked it out. I cropped all images to the shape of the screen. Then created the imageView inside a subview of the cell. This gave me control over the offset.

    let parallaxCellIdentifier = "parallaxCell"
    
    class HomeViewController: UICollectionViewController {
    
    
        var images = [UIImage]()
        let OffsetSpeed: CGFloat = 1025
        let cellsPerScreen: CGFloat = 7.5
        let ImageWidth: CGFloat = UIScreen.mainScreen().bounds.size.width
        let imageHeight: CGFloat = UIScreen.mainScreen().bounds.size.height
    
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setup()
        }
    
        func setup() {
            for i in 1...22 {
                let img = cropToBounds(UIImage(named:"\(i)@2x")!, width: Double(ImageWidth), height: Double(imageHeight))
                images.append(img)
            }
        }
    
        func cropToBounds(image: UIImage, width: Double, height: Double) -> UIImage {
            let contextImage: UIImage = UIImage(CGImage: image.CGImage!)
            let posX: CGFloat = 0.0
            let posY: CGFloat = 0.0
            var cgwidth: Double = width
            var cgheight: Double = height
            let imageRatio = contextImage.size.width / contextImage.size.height
            let contextRatio = CGFloat(width) / CGFloat(height)
            if imageRatio >= contextRatio {
                cgwidth = Double(contextImage.size.height) * Double(contextRatio)
                cgheight = Double(contextImage.size.height)
            } else {
                cgwidth = Double(contextImage.size.width)
                cgheight = (Double(contextImage.size.width) / Double(contextRatio))
            }
            let rect: CGRect = CGRectMake(posX, posY, CGFloat(cgwidth), CGFloat(cgheight))
            // Create bitmap image from context using the rect
            let imageRef: CGImageRef = CGImageCreateWithImageInRect(contextImage.CGImage, rect)!
            // Create a new image based on the imageRef and rotate back to the original orientation
            let image: UIImage = UIImage(CGImage: imageRef, scale: image.scale, orientation: image.imageOrientation)
            return image
        }
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
        }
        override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
            return 1
        }
        override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return images.count
        }
        override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
            let parallaxCell = collectionView.dequeueReusableCellWithReuseIdentifier(parallaxCellIdentifier, forIndexPath: indexPath) as! ParallaxCollectionViewCell
            let xOffset = -((UIScreen.mainScreen().bounds.size.width / cellsPerScreen) * CGFloat(indexPath.row))
            parallaxCell.offsetX = xOffset
            parallaxCell.viewDidLayoutSubviews()
            parallaxCell.rangeView.image = images[indexPath.row]
            parallaxCell.path = indexPath
            return parallaxCell
        }
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
            return 0
        }
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
            return 0
        }
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
            let widthOfPoster = collectionView.frame.size.width / cellsPerScreen
            let heightOfPoster = collectionView.frame.size.height
            return CGSizeMake(widthOfPoster, heightOfPoster)
        }
        override func scrollViewDidScroll(scrollView: UIScrollView) {
            if let visibleCells = collectionView!.visibleCells() as? [ParallaxCollectionViewCell] {
                for parallaxCell in visibleCells {
                    let xOffset = (((collectionView!.contentOffset.x ) / ImageWidth) * OffsetSpeed)
                    parallaxCell.offset(CGPointMake(xOffset, 0.0))
                }
            }
        }
    }
    

    and the Cell code. (the RangeImage, was just a blank UIImageView class)

    class ParallaxCollectionViewCell: UICollectionViewCell {
    
    
        let rangeView = RangeImage(frame: CGRectZero)
        let width = UIScreen.mainScreen().bounds.size.width
        let height = UIScreen.mainScreen().bounds.size.height
        var offsetX:CGFloat = 0.0
        var path = NSIndexPath()
    
        override func awakeFromNib() {
            rangeView.backgroundColor = UIColor.redColor()
            contentView.addSubview(rangeView)
        }
    
        func viewDidLayoutSubviews() {
            rangeView.frame = CGRect(x: offsetX, y: 0.0,
                width: width, height: height)
        }
        func offset(offset: CGPoint) {
            contentView.frame = CGRectOffset(contentView.bounds, offset.x, offset.y)
        }
    }