Part_Time_Nerd
Part_Time_Nerd

Reputation: 1014

How can I add a horizontal stack of buttons above a collection view in Swift?

I am trying to add three buttons above a collection view that pulls either the videos, photos, or all content from the photo library based on what button a user selects.

I managed to create the collection view and the buttons separately but when I try to combine them in a vertical stack view I get an error where I declare the UIStackView.

I am trying to combine a horizontal stack view (the buttons) into a vertical stack view (the button stack view on top and the collection view below).

I think the error is related to how I am declaring the buttonStack but everything I have tried has failed.

I assumed that two stacked views would be the best way to accomplish my goal but I am open to other/better suggestions. Regardless I would like to know why this is not working for me.

Code line and error message:

let stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])

Type of expression is ambiguous without more context

class TestVideoItemCell: UICollectionViewCell {
    
    var stackView = UIStackView()
    var vid = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        vid.contentMode = .scaleAspectFill
        vid.clipsToBounds = true
        self.addSubview(vid)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        vid.frame = self.bounds
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        grabVideos()
    }
    
    //MARK: CollectionView
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return videoArray.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
        cell.vid.image = videoArray[indexPath.item]
        return cell
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.frame.width
        return CGSize(width: width/4 - 1, height: width/4 - 1)
    }
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        myCollectionView.collectionViewLayout.invalidateLayout()
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    
    //MARK: grab videos
    func grabVideos(){
        videoArray = []
        
        DispatchQueue.global(qos: .background).async {
            let imgManager = PHImageManager.default()
            
            let requestOptions = PHImageRequestOptions()
            requestOptions.isSynchronous = true
            requestOptions.deliveryMode = .highQualityFormat
            
            let fetchOptions = PHFetchOptions()
            fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
            
            let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
            print(fetchResult)
            print(fetchResult.count)
            if fetchResult.count > 0 {
                for i in 0..<fetchResult.count{
                    imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
                        self.videoArray.append(image!)
                    })
                }
            } else {
                print("No videos found.")
            }
            DispatchQueue.main.async {

                self.myCollectionView.reloadData()
            }
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    //    MARK: SetUp Methods
    func buttonsStack() {
        let buttonChoices = ButtonStackView()
        buttonChoices.setupButtonsStackView()
    }
    func setupCollection() {
        let layout = UICollectionViewFlowLayout()
        myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
        myCollectionView.backgroundColor = UIColor.white
        self.view.addSubview(myCollectionView)
        
        myCollectionView.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.RawValue(UInt8(UIView.AutoresizingMask.flexibleWidth.rawValue) | UInt8(UIView.AutoresizingMask.flexibleHeight.rawValue)))
        
    }
    func setupStack() {
        let stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.distribution = .fillEqually
        stackView.axis = .horizontal
        stackView.spacing = 8
        
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        ])
    }
}

UPDATE Below is an image example of the general concept of the UI I am trying to create. stack view UI

Upvotes: 0

Views: 1697

Answers (3)

DonMag
DonMag

Reputation: 77672

Try taking things step-by-step...

Start with this code (assigned to a plain UIViewController as the root controller of a UINavigationController):

class TestVideoViewVC: UIViewController {
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
    }

}

If you run the app as-is, you'll see this:

enter image description here

The stack view is there, but we haven't added any subviews to it.

So, let's add two views (labels)... the top one at 50-pts in height:

class TestVideoViewVC: UIViewController {
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // add two arranged subviews, so we can see the layout
        let v1 = UILabel()
        v1.textAlignment = .center
        v1.text = "Buttons will go here..."
        v1.backgroundColor = .green
        
        let v2 = UILabel()
        v2.textAlignment = .center
        v2.text = "Collection view will go here..."
        v2.backgroundColor = .yellow
        
        // let's give the top view a height of 50-pts
        v1.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        
        mainStack.addArrangedSubview(v1)
        mainStack.addArrangedSubview(v2)
    }

}

Run that code, and we get:

enter image description here

Now, let's replace v1 (the "top" label) with a horizontal stack view with 3 red, 50-pt tall buttons:

class TestVideoViewVC: UIViewController {
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // create a horizontal stack view
        let buttonsStack = UIStackView()
        buttonsStack.axis = .horizontal
        buttonsStack.spacing = 8
        buttonsStack.distribution = .fillEqually

        // create and add 3 50-pt height buttons to the stack view
        ["Videos", "Photos", "All"].forEach { str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
            b.backgroundColor = .red
            buttonsStack.addArrangedSubview(b)
            b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        }
        
