Can I add a collection view to a collection view section header? - ios

So I'm trying to figure out how to add two collection views on the name view controller. Right now the current collection view I have is a vertical scrolling collection view that displays users posts on the feed. I would like to add a "people to follow" section that scrolls horizontally on the top. Please note I would like the horizontal collection view to scroll down with the whole view.
Something that will look like this
I thought about adding a section header, then trying to add a collection view in that but I'm not sure if that is an illegal configuration.
I'm also not sure if I need to add 2 sections in the number of sections line.
Here is the collection view code currently.
#IBOutlet weak var collectionView: UICollectionView!
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if posts.count > 4 {
if indexPath.item == posts.count - 1 {
fetchPosts()
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if viewSinglePost {
return 1
} else {
if posts.count == 0 {
self.collectionView.setEmptyMessage("You haven't followed anyone yet.")
} else {
self.collectionView.restore()
}
return posts.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostsCell", for: indexPath) as! FollowingCell
cell.delegate = self
if viewSinglePost {
if let post = self.post {
cell.post = post
}
} else {
cell.post = posts[indexPath.item]
}
handleUsernameLabelTapped(forCell: cell)
handleMentionTapped(forCell: cell)
handleHashtagTapped(forCell: cell)
return cell
}
Right now its just a basic feed that will fetch posts users upload. I thought it would be great user experience to include a people to follow section for new uses. What is the best way I should go about this?

Seems like you want to add two collection views, one is for upper that is horizontal and the lower is vertical
Add two collection views in the storyboard, give the first one a
static height, take outlet from the storyboard and give outlet name something like UpperCollectionView and LowerCollectionView
set delegate and data source to the collectionViews
upperCollectionView.delegate = self
upperCollectionView.dataSource = self
lowerCollectionView.delegate = self
lowerCollectionView.dataSource = self
in case of all delegate methods , implement the collectionViews using if - else method
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == upperCollectionView {
//do code for upperCollectionView
} else {
//do code for lowerCollectionView
}
}
if look like complex, then I will suggest you to split, the upper part in the parent viewController and lower part in a container view.
If you want to scroll up the whole thing like a tableView scrolling, add all of this on a UIScrollView , this will handle the scrolling.

if you ask me i would suggest a table view with 2 cells, fist cell is for collection view and second cell is your normal cell. in that way you can scroll in both directions. But if you want to use collectionview only then you need to add tag for collection view and configure it according to your tag i.e
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView.tag == 1 {
// your horizantal collection view
} else {
//vertical one
}

You could define the first index as another collection view.

Related

Swift UICollection view different number of cells per row

How can I have a UICollection View, with One cell in the first row, and three cells in the second row?
I want to display a picture in the first row, and below it 3 pictures.
You need to use collectionView's Datasource method number of section to specify how many section you required in your collectionview. in that section you can pass number of cell you want to display. and in cell for item you can set data according to section and Item value.
try like this.
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 3 // you can return as per your requirement
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 2 // number of cell you want to display in each section
}else if section == 1 {
return 3 // number of cell you want to display in each section
}else {
return 4 // number of cell you want to display in each section
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifierCollection, for: indexPath) as! HomeCollectionViewCell
return cell
}
You need to implement your own custom layout. By default the collection view has UICollectionViewFlowLayout, that fills the cells in a horizontal/vertical grid.
You can change this behaviour with your own layout, i.e. a class implementing UICollectionViewLayout.
To get started, I would recommend checking this and this tutorials.

Rearranging different-sized items of UICollectionView with UICollectionViewFlowLayout

