Reproducing Popular iOS Controls - Part 1: | Ray Wenderlich Videos

Have you ever used an app where everything you did and touched felt great? In this video, we look at the four great apps we'll learn to build in this course.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/5298-reproducing-popular-ios-controls/lessons/1

Great tutorial. Couple observations.

  1. The starter and final projects are missing the code below in the NavigationView. As such the user is unable to manually scroll the views in the Scroller container.

override func hitTest(_ point: CGPoint, with event: UIEvent?) → UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}

  1. The final project appears to be missing the filters in the LensViewController

Which starter and final projects are you referring to - from which part of the video?

I’m referring to the starter and final projects retrieved from the download button in the Scrolling Navigation section of the tutorial.

For the 3rd video that’s what you should expect. We go through building the interactive scrolling in the next video, and the filters get added later on :slight_smile:

Yes, but in the 4th video (timestamp 5:07) the hitTest function appears on the screen but in the audio you don’t mention that it needs to be added. I assumed it was already added somewhere in the code base.

Also, in the final project, one would expect all the code for the video series to be included.

The starter project in video 4 is different than the final project in video 3 and adds in some boilerplate/non-essential code. That’s why we suggest always starting with the starter project from the materials on the video you’re on. When I was making the code, I decided that the hitTest function didn’t need to be specifically called out to get the point across for what we’re building.

In the final project for the course, all the code should be included (that’s video 9) - is that the final project you’re referring to? Otherwise the starter and final project for each videos just refer to the start and final project you start and end up with during the video.

Awesome course! the code has a nice style and the same goes for your way to present (not too cartoonish like others)

Anyway, keep the good job and i am specially looking forward for the appstore transition :slight_smile:

Hi,

in the tutorial I cannot select the icons on the border side in the snap navigation. How can I fix this?

greetings
Thomas

Hi, if I scroll the collection view to the left side, it snap automatic to the middle. Can you check this bug.

greetings

Hi Thomas!

To answer your first question:

If you add some contentInsets (left and right insets) to the lensCollectionView in the viewDidLoad, you’ll be able to select the icons at both ends!

Something like this:

let screenWidth = UIScreen.main.bounds.width
lensCollectionView.contentInset = UIEdgeInsets(top: 0, left: screenWidth/2, bottom: 0, right: screenWidth/2)

Hi again Thomas!

So, with regards to scrolling to the left side and the collectionView snapping to the middle. This happens because I put the initial scrolling to the middle cell into viewDidLayoutSubviews. It seems that sometimes when a cell is reused, viewDidLayoutSubviews gets called. In this case, it happens, particularly, when the cell with IndexPath 0,2 is selected.

There’s a couple of ways to avoid this problem.

You could created a separate variable called selectedCell where you keep track of the cell that’s selected and you only scroll to the middle on viewDidLayoutSubviews if there’s no cell selected, which is the state of the collectionView when it first appears. That would involve a little mode code refactoring than I think I can go through in a comment, but I hope you get the gist.

The other way is to remove the implementation of viewDidLayoutSubviews in the LensViewController and instead use viewDidAppear to scroll to the middle initially, where you can force the lensCollectionView to layout like so:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    lensCollectionView.setNeedsLayout()
    lensCollectionView.layoutIfNeeded()
    
    let middleIndexPath = IndexPath(item: lensFiltersImages.count/2, section: 0)
    selectCell(for: middleIndexPath, animated: false)
  }

Hope that helped!

Hey ! I really want to learn how to replicate the IOS control (Semi circle slider ) which you had showcased in your intro video ! Is it covered under this tutorial ?

No, that’s not covered - sorry. Perhaps in a future course :slight_smile:

Hi Lea,

thanks for your amazing videos and your fast answers. I tested the contentInset already, but with your code come a another side effect. The collectionView snapped now to the left side. I added your code in the Chapter 9 Final Project and this have the same effect. Can you check this if you have the time.

greetings
Thomas

Hi Thomas!

Did you add both the code snippets I mentioned?

First the contentInset

let screenWidth = UIScreen.main.bounds.width
lensCollectionView.contentInset = UIEdgeInsets(top: 0, left: screenWidth/2, bottom: 0, right: screenWidth/2)

And then remove this part:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    let middleIndexPath = IndexPath(item: lensFiltersImages.count/2, section: 0)
    selectCell(for: middleIndexPath, animated: false)
  }

