3

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:

enter image description here

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.

rooz far
  • 353
  • 4
  • 10

3 Answers3

6

I would do the following.

  1. 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
    }
}


  1. 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")
    }
    
}


  1. 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")
    }
}


  1. 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

mahan
  • 4,950
  • 1
  • 12
  • 42
3

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

  • 2
    Bro he asked question in swift not in swiftUI – Wings Jan 28 '21 at 12:56
  • 1
    @Wings these two aren't opposites or different languages. You can replicate this in a storyboard or in code using the UIKit using the same components. Its just easier writing it this way – Francisco Montaldo Jan 28 '21 at 13:09
  • Can I use this in UIHostingController ? – user2056633 Jan 28 '21 at 13:34
  • yes, UIHostingController is just a child of UIViewController that hosts a SwiftUI View. You should create the SwiftUI view and then use it to initialize the UIHostingController. I recommend to read apple's documentation: https://developer.apple.com/documentation/swiftui/uihostingcontroller Theres an other question that explains the topic: https://stackoverflow.com/questions/56819063/in-swiftui-how-to-use-uihostingcontroller-inside-an-uiview-or-as-an-uiview – Francisco Montaldo Jan 28 '21 at 14:58
  • 2
    This a good slution if you intend to use SwiftUI or integrate it in UIKit. Keep in mind that SwiftUI is compatible with iOS 13+. – mahan Jan 28 '21 at 16:45
  • 1
    @mahan, yes, I am more inclined to do use SwiftUI in my UIKit project as SwiftUI seems to be a lot simpler to achieve things than UIKit. – rooz far Jan 28 '21 at 16:51
  • @manhan Right. Thanks for pointing it out. This code will only work on iphone 6s onwards that are updated to, at least, iOS 13. – Francisco Montaldo Jan 29 '21 at 03:57
2

The best way is to achieve this:-

  1. Create a UITableView for main ViewController.

  2. 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
    
  3. 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.

  1. 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 {



}

UITableViewCell Class

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.

Wings
  • 2,388
  • 16
  • 36
  • 1
    @user2056633....yes please check my answer again ;) – Wings Jan 28 '21 at 14:49
  • @roozfar....Can you show the code what you are doing? – Wings Jan 28 '21 at 15:14
  • @Wings, My code is identical to your example. isn't `let firstVC = FirstViewController() `should be `let firstVC = UIViewController()` ? – rooz far Jan 28 '21 at 15:38
  • Well you are doing wrong mate...just make another ViewController with name FirstViewController subclass of UIViewController() – Wings Jan 28 '21 at 15:41
  • @Wings, sorry for asking silly questions. I'm just learning. what is the purpose of the FirstViewController and SecondViewController ? – rooz far Jan 28 '21 at 15:55
  • Well in FirstViewController and SecondViewController you can make collectionView for separate catogries like FirstViewController for mental fitness category SecondViewController for sleep stories category and so on. – Wings Jan 28 '21 at 15:59
  • https://www.youtube.com/watch?v=B5-1_aR20rE. see this video I hope you will understand :) – Wings Jan 28 '21 at 16:01
  • @Wings, okay, now I feel like I've started to understand the concept. However, don't you think this is gonna be too repetitive if I have too many Categories? also, what if I want to create these ViewControllers programatically so I won't have 100's of Files in my project/app? – rooz far Jan 28 '21 at 16:02
  • If you can manage the api data in one view controller you can try answer of mahan but in that you will end up having lots of complexity in your code but this way you can handle data easily ;). – Wings Jan 28 '21 at 16:11
  • Yeah but this way means every time there's a new Category, I will have to edit the app's code and publish a new version of the app so it can display the new Category. still struggling to understand how this is the best way! – rooz far Jan 28 '21 at 16:36
  • 2
    In regards to performance, It is a horrible solution should you create a UIViewController for each of the horizontal scroll-view. – mahan Jan 28 '21 at 16:42
  • @mahan, Yes, agreed. I quickly figured that out. The only reason I was persistent to find out about Wing's solution was because at the top of his answer, he stated that 'this is the best way...' and I quickly realised that this definitely is not the best way in any shape or form. – rooz far Jan 28 '21 at 16:49