Having some UITableViews and some UICollectionViews in a single screen - ios

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()
}

Related

Adding subview to UICollectionView breaks VoiceOver and Voice Control

I’ve got a ViewController with a UICollectionView using an edge offset to lower its content on the screen, and a search bar that is added as a subview, whose frame has a negative y value so that it appears in the collection view above the content.
But as the search bar’s frame’s y value goes beyond a certain value (in this example, -45.0, but it depends on the size of the search bar), accessibility breaks: voice control will completely ignore it, and VoiceOver will ignore the search bar on the way down (it jumps directly from the ‘done’ button to the first collection view cell), although it will focus on the search bar on the way up.
If the frame's y > -45.0, accessibility functions as expected.
This isn’t caused by the search bar per se, as any UIView will cause the same issue
Is this a bug in UIAccessibility? Or is there something going on here that can explain this behaviour?
class ViewController: UIViewController {
var myCollectionView: UICollectionView?
var searchBar = UISearchBar()
let searchBarHeight: CGFloat = 56
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView()
view.backgroundColor = .white
setupCollectionView()
addNavbar()
addSearchBar()
}
func setupCollectionView() {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 80, height: 80)
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "MyCell")
myCollectionView?.backgroundColor = .red
myCollectionView?.contentInset = UIEdgeInsets(top: 200,
left: 0,
bottom: 0,
right: 0)
myCollectionView?.delegate = self
myCollectionView?.dataSource = self
view.addSubview(myCollectionView ?? UICollectionView())
self.view = view
}
func addNavbar() {
let navBar = UINavigationBar(frame: CGRect(x: 0, y: 44, width: view.frame.size.width, height: 44))
view.addSubview(navBar)
let navItem = UINavigationItem()
let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: nil)
navItem.rightBarButtonItem = doneItem
navBar.setItems([navItem], animated: false)
}
func addSearchBar() {
searchBar.backgroundColor = .brown
searchBar.frame = CGRect(x: 0,
y: -searchBarHeight * 2,
width: view.frame.width,
height: searchBarHeight)
myCollectionView?.addSubview(searchBar)
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 8
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath)
cell.backgroundColor = .cyan
cell.isAccessibilityElement = true
cell.accessibilityLabel = "cell \(indexPath.row)"
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = .darkGray
}
}

UITableview scrolling is not responding properly?

