-1

right now this is all I have in my project:

enter image description here

In the end it should look and function pretty like this:

enter image description here enter image description hereenter image description here

1. How do I add items into the ScrollView (in a 2 x X View)

2. How do I make the ScrollView actually be able to scroll (and refresh like in the 3 pictures below) or is this maybe solvable with just a list?

UPDATE

The final view should look like this:

enter image description here

The "MainWishList" cell and the "neue Liste erstellen" (= add new cell) should be there from the beginning. When the user clicks the "add-Cell" he should be able to choose a name and image for the list.

Chris
  • 1,828
  • 6
  • 40
  • 108
  • The example you've given is almost certainly using a `UICollectionView`, which is a subclass of `UIScrollView`. Check out this tutorial: https://www.raywenderlich.com/9334-uicollectionview-tutorial-getting-started – David Chopin Oct 21 '19 at 16:03
  • You add something to your scroll view like a table view or as in your example a collection view. Then you add your items to the data source of the collection view. The scroll view will automatically be scrollable if the content is larger than the display, you really don't have to interact with the scroll view at all. – Joakim Danielson Oct 21 '19 at 16:03
  • @JoakimDanielson so I just add a collection view and put it on top of the scrollView or do I have to connect them somehow? – Chris Oct 21 '19 at 16:09
  • Actually if you use a storyboard and you add a collection view you also get a scroll view as part of it. – Joakim Danielson Oct 21 '19 at 16:14
  • right, got that. Is there a way to add a responsive "add-item" button from storyboard? It should look like the one from the 3 screenshots and also moves to the last place if a items gets added – Chris Oct 21 '19 at 16:20
  • @Chris - collection views have built-in scrolling... you don't need to (and shouldn't) embed it in a scrollView. You probably want to design your app so that the last item (cell) in your collection view is your "add-item" button view. – DonMag Oct 22 '19 at 12:18

1 Answers1

2

Part of the built-in functionality of a UICollectionView is automatic scrolling when you have more items (cells) than will fit in the frame. So there is no need to embed a collection view in a scroll view.

Here is a basic example. Everything is done via code (no @IBOutlet, @IBAction or prototype cells). Create a new UIViewController and assign its class to ExampleViewController as found below:

//
//  ExampleViewController.swift
//  CollectionAddItem
//
//  Created by Don Mag on 10/22/19.
//

import UIKit

// simple cell with label
class ContentCell: UICollectionViewCell {

    let theLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.textAlignment = .center
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {
        contentView.backgroundColor = .yellow
        contentView.addSubview(theLabel)
        // constrain label to all 4 sides
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
            theLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            theLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            theLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
        ])
    }

}

// simple cell with button
class AddItemCell: UICollectionViewCell {

    let btn: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.setTitle("+", for: .normal)
        v.setTitleColor(.systemBlue, for: .normal)
        v.titleLabel?.font = UIFont.systemFont(ofSize: 40.0)
        return v
    }()

    // this will be used as a "callback closure" in collection view controller
    var tapCallback: (() -> ())?

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {
        contentView.backgroundColor = .green
        contentView.addSubview(btn)
        // constrain button to all 4 sides
        NSLayoutConstraint.activate([
            btn.topAnchor.constraint(equalTo: contentView.topAnchor),
            btn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            btn.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            btn.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
        ])
        btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
    }

    @objc func didTap(_ sender: Any) {
        // tell the collection view controller we got a button tap
        tapCallback?()
    }

}

class ExampleViewController: UIViewController, UICollectionViewDataSource {

    let theCollectionView: UICollectionView = {
        let v = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .white
        v.contentInsetAdjustmentBehavior = .always
        return v
    }()

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 100, height: 100),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    // track collection view frame change
    var colViewWidth: CGFloat = 0.0

    // example data --- this will be filled with simple number strings
    var theData: [String] = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemYellow

        view.addSubview(theCollectionView)

        // constrain collection view
        //      100-pts from top
        //      60-pts from bottom
        //      40-pts from leading
        //      40-pts from trailing
        NSLayoutConstraint.activate([
            theCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100.0),
            theCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60.0),
            theCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
            theCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
        ])

        // register the two cell classes for reuse
        theCollectionView.register(ContentCell.self, forCellWithReuseIdentifier: "ContentCell")
        theCollectionView.register(AddItemCell.self, forCellWithReuseIdentifier: "AddItemCell")

        // set collection view dataSource
        theCollectionView.dataSource = self

        // use custom flow layout
        theCollectionView.collectionViewLayout = columnLayout

    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // only want to call this when collection view frame changes
        // to set the item size
        if theCollectionView.frame.width != colViewWidth {
            let w = theCollectionView.frame.width / 2 - 15
            columnLayout.itemSize = CGSize(width: w, height: w)
            colViewWidth = theCollectionView.frame.width
        }
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // return 1 more than our data array (the extra one will be the "add item" cell
        return theData.count + 1
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        // if item is less that data count, return a "Content" cell
        if indexPath.item < theData.count {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ContentCell", for: indexPath) as! ContentCell
            cell.theLabel.text = theData[indexPath.item]
            return cell
        }

        // past the end of the data count, so return an "Add Item" cell
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AddItemCell", for: indexPath) as! AddItemCell

        // set the closure
        cell.tapCallback = {
            // add item button was tapped, so append an item to the data array
            self.theData.append("\(self.theData.count + 1)")
            // reload the collection view
            collectionView.reloadData()
            collectionView.performBatchUpdates(nil, completion: {
                (result) in
                // scroll to make newly added row visible (if needed)
                let i = collectionView.numberOfItems(inSection: 0) - 1
                let idx = IndexPath(item: i, section: 0)
                collectionView.scrollToItem(at: idx, at: .bottom, animated: true)
            })
        }

        return cell

    }

}


