Multiple Horizontal ScrollViews In One Vertical ScrollView in Swift? - ios

I'm trying to achieve one of the most standard layouts in the apps in Swift.
which is basically having Multiple Horizontal ScrollViews In One Vertical ScrollView.
Each of these sub-Horizontal ScrollViews Will hold a few views with images.
Something like this:
what is the best way of achieving this?
P.S. I need to do this using code as the content is pulled via a remote JSON file.
any pointers would be appreciated.

I would do the following.
Use a UITableView for the vertical scroll-view.
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
self.tableView.register(TableViewHeader.self, forHeaderFooterViewReuseIdentifier: TableViewHeader.identifier)
self.tableView.dataSource = self
self.tableView.delegate = self
}
}
extension TableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier,
for: indexPath) as! TableViewCell
return cell
}
}
extension TableViewController {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 250
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: TableViewHeader.identifier)
header?.textLabel?.text = "Header \(section)"
return header
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
}
Use a UICollectionView for the horizontal-scroll-view.
class CollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.backgroundColor = .white
self.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.identifier)
self.dataSource = self
self.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier,
for: indexPath) as! CollectionViewCell
cell.index = indexPath.row
return cell
}
}
extension CollectionView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 200, height: 250)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
class CollectionViewCell: UICollectionViewCell {
static let identifier = "CollectionViewCell"
var index: Int? {
didSet {
if let index = index {
label.text = "\(index)"
}
}
}
private let label: UILabel = {
let label = UILabel()
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.backgroundColor = .red
self.contentView.addSubview(label)
let constraints = [
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
]
NSLayoutConstraint.activate(constraints)
label.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Each UITableViewCell contains a UICollectionView (horizontal-scroll-view).
class TableViewCell: UITableViewCell {
static let identifier = "TableViewCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .white
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let subView = CollectionView(frame: .zero, collectionViewLayout: layout)
self.contentView.addSubview(subView)
let constraints = [
subView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 1),
subView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 1),
subView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 1),
subView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 1)
]
NSLayoutConstraint.activate(constraints)
subView.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Use a UITableViewHeaderFooterView (tableView(_:viewForHeaderInSection:) ) for the title of the horizontal-scroll-view
class TableViewHeader: UITableViewHeaderFooterView {
static let identifier = "TableViewHeader"
}
The code that I have added a complete working example. Just use the TableViewController and you are good to go.
Update
UITableViewCells should have dynamic cell height.
Afte testing, I found out that it is better you use fixed cell-size instead of dynamic because cell might have different height should use UITableView.automaticDimension

Pretty easy with SwiftUI.
You should use a the VStack inside a ScrollView for the vertical one;
and a HStack inside a ScrollView for the horizontal one.
here's an example:
struct ContentView: View {
var body: some View {
ScrollView{
ForEach(0..<10) {_ in
VStack{
ScrollView(.horizontal){
HStack(spacing: 20) {
ForEach(0..<10) {
Text("Item \($0)")
.font(.headline)
.frame(width: 100, height: 100)
.background(Color.gray)
}
}
}
}
}
}
}
}
I made a ForEach to replicate the example items in each stack but you should replace them with your custom views or content. In the picture you uploaded each item is a ZStack with an image and text.
image of compiled code

Create a UITableView for main ViewController.
The views you have to create inside it make their separate ViewController.
For ex:- for mental fitness - Create separate mental fitness ViewController for that
for sleep stories - Create separate Sleep Stories ViewController
Now the climax come here called addChild() method.
Access your all ViewControllers in your main ViewController class and add them in your viewDidLoad() method inside addChild() method.
The last thing you have to do is you just have to add that child ViewControllers in your particular cell as view.
For reference you can check these examples:-
https://www.hackingwithswift.com/example-code/uikit/how-to-use-view-controller-containment
https://www.swiftbysundell.com/basics/child-view-controllers/
Advantage:-
This way you can easily manage your data coming from the server
For example:-
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
//subViewControllers
let firstVC = FirstViewController()
let secondVC = SecondViewController()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.addChild(firstVC) //adding childVC here
self.addChild(secondVC)
}
}
UITableViewDataSource and Delegate Method
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 250
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? MainCell else {
return UITableViewCell()
}
if indexPath.row == 0 {
cell.contentView.addSubview(firstVC.view)
}
if indexPath.row == 1 {
cell.contentView.addSubview(secondVC.view)
}
return cell
}
}
UITableViewCell Class
class MainCell: UITableViewCell {
}
This way you can easily manage your data which is coming from server. Because it will give you an advantage for showing particular cell data in separate ViewController and much more.

