How do I create a parallax effect in UITableView with UIImageView in their prototype cells
Asked Answered
N

4

20

I'm building an app in iOS 8.4 with Swift.

I have a UITableView with a custom UITableViewCell that includes a UILabel and UIImageView. This is all fairly straight forward and everything renders fine.

I'm trying to create a parallax effect similar to the one demonstrated in this demo.

I currently have this code in my tableView.cellForRowAtIndexPath

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = self.tableView.dequeueReusableCellWithIdentifier("myitem", forIndexPath: indexPath) as! MixTableViewCell
    cell.img.backgroundColor = UIColor.blackColor()
    cell.title.text = self.items[indexPath.row]["title"]

    cell.img.image = UIImage(named: "Example.png")

    // ideally it would be cool to have an extension allowing the following
    // cell.img.addParallax(50) // or some other configurable offset

    return cell
}

That block exists inside a class that looks like class HomeController: UIViewController, UITableViewDelegate, UITableViewDataSource { ... }

I am also aware that I can listen to scroll events in my class via func scrollViewDidScroll.

Other than that, help is appreciated!

Necrology answered 13/7, 2015 at 0:2 Comment(0)
N
37

I figured it out! The idea was to do this without implementing any extra libraries especially given the simplicity of the implementation.

First... in the custom table view Cell class, you have to create an wrapper view. You can select your UIImageView in the Prototype cell, then choose Editor > Embed in > View. Drag the two into your Cell as outlets, then set clipToBounds = true for the containing view. (also remember to set the constraints to the same as your image.

class MyCustomCell: UITableViewCell {
    @IBOutlet weak var img: UIImageView!
    @IBOutlet weak var imgWrapper: UIView!
    override func awakeFromNib() {
        self.imgWrapper.clipsToBounds = true
    }
}

Then in your UITableViewController subclass (or delegate), implement the scrollViewDidScroll — from here you'll continually update the UIImageView's .frame property. See below:

override func scrollViewDidScroll(scrollView: UIScrollView) {
    let offsetY = self.tableView.contentOffset.y
    for cell in self.tableView.visibleCells as! [MyCustomCell] {
        let x = cell.img.frame.origin.x
        let w = cell.img.bounds.width
        let h = cell.img.bounds.height
        let y = ((offsetY - cell.frame.origin.y) / h) * 25
        cell.img.frame = CGRectMake(x, y, w, h)
    }
}

See this in action.

Necrology answered 13/7, 2015 at 19:4 Comment(6)
I had to execute the code in scrollViewDidScroll in my viewDidAppear to set the initial image position and avoid the glitch I had when I start scrolling.Miguel
@Necrology Great answer, any thoughts on how to do this parallax effect? #41177272Rheumy
@budidino, how did you prevent the initial glitch? I tried to do but it did not work out.Favored
@EmrahAkgül I think I just called scrollViewDidScroll() in my viewWillAppear or viewDidAppear. I don't have that code now so it's hard to experiment but I know you'll figure it out. Please post here whatever worked for you ;)Miguel
@budidino, viewDidAppear causes glitch as well. If you remember the exact solution, I will be appreciated, if not, never mind, I found completely different solution :)Favored
it changes the height with makes the view too ugly !!!!!!!!!!!Rebeckarebeka
C
30

I wasn't too happy with @ded's solution requiring a wrapper view, so I came up with another one that uses autolayout and is simple enough.

In the storyboard, you just have to add your imageView and set 4 constraints on the ImageView:

  • Leading to ContentView (ie Superview) = 0
  • Trailing to ContentView (ie Superview) = 0
  • Top Space to ContentView (ie Superview) = 0
  • ImageView Height (set to 200 here but this is recalculated based on the cell height anyway)

enter image description here

The last two constraints (top and height) need referencing outlets to your custom UITableViewCell (in the above pic, double click on the constraint in the rightmost column, and then Show the connection inspector - the icon is an arrow in a circle)

Your UITableViewCell should look something like this:

class ParallaxTableViewCell: UITableViewCell {

    @IBOutlet weak var parallaxImageView: UIImageView!

    // MARK: ParallaxCell

    @IBOutlet weak var parallaxHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var parallaxTopConstraint: NSLayoutConstraint!

    override func awakeFromNib() {
        super.awakeFromNib()

        clipsToBounds = true
        parallaxImageView.contentMode = .ScaleAspectFill
        parallaxImageView.clipsToBounds = false
    }
  }

So basically, we tell the image to take as much space as possible, but we clip it to the cell frame.

Now your TableViewController should look like this:

class ParallaxTableViewController: UITableViewController {
    override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return cellHeight
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! ParallaxTableViewCell
        cell.parallaxImageView.image = … // Set your image
        cell.parallaxHeightConstraint.constant = parallaxImageHeight
        cell.parallaxTopConstraint.constant = parallaxOffsetFor(tableView.contentOffset.y, cell: cell)
        return cell
    }

    // Change the ratio or enter a fixed value, whatever you need
    var cellHeight: CGFloat {
        return tableView.frame.width * 9 / 16
    }

    // Just an alias to make the code easier to read
    var imageVisibleHeight: CGFloat {
        return cellHeight
    }

    // Change this value to whatever you like (it sets how "fast" the image moves when you scroll)
    let parallaxOffsetSpeed: CGFloat = 25

    // This just makes sure that whatever the design is, there's enough image to be displayed, I let it up to you to figure out the details, but it's not a magic formula don't worry :)
    var parallaxImageHeight: CGFloat {
        let maxOffset = (sqrt(pow(cellHeight, 2) + 4 * parallaxOffsetSpeed * tableView.frame.height) - cellHeight) / 2
        return imageVisibleHeight + maxOffset
    }

    // Used when the table dequeues a cell, or when it scrolls
    func parallaxOffsetFor(newOffsetY: CGFloat, cell: UITableViewCell) -> CGFloat {
        return ((newOffsetY - cell.frame.origin.y) / parallaxImageHeight) * parallaxOffsetSpeed
    }

    override func scrollViewDidScroll(scrollView: UIScrollView) {
        let offsetY = tableView.contentOffset.y
        for cell in tableView.visibleCells as! [MyCustomTableViewCell] {
            cell.parallaxTopConstraint.constant = parallaxOffsetFor(offsetY, cell: cell)
        }
    }
}