// custom FlowLayout class to left-align collection view cells
// found here: https://stackoverflow.com/a/49717759/6257435
class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }
}

When you run this, the data array will be empty, so the first thing you'll see is:

enter image description here

Each time you tap the "+" cell, a new item will be added to the data array (in this example, a numeric string), reloadData() will be called, and a new cell will appear.

enter image description here

Once we have enough items in our data array so they won't all fit in the collection view frame, the collection view will become scrollable:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • this look awesome thanks! But how exactly do I connect it with my code? Right now I have a HomeViewController which is the first picture (without the CollectionView). How do I connect it with the already existing HomeViewController? – Chris Oct 22 '19 at 16:17
  • assigned the ExampleViewController Class to my HomeViewController via Storyboard but all I get is a black screen. Any idea? – Chris Oct 22 '19 at 19:16
  • 1
    Create a new single-view project... Replace everything in the default `ViewController.swift` file with the above code... Open the Storyboard, and change the default view controller Class from `ViewController` to `ExampleViewController`. See if you can run it. If so, then study the code to understand what it's doing and then try to implement something similar into your project. – DonMag Oct 22 '19 at 20:03
  • implemented it in my code now and it works. But first question: How do I change the size of the items? The only thing that i could think of was inside the "let columnLayout" where it even says "itemSize". But that doesnt effect the item size at all – Chris Oct 22 '19 at 21:28
  • Do I have do change something in the FlowLayout Class? I tried 'self.itemSize = CGSize(width: 50, height 50)' but thats not working either – Chris Oct 23 '19 at 11:21
  • 1
    @Chris - Look at the `viewDidLayoutSubviews()` override. Because I used Leading and Trailing constraints on the collection view (instead of a hard-coded width), that's where the view's frame is set... so that's where I set the `itemSize` to get two equal width "columns". If you are giving your collection view a constant width (regardless of device size), then you can remove that code and use whatever size you want in the `let columnLayout =` declaration. – DonMag Oct 23 '19 at 11:53
  • worked perfectly thanks. Next question :D How do I add items in the code? I would like to have on contentCell and the addItemCell at the beginning but i cant figure out how to do it. I tried `self.theData.append`at the beginning of `func collectionVIew -> UICollectionViewCell`but it just gave me an error. – Chris Oct 23 '19 at 13:00
  • @Chris - I strongly recommend that you go through a few tutorials on how to use collection views. In my example, I defined a "ContentCell" and an "AddItemCell". That's where you would setup *your* cell type(s). Presumably, you have some type of data structure in mind for each added cell, and that is the object you would use to track with the data source. – DonMag Oct 23 '19 at 19:44
  • I am really struggling here. I watched a couple of tutorials and I am getting the basics of collectionView now. However I'm still not able to display one content cell and one addItemCell at the beginning. It's either 2 contentCells or 2 addItemCells .. Basically what I am trying to do is returning 2 different cells in `func collectionView` – Chris Oct 25 '19 at 11:50
  • @Chris - if you put your project up on GitHub (or a file-share site) I'll take a look and give you some tips. – DonMag Oct 25 '19 at 11:59
  • @Chris - is your "WishListCell" supposed to replace the "ContentCells"? Or, is your goal to have Content Cells followed by a WishListCell and an AddItemCell? – DonMag Oct 25 '19 at 12:34
  • my goal is to have one permanent wishlist cell, one permanent addItemCell and the other cells should be contentCells (which in the end should be configurable by the user -> cellBackgroundImage) – Chris Oct 25 '19 at 12:37
  • I hope you you understand what I'm trying to say :D – Chris Oct 25 '19 at 12:37
  • 1
    @Chris - I put the edited ViewController.swift file here: https://pastebin.com/2u92UC2V ... review the comments beginning with `// DonMag -` – DonMag Oct 25 '19 at 12:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/201413/discussion-between-chris-and-donmag). – Chris Oct 25 '19 at 13:08
  • just opened our chat again, could you help me out please?:) – Chris Nov 04 '19 at 18:05
  • In your code you implemented `Flowlayout` to left-align the `cells`. How can I change that so its centered? – Chris Nov 12 '19 at 18:31
  • 1
    @Chris - google search for `uicollectionview center align layout` ... you'll find many resources, examples, etc. – DonMag Nov 12 '19 at 19:33
  • I found this (https://stackoverflow.com/questions/13588283/how-to-center-align-the-cells-of-a-uicollectionview) but I can not figure out where I can set `cellSize` – Chris Nov 12 '19 at 21:19
  • I Implemented the second answer – Chris Nov 12 '19 at 21:21