Related

I'm trying to load a collectionView in a TableViewController and adding the collectionView in the subView of tableView

This is the complete code which configures the tableView controller.
class SearchViewControllerTableViewController: UITableViewController, UISearchBarDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var users = [User]()
var filteredUsers = [User]()
var searchBar = UISearchBar()
var inSearchMode = false
var collectionView: UICollectionView!
var collectionViewEnabled = true
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(SearchUserCellTableViewCell.self, forCellReuseIdentifier: reusableIdentifier)
tableView.separatorStyle = .none
tableView.alwaysBounceVertical = true
tableView.keyboardDismissMode = .onDrag
configureSearchBar()
configureCollectionView()
fetchUsers()
print("Collection View \(collectionView.isHidden)")
}
This is the code for configuring collectionView.
func configureCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - (tabBarController?.tabBar.frame.height)! - (navigationController?.navigationBar.frame.height)!)
collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.alwaysBounceVertical = true
collectionView.backgroundColor = .white
tableView.addSubview(collectionView)
collectionView.register(SearchPostCell.self, forCellWithReuseIdentifier: reusableIdentifierCollectionView)
}
This is code for tableView. Which loads up some users in the app.
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if inSearchMode {
return filteredUsers.count
} else {
return users.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reusableIdentifier, for: indexPath) as! SearchUserCellTableViewCell
var user: User!
if inSearchMode {
user = filteredUsers[indexPath.row]
} else {
user = users[indexPath.row]
}
cell.user = user
cell.selectionStyle = .none
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 69
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var user: User!
if inSearchMode {
user = filteredUsers[indexPath.row]
} else {
user = users[indexPath.row]
}
// Instance for user profile VC
let userProfileView = UserProfileCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
// Assign the selected user.
userProfileView.user = user
// Navigate to the selected user's profile
navigationController?.pushViewController(userProfileView, animated: true)
userProfileView.setNavigationBar()
}
This is the code for collectionView dataSource and delegate.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (view.frame.width - 2) / 3
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reusableIdentifierCollectionView, for: indexPath) as! SearchPostCell
return cell
}
On top of the tableView I want to add a CollectionView which should be hidden by default.
But the collectionView is not showing up. But on loading up the view "collectionView.isHidden" returns false
You should not try to add subviews directly to table views or collection views.
This line: tableView.addSubview(collectionView) won't work.
Further, UITableViewController and UICollectionViewController are very specialized view controllers that will only manage a table view/collection view. You can't add other subviews to those view controller classes.

Synchronised Scrolling UICollectionViews in UITableViewCell in Swift