Let's say I have a UICollectionView with a UICollectionViewFlowLayout, and my items are different sizes. So I've implemented collectionView(_:layout:sizeForItemAt:).
Now let's say I permit the user to rearrange items (collectionView(_:canMoveItemAt:)).
Here's the problem. As a cell is being dragged and other cells are moving out of its way, collectionView(_:layout:sizeForItemAt:) is called repeatedly. But it's evidently called for the wrong index paths: a cell is sized with the index path for the place it has been visually moved to. Therefore it adopts the wrong size during the drag as it shuttles into a different position.
Once the drag is over and collectionView(_:moveItemAt:to:) is called, and I update the data model and reload the data, all the cells assume their correct size. The problem occurs only during the drag.
We clearly are not being given enough information in collectionView(_:layout:sizeForItemAt:) to know what answer to return while the drag is going on. Or maybe I should say, we're being asked for the size for the wrong index path.
My question is: what on earth are people doing about this?
The trick is to implement
override func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath,
toProposedIndexPath prop: IndexPath) -> IndexPath {
During a drag, that method is called repeatedly, but there comes a moment where a cell crosses another and cells are shoved out of the way to compensate. At that moment, orig and prop have different values. So at that moment you need to revise all your sizes in accordance with how the cells have moved.
To do that, you need to simulate in your rearrangement of sizes what the interface is doing as the cells move around. The runtime gives you no help with this!
Here's a simple example. Presume that the user can move a cell only within the same section. And presume that our data model looks like this, with each Item remembering its own size once collectionView(_:layout:sizeForItemAt:) has initially calculated it:
struct Item {
var size : CGSize
// other stuff
}
struct Section {
var itemData : [Item]
// other stuff
}
var sections : [Section]!
Here's how sizeForItemAt: memoizes the calculated sizes into the model:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
if memosize != .zero {
return memosize
}
// no memoized size; calculate it now
// ... not shown ...
self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
return sz
}
Then as we hear that the user has dragged in a way that makes the cells shift, we read in all the size values for this section, perform the same remove-and-insert that the interface has done, and put the rearranged size values back into the model:
override func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath, toProposedIndexPath
prop: IndexPath) -> IndexPath {
if orig.section != prop.section {
return orig
}
if orig.item == prop.item {
return prop
}
// they are different, we're crossing a boundary - shift size values!
var sizes = self.sections[orig.section].rowData.map{$0.size}
let size = sizes.remove(at: orig.item)
sizes.insert(size, at:prop.item)
for (ix,size) in sizes.enumerated() {
self.sections[orig.section].rowData[ix].size = size
}
return prop
}
The result is that collectionView(_:layout:sizeForItemAt:) now gives the right result during the drag.
The extra piece of the puzzle is that when the drag starts you need to save off all the original sizes, and when the drag ends you need to restore them all, so that when the drag ends the result will be correct as well.
While the accepted answer is pretty clever (props to you Matt ๐Ÿ‘), it's actually an unnecessarily elaborate hack. There is a MUCH simpler solution.
The key is to:
Store cell sizes within the data itself.
Manipulate or "rearrange" the data at the point when the "moving" cell enters a new indexPath (NOT when the cell finishes moving).
Fetch cell sizes directly from the data (which is now properly arranged).
Here's what this would look like...
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// (This method will be empty!)
// As the Docs states: "You must implement this method to support
// the reordering of items within the collection view."
// However, its implementation should be empty because, as explained
// in (2) from above, we do not want to manipulate our data when the
// cell finishes moving, but at the exact moment it enters a new
// indexPath.
}
override func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
// This will be true at the exact moment the "moving" cell enters
// a new indexPath.
if originalIndexPath != proposedIndexPath {
// Here, we rearrange our data to reflect the new position of
// our cells.
let removed = myDataArray.remove(at: originalIndexPath.item)
myDataArray.insert(removed, at: proposedIndexPath.item)
}
return proposedIndexPath
}
// MARK: UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Finally, we simply fetch cell sizes from the properly arranged
// data.
let myObject = myDataArray[indexPath.item]
return myObject.size
}
Based on Matts answer I have adapted the code to fit for UICollectionViewDiffableDataSource.
Track the indexPath wile moving the cells:
/// Stores remapped indexPaths during reordering of cells
var changedIndexPaths = [IndexPath: IndexPath]()
func collectionView(_ collectionView: UICollectionView,
targetIndexPathForMoveFromItemAt orig: IndexPath,
toProposedIndexPath prop: IndexPath) -> IndexPath {
guard orig.section == prop.section else { return orig }
guard orig.item != prop.item else { return prop }
let currentOrig = changedIndexPaths[orig]
let currentProp = changedIndexPaths[prop]
changedIndexPaths[orig] = currentProp ?? prop
changedIndexPaths[prop] = currentOrig ?? orig
return prop
}
Calculate size of cells:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// remap path while moving cells or use indexPath
let usedPath = changedIndexPaths[indexPath] ?? indexPath
guard let data = dataSource.itemIdentifier(for: usedPath) else {
return CGSize()
}
// Calculate your size for usedPath here and return it
// ...
return size
}
Reset the indexPath map (changedIndexPaths) after final movement of cell is finished:
class DataSource: UICollectionViewDiffableDataSource<Int, Data> {
/// Is called after an cell item was successfully moved
var didMoveItemHandler: ((_ sourceIndexPath: IndexPath, _ target: IndexPath) -> Void)?
override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
didMoveItemHandler?(sourceIndexPath, destinationIndexPath)
}
}
dataSource.didMoveItemHandler = { [weak self] (source, destination) in
self?.dataController.reorderObject(sourceIndexPath: source, destinationIndexPath: destination)
self?.resetProposedIndexPaths()
}
func resetProposedIndexPaths() {
changedIndexPaths = [IndexPath: IndexPath]() // reset
}

