My view controller has a UISegmentedControll with 6 buttons/segment. It also has a UITableView under that Segmented Controll. If we switch segment by click then table view is reloading with different data. Now we are trying to implement a way where if user swipe the table view either in left-to-right or right-to-left, a pagination like behaviour will be appeared in the view controller. Means the table view will horizontally scrolled along with segmented controll. How can we implement this feature in my app in best approach ? My table has three different custom cell, one of which also has a UICollectionView inside the cell. Pls help me out. Thanks.
The idea I would suggest is to use tableView inside collectionView. Steps to follow-:
1) Add segmentedControl and collectionView on your controller.
2) Connect dataSource and delegate by control dragging from collectionView to controller yello icon.
3) Adjust your cell height.
4) Select collectionView, and go to attribute inspector. Look for-: 1) scroll direction and make horizontal. 2) Check Paging Enabled parameter.
5) Apply Constraints on views.
6) Give collectionView cell identifier.
7) Give your cell a custom collectionView cell class.
8) Connect all Outlets to respected views.
Controller class -:
import UIKit
class ViewController: UIViewController {
// OUTLETS
#IBOutlet weak var controls: UISegmentedControl!
#IBOutlet weak var horizontalCollectionView: UICollectionView!
// ARRAy OF TYPE UICOLOR
var collectionViewColors = [UIColor.gray,UIColor.green,UIColor.red,UIColor.yellow,UIColor.blue,UIColor.brown]
// ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
}
//didReceiveMemoryWarning
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// Function to calculate cell index on scroll end.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>){
let pagingIndex = targetContentOffset.pointee.x/self.view.frame.width
// Set segmented controll current index
controls.selectedSegmentIndex = Int(pagingIndex)
}
}
// Collection view methods
extension ViewController : UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
// number of section
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
// number of items in section
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionViewColors.count
}
// deque cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collection", for: indexPath) as! HorizontalCell
cell.horizonatlColorsView.backgroundColor = collectionViewColors[indexPath.item]
return cell
}
// set minimum line spacing to zero.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
Custom cell class-:
import UIKit
class HorizontalCell: UICollectionViewCell {
// UIView outlet
#IBOutlet weak var horizonatlColorsView: UIView!
}
After setting up everything , scroll horizontally you will get the output you want.Also you can add tableView inside collectionView now.
Output-:
Related
So i have a horizontal UICollectionView of images that is inside a vertical UICollectionView and i want to detect which cell is in the center on the horizonal UICollectionView when i select a cell from the vertical one
I tried to send a notification and call a function that does the work but its called multiple times since its reusing the same cell so at the end i dont get the appropriate indexPath.
And also when i tap on an image the "didSelectItemAt" of the horizontal collectionView is called, is there a way to get the vertical one to be called instead ?
thanks
Your best bet would be delegates via protocols
Create protocol for your collection view cells
protocol yourDelegate: class {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) -> Void
}
In your cells, create a function called setup which you can call at cellForRow.
In your cells, create a touch recogniser for self.
Disable cell selection for your collection views since these delegates will be called when the user touches given cell.
class yourCell: UICollectionViewCell {
var indexPath: IndexPath? = nil
var collectionView: UICollectionView? = nil
weak var delegate: yourDelegate? = nil
override func awakeFromNib() {
super.awakeFromNib()
let selfTGR = UITapGestureRecognizer(target: self, action: #selector(self.didTouchSelf))
self.contentView.addGestureRecognizer(selfTGR)
}
#objc func didTouchSelf() {
guard let collectionView = self.collectionView, let indexPath = self.indexPath else {
return
}
delegate?.didSelectCell(WithIndecPath: indexPath, InCollectionView: collectionView)
}
func setupCell(WithIndexPath indexPath: IndexPath, CollectionView collectionView: UICollectionView, Delegate delegate: yourDelegate) {
self.indexPath = indexPath
self.collectionView = collectionView
self.delegate = delegate
}
}
In your viewController, create extension for this protocol and if you do everything right, when the user touches your cell, the cell will call you via this delegation.
extension YourViewController: yourDelegate {
func didSelectCell(WithIndecPath indexPath: IndexPath, InCollectionView collectionView: UICollectionView) {
//You have your index path and you can "if" your colllection view
if collectionView == self.yourFirstCollectionView {
} else if collectionView == self.yourSecondCollectionView {
}
//and so on..
}
}
since protocol is ": class", we can use weak for your delegate property so no memory leak will occur. I use this method for tableViews and collectionViews throughout my projects.
I have a collection view with multiple cells. Users can scroll horizontality. However, I want it to center the nearest cell once the user lets go. Or, if not, have some form of paging but for individual cells. I'm making basically an icon/profile picture picker.
#IBOutlet weak var profilePicScrollView: UIScrollView!
var profilePicsArray = [#imageLiteral(resourceName: "sample_user_photo"), #imageLiteral(resourceName: "settings_icon"), #imageLiteral(resourceName: "usernametextfield_dark")]
#IBOutlet weak var profilePicCollectionView: UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return profilePicsArray.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionat section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageViewCellReuseID", for: indexPath) as! SettingsCollectionViewCell
cell.imageInCollection.image = profilePicsArray[indexPath.row]
cell.imageInCollection.backgroundColor = UIColor.getRandomColor()
return cell
}
Assuming your collection view's cells are equal to the width of your collection view's bounds, you could just set the collection view's isPagingEnabled property to true – considering UICollectionView inherits from UIScrollView.
If that's not the case, then you might try the following:
First, implement the scrollViewDidEndDragging(_:willDecelerate:) method of your collection view's delegate. Then, in that method's body, determine which cell in collectionView.visibleCells is most visible by comparing each of their centers against your collection view's center. Once you find your collection view's most visible cell, scroll to it by calling scrollToItem(at:at:animated:).
I am an android developer and developing my first iOS app, I need to implement GridView in order to add some social icon so after some search I found UICollectionView but it's not working as expected. How to set dynamic height of the UICollection view? Actually I want to display all icons but it showing only a row and others after scrolling the UICollectionView.
below is the example:
This is what I am getting
This is what I want:
I am using below code:
import UIKit
class CollectionViewDemo: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var socialHandleCollection: UICollectionView!
#IBOutlet weak var socialImageView: UIImageView!
var socialHandleArray:[String] = ["Facebook", "Twitter", "Youtube","Vimeo", "Instagram", "Custom URL", "Linkedin", "pinterest"]
override func viewDidLoad() {
self.socialHandleCollection.delegate = self
self.socialHandleCollection.dataSource = self
// socialHandleCollection.frame.size.height = 130
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return socialHandleArray.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: colvwCell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! colvwCell
cell.imgCell.image = UIImage(named: "demo_img.png")
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print(self.socialHandleArray[indexPath.row])
}
}
Any help would be appreciated.
Assuming you are using autoLayout
Initially declare a specific number of columns that you desire
let numberOfColumns = 5
Okay so first things first. You will have to make your class conform to the UICollectionViewDelegateFlowLayout. Now implement the function -
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize{
return CGSize(width: collectionView.bounds.size.width/numberOfColumns, height: collectionView.bounds.size.width/numberOfColumns);
}
Here 5 can be changed to the number of columns you want in each row and passing same value as height will ensure that the shape is always square. And hence you can apply corner radius to make it circular.
Now moving on. From your interface builder, ctrl + click and drag the UICollectionView height constraint to your UIViewController (similar to how you would do for a UIView but do it for the constraint)
Now once you know the number of items that you need to display inside your UICollectionView, you can do something like:
//replace 5 with the number of columns you want
//array contains the items you wish to display
func figureOutHeight(){
if(array.count == 0){
//as no items to display in collection view
return
}
//ceil function is just to round off the value to the next one, for example 6/5 will return 1 but we need 2 in this case. Ensure all arguments inside ceil function are float
let rowsCount = ceil(Float(array.count)/Float(numberOfColumns))
//now you have number of rows soo just update your height constraint by multiplying row count with height of each item inside UICollectionView
collectionViewHeightConstraint.constant = rowsCount * collectionView.bounds.size.height / numberOfColumns;
view.layoutIfNeeded()
}
Also if you haven't change the scroll direction to vertical.
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 :)
I am trying to make a transition animation like the demonstration in the link here. So when I clicked the cell, it expands and covers the whole screen.
Here are my codes(I have to admit that I am not familiar with CollectionView)`
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var mainDesLabel: UILabel!
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var secDesLabel: UILabel!
let searchBar = UISearchBar()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.backgroundColor = UIColor.clearColor()
////////////////////////////////////////////////////////////////////////
self.searchBar.frame = CGRect(x: 175, y: 0, width: 200, height: 50)
self.searchBar.searchBarStyle = UISearchBarStyle.Minimal
self.searchBar.backgroundColor = UIColor.whiteColor()
self.view.addSubview(searchBar)
////////////////////////////////////////////////////////////////////////
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as UICollectionViewCell
cell.layer.cornerRadius = 5
return cell
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
//Use for size
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.collectionView.frame = self.view.bounds
let cell = collectionView.cellForItemAtIndexPath(indexPath)
cell!.frame = CGRectMake(0, 0, self.view.bounds.width, self.view.bounds.height)
}
}
So I thought use 'didSelectItemAtIndexPath' would help, however it turns out like this
thoughts? Any help would be highly appreciated!
Or what you can do is expand the item and change its frame with UIAnimation.
And when he cell is tapped, you get the views inside the cell to be expanded also using auto layout and I'm hinting towards (clips to bounds).
something like this:
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let item = collectionView.cellForItemAtIndexPath(indexPath) as! cellCollectionViewExpress // or whatever you collection view cell class name is.
UIView.animateWithDuration(1.0, animations: {
self.view.bringSubviewToFront(collectionView)
collectionView.bringSubviewToFront(item)
item.frame.origin = self.view.frame.origin /// this view origin will be at the top of the scroll content, you'll have to figure this out
item.frame.size.width = self.view.frame.width
item.frame.size.height = self.view.frame.height
})
}
I would suggest you that you use UICollectionView Controller, so things are at ease in general with using that.
You cannot just use didSelectItemAtIndexPath or any similar methods to update the size of a UICollectionViewCell once the UICollectionView is done performing the view layout.
To update cell height,
You can first capture which cell had been tapped in didSelectItemAtIndexPath.
Then, you can either reload the entire collection view with the new cell frame being passed in the sizeForItemAtIndexpath.
Or, you can just reload the specific cell with reloadItemsAtIndexPaths, but you still need to pass the updated size of the cell via sizeForItemAtIndexpath.
UPDATE
I now see the question details have been updated by an animation which you desire to have.
I had performed a similar animation by:-
Capturing the cell which had been tapped in didSelectItemAtIndexPath.
Adding a replica view to the UIViewContrller, but with its frame totally coinciding with the cell which had been tapped.
Then, animating this view which had been added. Thus giving an impression that the cell was animated.
Any additional functionality which has to be given can also be written in this view. Thus the code of the cell and the animated view is separated too.
When a cell tapped or a button or any tappable thing got tapped inside the cell, then you get the call from
didSelectItemAtIndexPath or through delegate, then to give the cell the required size, you have to invalidate the layout of the current collectionview. After this, size for item will get called and give the new size for the,
This will update the size of the collectioncell without reloading it.
You can give animation also.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.invalidateLayout()
}
}
extension UICollectionView {
func transactionAnimation(with duration: Float, animateChanges: #escaping () -> Void) {
UIView.animate(withDuration: TimeInterval(duration)) {
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(duration))
CATransaction.setAnimationTimingFunction(.init(name: .default))
animateChanges()
CATransaction.commit()
}
}
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
collectionView.transactionAnimation(with: 0.3) {
self.carInfoCollectionView?.performBatchUpdates({}, completion: { _ in })
}
}