booktable.frame = CGRect(x: 0, y: booktopview.bounds.height, width: screenWidth, height: screenHeight-booktopview.bounds.height-tabbarView.bounds.height)
booktable.register(UITableViewCell.self, forCellReuseIdentifier: "mycell")
booktable.dataSource = self
booktable.delegate = self
booktable.separatorColor = UIColor.lightGray
booktable.backgroundColor = UIColor.clear
booktable.separatorStyle = .singleLine
bookview.addSubview(booktable)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(tableView == booktable)
{
let cell1 = booktable.dequeueReusableCell(withIdentifier: "mycell")
for object in (cell1?.contentView.subviews)!
{
object.removeFromSuperview();
}
let img :UIImageView = UIImageView()
let lbl : UILabel = UILabel()
img.frame = CGRect(x: 15, y: 15, width: 80, height: 130)
img.image = imgarray[indexPath.row]
img.layer.borderWidth = 1.0
img.layer.borderColor = UIColor.lightGray.cgColor
cell1?.contentView.addSubview(img)
imgheight = img.bounds.height
lbl.frame = CGRect(x: img.bounds.width + 40, y: (imgheight+40-80)/2, width: booktable.bounds.width-img.bounds.width + 40 - 100, height: 80)
lbl.text = imgname[indexPath.row]
lbl.numberOfLines = 0
lbl.textAlignment = .left
lbl.font = UIFont(name: "Arial", size: 23)
lbl.textColor = UIColor.black
cell1?.selectionStyle = .none
cell1?.contentView.addSubview(lbl)
return cell1!
}
The code shown above is for book table, which sometimes scrolls like normal and sometimes not scrolling at all. I am doing all the code programatically. I have tested this on both simulators and devices but still the problem exists. Any help is appreciated...
Create Custom UITableViewCell, let's say it is ListTableCell
class ListTableCell: UITableViewCell {
#IBOutlet weak var lblTemp: UILabel!
#IBOutlet weak var imgTemp: UIImage!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
I've created UITableViewCell with xib like this and bind IBOutlets
Let's say we have struct Model and array like this
struct Model {
let image : UIImage
let name: String
}
for i in 0...10 {
let model = Model(image: #imageLiteral(resourceName: "Cat03"), name: "Temp \(i)")
array.append(model)
}
Now on ViewController viewDidLoad() method,
tableView.register(UINib(nibName: "ListTableCell", bundle: nil), forCellReuseIdentifier: "ListTableCell")
Implement UITableViewDataSource methods like this,
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ListTableCell") as! ListTableCell
let model = array[indexPath.row]
cell.lblTemp.text = model.name
cell.imgTemp.image = model.image
return cell
}
FYI
For different tableviews, you can create different custom cell the same way and cellForRowAt indexPath and numberOfRowsInSection method will change appropriately.
Let me know in case of any queries.
UPDATE
Follow this and this to create CustomTableCell programmatically

Invalid Selector Using Delegate Pattern

I am attempting to use the delegate pattern to animate a change in height for a collectionView. The button that triggers this change is in the header. However when I press the button not only does the height not change but it also crashes with the error
'NSInvalidArgumentException', reason: '-[UIButton length]: unrecognized selector sent to instance 0x12f345b50'
I feel like I have done everything right but it always crashes when I click the button. Does anyone see anything wrong and is there anyway that I can animate the change in height for the cell the way I want it to. This is the cell class along with the protocol and the delegate.
import Foundation
import UIKit
protocol ExpandedCellDelegate:NSObjectProtocol{
func viewEventsButtonTapped(indexPath:IndexPath)
}
class EventCollectionCell:UICollectionViewCell {
var headerID = "headerID"
weak var delegateExpand:ExpandedCellDelegate?
public var indexPath:IndexPath!
var eventArray = [EventDetails](){
didSet{
self.eventCollectionView.reloadData()
}
}
var enentDetails:Friend?{
didSet{
var name = "N/A"
var total = 0
seperator.isHidden = true
if let value = enentDetails?.friendName{
name = value
}
if let value = enentDetails?.events{
total = value.count
self.eventArray = value
seperator.isHidden = false
}
if let value = enentDetails?.imageUrl{
profileImageView.loadImage(urlString: value)
}else{
profileImageView.image = imageLiteral(resourceName: "Tokyo")
}
self.eventCollectionView.reloadData()
setLabel(name: name, totalEvents: total)
}
}
let container:UIView={
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 16
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.3
return view
}()
//profile image view for the user
var profileImageView:CustomImageView={
let iv = CustomImageView()
iv.layer.masksToBounds = true
iv.layer.borderColor = UIColor.lightGray.cgColor
iv.layer.borderWidth = 0.3
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
//will show the name of the user as well as the total number of events he is attending
let labelNameAndTotalEvents:UILabel={
let label = UILabel()
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
let seperator:UIView={
let view = UIView()
view.backgroundColor = .lightGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//collectionview that contains all of the events a specific user will be attensing
lazy var eventCollectionView:UICollectionView={
let flow = UICollectionViewFlowLayout()
flow.scrollDirection = .vertical
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = 0
flow.minimumInteritemSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//will register the eventdetailcell
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .white
cv.register(EventDetailsCell.self, forCellWithReuseIdentifier: "eventDetails")
cv.register(FriendsEventsViewHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: headerID)
cv.delegate = self
cv.dataSource = self
cv.backgroundColor = .blue
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
cv.showsVerticalScrollIndicator = false
cv.bounces = false
return cv
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCell()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setLabel(name:String,totalEvents:Int){
let mainString = NSMutableAttributedString()
let attString = NSAttributedString(string:name+"\n" , attributes: [NSAttributedStringKey.foregroundColor:UIColor.black,NSAttributedStringKey.font:UIFont.systemFont(ofSize: 14)])
mainString.append(attString)
let attString2 = NSAttributedString(string:totalEvents == 0 ? "No events" : "\(totalEvents) \(totalEvents == 1 ? "Event" : "Events")" , attributes: [NSAttributedStringKey.foregroundColor:UIColor.darkGray,NSAttributedStringKey.font:UIFont.italicSystemFont(ofSize: 12)])
mainString.append(attString2)
labelNameAndTotalEvents.attributedText = mainString
}
}
//extension that handles creation of the events detail cells as well as the eventcollectionview
//notice the delegate methods
//- Mark EventCollectionView DataSource
extension EventCollectionCell:UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return eventArray.count
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerID, for: indexPath) as! FriendsEventsViewHeader
header.viewEventsButton.addTarget(self, action: #selector(viewEventsButtonTapped), for: .touchUpInside)
return header
}
#objc func viewEventsButtonTapped(indexPath:IndexPath){
print("View events button touched")
if let delegate = self.delegateExpand{
delegate.viewEventsButtonTapped(indexPath: indexPath)
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"eventDetails" , for: indexPath) as! EventDetailsCell
cell.details = eventArray[indexPath.item]
cell.backgroundColor = .yellow
cell.seperator1.isHidden = indexPath.item == eventArray.count-1
return cell
}
}
//- Mark EventCollectionView Delegate
extension EventCollectionCell:UICollectionViewDelegateFlowLayout{
//size for each indvidual cell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 50)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 40)
}
}
This is the view that ultimately is supposed to be handling the expansion via the delegate function.
import UIKit
import Firebase
class FriendsEventsView: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
var friends = [Friend]()
var followingUsers = [String]()
var notExpandedHeight : CGFloat = 100
var expandedHeight : CGFloat?
var isExpanded = [Bool]()
//so this is the main collectonview that encompasses the entire view
//this entire view has eventcollectionCell's in it which in itself contain a collectionview which also contains cells
//so I ultimately want to shrink the eventCollectionView
lazy var mainCollectionView:UICollectionView={
// the flow layout which is needed when you create any collection view
let flow = UICollectionViewFlowLayout()
//setting the scroll direction
flow.scrollDirection = .vertical
//setting space between elements
let spacingbw:CGFloat = 5
flow.minimumLineSpacing = spacingbw
flow.minimumInteritemSpacing = 0
//actually creating collectionview
let cv = UICollectionView(frame: .zero, collectionViewLayout: flow)
//register a cell for that collectionview
cv.register(EventCollectionCell.self, forCellWithReuseIdentifier: "events")
cv.translatesAutoresizingMaskIntoConstraints = false
//changing background color
cv.backgroundColor = .red
//sets the delegate of the collectionView to self. By doing this all messages in regards to the collectionView will be sent to the collectionView or you.
//"Delegates send messages"
cv.delegate = self
//sets the datsource of the collectionView to you so you can control where the data gets pulled from
cv.dataSource = self
//sets positon of collectionview in regards to the regular view
cv.contentInset = UIEdgeInsetsMake(spacingbw, 0, spacingbw, 0)
return cv
}()
//label that will be displayed if there are no events
let labelNotEvents:UILabel={
let label = UILabel()
label.textColor = .lightGray
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = UIFont.italicSystemFont(ofSize: 14)
label.text = "No events found"
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
//will set up all the views in the screen
self.setUpViews()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
}
func setUpViews(){
//well set the navbar title to Friends Events
self.title = "Friends Events"
view.backgroundColor = .white
//adds the main collection view to the view and adds proper constraints for positioning
view.addSubview(mainCollectionView)
mainCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
mainCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
mainCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
mainCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
//adds the label to alert someone that there are no events to the collectionview and adds proper constrains for positioning
mainCollectionView.addSubview(labelNotEvents)
labelNotEvents.centerYAnchor.constraint(equalTo: mainCollectionView.centerYAnchor, constant: 0).isActive = true
labelNotEvents.centerXAnchor.constraint(equalTo: mainCollectionView.centerXAnchor, constant: 0).isActive = true
//will fetch events from server
self.fetchEventsFromServer()
}
// MARK: CollectionView Datasource for maincollection view
//will let us know how many eventCollectionCells tht contain collectionViews will be displayed
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(friends.count)
isExpanded = Array(repeating: false, count: friends.count)
return friends.count
}
//will control the size of the eventCollectionCells that contain collectionViews
height is decided for the collectionVIew here
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let event = friends[indexPath.item]
if let count = event.events?.count,count != 0{
notExpandedHeight += (CGFloat(count*40)+10)
}
self.expandedHeight = notExpandedHeight
if isExpanded[indexPath.row] == true{
return CGSize(width: collectionView.frame.width, height: expandedHeight!)
}else{
return CGSize(width: collectionView.frame.width, height: 100)
}
}
//will do the job of effieicently creating cells for the eventcollectioncell that contain eventCollectionViews using the dequeReusableCells function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "events", for: indexPath) as! EventCollectionCell
cell.backgroundColor = UIColor.orange
cell.indexPath = indexPath
cell.delegateExpand = self
cell.enentDetails = friends[indexPath.item]
return cell
}
}
extension FriendsEventsView:ExpandedCellDelegate{
func viewEventsButtonTapped(indexPath:IndexPath) {
isExpanded[indexPath.row] = !isExpanded[indexPath.row]
print(indexPath)
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.mainCollectionView.reloadItems(at: [indexPath])
}, completion: { success in
print("success")
})
}
}
I used this post for reference to implement
Expandable UICollectionViewCell
This is a very common mistake.
The passed parameter in a target / action selector is always the affected UI element which triggers the action in your case the button.
You cannot pass an arbitrary object for example an indexPath because there is no parameter in the addTarget method to specify that arbitrary object.
You have to declare the selector
#objc func viewEventsButtonTapped(_ sender: UIButton) {
or without a parameter
#objc func viewEventsButtonTapped() {
UIControl provides a third syntax
#objc func viewEventsButtonTapped(_ sender: UIButton, withEvent event: UIEvent?) {
Any other syntax is not supported.