I have the structure like this:
UITableView -> UITableViewCell -> UICollectionView ->
UICollectionViewCell
So what I’m trying to achieve is that I want to make UICollectionViews in UITableViewCells to scroll synchronised. For example when you scroll manually the first UICollectionView on the first row, I want the rest of UICollectionViews to follow, but the Text Labels to stay in the same position all the time. (Please see the image below)
EDIT: I know that I have to use contentOffset somehow, but don’t know how to implement in this case scenario. Any help would be appreciated.
Click to see the image
Click to see the gif
Okay I managed to get this working, Please keep in mind the code is just for the question purposes and contains lot of non-generic parameters and force casting that should be avoided at any cost.
The class for MainViewController containing the tableView:
protocol TheDelegate: class {
func didScroll(to position: CGFloat)
}
class ViewController: UIViewController, TheDelegate {
func didScroll(to position: CGFloat) {
for cell in tableView.visibleCells as! [TableViewCell] {
(cell.collectionView as UIScrollView).contentOffset.x = position
}
}
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as? TableViewCell else { return UITableViewCell() }
cell.scrollDelegate = self
return cell
}
}
The class for your tableViewCell:
class TableViewCell: UITableViewCell {
#IBOutlet var collectionView: UICollectionView!
weak var scrollDelegate: TheDelegate?
override func awakeFromNib() {
super.awakeFromNib()
(collectionView as UIScrollView).delegate = self
collectionView.dataSource = self
}
}
extension TableViewCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! CollectionViewCell
cell.imageView.image = #imageLiteral(resourceName: "litecoin.png")
return cell
}
}
extension TableViewCell: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollDelegate?.didScroll(to: scrollView.contentOffset.x)
}
}
The class for the collectionViewCell is irelevant since it's just implementation detail. I will post this solution to github in a second.
Disclaimer: This works just for visible cells. You need to implement the current scroll state for the cells ready for reuse as well. I will extend the code on github.
I came up with a working solution you can test on a playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyCollectionCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate {
var originatingChange: Bool = false
var observationToken: NSKeyValueObservation!
var offsetSynchroniser: OffsetSynchroniser? {
didSet {
guard let offsetSynchroniser = offsetSynchroniser else { return }
collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
observationToken = offsetSynchroniser.observe(\.currentOffset) { (_, _) in
guard !self.originatingChange else { return }
self.collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
}
}
}
lazy var collection: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 40, height: 40)
layout.scrollDirection = .horizontal
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.backgroundColor = .white
collection.dataSource = self
collection.delegate = self
collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
return collection
}()
override func layoutSubviews() {
super.layoutSubviews()
collection.frame = contentView.bounds
contentView.addSubview(collection)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 1
cell.backgroundColor = .white
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
originatingChange = true
offsetSynchroniser?.currentOffset = scrollView.contentOffset
originatingChange = false
}
}
class OffsetSynchroniser: NSObject {
#objc dynamic var currentOffset: CGPoint = .zero
}
class MyViewController : UIViewController, UITableViewDataSource {
var tableView: UITableView!
let offsetSynchroniser = OffsetSynchroniser()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
tableView = UITableView(frame: .zero, style: .plain)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(tableView)
tableView.dataSource = self
tableView.register(MyCollectionCell.self, forCellReuseIdentifier: "cell")
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.selectionStyle = .none
cell.collection.reloadData()
cell.offsetSynchroniser = offsetSynchroniser
return cell
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
To make it work with a playground you will see a lot of code that if you are using storyboards or xib is not needed. I hope anyway that the base idea is clear.
Explanation
Basically I created an object called OffsetSynchroniser which has an observable property called currentOffset. Each cell of the tableView accepts an offsetSynchroniser and on didSet they register with KVO for notifications of currentOffset changes.
Each cells also registers to its own collection's delegate and implements the didScroll delegate method.
When any of those collectionView causes this method to be triggered the currentOffset var of the synchroniser is changed and all the cells that are subscribed through KVO will react to the changes.
The Observable object is very simple:
class OffsetSynchroniser: NSObject {
#objc dynamic var currentOffset: CGPoint = .zero
}
then your tableViewCell will have an instance of this object type and on didSet will register with KVO to the var currentOffset:
var originatingChange: Bool = false
var observationToken: NSKeyValueObservation!
var offsetSynchroniser: OffsetSynchroniser? {
didSet {
guard let offsetSynchroniser = offsetSynchroniser else { return }
collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
observationToken = offsetSynchroniser.observe(\.currentOffset) { (_, _) in
guard !self.originatingChange else { return }
self.collection.setContentOffset(offsetSynchroniser.currentOffset, animated: false)
}
}
}
The originatingChange variable is to avoid that the collectionView that is actually initiating the offset change will react by causing the offset to be re-set twice.
Finally, always in your TableViewCell, after registering itself as collectionViewDelegate you will implement the method for didScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
originatingChange = true
offsetSynchroniser?.currentOffset = scrollView.contentOffset
originatingChange = false
}
In here we can change the currentOffset of the synchroniser.
The tableViewController will at this point just have the ownership for the synchroniser
class YourTableViewController: UItableViewController { // or whatever ViewController contains an UITableView
let offsetSynchroniser = OffsetSynchroniser()
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCollectionCell
...
cell.offsetSynchroniser = offsetSynchroniser
return cell
}
}
The best way I can think of off the top of my head to do something like this would be to store all of your collectionViews in a collection object. You can then use the UIScrollView's scrollViewDidScroll delegate method from those collectionViews. Just make sure you have your delegate set correctly.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for view in collectionViewCollection where view.scrollView != scrollView{
view.scrollView.contentOffset = scrollView.contentOffset
}
}
This is untested code, so not a complete answer but it should get you started.

How to avoid collection view flickering effect within tableview when changing flowlayouts and provide a smoother transition?