How to prevent dequeued collection view cells from overlapping each other?

I've been trying to set up a collection view where I have the user submit several strings which I toss in an array and call back through the collection view's cellForItemAt function. However, whenever I add a row to to the top of the collection view, it adds the cell label literally on top of the last cell label so they stack like this. Notice how every new word I add includes all the other previous words in the rendering.
The code I have at cellForItemAt is
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "InterestsCell", for: indexPath) as? InterestsCell {
cell.interestLabel.text = array[indexPath.row]
return cell
} else {
return UICollectionViewCell()
}
}
and the code I have when the add button is pressed is
func addTapped() {
let interest = interestsField.text
array.insert(interest!, at: 0)
interestsField.text = ""
collectionView.reloadData()
}
I'm not sure what's going on. I looked everywhere and tried to use prepareForReuse() but it didn't seem to work. I later tried deleting cells by calling didSelect and the cells would not disappear again. Any help is appreciated!
This is the code I have in my custom collection view cell implementation in the event that this is causing the error
To do this paste these functions in your project
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1
}
you can play around with the values :)
This can be easily implemented using UITableView. So try to use UITableView instead of UICollectionView.
It looks like you're adding a new label every time you set the text. If you want to lazily load the UILabel you need to add lazy
lazy var interestLabel: UILabel = {
...
}()
I wouldn't do it this way though, I would create a reference to the label
weak var interestLabel: UILabel!
Then add the label in your setupViews method
func setupViews() {
...
let interestLabel = UILabel()
...
contentView.addSubview(interestLabel)
self.interestLabel = interestLabel
}

How to remove linebreak from UICollectionViewFlowLayout

