Nest UICollectionView inside UITableView for independent scrolling
Want to give users a way to casually explore different categories of content without having to tap into a detail page? Independently scrolling rows can help you achieve this effect. Netflix and the App Store use this type of UI for their discovery dashboards.
Since the images are in sort of a grid, a Collection View seems like a natural tool of choice. Collection Views do support scrolling in both directions, but its sections are not designed to scroll independently. This is where Table Views come into the picture.
This tutorial covers how to leverage a Table View to handle vertical scrolling, while using a Collection View for the horizontal part. If you’re new to Collection Views, they’re very similar to Table Views in that they have a Data Source, Delegate, and reusable cells. Previously, we covered Table Views in this intro tutorial.
Update: There is now a Part 2 which covers how to make an API call to IMDB and populate the cells with movie images. It also integrates some of the questions raised in the comments, such as creating data models.
The Approach
The Table View is going to handle vertical scrolling. Each genre (action, drama, etc) is a table section that contains one header and one cell. The key really is to nest a Collection View inside the Table View row. Just remember to point the Collection View DataSource to the table cell rather than the View Controller, and everything will be just fine.
Set Up the Table View
1. Create a new Single View Application using Swift. In Xcode, go to FileNewProject, and choose iOSApplicationSingle View Application.
2. In the Storyboard, drop a Table View into the View Controller, but try to line up the edges before you let go.
3. From the Resolve Auto Layout Issues icon popup, choose Add Missing Constraints. Xcode doesn’t always add the constraints you have in mind, but you can use this when you’re feeling lucky.
4. Control-drag from the Table View to the View Controller and add the DataSource outlet.
5. Within ViewController.swift, add the UITableViewDataSource protocol to the class declaration. It should look something like this:
class ViewController: UIViewController, UITableViewDataSource { }
6. Inside the ViewController class, add an array of the different video genres.
var categories = ["Action", "Drama", "Science Fiction", "Kids", "Horror"]
7. Implement numberOfSectionsInTableView so that there’s one section per genre.
func numberOfSectionsInTableView(tableView: UITableView) -> Int { return categories.count }
8. Implement titleForHeaderInSection so that the section header title is simply the genre name.
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return categories[section] }
9. Implement numberOfRowsInSection so that there’s only one row per section.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }
10. Implement cellForRowAtIndexPath. Use cell as the identifier and CategoryRow as the class.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow return cell }
Note: the compiler will complain about CategoryRow. Don’t worry, you will create this momentarily.
Your ViewController.swift file should look something like this:
import UIKit class ViewController: UIViewController, UITableViewDataSource { var categories = ["Action", "Drama", "Science Fiction", "Kids", "Horror"] func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return categories[section] } func numberOfSectionsInTableView(tableView: UITableView) -> Int { return categories.count } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow return cell } }
11. Now to silence that compiler warning… Create a file (FileNewFile…) and choose iOSSource. Name the file CategoryRow and add the following code to subclass UITableViewCell:
import UIKit class CategoryRow : UITableViewCell { }
12. One final loose end to tie up. Back in the Storyboard, add a prototype cell to the Collection View (edited) Table View. Click on the cell and set the Identifier to cell. Also set the cell class to CategoryRow.
13. Build and Run. You should see something like this:
Set Up the Collection View
The Table View Cell represents the videos within a single genre. Not only does it contain a Collection View, it also serves as its DataSource.
1. Back in the Storyboard, resize theTable View Cell to make it taller. Drop in a Collection View, and line up the constraints.
2. The Collection View already comes with a prototype cell, so set that cell’s Identifier to videoCell.
3. Now for the tricky part — wiring the Collection View’s DataSource and Delegate to the cell. It’s easier if you drag from the Collection View’s Connections Inspector to the Table View Cell within the view hierarchy.
4. To pretty things up, go to the Size Inspector of the Collection View and set the Min Spacing and Section Insets all to 5.
5. On the Attributes Inspector of the Collection View, change the Scroll Direction to horizontal.
6. Switch to the Collection View Cell and set its background color to red.
7. Within CategoryRow.swift, implement the Collection View DataSource method numberOfItemsInSection. For now, hard-code 12 videos per genre.
extension CategoryRow : UICollectionViewDataSource { func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 12 } }
8. Also implement cellForItemAtIndexPath and dequeue the cell using the videoCell identifier we set in the Storyboard earlier.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("videoCell", forIndexPath: indexPath) as! UICollectionViewCell return cell }
9. Almost done! For a Table View Cell’s height, you might be used to heightForRowAtIndexPath. Collection Views use a similar method sizeForItemAtIndexPath that determines both height and width. Drop in the code snippet below to fit a handful of cells on each row at a time.
extension CategoryRow : UICollectionViewDelegateFlowLayout { func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { let itemsPerRow:CGFloat = 4 let hardCodedPadding:CGFloat = 5 let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding let itemHeight = collectionView.bounds.height - (2 * hardCodedPadding) return CGSize(width: itemWidth, height: itemHeight) } }
The width is just the Collection View width divided by four, and the height is just the Collection View height. Don’t worry about the padding adjustments — they were tweaked using trial and error. If anyone remembers their algebra from high school, I’m open to suggestions for a legitimate algorithm!
Your CategoryRow.swift file should look like this:
import UIKit class CategoryRow : UITableViewCell { } extension CategoryRow : UICollectionViewDataSource { func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 12 } func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("videoCell", forIndexPath: indexPath) as! UICollectionViewCell return cell } } extension CategoryRow : UICollectionViewDelegateFlowLayout { func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { let itemsPerRow:CGFloat = 4 let hardCodedPadding:CGFloat = 5 let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding let itemHeight = collectionView.bounds.height - (2 * hardCodedPadding) return CGSize(width: itemWidth, height: itemHeight) } }
10. Build and run, and you should now have independently scrolling rows that look something like this:
More Information
A sample project of this tutorial is available on GitHub.
If you’re using Objective-C, here are some other tutorials on this topic:
- http://ashfurrow.com/blog/putting-a-uicollectionview-in-a-uitableviewcell/
- http://www.raywenderlich.com/4723/how-to-make-an-interface-with-horizontal-tables-like-the-pulse-news-app-part-2
Although Collection Views are a fundamental piece of UI, they can actually get super complicated once you start messing with the layout. If you have a subscription at Ray Wenderlich, you might find these video tutorial series to be really helpful:
Have any suggestions for future tutorial topics? Feel free to add your thoughts to the comments.
Like this post? Please share it below! Then follow us on Twitter @thorntech or join our mailing list for future updates.
Above in 12, it should be add a prototype cell to the Table View not Collection View.
Thanks for catching that @ronm333! I updated the wording on Step 12.
Great content at https://medium.com/@ronm333 by the way.
How can I have a different number of items in the collection view? Thanks
Sorry about the late response! In CategoryRow.swift, you can change line 18 to “return 12” or some other number, in the collectionView:numberOfItemsInSection method.
Amazing — this is exactly what I’ve been trying to create!
Do you think I could create the same thing with UIStackView instead of using UITableView?
Thanks!
UIStackView looks pretty cool, but I think it’s more for laying out pre-determined content, like a form, or the elements inside a single cell. I think you might still need the UITableView for repeating content.
I suppose you could force the use of UIStackView, but you would need to hard-code say eight genres, and you would need to wire up eight separate UICollectionViews. So it’s not quite suited for repeating content as a table/collection would be.
Mixpanel says iOS 8 is still at 28%, and UIStackView is available iOS 9 and later last time I checked. So although it makes constraints a breeze, it’s at the exclusion of a decent chunk of potential users.
How can i get the selected cell by category?, becouse i can get the cell selected but not the cell’s category
Leonardo, sorry to keep you waiting!
It sounds like you want the user to tap on the category, like “Action” or “Drama”. The UITableViewDelegate doesn’t have a method that supports section header selection. So you would probably need a custom view and maybe add a tap gesture.
Best way is to use protocol in this case
Again, sorry for the delayed response.
I created a branch with some sample code for adding a tap gesture to the section headers:
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/sectionHeaderTap
I ended up doing the following:
1. Replaced the built-in section header title with a custom View/cell
2. Programmatically add an UITapGestureRecognizer to the header cell. Note: doing it via Storyboard doesn’t seem to work for repeatable content like cells.
3. The gesture recognizer action has a “view” property. You can figure out the tap coordinates within the header, and convert it to the table view coordinate space.
4. Iterate over each section header of the table view, and see if the tap point is inside any of the headers. Sadly, we can’t use tableView.indexPathForRowAtPoint because they don’t work for section headers.
Hope this helps.
Thanks, you really helped me. Just one more question, when click on a cell, like, i click on the cells of the collectionview of dramas category, how can i know that i am clicking in the cell (cell of collection view ) of that category…I dont know if its clear what i want to say but, what i want to know is, when i click in one of the cells of collection view, i want to know from what category
I just pushed up a commit that prints the name of the category when a video cell is tapped. When instantiating the cells, I just pass forward the current category name. This has to be done twice — once for the table view row (categoryRow), and another for the collection view item (videoCell).
Thanks, you really helped me. Excellent tutorial
HI, i have another problem, i’ve been trying resolve this but i coudn’t… what i want is when a click in the cell of collection view, go to other view, but i want to pass the information of the cell indexPath.row and the selectedCell.categoryName to the other view, but the CategoryRow class doesnt accepts prepareForSegue method or performSegueWithIdentifier. I dont know how can i, click in the cell, go to the other view and pass the informations to the other view
I pushed up another commit to our sectionHeaderTap branch that creates a segue and passes the collection row number and category/genre name.
This is how it works:
1. I dropped in a View Controller and created a
UIViewController
class for it calledDetailPage
2. I created a segue called ShowDetail
3. Only the ViewController can perform the segue, so in order for its children to tell it to do things, I create a protocol:
protocol ShowDetailDelegate {
func showDetail(displayText:String)
}
4. I implement the protocol on the ViewController:
extension ViewController : ShowDetailDelegate {
func showDetail(displayText:String){
performSegueWithIdentifier("ShowDetail", sender: displayText)
}
}
5. To deliver the displayText to the child page, I implement prepareForSegue and drop in the text payload.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let detailPage = segue.destinationViewController as? DetailPage,
let displayString = sender as? String {
detailPage.displayString = displayString
}
}
6. In ViewController, I give the categoryRow a handle to its parent by adding a line to cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow
cell.categoryName = categories[indexPath.section]
cell.showDetailDelegate = self // NEW!!
return cell
}
7. CategoryRow doesn’t have a showDetailDelegate variable so you need to add it:
var showDetailDelegate:ShowDetailDelegate? = nil
8. Now, CategoryRow can trigger the parent’s segue and even pass data:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let selectedCell = collectionView.cellForItemAtIndexPath(indexPath) as? VideoCell {
let displayText = "selected cell number: (indexPath.row) from category: (selectedCell.categoryName)"
showDetailDelegate?.showDetail(displayText)
}
}
9. In order to display the payload in the DetailPage, you just put the displayString into the label’s text in viewDidLoad:
import UIKit
class DetailPage: UIViewController {
@IBOutlet weak var displayLabel: UILabel!
var displayString = ""
override func viewDidLoad() {
displayLabel?.text = displayString
}
}
Hope this makes sense. It’s a bit much to put into comment.
Long story short is that you’re right — a cell cannot perform a segue (only a View Controller can do this). So in order for a cell to tell its parent to do something, you use a protocol. The parent implements the protocol, so the child can tell the parent what to do.
I understand all that you have provided however i constantly get an error that states Could not cast value of type ‘UITableViewCell’ (0x1079c4b80) to ‘CategoryRow’ (0x10507a520).’
Doesnt make sense to me as the class is of type UITableViewCell
Vice, in step 12, when you set the Table View Cell identifier to “cell”, did you also set its Class to “CategoryRow”?
Omitting this step was the only way I was able to reproduce the same error message.
If this doesn’t work for you, would you mind zipping up what you have and emailing it to me robert.chen@thorntechnologies.com? Thanks!
Thanks very much for the tutorial. I am new to Swift and iOS programming and it helped me a great deal, but I am a bit baffled about how to get data into the cells.
Say I wanted to put a title and image into each cell based on a sort returning results that would belong each row. The data retrieval and sorting is easy. What I can’t figure out is how get data to an individual row.
Say I have an array of results for what should go in the “Action” row, another for “Drama”, etc. I have cell class with the appropriate image and title outlets. How can the outlets in the Drama row cells be set with the data from the Drama search?
Hi prwiley,
I pushed up a commit with some sample code to a new branch “titleAndImage”:
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/titleAndImage
Note: this code will not compile, because it assumes that AlamoFire and a special image serializer is setup per this other article: https://www.thorntech.com/2015/07/4-essential-swift-networking-tools-for-working-with-rest-apis/
So, it sounds like you have the label and imageView outlets all wired up already. You would need to add a variable to hold your data model (see line 19 of VideoCell.swift in the commit on the special branch). Notice there’s a “didSet” which observes when the variable changes. Here, you can unpack the data model and set your label and imageView. In the collection view dataSource, you would pass the data model into your custom cell.
There are a few other things you might notice (“currentImageRequest” and “prepareForReuse”). Cells often have garbage from the previous rendering, and they don’t always get overwritten immediately — especially if you’re downloading an image. So these are cleanup steps for wiping clean the image when the cell goes off-screen, as well as cancelling the outbound image download request so it doesn’t come back and clobber the cell after it’s already moved onto a different image.
Hope this makes sense. Please let me know if I need to elaborate on anything.
Rob
Im using a collection view, in Sprite Kit loads game images for navigation at the top of the Game View Controller. How can create a horizontal scroll effect where the image being tap scroll toward user. Would I define a CG Point for the image to scroll to? or would it be float.
Hi Michael,
In order to scroll to a cell when it’s tapped, you can implement a collection view delegate method. This just horizontally centers the tapped cell.
extension CategoryRow : UICollectionViewDelegate {
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
}
Depending on what you’re doing, this method could come in handy. For example, if you have a button inside a cell, you can figure out what cell the button belongs to.
@IBAction func tapButtonInsideCell(sender: UIButton) {
let rootViewPoint = sender.convertPoint(sender.bounds.origin, toView: tableView)
if let indexPath = tableView.indexPathForRowAtPoint(rootViewPoint) {
// scroll or do something
}
}
Does this work for you?
Rob
hii robert i want to display different number of items in every cell of tableview
Dynamically ,But I’m not able to do so on runtime it always display the number of items for the last cell of tableview for every row
Hi Arun,
I created a new branch and committed some code that might be what you’re looking for:
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/dataModel
There needs to be some kind of data model structure. For example, there could be a Video Model that has a bunch of categories like “action” or “kids”. Each category could then have a bunch of videos, like “Terminator” or “Matrix”.
The app architecture has two layers. The table view handles the categories, and the collection view handles the individual videos. So, the table view uses the data model categories array. The category name becomes the section header, and the category object gets passed down to the collection view.
The CategoryRow class (collection view) receives the category object. The category video array determines the number of video cells.
Check out the diff on the latest commit, and let me know what you think. It contains a DataModel.swift file that has structs for VideoModel, Category, and Video. There’s also some glue code in CategoryRow.swift and ViewController.swift.
Rob
Thanks so much Robert , this will exactly do what i needed. Thanks a million
Hi,
can somebody explain to me where the black background color in each cell comes from?
The black background color comes from the Collection View’s background, which defaults to black.
So, in the View Hierarchy, go to:
Table View > cell > Content View > Collection View
Then in the Attributes Inspector, scroll down to Background, which will be set to Default. Default shows up as white in the Storyboard, but as black when deployed to Simulator.
Thanks very much for the tutorial. I am new to Swift and iOS programming and it helped me a great deal, but I am a bit baffled about how to make special style. Special with of red block of ‘Horror’ row for example?
You can change the background color of a table view cell. In ViewController.swift, set the cell background color based on the genre. In this case, horror will be red, and everything else is black:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! CategoryRow
cell.backgroundColor = (categories[indexPath.section] == "Horror") ? .redColor() : .blackColor()
return cell
}
You wont see a change yet, because the CollectionView’s black background is obscuring the cell color. So, the next step is to find the “Collection View” within the View Hierarchy. Then on the Attributes Inspector, change the Background Color from Default to Clear Color.
I also created a new git branch that includes the above changes.
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/redHorror
Is this what you had in mind?
Rob
Thanks for this great tutorial. I’m a newbie, I want to add picture to every cell and when you click on it, direct you to another window with a picture and the description. Also, add a uTube video and a button “server” to play the video directly from the server. “View More” on top right of the main view, table.
Hi Melvin, thanks for sharing these requirements. I can try to answer the first half, since the solution would benefit most people. However, the part about uTube and playing video directly from the server might be too app-specific for me to answer in this forum.
For adding a picture to every cell, there’s actually a lot to it. You would need to make an API call, get an image URL, download the image URL, and then cache it. You might want to check out this other blog post I wrote on using AFNetworking: https://www.thorntech.com/2015/07/4-essential-swift-networking-tools-for-working-with-rest-apis/ Unfortunately, that post needs some updating, since AlamoFire and Swift has been changing at a furious pace.
So this morning, I tried to create a solution that would give you a good head start. It makes real API calls to IMDB. It downloads images. And it opens a detail page, containing the movie image, name, and description. I created a branch on the git repo called “melvin”, to help others associate the git branch with this question thread. https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/melvin
Please checkout the new branch, and give it a try. I will also try to create some instructions, and reply with a link.
Melvin, I just published a mini-blog post on how to make an API call and wire images to the cells. Here’s the link: https://www.thorntech.com/2016/01/scrolling-in-two-directions-like-netflix-part-2-making-api-calls/
Hope this helps,
Rob
Dear Robert Chen
thank you for the great tutorial. It is really useful.
However, I have a problem. I want the collection view appears from right. It means users scroll from right to left. I have searched many in the internet and understood scrollToItemAtIndexPath method will do but I did’t successful. I don’t know where should I put this line of code? and is it necessary to do something with my tableview? I would appreciate if you could please help me. Many thanks.
Hi Mohammad, this is a good question. I don’t think there’s an easy way to do RTL with Collection Views. I think the right approach would be to use a custom collection view layout (there’s one in Objective-C that I’ll try to translate to Swift — I’ll try to do it this weekend if I’m able to get around to it.)
But to elaborate on the problem: iOS 9 supports RTL. You can set this up in Xcode by going to your Scheme > Options > Application Language > Right to Left Pseudolanguage. This works great for Table Views and their built-in Basic cells. The problem though is that Collection Views do not invert their horizontal layout. I’ve even tried raising the deployment target to 9.0 and setting View > Semantic > Force Right-to-Left in the Storyboard to no avail.
One hack I found on stack overflow was to horizontally invert the Collection View , and then horizontally invert each cell. This actually pretty clever:
http://stackoverflow.com/questions/25598194/aligning-right-to-left-on-uicollectionview
On that same stack article, there’s a custom collection view layout in Objective-C. This feels like a more sound approach, but I’d have to first translate it to Swift and do some testing. If it works out, I might write a blog post on this topic.
Finally, check out this WWDC video:
https://developer.apple.com/videos/play/wwdc2015-222/
If you look at the PDF slides, check out slide #51/153 which talks about reversing the math on the custom flow layout. I’ll probably be re-watching this video as well.
Dear Robert Chen
Thank you for your solutions. I have tried all of them however they did’t do what I wanted. Maybe I should have explained more about my problem. what I want exactly is when for the first time the collection view appears it shows the most right hand item in the collection view instead of the most left hand one. I Don’t have any idea how to use scrollToItemAtIndexPath method. I think it is the best and also easiest solution. I would appreciate if you help me. I am waiting to hear from you. thanks a lot.
You could try something like this in CategoryRow:
override func didMoveToSuperview() {
let itemsCount = collectionView.numberOfItemsInSection(0) – 1
let indexPath = NSIndexPath(forItem: itemsCount, inSection: 0)
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Right, animated: true)
}
This approach has several problems though:
1. CategoryRow is a Table View Cell, so it doesn’t have viewDidAppear(). This means you have to rely on something like didMoveToSuperview() which is a later step in the UIView lifecycle, but not really designed for this sort of use case I don’t think.
2. You have to set the animation to true, so the user will see the rows scrolling as they render on the page. This can be disorienting. If you set the animation to false, it doesn’t even scroll.
3. It doesn’t scroll all the way to the end. Not sure why this is, so it’ll take some investigating to fix this.
4. The data is in reverse order, assuming you want to first item to be the one on the right end.
I think the custom collection view layout is the correct approach, even though it’s harder to understand. I’ll try playing around with the ObjC conversion to Swift and see how that goes.
I think you’re right custom collection view layout maybe is a better way. Thanks so much.
FYI, I just added a reply to this in the comments.
Hi Mohammad,
Please disregard some of my earlier comments. You are right, and I was mistaken; scrolling to the item appears to be the correct approach. If you want to see how it works, check out this new branch I created, called “RTL”.
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/RTL
• Turns out the built-in Collection View layout does support RTL layout after all. I added some placeholder labels to show the indexPath.row, and they list the cells in decreasing order when using the RTL scheme.
• The problem is actually a potential bug with the API itself. See http://stackoverflow.com/questions/33130331/uicollectionview-ios-9-issue-on-project-with-rtl-languages-support. Basically, contentOffset.x == 0 uses the left (wrong) edge. So, the collection view will look like [11, 10, 9…] (wrong) instead of […2, 1, 0] (correct).
• The trick then is to get the collection view to scroll to index 0 on load. Since we’re nesting the collection view in a table cell, we have to hook into drawRect() instead of viewDidAppear().
• One gotcha that confused me earlier, is that everything is now backwards. So, rather than trying to scroll the Last item to the .Right, you have to now scroll the First item to the .Left.
Here’s a snippet of the relevant code in CategoryRow:
class CategoryRow : UITableViewCell {
@IBOutlet weak var collectionView: UICollectionView!
override func drawRect(rect: CGRect) {
super.drawRect(rect)
scrollToBeginning()
}
override func prepareForReuse() {
scrollToBeginning()
}
func scrollToBeginning(){
guard collectionView.numberOfItemsInSection(0) > 0 else { return }
let indexPath = NSIndexPath(forItem: 0, inSection: 0)
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: false)
}
}
• As a clean-up step, prepareForReuse() needs to reset each new cell to the First/.Left position.
So, check out the latest commit in the RTL branch. Hope this helps.
Rob
Dear Robert Chen
I’m really happy since my problem solved. I’m very grateful for your useful tutorial, your help and your accountability.
Hi, Robert, your tutorial is so great! I want to display one collection view per page without header just like Kickstarter’s iOS APP do, I tried and I still have no idea. I would appreciate if you help me. I am waiting to hear from you. thanks a lot.
UIPageViewController is designed to display one page at a time. I have a tutorial for this:
https://www.thorntech.com/2015/08/need-to-create-an-onboarding-flow-for-your-mobile-app-heres-how-to-do-it-using-uipageviewcontroller-in-swift/
If you want to stick with UICollectionView (like if there’s more content further down on the page like kickstarter), you can tinker with UICollectionViewDelegateFlowLayout. So within CategoryRow.swift, change itemsPerRow from 4 to 1:
let itemsPerRow:CGFloat = 1
Then within the Storyboard, select the Collection View’s Attributes Inspector and check the box for Paging Enabled.
You should see just a single cell, and swiping it will take you to the next cell.
You can further tweak this so that the black margin to the left is equal to the right side. To do this, go to the Collection View’s Size Inspector and set the following values to 2.5:
The next issue you’ll face is getting the user to understand that the collection view can be scrolled horizontally. You can either add paging dots to overlay the collection view, or adjust the numbers so that a fraction of the next cell is showing at all times (which is what both kickstarter and the App Store app do).
Dear Robert,
Thank you for your tutorials but it’s still not clear how to assign an image to the cell? thank you in advance
Hi Noura,
Adding images to the cell is pretty complicated. There’s adding an Image View and wiring it up to the cell. But there’s also downloading the image asynchronously, cancelling the download when the cell scrolls off the screen, caching images, wiring up an API, and parsing the JSON response.
All this is fairly common though, and there’s actually a part 2 of this blog post that covers all this:
https://www.thorntech.com/2016/01/scrolling-in-two-directions-like-netflix-part-2-making-api-calls/
Good luck!
Robert
why i can’t show the red box … 🙁 only show text and empty collection view
http://stackoverflow.com/questions/36298396/how-to-make-hozirontal-uitableview-with-custom-uitableviewcell/36300544?noredirect=1#comment60228753_36300544
Can you help me … i a problem in your tutorial… 🙁 i try to fix it 2 days, but i can’t… i’m a newbie in swift 🙁
Hi Evan, sorry for the late reply — I was on vacation for the past few days. I responded to your stackoverflow thread. Take a look and let me know what you think.
So thank u for this amazing tutorial.
I wonder how u use Firebase to populate your CollectionView. I can’t quite figure it out yet.
Thank you Robert for this great tutorial, but have a question for you.. have you try implementing this on AppleTV/tvos?
Following these steps, it displays the correct layout, except I can’t focus on individual collection-cells, but when moving it selects a whole row instead (the whole row ‘pops out’ default appletv style) do you have any idea what I’m missing to make this work?
Cristian, did you find the way to fix it? 🙂
I have the same problem :((
Cristian and Darya, sorry for the late reply! To be honest, I haven’t worked with tvOS much at all, so I’ve been reluctant to answer this question.
I just pushed some example code up to GitHub, which seems to be working:
https://github.com/ThornTechPublic/horizontalScroller-tvOS
As Cristian pointed out, the TableViewCell ‘pops out’ and steals the focus. So on ViewController.swift line 27, I implement tableView(_:canFocusRowAtIndexPath:) and simply return `false` to prevent table focus.
The second piece of the puzzle has to do with the CollectionViewCells. By default, they don’t pop out, so it’s not obvious whether they’re receiving focus.
Turns out you can just drop an imageView into the cell. Then on the attributes inspector of the imageView, check the box for “Focus √ Adjust image when focused”. The image should pop out when the collection view cell is focused.
Yoiu’re was help ful to us, how to do performseguewithidentifier from collectionview cell ? can u share source for this ?
You can hold down CTRL and drag from the Collection View Cell to a new View Controller scene. From the popover menu, choose Push.
Check out this branch to see an example. If you select the videoCell, the Connections Inspector will show a Triggered Segue:
https://github.com/ThornTechPublic/HorizontalScrollingCollectionView/tree/kaliyarajalu
To get a handle to the cell that the user tapped, add the following code to ViewController.swift:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let selectedCell = sender as? VideoCell {
print(“sender is the Video Cell”)
}
}
How to setup second section list value of diff
Can you rephrase the question?
hey, how i put the diff images in diff section?
Hi Robert chen,
Thanks for the great tutorial. I need a help, i am little bit confusing in datasource for tableview and collection view. Are you having any blog for creating the datasource. Explaining to create the struct and class.
thanks for the wonderful tutorial. How to assign the dynamic data for the video data in your example.
Dear Robert,
first of all, thank your very much for this tutorial it helped me a lot.
Anyhow i have a question which refers to your implementation of individual numbers of collection view cells with die videoModel.
If you use a lot more than 4 categories, for example 12 or more with an individual number of cells, the view only shows the correct number of collection view cells for the first table view cells. In my case that are 6 table view cells which are visible at the time the view is loaded.
The Problem is that the numbers of collectionview cells of the tableview cell 7 or higher are the same as the first loaded tableview cells.
For example: tableview cell 7 hast the same number of collection cells then tableview cell 0. Tableview cell 8 the same as 1 and so on.
I found out that the function:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return category!.videos.count
}
isnt called when the tableview cell is higher then 7 in my case.
I don’t understand why and how i can change this.
Thanks in advance
I am looking forward to hearing from you
hello
i want scroll collection view in the tableView right to left
i use from this code:
cell.collectionView.scrollToItem(at: (IndexPath(row: 2, section: 0)), at: UICollectionViewScrollPosition.right, animated: true)
in the tableview cell before call collectionView delegate.
but in the first time when tableview load my collection view alignment left to right and after swipe tableview to up and down collection view reload right to left.
how i can set collection view set right to left in the first time tableview load?
Try calling scrollToItem from within the UITableViewCell’s drawRect method. Also, this StackOverflow article might help: http://stackoverflow.com/questions/25598194/aligning-right-to-left-on-uicollectionview/35532697#35532697
Hi Robert,
Thanks for provide this tutorial, very nice.
But I’m quite new to iOS development, and I’m trying to create some horizontal list. But just one, not like what you do by create several per categories. How I can achieve that? And one more question, how to set margin between that square page? Thanks!
For a horizontal list, you can try using a UICollectionView without nesting it inside a TableView row. As for margins, are you referring to the spacing between collection view items? If so, try tweaking the values in sizeForItemAtIndexPath.
hey i want to list different item in different section . can you please update ur code and push it git ?
Within cellForItemAtIndexPath of the collection view data source, you can try making a case statement, and return a different cell type depending on the section.
Hi,
Thanks for the tutorial!
I got a question. Since the table rows are dynamic, they get generated for every category. They are also reusable. Because of this reuseableness, some of my collectionview’s scrolling are “synced”. For example, I have 6 categories, when I scroll the collection in category number 2, the collection in cat number 5 scrolls with it. This happens for cat 1-4 and 3-6 aswell. I guess this has something to do with the reusableness of the collectionviewrows, so that collection in row number 1 is actually the same collection as in row number 4.
Does this happen to you aswell? Do you know what the fault might be? I copy pasted almost eveything from your tut so I guess you would have the same problem.
Good job in finding this bug. You could try scrolling (without animation) the collection view back to it’s origin position when it reappears. Or if scroll position is important, maybe save the scroll position state for each row (as this link suggests http://stackoverflow.com/questions/29529517/ios-swift-save-restore-scroll-position-when-recycling-cells). If you or anyone else comes across an elegant solution, I would love to hear about it!
Hi Robert, thanks for the tutorial – its exactly what I was looking for. However Im getting alot of errors so Im not able to run the app. Have you tried the code on xcode 8.3.2? // Erik
Hey Robert,
Great tutorial mate, thanks a lot for putting it together. Are you able to tell me how to add a footer to each image cell that shows multiple labels? I’m really struggling with the setup of it all.
I have a problem,
https://stackoverflow.com/questions/44626821/collectionviewcell-in-tableviewcell-didnt-retrieve-related-data-in-swift3
Hi
I have one problem in my code.
https://stackoverflow.com/questions/44626821/collectionviewcell-in-tableviewcell-didnt-retrieve-related-data-in-swift3
Hi,
I’ve implemented your tutorial it was great – thanks. I’m dealing some other problem now. I need to have sticky cells in a section – I mean that the whole rows will scroll horizontally together.
Maybe you have an idea or reference for this kind of feature?
It will help me a lot.
Thanks again 🙂
Hi, I’ve a problem.
I receive in ViewController a multiple array like “categories” and in every category we have a lot of videos inside. How we can get from CategoryRow all these content inside in every category. Nothing appears when I simulate the app.
Thank you!
hi, how i can add shopping cart ? with this example?
Hi,
Really this is a great tutorial, but how can it be achieved programmatically without using Storyboard?
This is pretty nice. Just one question. Is there a reason why you didn’t use a “UITableViewController”. You’re using a regular UIViewController with a UITableView. Just wondering if using UITableViewController causes problems for this.
Good point! I should have used UITableViewController instead. Putting a UITableView in a UIViewController was due to my inexperience at the time I wrote this.
Although using UItableViewController is limiting.
Hi Robert,
How do I make the horizontal scroll move one cell at a time? like if I scroll left then it will move only one cell?
Robert, thanks so much for this tutorial.
After the first section, only the first section (Action) is loading, rather than all of the categories. I cannot seem to sort this out. Any thoughts on why this may be?
Did you ever fix this problem? Same thing happens to me.
I had the same issue. For some reason i had the following in my ViewController.swift..
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return categories.count
}
it should have been..
func numberOfSections(in tableView: UITableView) -> {
return categories.count
}
I am in the process of getting myself acquainted with the swift programming code and found your iOS Tutorial: “Want your Swift app to scroll in two diretions, like Netflix? Here’s how” very interesting. I tried to follow your well documented steps. However, when I am building and running step 13 I am getting the following error message “Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value” when the program reaches line “let cell = tableView.dequeueReusableCell(withIdentifier: “cell”) as! CategoryRow”. Do you have any idea to why I am getting this error?
I got the same error and I put (withIdentifier: “Cell”) instead and that fixed it. Check your identifier on the main storyboard to make sure that the case is the same.
I’ve tried your answer & im still getting the error… i’m being told to take out the ! but I think I need it to run my function.
Here’s my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”) as! CategoryRow
return cell
}
Hello, is it possible I do this using fire base?. Thanks.
Hi,
Thanks for the great tutorial.I have few questions,
Is it possible to do the same with xib instead of storyboard?
And what if I want a different number of collection items in each row(category),means in Action – 5, Drame – 10, Science Fiction – 7. How do I implement this?
try this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “yourCellIdentifier”) as! CategoryRow
cell.collectionViewNumberOfItems = moreCategories[indexPath.section].count
cell.stringArray = moreCategories[indexPath.section]
return cell
}
where “var collectionViewNumberOfItems: Int?
var stringArray: [String]?” were declared inside class CategoryRow: UITableViewCell {}
Thanks a lot for this tutorial! I was wondering ways to make a dynamic layout like this but I was getting error from Xcode that I couldn’t figure out how to solve. For me, all the “count” and content are dynamic and I have sorted that thing out also.
Thanks a lot again!
Hey Robert,
I am creating the EPG View. I am almost there but how to make the cell width dynamic during loading the content in collection view. Let me send you link of stackoverflow. I have asked the question but did not get the answer. If you can help me out. https://stackoverflow.com/questions/51264409/i-want-to-create-the-collectionview-like-it-has-in-jiotv-app-this-is-something
Please check the JIOTV app
https://stackoverflow.com/questions/51264409/i-want-to-create-the-collectionview-like-it-has-in-jiotv-app-this-is-something
I am almost there. only issue is making the cell width dynamic at runtime and load the content as well.
Please check this issue
https://stackoverflow.com/questions/53176687/tableview-cell-child-scrollview-is-not-scrolling-horizontally
how to set different numberOfItemsInSection in collection view?
Robert, thanks so much for this detail tutorial.
As you mentioned in this tutorial that connecting the collection views to a data source is the challenge. Since we are storing the information needed for the collection view data source in the table view cell, does this violate the Model-View-Controller which says that views should not have direct access to models?
https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
I have the var categories in the viewController class but when I run the app only the first category “Action” appears. Any suggestions on how to fix that?