I have a collectionview embedded within a tableview. The collectionview can have either a vertical or horizontal flowlayout. When the user makes an action, the tableviewcell height changes from 200 to full tableview height and the collection view changes flowlayout. However, the collectionView flickers since there are not enough cells loaded yet when performing the layout change from horizontal to vertical. Also, when reverting to the horizontal flowLayout, there is no transition at all. Not sure why the transition occurs when changing flowLayout from horizontal to vertical but not vertical to horiztontal. How can I avoid the flicker when changing the layout and continue to utilize Apple's transition animation when reverting to the horizontal layout?
GitHub
class ViewController: UIViewController {
// If horizontal layout, then display 3 rows (with heights 200)
// If veritcal layout, then display 1 row (with height of full tableview height)
var isHorizontal = true
#IBOutlet weak var tableView: UITableView! {
didSet {
tableView.dataSource = self
tableView.delegate = self
}
}
#IBAction func barButtonTapped(_ sender: UIBarButtonItem) {
isHorizontal = !isHorizontal
sender.title = isHorizontal ? "Vertical" : "Horizontal"
tableView.reloadData()
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isHorizontal ? 3 : 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EmbeddedTableViewCell", for: indexPath) as! EmbeddedTableViewCell
cell.setup(isHorizontal: isHorizontal)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return isHorizontal ? 200 : tableView.frame.height
}
}
class EmbeddedTableViewCell: UITableViewCell {
#IBOutlet weak var collectionView: UICollectionView! {
didSet {
collectionView.dataSource = self
collectionView.delegate = self
}
}
fileprivate let horizontalFlowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 10
flowLayout.minimumLineSpacing = 10
flowLayout.scrollDirection = .horizontal
return flowLayout
}()
fileprivate let verticalFlowLayout: UICollectionViewFlowLayout = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 10
flowLayout.minimumLineSpacing = 10
flowLayout.scrollDirection = .vertical
return flowLayout
}()
func setup(isHorizontal: Bool) {
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
if isHorizontal && flowLayout.scrollDirection != .horizontal {
collectionView.setCollectionViewLayout(horizontalFlowLayout, animated: true)
} else if !isHorizontal && flowLayout.scrollDirection != .vertical {
collectionView.setCollectionViewLayout(verticalFlowLayout, animated: true)
}
}
}
extension EmbeddedTableViewCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SimpleCollectionViewCell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: 150)
}
}

Collection View inside Table View Cell

I want to create collection view inside the table view cell for horizontal scrolling of elements. But I have the problem how to fill right my collection views. I need a way to connect collection views with my data.
How can I get and loop through Table View Cells to find needed collection view and return the cells with data?
below is a general example of how you can pass data to collection views inside your table view cell. Also this link is a youtube tutorial on this subject.
Models:
class ListOfParents: NSObject {
var parents:[Parent]?
}
class Parent: NSObject {
var children: [Child]?
static func fetchParents(_ completionHandler: #escaping (ListOfParents) -> ()) {
//fetch parents data
}
}
class Child: NSObject {
}
TableView cell:
class CustomTableViewController: UITableViewController {
var listOfParents: ListOfParents?
override func viewDidLoad() {
super.viewDidLoad()
Parent.fetchparents { (listOfParents) in
self.listOfParents = listOfParents
}
tableView.register(CustomParentCell.self, forCellReuseIdentifier: "tableCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let parentsCount = listOfParents?.parents?.count else {return 0}
return parentsCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! CustomParentCell
cell.parent = listOfParents?.parents?[indexPath.item]
return cell
}
}
Parent cell:
class CustomParentCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource {
var parent: Parent? {
didSet {
// set child value here
}
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
return collectionView
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(CustomChildCell.self, forCellWithReuseIdentifier: "childCellID")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let childrenCount = parent?.children?.count else {return 0}
return childrenCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "childCellID", for: indexPath) as! CustomChildCell
cell.child = parent?.children?[indexPath.item]
return cell
}
}
Child cell:
class CustomChildCell: UICollectionViewCell {
var child: Child?
}

UICollectionView Multiple Sections and Headers

