I have a horizontal CollectionView on top and TableView under it
I want as I scroll TableView my CollectionView should scroll and animate along with to some level I have achieved with scrollItemTo but CollectionView scrolling to slow but I want it working as it is working in uberEats iOS app in restaurant items list details and same is working in urbanClap app
It's like moving a view cell by cell as table view section header reach top
The uber eats app functionality that you're referring works like: whenever a particular section of tableView reaches the top, that particular collectionViewCell is selected.
As evident from the above statement,
number of items in collectionView = number of sections in tableView
You can achieve the solution to this particular problem by tracking the top visible section index of tableView and then selecting the collectionViewCell at that particular index, i.e.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView === self.tableView {
if
let topSectionIndex = self.tableView.indexPathsForVisibleRows?.map({ $0.section }).sorted().first,
let selectedCollectionIndex = self.collectionView.indexPathsForSelectedItems?.first?.row,
selectedCollectionIndex != topSectionIndex {
let indexPath = IndexPath(item: topSectionIndex, section: 0)
self.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
}
}
}
Changing collectionViewCell color on selection:
Create a custom UICollectionViewCell and override **isSelected** property to handle selected and un-selected state, i.e.
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var titleLabel: UILabel!
override var isSelected: Bool {
didSet {
if self.isSelected {
self.contentView.backgroundColor = #colorLiteral(red: 0.2588235438, green: 0.7568627596, blue: 0.9686274529, alpha: 1)
} else {
self.contentView.backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
}
}
}
}
You won't need to manually update the backgroundColor of cell elsewhere after this.
You can implemement scrollViewDidScroll of UIScrollViewDelegate for your tableView then manually scroll your UICollectionView from there
class XYZ: UIViewController, UIScrollViewDelegate{
func viewDidLoad(){
super.viewDidLoad()
tableView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView){
let tableView = scrollView //assuming the only scrollView.delegate you conform to is the tableVeiw
collectionView.contentOffset = someMappingFrom(tableView.contentOffset) //or some other scrolling mechanism (scrollToItem)
}
}
You need to change selection on scrollViewDidScroll.
I've attached the link to repo for the same code
Github Repo for solution
Related
I have a UITableviewcontroller and a activity indicator added to it.
When ever my table scroll, the indicator also moves up and goes out of bounds. Instead I want to display indicator even if the table scrolls
I checked this post: UIActivityIndicator scrolls with tableView which says I need to use UIViewcontroller instead of Tableviewcontroller.
Since I have many tableviewcontrollers added in application, isnt it possible to scroll indicator along with tableview.
My UIActivityIndicator is helper class across entire application where I control to display and remove spinner
extension UIViewController {
class func displaySpinner(onView : UIView) -> UIView {
let spinnerView = UIView.init(frame: onView.frame)
spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5)
let ai = UIActivityIndicatorView.init(activityIndicatorStyle: .whiteLarge)
ai.startAnimating()
ai.center = spinnerView.center
DispatchQueue.main.async {
spinnerView.addSubview(ai)
onView.addSubview(spinnerView)
}
return spinnerView
}
class func removeSpinner(spinner :UIView) {
DispatchQueue.main.async {
spinner.removeFromSuperview()
}
}
}
You can do it if it's not an extension say the code is inside the tableController and
let indicator = UIActivityIndicator()
then use
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// change frame of indicator by scrollview.contentOffset.y
}
You can also extend
class MyTableConWithIndicator:UITableViewController {
let indicator = UIActivityIndicator()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// change y'frame of indicator by scrollview.contentOffset.y
}
func addIndicator() {}
func removeIndicator() ()
}
Then extend from it in every tableController
class MyTable:MyTableConWithIndicator {}
I have one UICollectionView with five custom cells that need to be render on specific conditions, cells are getting generated. Now problem I am going through is consider I have selected one cell at specific index, and now I scroll downwards and now again I long scroll. When UICollectionView stops scrolling, if the index is the same as which we selected, UICollectionView doesn't show that cell as selected. But now if I even try to move the cell a little bit, even a bit, UICollectionView shows that cell as selected cell.
Following is my code, that I have wrote in prefetchItem:
(cell as? PATemplateTypeOneCollectionCell)?.fillCellData(row: indexPath.row,section:indexPath.section, paCategoryQuestions: currentIndexQuestion, paQuestionCollection: currentIndexCollection)
cell!.alpha = 0.4
if self.multipleIndexPathsArray[indexPath.section][0] != []{
collectionView.selectItem(at: self.multipleIndexPathsArray[indexPath.section][0], animated: true, scrollPosition: .right)
}
else{
print("self.multipleIndexPathsArray[indexPath.section][0] is empty")
}
UICollectionViewCell:
override var isSelected: Bool {
didSet {
if isSelected {
self.alpha = 1.0
self.layer.borderWidth = 2.0
self.layer.borderColor = ColorConstants.colorFromHexString(hexString: paCategoryQuestions.selection_color).cgColor
}else {
self.alpha = 0.4
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.lightGray.cgColor
}
}
Kindly request you guys to help with this issue.
So after hours of thinking I came across one solution. So as my cellForItem was not rendering selected scroll properly after long scroll, what I tried is recognizing UIScrollView's delegate scrollViewDidEndDecelerating. And recognizing if currently visible indexPath cells were selected previously or not by using my saved values array. Following is the code that worked for me:
fileprivate func checkIfCellIsSelected(){
for eachCell in afterPaymentPACollectionView.visibleCells{
let indexPath = afterPaymentPACollectionView.indexPath(for: eachCell)
for eachIndexSelected in multipleIndexPathsArray{
if eachIndexSelected.contains(indexPath!){
afterPaymentPACollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .right)
}
}
}
}
//MARK: UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
checkIfCellIsSelected()
}
I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.
I have a table view cell with a scroll view for scrolling through images. There are a couple of issues I'm having with it.
When the table view is shown, the cell with the scroll view and images shows like the first image and part of the second image and the paging doesn't work and it lets you scroll in any direction.
Once I scroll past this cell and then scroll back to it, it is displayed correctly and paging works like it should.
In cellForRowAtIndexPath()
if indexPath.section == 0 {
let imageCell = tableView.dequeueReusableCellWithIdentifier("imageCell", forIndexPath: indexPath) as! ImageCell
imageCell.selectionStyle = .None
if imageFiles.count > 0 {
if imageFiles.count == 1 {
imageCell.pageControl.hidden = true
}
imageCell.pageControl.numberOfPages = imageFiles.count
imageCell.pageControl.currentPage = 0
var index = 0
for photo in imageFiles {
var frame = CGRect()
frame.origin.x = (imageCell.imageScrollView.frame.size.width * CGFloat(index))
frame.origin.y = 0
frame.size = CGSizeMake(imageCell.imageScrollView.frame.size.width, imageCell.imageScrollView.frame.size.height)
let imageView = UIImageView(frame: frame)
imageView.image = photo
imageCell.imageScrollView.addSubview(imageView)
imageCell.imageScrollView.contentSize = CGSizeMake(imageCell.imageScrollView.frame.size.width * CGFloat(imageFiles.count), imageCell.imageScrollView.frame.size.height)
index += 1
}
} else {
imageCell.pageControl.hidden = true
let imageView = UIImageView(frame: imageCell.imageScrollView.frame)
imageView.image = UIImage(named: "placeholder")
imageCell.imageScrollView.addSubview(imageView)
}
My custom cell:
import UIKit
class ImageCell: UITableViewCell, UIScrollViewDelegate {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var imageScrollView: UIScrollView!
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
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
pageControl.currentPage = Int(scrollView.contentOffset.x / pageWidth)
}
}
I was using SDWebImageDownloader. viewDidLoad() to download images to use in the scroll view but I wasn't reloading the table view data. Once I added that, this works perfectly.
func downloadImages() {
let downloader = SDWebImageDownloader.sharedDownloader()
for imageURL in images {
let url = NSURL(string: imageURL)
downloader.downloadImageWithURL(url, options: SDWebImageDownloaderOptions.UseNSURLCache, progress: nil,
completed: { (image, data, error, bool) -> Void in
self.imageFiles.append(image)
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
})
}
}
You are doing this:
frame.size = CGSizeMake(imageCell.imageScrollView.frame.size.width, imageCell.imageScrollView.frame.size.height)
I would guess that when your view initially loads, imageScrollView has not been fully laid out, so it doesn't have a valid size. Scrolling away from this cell & back to it after the view is fully loaded would result in the scroll view having a valid size.
You should validate that imageScrollView has the width you think it does, and if it doesn't, maybe use the width of the screen instead.
I have implemented a similar thing in one of my apps, so as another possible option, instead of putting a scroll view in your first cell, you could place a CollectionView in there instead and populate your collection view with cells of images. This would A) allow your layout to dynamically change with the view, and B) rather than loading up your scroll view with all the images, they can be dynamically loaded as needed.
Chat set up. messagetableView bottom constraint attached to top of dockView. Unfortunately the tableView messageCell doesn't conform to the same constraint forced on the tableView. In my screenshot you can see the yellow tableView. It starts at the top of the superView and extends down to the top of the UIViewDock. The white messageCell does not follow the tableView. Obviously the messageCell is doing this because there are only 5 rows of messages so how do I make the white cell start at the bottom of the tableView like chat apps do?
Someone in another old thread said to use this Obj-C.
#property (nonatomic, assign) BOOL shouldScrollToLastRow;
- (void)viewDidLoad
{
[super viewDidLoad];
_shouldScrollToLastRow = YES;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// Scroll table view to the last row
if (_shouldScrollToLastRow)
{
_shouldScrollToLastRow = NO;
[self.tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
}
}
Is this correct and how would I do it in Swift?
Make your messageCell the footer of the table view so it will always stick to the bottom of the table. You can add a view to the table in IB, and give it an IBOutlet (I call mine footer in the code below).
#IBOutlet weak var footer: UIView!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 44
tableView.tableFooterView = footer
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentSize.height), animated: false)
}
In the "Send" button's action method, you can do this to add a new row and keep the table scrolled to the bottom,
#IBAction func sendMessage(sender: UIButton) {
var message = messageField.text
messages.append(message)
let lastPath = NSIndexPath(forRow: messages.count - 1, inSection: 0)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([lastPath], withRowAnimation: .Automatic)
tableView.contentSize = CGSize(width: tableView.contentSize.width, height: tableView.contentSize.height + tableView.rowHeight)
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentSize.height), animated: true)
tableView.endUpdates()
}