How To Share Same UIView Between Multiple UITableView Cells

Objective
When the user clicks on any of the 3 blue buttons, all buttons change to the same color.
Note: this is an abstraction of a shared progress view problem, it's therefore important that only one UIView is shared (or mimicked) across my three rows
Here is a compilable Swift project:
import UIKit
class ToggleButton: UIButton {
var connectedView: UIView?
func onPress() {
self.isHidden = true
self.connectedView?.isHidden = false
}
}
class ViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView = UITableView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var myView: UIView? = nil
var toggleBtn: ToggleButton? = nil
override func viewDidLoad() {
self.setupTableView()
}
fileprivate func setupTableView() {
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.backgroundColor = UIColor.white
self.tableView.isOpaque = true
self.view.addSubview(self.tableView)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "CellIdentifier")
let frame = CGRect(x: 10, y: 10, width: 30, height: 30)
if let view = self.myView, let btn = self.toggleBtn {
cell.addSubview(view)
cell.addSubview(btn)
} else {
let myView = UIView(frame: frame)
myView.backgroundColor = UIColor.green
myView.isHidden = true
cell.addSubview(myView)
let toggleBtn = ToggleButton(frame: frame)
toggleBtn.backgroundColor = UIColor.blue
toggleBtn.addTarget(self, action: #selector(onPress), for: .touchUpInside)
toggleBtn.connectedView = myView
cell.addSubview(toggleBtn)
}
return cell
}
#objc func onPress(_ sender: Any) {
if let button = sender as? ToggleButton {
button.onPress()
}
}
}
Any help appreciated.
The concept of UITableViewCell is made to be very independent each other.
So the only thing you can do it having a bool flag in your ViewController, then you init your 3 cells with this flags.
And finally each time the button is pressed you toggle the flag en reload your tableView.

How to transition a UICollectionViewCell via a button to another ViewController?

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.)

Resources