I am struggling trying to do multiple sections in my collection view with a header for each section. I don't know Obj-C and I've found a good amount of tutorials for it, but haven't been able to figure out how to convert it into Swift.
All my data is static so all I need is some sort of array or dictionary that I can use to create the multiple sections. I already have a collection view with 1 section working, so if you have any insight or code for multiple sections that'd be helpful.
I know how to set multiple sections using
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return sectionData.count
}
I think the main thing I need help with is implementing this func
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { }
and setting up the data!
UICollectionView and UITableView are almost exactly the same, so if you know how to do multiple sections in a UITableView in Swift, your help is also appreciated
The cellForItemAtIndexPath function handles populating each section with cells, it does not handle sections or supplementaryViews, and therefore is not the main thing you need help with when it comes to creating section headers.
the method you need to implement is viewForSupplementaryElementOfKind. Its signature is:
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {}
Assuming that your collectionView is working correctly for 1 section (you have properly filled out the body of your cellForItemAtIndexPath and your sectionData array properly reflects the number of sections you want to display), you should be able to implement section headers using the following pointers:
Along with cells, UICollectionView also supports "supplementary" view objects, typically used for headers or footers. These Supplementary Views act very similarly to UICollectionViewCell objects. In the same way that cellForItemAtIndexPath handles cells, The viewForSupplementaryElementOfKind function handles supplementary views.
To implement it, you will need to first prepare your ViewController to do so. First edit your layout object to reflect an appropriate header size, that each header will adhere to:
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.headerReferenceSize = CGSize(width: self.view.frame.size.width, height: 30)
NOTE: I am using a UICollectionViewFlowLayout
Next, if you haven't already done so, create a SectionHeader class that defines each section header object, so you can then register that class with your collectionView object like so:
collectionView!.registerClass(SectionHeaderView.self, forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "SectionHeaderView");
Here, the first and third argument passed in are the same as a UICollectionViewCell Class registration, the first argument in this method is the reference to the section header class you created. The third is the reuse identifier for the Supplementary View.
The second argument is specific to Supplementary Views, this sets the kind of the SupplementaryView, which in this case is a header, the constant string provided by the UICollectionViewFlowLayout class UICollectionElementKindSectionHeader is used for it. If you noticed the parameters on the viewForSupplementaryElementOfKind, this kind is later passed in as the kind: String parameter.
Fill in the body of your viewForSupplementaryElementOfKind the same way you would for a cellForItemAtIndexPath function-- Using the dequeueReusableSupplementaryViewOfKind method to create a SectionHeader object, then set any attributes as necessary (labels, colors, etc.) and finally return the header object.
Hope this helps!!
Reference points:
https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UICollectionViewDataSource_protocol/index.html#//apple_ref/occ/intfm/UICollectionViewDataSource/
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewFlowLayout_class/index.html#//apple_ref/c/data/UICollectionElementKindSectionHeade
Define your UICollectionViewCell which will be your Header view of kind UICollectionElementKindSectionHeader - In my case I have two headers - OfferHeaderCell and APRHeaderCell defined as below:
verticalCollectionView.register(UINib(nibName: "OfferHeaderCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "OfferHeaderCell")
verticalCollectionView.register(UINib(nibName: "APRHeaderCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "APRHeaderCell")
Go ahead and return a header for each section and then set the size of the section header to have a size of zero in this UICollectionViewDelegateFlowLayout function
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if(section==0) {
return CGSize.zero
} else if (section==1) {
return CGSize(width:collectionView.frame.size.width, height:133)
} else {
return CGSize(width:collectionView.frame.size.width, height:100)
}
}
Important to define the viewForSupplementaryElementOfKind for two different sections as below:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
var reusableview = UICollectionReusableView()
if (kind == UICollectionElementKindSectionHeader) {
let section = indexPath.section
switch (section) {
case 1:
let firstheader: OfferHeaderCell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "OfferHeaderCell", for: indexPath) as! OfferHeaderCell
reusableview = firstheader
case 2:
let secondHeader: APRHeaderCell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "APRHeaderCell", for: indexPath) as! APRHeaderCell
reusableview = secondHeader
default:
return reusableview
}
}
return reusableview
}
And lastly the Datasource,
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if (section==2) {
return 2
}
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = verticalCollectionView.dequeueReusableCell(withReuseIdentifier: "ReviseOfferCell", for: indexPath)
cell.backgroundColor = UIColor.white
return cell
}
Note: Don't forgot to add UICollectionFlowLayout as below:
// MARK: UICollectionViewDelegateFlowLayout
extension MakeAnOfferController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return CGSize(width: self.view.frame.size.width, height: 626.0)
}
return CGSize()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if(section==0) {
return CGSize.zero
} else if (section==1) {
return CGSize(width:collectionView.frame.size.width, height:133)
} else {
return CGSize(width:collectionView.frame.size.width, height:100)
}
}
}
Here is the code that worked for me
create the header cell. To do which i created a custom cell class and a nib to do the customization of the cell in the graphic editor
In viewDidLoad add the following
self.collectionView?.registerNib(UINib(nibName: "KlosetCollectionHeaderViewCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")
Then you add the delegate function
override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> KlosetCollectionHeaderViewCell {
let headerCell = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "HeaderCell", forIndexPath: indexPath) as? KlosetCollectionHeaderViewCell
return headerCell!
}
This will put the HeaderCell in the SectionView of the PFCollectionView
The controls that show in the cell you add them to the xib file as well as the outlets and actions
Here is the code to achieve UICollection multiple sections made programmatically using SnapKit
ViewController
import SnapKit
import UIKit
class SelectIconViewController: GenericViewController<SelectIconView>, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
weak var delegate: SpaceAddViewController?
struct Section {
var sectionName : String
var rowData : [String]
}
var sections : [Section]!
init(delegate: SpaceAddViewController) {
self.delegate = delegate
super.init()
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
contentView.closeButton.addTarget(self, action: #selector(self.back), for: .touchUpInside)
self.sections = [
Section(sectionName: "SPACES", rowData: ["Air Conditioner", "Apple HomePod"]),
Section(sectionName: "HOME APPLIANCES", rowData: ["Ceiling Fan", "Fan", "Desk Lamp", "Iron", "PC on Desk", "Plug", "Power Strip", "Lorem", "Lorem", "Lorem", "Lorem"]),
]
self.contentView.collectionView.dataSource = self
self.contentView.collectionView.delegate = self
self.contentView.collectionView.register(SelectIconHeaderViewCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SelectIconHeaderViewCell.reuseId)
self.contentView.collectionView.register(SelectIconViewCell.self, forCellWithReuseIdentifier: SelectIconViewCell.reuseId)
}
#objc func back() {
self.dismiss(animated: true, completion: nil)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.sections.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.sections[section].rowData.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: getTotalSpacing(), height: getTotalSpacing())
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width-40
return CGSize(width: screenWidth-80, height: 50)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
// MARK: Cells
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.contentView.collectionView.dequeueReusableCell(withReuseIdentifier: SelectIconViewCell.reuseId, for: indexPath as IndexPath) as! SelectIconViewCell
cell.initializeUI()
cell.createConstraints()
cell.setValues(iconName: "", label: self.sections[indexPath.section].rowData[indexPath.row])
return cell
}
// MARK: Header
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let cell = self.contentView.collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SelectIconHeaderViewCell.reuseId, for: indexPath) as! SelectIconHeaderViewCell
cell.initializeUI()
cell.createConstraints()
cell.setTitle(title: self.sections[indexPath.section].sectionName)
return cell
default: fatalError("Unexpected element kind")
}
}
func getTotalSpacing() -> CGFloat {
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let numberOfItemsPerRow:CGFloat = 3
let spacingBetweenCells:CGFloat = 0
let sideSpacing:CGFloat = 20
return (screenWidth-(2 * sideSpacing) - ((numberOfItemsPerRow - 1) * spacingBetweenCells))/numberOfItemsPerRow
}
}
The View:
import UIKit
import SnapKit
class SelectIconView: GenericView {
private let contentView = UIView(frame: .zero)
private (set) var closeButton = UIButton(type: .system)
internal var collectionView: UICollectionView!
internal override func initializeUI() {
self.backgroundColor = Theme.Color.white
self.addSubview(contentView)
contentView.addSubview(closeButton)
if let image = UIImage(named: "icon_close") {
image.withRenderingMode(.alwaysTemplate)
closeButton.setImage(image, for: .normal)
closeButton.tintColor = Theme.Color.text
}
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
contentView.addSubview(collectionView)
collectionView.backgroundColor = Theme.Color.background
}
internal override func createConstraints() {
contentView.snp.makeConstraints { (make) in
make.top.equalTo(safeAreaLayoutGuide.snp.top).priority(750)
make.left.right.equalTo(self).priority(1000)
make.bottom.equalTo(safeAreaLayoutGuide.snp.bottom)
}
closeButton.snp.makeConstraints { make in
make.right.equalTo(safeAreaLayoutGuide.snp.right).offset(-10)
make.top.equalTo(contentView.snp.top).offset(10)
make.height.equalTo(40)
make.width.equalTo(40)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(closeButton.snp.bottom).offset(20)
make.left.equalTo(safeAreaLayoutGuide.snp.left)
make.right.equalTo(safeAreaLayoutGuide.snp.right)
make.bottom.equalTo(contentView.snp.bottom)
}
}
}
The customized section Header
import UIKit
class SelectIconHeaderViewCell: UICollectionViewCell {
internal let mainView = UIView()
internal var title = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initializeUI() {
self.backgroundColor = UIColor.clear
self.addSubview(mainView)
mainView.backgroundColor = UIColor.clear
mainView.addSubview(title)
title.text = "Pick nameA"
title.font = Theme.Font.body()
title.textAlignment = .left
title.textColor = Theme.Color.text
title.numberOfLines = 1
}
internal func createConstraints() {
mainView.snp.makeConstraints { (make) in
make.edges.equalTo(self)
}
title.snp.makeConstraints { (make) in
make.centerY.equalTo(mainView.snp.centerY)
make.leading.equalTo(mainView).offset(20)
make.trailing.equalTo(mainView).offset(-20)
}
}
func setTitle(title: String) {
self.title.text = title
}
static var reuseId: String {
return NSStringFromClass(self)
}
}
And the cell:
import UIKit
class SelectIconViewCell: UICollectionViewCell {
internal let mainView = UIView()
internal var iconImage = UIImageView()
internal var label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initializeUI() {
self.backgroundColor = UIColor.clear
self.addSubview(mainView)
mainView.backgroundColor = UIColor.clear
mainView.layer.masksToBounds = true
mainView.layer.borderColor = Theme.Color.backgroundCell.cgColor
mainView.layer.borderWidth = 1.0
mainView.addSubview(iconImage)
iconImage.image = UIImage(named: "icons8-air-conditioner-100")
mainView.addSubview(label)
label.font = Theme.Font.footnote()
label.textAlignment = .center
label.textColor = Theme.Color.textInfo
label.numberOfLines = 1
}
internal func createConstraints() {
mainView.snp.makeConstraints { (make) in
make.edges.equalTo(self)
}
iconImage.snp.makeConstraints { (make) in
make.center.equalTo(mainView.snp.center)
make.width.height.equalTo(20)
}
label.snp.makeConstraints { (make) in
make.top.equalTo(iconImage.snp.bottom).offset(6)
make.leading.equalTo(mainView).offset(5)
make.trailing.equalTo(mainView).offset(-5)
}
}
func setValues(iconName: String, label: String) {
//self.iconImage.image = UIImage(named: iconName)
self.label.text = label
}
static var reuseId: String {
return NSStringFromClass(self)
}
}
After creating and registering custom header (and/or footers), you can easily specify different header (or footers for that matter) for different section. Here's an example:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let section = indexPath.section
switch section {
case 0:
let userHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: userHeaderReuseIdentifier, for: indexPath) as! UserHeader
return userHeader
default:
let postHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: postHeaderReuseIdentifier, for: indexPath) as! PostHeader
return postHeader
}
case UICollectionElementKindSectionFooter:
let userFooter = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: userFooterReuseIdentifier, for: indexPath) as! UserFooter
return userFooter
default:
return UICollectionReusableView()
}
}
Make sure to specify correct number of sections, too:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
Worked solution for Swift-3
i)Create Custom Cell && corresponding xib
class SectionHeaderView: UICollectionViewCell {
static let kReuseIdentifier = "SectionHeaderView"
#IBOutlet weak var invitationsSectionHeader: UILabel!
#IBOutlet weak var numberOfPerson: UILabel!
}
ii)Register Custom Collection View Cell for HeaderView
self.collectionView.register(UINib(nibName: SectionHeaderView.kReuseIdentifier, bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: SectionHeaderView.kReuseIdentifier)
iii)Call delegate function to render Custom Header View.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView: SectionHeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeaderView.kReuseIdentifier, for: indexPath) as! SectionHeaderView
return headerView
default:
return UICollectionReusableView()
}
}
iv)Mention Height of the Custom Header View
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width:collectionView.frame.size.width, height:30)
}
#Tarun's answer worked a treat for me; I was missing collectionView(_:layout:referenceSizeForHeaderInSection:), which I needed since sometimes the data to be shown would be sorted and sometimes not.
In addition, pinning the section header to the top of the screen (as in the table view Apple's Address Book app) was accomplished by adding the following to viewDidLoad() in the UICollectionViewController:
if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.sectionHeadersPinToVisibleBounds = true
}

Resources