I am trying to remove line breaks for the items within my section using the UICollectionViewFlowLayout .
At the moment I have
|header |
|0-0 0-1 0-2 0-3|
|0-4 0-5 |
|header |
|1-0 1-1 1-2 0-3|
|1-4 1-5 |
and I need :
|header |
|0-0 0-1 0-2 0-3| 0-4 0-5
|header |
|1-0 1-1 1-2 1-3| 1-4 1-5
so users can horizontally scroll . On iOS i resolve this by creating two nested collectionViews but on tvOS I am unable to replicate the solution because i am unable to focus on the inner cells .
after several tries I override the preferredFocusedView variable on the tableCell where the nested UICollectionView is :
override var preferredFocusedView: UIView? {
return moviesCollection
}
This behavior allows me to swipe horizontally between the inner elements of the collection but i cannot swipe vertically to change between table cells.
I am kind of lost ,Any kind of help would be greatly appreciated.
Thanks .
This can be solved very quickly with a custom UICollectionViewLayout
Subclass UICollectionViewLayout & override the following properties and methods
var collectionViewContentSize: CGSize
func prepare()
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
Make sure to return the correct frame for each cell
Here is my working solution that also conveniently provides some properties of UICollectionViewFlowLayout (itemSize, minimumInteritemSpacing, minimumLineSpacing).
You might not need to change anything and just add it to your project.
https://gist.github.com/kflip/52ec15ddb07aa830d583856909fbefd4
Feel free to add scrollDirection support ... its scrolling in both directions already, but you might want to change if one section should be displayed as column instead of a row...
You could also add FlowLayout-like protocol functionalities if you need more customisations based on IndexPaths (e.g. CellSize)
Updated
Here's a GIF showing the end result:
Original
I've just created a test ViewController and this seems to work alright.
Basically, what I have there is a UICollectionView with a vertical flow layout. Each cell of that collectionView has it's own UICollectionView set with an horizontal flow layout.
The only incovenient for the "out of the behavior" is that the horizontal flow layour collection view does not "remember" the selected item when coming back to it but that can be solved with a little bit of playing around.
Hope it helps. Here's my code for it:
class ViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [collectionView]
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "ParentTestCell", for: indexPath)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "TestHeader", for: indexPath)
}
}
// Parent cell - included in the vertical collection view & includes the horizontal collection view
class ParentTestCell: UICollectionViewCell, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [collectionView]
}
// MARK: - UICollectionViewDataSource
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 {
return collectionView.dequeueReusableCell(withReuseIdentifier: "TestCell", for: indexPath)
}
}
// Child cell - included in the horizontal collection view
class TestCell: UICollectionViewCell {
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.isFocused {
self.backgroundColor = .red
}
else {
self.backgroundColor = .blue
}
}, completion: nil)
}
}
And here's the storyboard structre:
Just a suggestion if it can help you.
Add tableView
Create Custom TableVeiwCell which contains collection view inside the cell with horizontal scrolling.
for each section you have, will become number rows for tableview
for items in row becomes number of items for the respective collection view.
A UICollectionView lay out its cells in a very specific way when using UICollectionViewFlowLayout, I'm not sure it's possible to make that kind of behaviour with a collectionview, whitout making your own UICollectionViewLayout. See this guide on how to make a UIColloctionView with a custom layout.
I would suggest trying to solve the problem, by using nested UITableViews. Make a vertical scrolling tableview, where every cell in the tableview has it's own horizontal scrolling tableview. The cells in the horizontal tableview, will be the actual cells that the user can select. See this guide on how to do this.
This should work in tvOS as well, since the focus engine should look through the cells subviews, and find the focusable ones. I hope it helps :)

UICollectionView not loading fully until I scroll

I have a collection view that I want to display hourly weather in. I seem to have a problem with loading the cell, and for some reason scrolling forwards and then back loads the cell fully. Before I scroll the collection view, all of the constraints do not work and one label doesn't show it's info.
Before scrolling
After scrolling (this is how I want the cells to look like)
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return newhourlyWeather.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! hourlyWeatherCell
// Configure the cell
let hWeather = newhourlyWeather[indexPath.row]
if let HourlyTemp = hWeather.temperatureh {
cell.temperatureHLabel.text = "\(HourlyTemp)ยบ"
}
if let HourlyTime = hWeather.convertedTimeH {
cell.timeHLabel.text = "\(HourlyTime)"
}
if let HourlyRain = hWeather.precipProbabilityh {
cell.rainChanceHLabel.text = "\(HourlyRain)%"
}
cell.iconhView.image = hWeather.iconh
return cell
self.collectionView.reloadData()
}
Seems like you populate your cells asynchronously, if so then add a mycollectionview.reloadData() at the end.
I fixed the problem by adding cell.layoutIfNeeded() before the return cell. Everything loaded as expected without any scrolling!
I had the same issue and I solved calling cell.layoutIfNeeded() inside collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath).
Also if you are using UICollectionViewDiffableDataSource and you are applying the snapshot inside the viewWillAppear, you need to add some delay to make it work correctly like this:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
// apply here the snapshot
}

Resources