        // add the buttons stack view to the main stack view
        mainStack.addArrangedSubview(buttonsStack)
        
        // create a label (this will be our collection view)
        let v2 = UILabel()
        v2.textAlignment = .center
        v2.text = "Collection view will go here..."
        v2.backgroundColor = .yellow

        // add the label to the main stack view
        mainStack.addArrangedSubview(v2)
    }

}

Run that code, and we get:

enter image description here

Now we can replace v2 with our collection view:

class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // create a horizontal stack view
        let buttonsStack = UIStackView()
        buttonsStack.axis = .horizontal
        buttonsStack.spacing = 8
        buttonsStack.distribution = .fillEqually

        // create and add 3 50-pt height buttons to the stack view
        ["Videos", "Photos", "All"].forEach { str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
            b.backgroundColor = .red
            buttonsStack.addArrangedSubview(b)
            b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        }
        
        // add the buttons stack view to the main stack view
        mainStack.addArrangedSubview(buttonsStack)
        
        // setup the collection view
        setupCollection()
        
        // add it to the main stack view
        mainStack.addArrangedSubview(myCollectionView)
        
        // start the async call to get the assets
        grabVideos()
    }

    func setupCollection() {
        let layout = UICollectionViewFlowLayout()
        myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
        myCollectionView.backgroundColor = UIColor.white
    }

    //MARK: CollectionView
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return videoArray.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
        cell.vid.image = videoArray[indexPath.item]
        return cell
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.frame.width
        return CGSize(width: width/4 - 1, height: width/4 - 1)
    }
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        myCollectionView.collectionViewLayout.invalidateLayout()
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 1.0
    }
    
    //MARK: grab videos
    func grabVideos(){
        videoArray = []
        
        DispatchQueue.global(qos: .background).async {
            let imgManager = PHImageManager.default()
            
            let requestOptions = PHImageRequestOptions()
            requestOptions.isSynchronous = true
            requestOptions.deliveryMode = .highQualityFormat
            
            let fetchOptions = PHFetchOptions()
            fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
            
            //let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
            let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
            print(fetchResult)
            print(fetchResult.count)
            if fetchResult.count > 0 {
                for i in 0..<fetchResult.count{
                    imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
                        self.videoArray.append(image!)
                    })
                }
            } else {
                print("No videos found.")
            }
            DispatchQueue.main.async {
                
                self.myCollectionView.reloadData()
            }
        }
    }
    
}

class VideoItemCell: UICollectionViewCell {
    
    var stackView = UIStackView()
    var vid = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        vid.contentMode = .scaleAspectFill
        vid.clipsToBounds = true
        self.addSubview(vid)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        vid.frame = self.bounds
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

And we should have this (I just have the default photos, no videos, so I changed grabVideos() to get photos):

enter image description here

Upvotes: 3

Rob
Rob

Reputation: 2164

Well, I will suggest to go ahead programatically. But, I didn't under stood your code a lot, but what I will suggest you that you can take a UIStackView and place it below safe area and then there will be the UICollectionView.

private let stackV: UIStackView = {

   let stackV = UIStackView()
   stackV.axis = .horizontal
   stackV.distribution = .horizontal
   stackV.distribution = .equalSpacing


   return stackV
}()

Then, just create the buttons:

private let collectionB: [UIButtons] = { 

  let buttonA = UIButton()

  let buttonB = UIButton()

  let buttonC = UIButton()

  return [buttonA, buttonB, buttonC]
}()

Them you can add it as an arranged subview to the stackV:

collectionB.forEach { stackV.addArrangedSubview($0) }

This will do it, you have to give a fixed heigh to the stackV and that is it then you can add the collectionV at the bottom and place your constraints accordingly.

I have created views programatically for a long time now and what you are trying to achieve is totally achievable, if I got to look at the UI then things would have been better, what I didn't understood is that why have you added a stackView in your UICollectionViewCell, may be it can be handled in a smoother way.

P.S.: I haven't typed this code on Xcode it may throw some error.

Upvotes: 1

andrija
andrija

Reputation: 342

You might be better off using AutoLayout instead of a vertical UIStackView.

So you should put the Button stack view on the top of the ViewController, and you might want to give it a fixed height. Then, instead of setting the frame of collectionView, set up its constraints, so that its edges stick to the viewController's view, except for top constraint, which should stick to the button stack's bottom constraint.

Side note, interface builder is not necessarily better than creating views programmatically. Personally, I would suggest you stick with the programmatic way.

Upvotes: 1

Related Questions