Notes:

  • it is important to use tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) and not tableView.dequeueReusableCellWithIdentifier("CellIdentifier"), otherwise the image won't be offset until you start scrolling

So there you have it, parallax UITableViewCells that should work with any layout, and can also be adapted to CollectionViews.

Caelum answered 22/11, 2015 at 11:35 Comment(14)
This is the perfect solution.Unijugate
Thanks @JohnTravolta, glad it helped you, and thanks for the edit suggestion even though it was rejected by those who reviewed it.Caelum
It's excatly what I'm looking for but how do you set those constraints on the ImageView? In my version I want to add a label on top for the picture's titleOpisthognathous
@Opisthognathous Just ctrl+click on the imageView, hold & drag on the container and pick the constraints you want. For your label, just add the label and then define constraints on the label, between the label and the image.Caelum
Yep I know how to set constraint but I don't have leading and trailing as a choiceOpisthognathous
Also where [TripsTableViewCell] come from ?Opisthognathous
TripsTableViewCell is my custom cell class name, I renamed it to MyCustomTableViewCell above.Caelum
And I don't see why leading and trailing wouldn't show, so you should post it as a new question, it's beyond the scope of this answer.Caelum
This is brilliant!Smith
Could you indicate us how to understand the "magic formula" to define the parallaxImageHeight?Smith
@FredA. It's the result of a quadratic equation (the other result is negative) representing the relation between the picture's overflow and the tableview visible height. Basically, the problem you look to solve is: how can I make sure that wherever the cell is, there's always content within the cell frame. To put it differently: the picture is taller than the cell frame; how much taller should it be so that when the cell reaches the end of the table visible frame, we're certain that the image still fills the cell entirely.Caelum
@Nycen I would greatly appreciate if you take a look at this question I have for a similar parallax effect, thank you: #41177272Rheumy
@Nycen What if my height parallaxHeightConstraint is not fix ? Let's say it is dynamic based on screen size of device.Kalman
It works fine If you put the image view in 2 nested view, I mean, View(View(ImageView())) something like this to to keep the height of the image always persistent and also make it to move :)Rebeckarebeka
E
2

This method works with table view and collection view.

  • first of all create the cell for the tableview and put the image view in it.

  • set the image height slightly more than the cell height. if cell height = 160 let the image height be 200 (to make the parallax effect and you can change it accordingly)

  • put this two variable in your viewController or any class where your tableView delegate is extended

let imageHeight:CGFloat = 150.0
let OffsetSpeed: CGFloat = 25.0
  • add the following code in the same class
 func scrollViewDidScroll(scrollView: UIScrollView) {
    //  print("inside scroll")

    if let visibleCells = seriesTabelView.visibleCells as? [SeriesTableViewCell] {
        for parallaxCell in visibleCells {
            var yOffset = ((seriesTabelView.contentOffset.y - parallaxCell.frame.origin.y) / imageHeight) * OffsetSpeedTwo
            parallaxCell.offset(CGPointMake(0.0, yOffset))
        }
    }
}
  • where seriesTabelView is my UItableview

  • and now lets goto the cell of this tableView and add the following code

func offset(offset: CGPoint) {
        posterImage.frame = CGRectOffset(self.posterImage.bounds, offset.x, offset.y)
    }
  • were posterImage is my UIImageView

If you want to implement this to collectionView just change the tableView vairable to your collectionView variable

and thats it. i am not sure if this is the best way. but it works for me. hope it works for you too. and let me know if there is any problem

Estus answered 19/12, 2015 at 21:59 Comment(0)
V
1

After combining answers from @ded and @Nycen I came to this solution, which uses embedded view, but changes layout constraint (only one of them):

  1. In Interface Builder embed the image view into a UIView. For that view make [√] Clips to bounds checked in View > Drawing

  2. Add the following constraints from the image to view: left and right, center Vertically, height

  3. Adjust the height constraint so that the image is slightly higher than the view

enter image description here

  1. For the Align Center Y constraint make an outlet into your UITableViewCell

  2. Add this function into your view controller (which is either UITableViewController or UITableViewControllerDelegate)

    private static let screenMid = UIScreen.main.bounds.height / 2
    private func adjustParallax(for cell: MyTableCell) {
        cell.imageCenterYConstraint.constant = -(cell.frame.origin.y - MyViewController.screenMid - self.tableView.contentOffset.y) / 10
    }
    

Note: by editing the magic number 10 you can change how hard the effect will be applied, and by removing the - symbol from equation you can change the effect's direction

  1. Call the function from when the cell is reused:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCellId", for: indexPath) as! MyTableCell
        adjustParallax(for: cell)
        return cell
    }
    
  2. And also when scroll happens:

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        (self.tableView.visibleCells as! [MyTableCell]).forEach { cell in
            adjustParallax(for: cell)
        }
    }
    
Vicenary answered 19/3, 2019 at 13:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.