Question:
How do I transition a UICollectionViewCell I've assigned a button for a UICollection to another View Controller?
Summary:
I've created a UICollection, and UICollectionCells for each image I have.
I create a CollectionCell object for each image I find.
I'm able to create images and buttons for each object, but once I set the target of the button for the cell, I cannot redirect to another ViewController.
Attempts:
I read some other attempts, but they all required that the function be called from the original view controller.
Starting the view controller from within the current view controller:
let secondViewController:SecondViewController = SecondViewController()
self.presentViewController(secondViewController, animated: true, completion: nil)
}
Calling a function from the view controller:
button.addTarget(self,action:#selector(YourControllerName.buttonClicked(_:)),
forControlEvents:.TouchUpInside)
Current Code:
class CollectionViewCell: UICollectionViewCell {
var text: String!
var image_view: UIImageView!
var button: UIButton!
override func awakeFromNib() {
print("Number:" + card_text)
image_view = UIImageView(frame: contentView.frame)
image_view.contentMode = .scaleAspectFill
mage_view.clipsToBounds = true
contentView.addSubview(image_view)
let x_value = contentView.frame.width - contentView.frame.width
let y_value = (contentView.frame.height)/3
let rectangle = CGRect(x: x_value, y: y_value, width: contentView.frame.width, height: 40)
button = UIButton(frame: rectangle)
button.backgroundColor = UIColor.darkGray
button.setTitle(card_text, for: .normal)
button.layer.cornerRadius = 3
button.clipsToBounds = true
button.addTarget(self,action: #selector(ObjectViewCell.buttonPressed), for: .touchUpInside)
contentView.addSubview(button)
}
func buttonPressed(sender:UIButton!) {
print("Practicing: " + self.card_text)
}
}
class Selector: UIViewController
{
var collectionView: UICollectionView!
let base_image = UIImage(named: "blank")
var images = [UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank"), UIImage(named: "blank")]
#IBOutlet weak var main_view: UIView!
override func viewDidLoad() {
super.viewDidLoad()
print("Loading view in Selector")
setupCollectionView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
// Add spacing around each cell.
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
// Set each cell size to the size of the image.
layout.estimatedItemSize = CGSize(width: (base_image?.size.width)!, height: (base_image?.size.height)!)
// Set the collection view to the size of the view frame.
collectionView = UICollectionView(frame: main_view.frame, collectionViewLayout: layout)
// Register all images with the CollectionViewCell object.
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "cell")
// Set the background color.
collectionView.backgroundColor = UIColor.white
collectionView.delegate = self
collectionView.dataSource = self
// Add the collections view to the main view.
view.addSubview(collectionView)
}
}
extension Selector: UICollectionViewDelegate, UICollectionViewDataSource
{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
cell.card_text = String(indexPath.item + 1)
cell.awakeFromNib()
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cardCell = cell as! ObjectViewCell
cardCell.image_view.image = images[indexPath.row]
}
Update:
Here is what I ended up doing. I read over everybody's comments and answers, and I resolved to make all necessary changes within the view controller who originates the UICollection cells. Therefore, I moved my logic out of the CollectionViewCell object, and instead perform all logic/subview additions in the initialization of the cells.
Now I just need to figure out how to transition a view controller. Thank you everyone.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let contentView = cell.contentView
let image_view = UIImageView(image: base_image)
image_view.contentMode = .scaleAspectFill
image_view.clipsToBounds = true
contentView.addSubview(image_view)
let x_value = contentView.frame.width - contentView.frame.width
let y_value = (contentView.frame.height)/3
let rectangle = CGRect(x: x_value, y: y_value, width: contentView.frame.width, height: 40)
let button = UIButton(frame: rectangle)
let text = String(indexPath.item + 1)
button.setTitle(text, for: .normal)
button.accessibilityHint = text
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
contentView.addSubview(button)
return cell
}
func buttonPressed(sender:UIButton!) {
print("Button pressed")
print(sender.accessibilityHint)
//let navController = UINavigationController(rootViewController: self)
// 2. Present the navigation controller
//self.present(navController, animated: true, completion: nil)
}
The problem is that your button's target is the cell, and so the buttonPressed action function is located in the cell. That's a pretty silly thing to do, because (as you rightly say) you cannot call present without a view controller to send it to.
What I would have done is set the button's target/selector in cellForItemAt:. That way, self is the view controller and we can set the target to self. But you didn't do that!
Thus, you need to get from the cell to the view controller that controls it.
However, there is a way. It's called walking the responder chain. Set a UIResponder variable to the button:
var responder : UIResponder = self
Now loop, calling next on responder to walk one step up the chain:
responder = responder.next
Each time, look to see if this responder is a UIViewController. When it is, stop looping and send it present! Thus:
var responder : UIResponder = self
repeat { responder = responder .next } while !(responder is UIViewController)
let vc = responder as! UIViewController
vc.present( // ...
(Still, even though the problem can be solved in this way, I think it was silly to get yourself into this mess in the first place. Making the view controller the target in the first place would have been a much better idea, in my opinion.)
Related
I have a two ViewControllers, and each one of them has a collectionView (let's name it CV).
The first CV Cell (named MainPageCell) is the main structure of the app, and among it's other components, it displays an UIView named imageViewGrid.
The purpose of the second controller is only to add it's CV to itself as a subview, and then, it can be added as a child of the first controller and then insert it's view on the imageViewGrid.
In a nutshell, I'm trying to display a CV inside of another CV component. But the cells of the second CV doesn't load, or if they do, they disappear when scrolling. Just one cell can display the content until I scroll.
This GIF describes my situation: Click here
First CV cellForItemAt:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: mainReuseIdentifier, for: indexPath) as! MainPageCell
displayController(contentController: photoGridController, on: cell.imageViewGrid)
return cell
}
imageViewGrid declaration inside MainPageCell:
var imageViewGrid: UIView = {
let ivg = UIView()
ivg.backgroundColor = Colors.lightBlue
ivg.translatesAutoresizingMaskIntoConstraints = false
return ivg
}()
DisplayController func:
func displayController(contentController content: UIViewController, on view: UIView) {
addChild(content)
view.addSubview(content.view)
content.view.frame = view.bounds
content.didMove(toParent: self)
}
Second CV cellForItemAt:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: photoReuseIdentifier, for: indexPath) as! PhotoGridCell
cell.backgroundColor = Colors.lightBlue
cell.item = names[indexPath.item]
return cell
}
Second CV cell components:
var item: String? {
didSet {
guard let myItem = item,
let image = UIImage(named: myItem) else { return }
if image == UIImage(named: myItem) {
gridImageView.image = image
} else {
gridImageView.image = UIImage(named: "fbicon")
}
}
}
let gridImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
I don't know if I'm supposed to reload something, or if I forget a step, but couldn't been able to make this work, yet. I'd appreciate some help.
I have created a View Controller that contains a UITableView and each UITableViewCell contains a UICollectionView.
I made 2 API calls and every collectionView present the first 5 results of each API call.
Also, I added a button on each TableView Header on the right corner with the title "Show All". You can see the screen on the Image below.
Here is how I add the tableView header button:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 100))
let showHideButton: UIButton = UIButton(frame: CGRect(x:headerView.frame.size.width - 80, y:0, width:75, height:35))
showHideButton.setTitle("Show All", for: .normal)
showHideButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
showHideButton.setTitleColor(#colorLiteral(red: 0.9607843137, green: 0.007843137255, blue: 0.03137254902, alpha: 1), for: .normal)
//showHideButton.addTarget(self, action: #selector(btnShowHideTapped), for: .touchUpInside)
headerView.addSubview(showHideButton)
return headerView
}
When I tap on "show all" button on tableView header, I want to jump to another view controller ("showAllViewController") and represent all the result of my object and when I tap on the Image of CollectionViewCell I want to jump to another view controller ("detailsViewController"). How can I do it using delegates and protocols?
Here is an example image with my screen:
Edit: I followed the following steps from this question (navigate on click of collectionview cell inside tableview) but I don't know what I need to write on the "cellTapped()" function:
ViewController.swift :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CategoryRow
cell.delegate = self
return cell
}
MyCell.swift :
protocol CategoryRowDelegate:class {
func cellTapped()
}
CategoryRow.swift :
class CategoryRow : UITableViewCell {
weak var delegate:CategoryRowDelegate?
#IBOutlet weak var collectionView: UICollectionView!
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if delegate!= nil {
delegate?.cellTapped()
}
}
Add the delegate function inside ViewController
func cellTapped(){
//code for navigation
//I don't know what to write
}
Can anybody help me?
First of all I would advise you to insert a tag to the button when you create it, so you know which button in the collection the user clicked on, then add:
showHideButton.tag = section // assign the section number to the tag of the button
then, as you already wrote in the code, you assign an action to the button click:
showHideButton.addTarget(self, action: #selector(self.btnShowHideTapped(sender:)), for: .touchUpInside)
so you're gonna get something like that eventually:
showHideButton.setTitle("Show All", for: .normal)
showHideButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
showHideButton.setTitleColor(#colorLiteral(red: 0.9607843137, green: 0.007843137255, blue: 0.03137254902, alpha: 1), for: .normal)
showHideButton.tag = section // section Number for Header
showHideButton.addTarget(self, action: #selector(self.btnShowHideTapped(sender:)), for: .touchUpInside) // Sender UIButton
And in your function you call back to click =>
#objc func btnShowHideTapped(sender: UIButton) {
print(sender.tag)
// Switch Action if is HeaderView 0 or HeaderView 1 etc...
// self.present(YourViewController...) OR self.performSegue(withIdentifier: "detailsViewController", sender: nil)
}
I hope I've been there for you. Let me know.
1] In separate dataSource method - create protocol
protocol myProductsDelegate: class {
func cellTaped()
}
class ProductsDataSource: NSObject, UICollectionViewDataSource {
var delegate: myProductsDelegate?
// func collectionView(_ collectionView: UICollectionView, //numberOfItemsInSection section: Int) -> Int
// {
// return
// }
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! ProductsCollectionViewCell
return cell
}
2] In separate delegate method -
class ProductsDelegate: NSObject, UICollectionViewDelegate {
var delegate: myProductsDelegate?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if delegate != nil {
delegate?.cellTaped()
}
}
3] Note: In viewDidload -
self.CollectionView.delegate = self.myDelegate
self.CollectionView.dataSource = self.myDataSource
self.myDelegate.delegate = self
4] In your main view controller where collectionView is available -
var myDelegate: ProductsDelegate = ProductsDelegate()
var myDataSource: ProductsDataSource = ProductsDataSource()
extension MainViewController: ProductsDelegate {
func cellTaped()
{
let vc = storyboard?.instantiateViewController(identifier: "SecondViewController") as? SecondViewController
self.present(vc!, animated: true, completion: nil)
}
I am currently working on a particular problem where I have to add a UITableView and a couple of UICollectionViews along with a couple of labels in a single screen.
Here is the mockup:-
Right now, This is how my view looks (I am just working on the UI for now):-
The UICollectionViews below 'Live now' and 'Related Stories' are horizontally scrollable and in the middle of those UICollectionViews is a UITableView
Rather than having to compress these subviews inside the UIViewController class that I have built, I wish to remake it in such a way that the whole view is scrollable while keeping the same scrolling experience of the subviews currently set in the view.
I considered using another UITableview that encompasses all the views(the collection views and the table view), but then, the scrolling of that particular table view would cause a bad scrolling experience with the table view that I have to add.
The above could be said for using a UIScrollView (UICollectionViews that would be added would have no problem since they are being scrolled horizontally).
Would it be best to use a UICollectionView?
Any Suggestion that could help is welcome. Do let me know if there is anything from my side
Here is my source code:
import UIKit
import SnapKit
import EasyPeasy
class ArticleViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UITableViewDelegate, UITableViewDataSource {
var liveLabel = UILabel()
var engageLabel = UILabel()
var storyLabel = UILabel()
var livelayout = UICollectionViewFlowLayout.init()
var storylayout = UICollectionViewFlowLayout.init()
var liveCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
var storyCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
var liveRows = [
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg",
"https://images.sportsflashes.com/australia-win-first-t20i-after-nail-biting-finish-at-vizag1551028137.jpg"
]
var articleRows = [
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg",
"https://c.ndtvimg.com/2019-03/6dkbrsrg_varun-dhawan_625x300_14_March_19.jpg"
]
var storyRows = [
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg",
"https://www.anime-planet.com/images/characters/takuma-mamizuka-83947.jpg"
]
let table = UITableView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.backgroundColor = UIColor.white
if self.navigationController == nil {
return
}
self.navigationController?.isNavigationBarHidden = false
self.navigationItem.leftBarButtonItem = UIBarButtonItem(image:(UIImage(named: "back_arrow")?.withRenderingMode(.alwaysOriginal)), style:.plain, target:self, action:#selector(backPress))
var dateBarButtonItem = UIBarButtonItem(title: "Mar 2019", style: .plain, target: self, action: nil)
dateBarButtonItem.tintColor = UIColor.black
self.navigationItem.rightBarButtonItem = dateBarButtonItem
// Create a navView to add to the navigation bar
let navView = UIView()
// Create the label
let nameLabel = UILabel()
nameLabel.text = "Pavan Vasan"
nameLabel.sizeToFit()
nameLabel.center = navView.center
nameLabel.textAlignment = NSTextAlignment.center
// Create the image view
let image = UIImageView()
image.image = UIImage(named: "twitter")
// To maintain the image's aspect ratio:
let imageAspect = image.image!.size.width/image.image!.size.height
// Setting the image frame so that it's immediately before the text:
image.frame = CGRect(x: nameLabel.frame.origin.x-nameLabel.frame.size.height*imageAspect, y: nameLabel.frame.origin.y, width: nameLabel.frame.size.height*imageAspect, height: nameLabel.frame.size.height)
image.contentMode = UIView.ContentMode.scaleAspectFit
// Add both the label and image view to the navView
navView.addSubview(nameLabel)
navView.addSubview(image)
// Set the navigation bar's navigation item's titleView to the navView
self.navigationItem.titleView = navView
// Set the navView's frame to fit within the titleView
navView.sizeToFit()
}
override func viewDidLoad() {
super.viewDidLoad()
setupLiveCollectionView()
setupTable()
setupStoryCollectionView()
}
func setupLiveCollectionView() {
self.view.addSubview(self.liveLabel)
self.liveLabel.text = "Live now"
self.liveLabel.font = UIFont.boldSystemFont(ofSize: 17.5)
self.liveLabel.textColor = UIColor.black
self.liveLabel.textAlignment = .center
self.liveLabel.easy.layout(
Left(10).to(self.view),
Top(75).to(self.view)
)
livelayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
livelayout.itemSize = CGSize(width: 75, height: 75)
livelayout.scrollDirection = .horizontal
liveCollectionView = UICollectionView(frame: CGRect(x: 0, y: 100, width: self.view.frame.width, height: 95), collectionViewLayout: livelayout)
liveCollectionView.dataSource = self
liveCollectionView.delegate = self
liveCollectionView.register(LiveViewCell.self, forCellWithReuseIdentifier: "MyCell")
liveCollectionView.backgroundColor = UIColor.white
liveCollectionView.showsHorizontalScrollIndicator = false
self.view.addSubview(liveCollectionView)
}
func setupStoryCollectionView() {
self.view.addSubview(self.storyLabel)
self.storyLabel.text = "Related Stories"
self.storyLabel.font = UIFont.boldSystemFont(ofSize: 17.5)
self.storyLabel.textColor = UIColor.black
self.storyLabel.textAlignment = .center
self.storyLabel.easy.layout(
Left(10).to(self.view),
Top(15).to(self.table)
)
storylayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
storylayout.itemSize = CGSize(width: 120, height: 120)
storylayout.scrollDirection = .horizontal
storyCollectionView = UICollectionView(frame: CGRect(x: 0, y: 250 + self.view.bounds.height*0.40, width: self.view.frame.width, height: 130), collectionViewLayout: storylayout)
storyCollectionView.dataSource = self
storyCollectionView.delegate = self
storyCollectionView.register(StoryViewCell.self, forCellWithReuseIdentifier: "StoryCell")
storyCollectionView.backgroundColor = UIColor.white
storyCollectionView.showsHorizontalScrollIndicator = false
self.view.addSubview(storyCollectionView)
}
func setupTable() {
table.delegate = self
table.dataSource = self
table.register(ArticleTableViewCell.self, forCellReuseIdentifier: "ArticleCell")
table.separatorStyle = .none
self.view.addSubview(table)
self.table.easy.layout(
Top(15).to(self.liveCollectionView),
Left(0).to(self.view),
Width(self.view.bounds.width),
Height(self.view.bounds.height*0.4)
)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == self.liveCollectionView {
return self.liveRows.count
} else if collectionView == self.storyCollectionView {
return self.storyRows.count
}
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.liveCollectionView {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath as IndexPath) as! LiveViewCell
myCell.configure(self.liveRows[indexPath.row])
return myCell
} else if collectionView == self.storyCollectionView {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: "StoryCell", for: indexPath as IndexPath) as! StoryViewCell
myCell.configure(self.storyRows[indexPath.row])
myCell.layer.borderColor = UIColor.black.cgColor
myCell.layer.cornerRadius = 15
myCell.layer.borderWidth = 0.5
myCell.layer.masksToBounds = true
return myCell
}
return UICollectionViewCell()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.articleRows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleCell", for: indexPath) as! ArticleTableViewCell
cell.configure(self.articleRows[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 325
}
#objc func backPress(sender:UIButton!) {
self.navigationController?.popViewController(animated: true)
}
}
It would be preferable in complex screens to use only one main list view type(UITableView or UICollectionView).
In it u can add UIStackView or more UITableViews or UICollectionView.
In your case i would have used a UICollectionView and change the size of the cell according to your needs
You can use a single tableview for full screen and create custom cell by adding UICollectionView inside that.
Having nested scroll views could be against the HIG guidelines, which advocates not to place a scroll view inside of another scroll view.
In fact, similar to our use case, Apple has given the example of their Stocks app, where stock quotes scroll vertically above company-specific information that scrolls horizontally.
For more details, please refer to the HIG documentation at https://developer.apple.com/design/human-interface-guidelines/ios/views/scroll-views/
You can do it without any major changes in your code just below updates.
First, need to set constraint outlet of Tableview Height. e.g consTblHeight
Add Tableview size change observer.
func setupTable() {
table.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil)
}
Observer Method
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
self.consTblHeight.constant = self.table.contentSize.height
self.view.layoutIfNeeded()
}
In my VC I have a view that pulls up from the bottom. I setup and add a UICollectionView in the viewDidLoad():
//Add and setup the collectionView
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: flowLayout)
collectionView?.register(PhotoCell.self, forCellWithReuseIdentifier: "photoCell")
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.backgroundColor = #colorLiteral(red: 0.9771530032, green: 0.7062081099, blue: 0.1748393774, alpha: 1)
collectionView?.allowsMultipleSelection = false
collectionView?.allowsSelection = true
pullUpView.addSubview(collectionView!)
pullUpView.bringSubview(toFront: collectionView!)
The UICollectionView Delegate methods are in an extension, for now in the same codefile as the VC:
//MARK: - Extension CollectionView
extension MapVC: UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imagesArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? PhotoCell {
let imageFromIndex = imagesArray[indexPath.item]
let imageView = UIImageView(image: imageFromIndex )
cell.addSubview(imageView)
return cell
} else {
return PhotoCell()
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selected")
//Create PopVC instance, using storyboard id set in storyboard
guard let popVC = storyboard?.instantiateViewController(withIdentifier: "popVC") as? PopVC else { return }
popVC.passedImage = imagesArray[indexPath.row]
present(popVC, animated: true, completion: nil)
}
}
The problem is that when I tap on a cell nothing happens. I've put a print statement inside the didSelectItemAt method, but that never gets printed. So, my cells never get selected or at least the didSelectItemAt method never gets triggered!
Been debugging and trying for hours, and I can't see what's wrong. Any help appreciated. Perhaps someone could open mijn project from Github to see what's wrong, if that is allowed ?
UPDATE:
Using Debug View Hierarchy, I see something disturbing: Each photoCell has multiple (many!) UIImageViews. I think that should be just one UIImageView per photoCell. I don't know what is causing this behaviour?
Debug View Hierarchy
I checked your code, there are few problems:
First of all you have to change your PhotoCell implementation, and add your imageView inside the class, only when the cell is created. Your cell is not loading a XIB so you have to add the imageView in init(frame:):
class PhotoCell: UICollectionViewCell {
var photoImageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupCell() {
photoImageView = UIImageView()
addSubview(photoImageView)
}
override func layoutSubviews() {
super.layoutSubviews()
photoImageView.frame = bounds // ensure that imageView size is the same of the cell itself
}
}
After this change, in cellForItem method you can do cell.photoImageView.image = imageFromIndex.
The problem of didSelect not called is caused by the fact your pullUpViewis always with height = 1, even if you're able to see the collectionView, it will not receive any touch.
First add an IBOutlet of the height constraint of pullUpView in your MapVc
When creating collection view, ensure the size of collection view is the same of the pullUpView, so it will be able to scroll; collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 300), collectionViewLayout: flowLayout)
Then change animateViewUp and animateViewDown to this
func animateViewUp() {
mapViewBottomConstraint.constant = 300
pullUpViewHeightConstraint.constant = 300 // this will increase the height of pullUpView
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
#objc func animateViewDown() {
cancelAllSessions()
//remove previous loaded images and urls
imagesArray.removeAll()
imageUrlsArray.removeAll()
mapViewBottomConstraint.constant = 0
pullUpViewHeightConstraint.constant = 0 // this will reset height to 0
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
By doing all of this the swipe down gesture will not work anymore because the touch is intercepted and handled by the collection view, you should handle this manually.
However I suggest you to change the online course, there are a lot of things that I don't like about this code.
My objective is to create a Pinterest-like iOS app. I use a collection view and then, programatically I add a stack view. I create the buttons and it's respective images within a function and then I return all the UIButtons in an array of [UIButtons]. I store the value of that function in a variable called buttonsArray, and then I use it in the respective Collection View data source and delegate methods. I also use PinterestLayout created by RayWenderlich. Everything works as expected except for the fact that instead of loading all buttons, it only loads the first two. The function where I add the items goes as follows:
func createArrayButtons() -> [UIButton]{
var items:[UIButton] = []
let item1 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item1.setImage(UIImage(named: "1"), for: .normal)
item1.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item1.tag = 1
items.append(item1)
let item2 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item2.setImage(UIImage(named: "2"), for: .normal)
item2.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item2.tag = 2
items.append(item2)
let item3 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item3.setImage(UIImage(named: "3"), for: .normal)
item3.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item3.tag = 3
items.append(item3)
let item4 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item4.setImage(UIImage(named: "4"), for: .normal)
item4.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item4.tag = 4
items.append(item4)
let item5 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item5.setImage(UIImage(named: "5"), for: .normal)
item5.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item5.tag = 5
items.append(item5)
let item6 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item6.setImage(UIImage(named: "6"), for: .normal)
item6.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item6.tag = 6
items.append(item6)
let item7 = UIButton(frame: CGRect(x:0, y:0, width:346,height:275))
item7.setImage(UIImage(named: "7"), for: .normal)
item7.addTarget(self, action: #selector(editButtonTapped), for: UIControlEvents.touchUpInside)
item7.tag = 7
items.append(item7)
return items
}
I give each one a respective tag and a respective image from the assets folder, then return it as an array.
I calculate the collectionViewDataSource as follows
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let buttonsArray = createArrayButtons()
return buttonsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let buttonsArray = createArrayButtons()
let stackView = UIStackView(arrangedSubviews: buttonsArray)
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.alignment = .fill
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(stackView)
return cell
}
And I calculate the height of each item as follows:
extension ViewController: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
let buttonArray = createArrayButtons()
let button = buttonArray[indexPath.item]
let height = button.imageView?.image?.size.height
return height!
}
}
As you can see, the app only shows the first two images of the createButtonsArray() variable stored in each dataSource Method
Every image that i store as the returning value of createArrayButtons is different, however it only loads up two. How could I solve this?
If it's of any help, my project can be found in the following link:
https://github.com/francisc112/PinterestTutorial
Update code at cellForItemAt indexPath as you are loading the whole button array for each cell.
let stackView = UIStackView(arrangedSubviews: [buttonsArray[indexPath.row]])
Please follow the steps below to get the required result,
1 - In storyboard, drag a UIStackView in collectionCell and set constraints to zero from all sides. Set distribution and spacing as shown in the image
2 - Update ButtonCollectionViewCell as below
class ButtonCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var button:UIButton!
#IBOutlet weak var stackView: UIStackView!
}
3 - Open storyboard again and make a connection with the stackView like in the image
4 - Change viewDidLoad to this
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
let collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.collectionViewLayout = collectionViewLayout
collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// Do any additional setup after loading the view, typically from a nib.
}
5 - Conform ViewController to this
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width/2 - 20, height: UIScreen.main.bounds.height)
}
}
6 - Update cellForItemAt with this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ButtonCollectionViewCell
for (_, view) in cell.stackView.arrangedSubviews.enumerated() {
cell.stackView.removeArrangedSubview(view)
}
let buttonsArray = createArrayButtons()
buttonsArray.forEach { button in
cell.stackView.addArrangedSubview(button)
}
cell.stackView.layoutIfNeeded()
cell.contentView.layoutIfNeeded()
return cell
}
Now you should be able to see the proper results.