And replace it with:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    lensCollectionView.setNeedsLayout()
    lensCollectionView.layoutIfNeeded()
    
    let middleIndexPath = IndexPath(item: lensFiltersImages.count/2, section: 0)
    selectCell(for: middleIndexPath, animated: false)
  }

That should do the trick. There are still some potential edge cases that remain, but unfortunately we don’t have time to cover all of that within a 10-minute video.

One of the edge cases is, for example, when the scroll view can’t determine one specific index path, because the xyPoint of where the user has scrolled to is smack down in the middle between two cells.

I’m referring to this piece of code:
guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else { return }

When that happens you kind of have to decide where you want the user to go - either left or right. For example, let’s say that if the user is scrolling to the left and the lensCollectionView can’t determine the indexPath from the xyPosition we want to go to the closest left cell, and if they’re scrolling to the right, we want to go to the closest right cell. We could write something like this:

guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else {
      let middleX = lensCollectionView.contentSize.width/2
      
      // Scrolling left
      if xPosition < middleX {
        
        let xyPoint = CGPoint(x: xOvershotPosition, y: yPosition)
          
        guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else { return }
        selectCell(for: indexPath, animated: true)
      }
      
      // Scrolling right
      else {
        
        let xyPoint = CGPoint(x: xOvershotPosition, y: yPosition)
          
        guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else { return }
        selectCell(for: indexPath, animated: true)
      }
      
      return
    }

This works well, but we’re still left with an edge case of what happens when the user scrolls to the end of the collection view on either end - left and right. We can determine if they’re at either end of the collection view by looking at the xPosition and the contentSize of the collection view. When the xPosition <= 0 that means that they’re at the left end. When the xPosition >= collectionView.contentSize.width that means that they’re at the right end. Knowing all of that, our final implementation can look something like this:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    let bounds = lensCollectionView.bounds
    
    let xPosition = lensCollectionView.contentOffset.x + bounds.size.width/2.0
    let yPosition = bounds.size.height/2.0
    let xyPoint = CGPoint(x: xPosition, y: yPosition)
    
    guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else {
      let middleX = lensCollectionView.contentSize.width/2
      
      // Scrolling left
      if xPosition < middleX {
        
        let xOvershotPosition = xPosition - 10
        if xOvershotPosition <= 0 || xPosition <= 0 {
          let firstIndex = IndexPath(row: 0, section: 0)
          selectCell(for: firstIndex, animated: true)
          
        } else {
          let xyPoint = CGPoint(x: xOvershotPosition, y: yPosition)
          
          guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else { return }
          selectCell(for: indexPath, animated: true)
        }
      }
      
      // Scrolling right
      else {
        
        let xOvershotPosition = xPosition + 10
        if xOvershotPosition >= bounds.width || xPosition >= bounds.width {
          let lastIndex = IndexPath(row: lensFiltersImages.count - 1, section: 0)
          selectCell(for: lastIndex, animated: true)
          
        } else {
          let xyPoint = CGPoint(x: xOvershotPosition, y: yPosition)
          
          guard let indexPath = lensCollectionView.indexPathForItem(at: xyPoint) else { return }
          selectCell(for: indexPath, animated: true)
        }
      }
      
      return
    }
    
    selectCell(for: indexPath, animated: true)
  }

Hope that helped!

Hi Lea,

Sorry for the circumstance, but it don’t work. The snap to the middle ist fixed with your modification. But Now the the collectionview scroll automatically to the left endpositon when i use the left scroll direction.
Please test it with your chapter 9 Final code if you have the time.

Here the codemodification that i used from you.

First I add the contentInset.

let screenWidth = UIScreen.main.bounds.width
lensCollectionView.contentInset = UIEdgeInsets(top: 0, left: screenWidth/2, bottom: 0, right: screenWidth/2)

then i removed this part:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    
    let middleIndexPath = IndexPath(item: lensFiltersImages.count/2, section: 0)
    selectCell(for: middleIndexPath, animated: false)
}

And replace it with:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    lensCollectionView.setNeedsLayout()
    lensCollectionView.layoutIfNeeded()
    
    let middleIndexPath = IndexPath(item: lensFiltersImages.count/2, section: 0)
    selectCell(for: middleIndexPath, animated: false)
  }

Thanks and greetings
Thomas

This series MUST never end! Please continue creating such amazing content! Learning things while creating those amazing apps will let us apply too much information and create higher quality apps.

1 Like

I really enjoyed this course and it would be great to learn how to create that “semi circle slider” showcased in the intro video :slight_smile:

@skeletom Really glad you like it! Cheers! :]