Is there a way to get the number of cells in a table view? - earlgrey

In case if we are to take in all the cells in a TableView into an array and iterate through it to click on elements. I am looking for a solution in swift.

If each cell have a differentiating factor, then you get those in an array.
To use the text strings, I have to get hold of the cells first. So that leads to - how to get count count of cells and after getting hold of the cells, drilling down to see if this is the text, then open the contextual menu, else do something.
Here's what you can do: Keep using 'atIndex:' on the cells. Use selectElementWithMatcher::withError. Loop through until you find an indexOutOfBoundsError and then you should have the text.
But with the looping using atIndex:, you should have the cells that you want. And to do the same, see below:
for (int i = 0; i < someLargeValue; i++) {
EarlGrey.selectElementWithMatcher(grey_accessibilityID("abc")).atIndex(i)
}
-> Ok and for getting the value of “someLargeValue”, use selectElementWithMatcher::withError. Loop through until you find an indexOutOfBoundsError and then you should have the text.

This could be achieved by implementing a custom GREYAssertionBlock for the UITableView:
func assertTableView(_ accessibilityID: String, hasRowCount rowCount: Int, inSection section: Int) {
let cellCountAssert = GREYAssertionBlock(name: "cell count") { (element, error) -> Bool in
guard let tableView = element as? UITableView, tableView.numberOfSections > section else {
return false
}
let numberOfCells = tableView.numberOfRows(inSection: section)
return numberOfCells == rowCount
}
EarlGrey.selectElement(with: grey_accessibilityID(accessibilityID)).assert(cellCountAssert)
}

Related

Calendar-like UICollectionView - how to add left inset before first item only?

I have the following UICollectionView:
It has vertical scrolling, 1 section and 31 items.
It has the basic setup and I am calculating itemSize to fit exactly 7 per row.
Currently it looks like this:
However, I would like to make an inset before first item, so that the layout is even and there are the same number of items in first and last row. This is static and will always contain 31 items, so I am basically trying to add left space/inset before first item, so that it looks like this:
I have tried using a custom UICollectionViewDelegateFlowLayout method:
collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int)
But since there is only 1 section with 31 rows, it insets all of the rows, not just the first.
I know I could probably add two more "blank" items, but I feel like there is a better solution I may not be aware of. Any ideas?
EDIT: I've tried Tarun's answer, but this doesn't work. Origin of first item changes, but the rest stays as is, therefore first overlaps the second and the rest remain as they were. Shifting them all doesn't work either. I ended up with:
You need to subclass UICollectionViewFlowLayout and that will provide you a chance to customize the frame for all items within the collectionView.
import UIKit
class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = self.collectionView else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
let leftMargin = self.sectionInset.left
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
// Check for `indexPath.item == 0` & do what you want
// cellAttribute.frame.origin.x = 0 // 80
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Now you can use this custom layout class as your flow layout like following.
let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout
Following Taran's suggestion, I've decided to use a custom UICollectionViewFlowLayout. Here is a generic answer that works for any number of items in the collectionView, as well as any inset value:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else { return nil }
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
if let cellAttribute = collectionView.collectionViewLayout.layoutAttributesForItem(at: attribute.indexPath) {
let itemSize = cellAttribute.frame.size.width + self.minimumInteritemSpacing
let targetOriginX = cellAttribute.frame.origin.x + CGFloat(self.itemInset) * itemSize
if targetOriginX <= collectionView.bounds.size.width {
cellAttribute.frame.origin.x = targetOriginX
} else {
let shiftedPosition = lround(Double((targetOriginX / itemSize).truncatingRemainder(dividingBy: CGFloat(self.numberOfColumns))))
cellAttribute.frame.origin.x = itemSize * CGFloat(shiftedPosition)
cellAttribute.frame.origin.y += itemSize
}
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
where:
self.itemInset is the value we want to inset from the left (2 for my initial question, but it can be any number from 0 to the number of columns-1)
self.numberOfColumns is - as the name suggests - number of columns in the collectionView. This pertains to the number of days in my example and would always be equal to 7, but one might want this to be a generic value for some other use case.
Just for the sake of the completeness, I provide a method that calculates a size for my callendar collection view, based on the number of columns (days):
private func collectionViewItemSize() -> CGSize {
let dimension = self.collectionView.frame.size.width / CGFloat(Constants.numberOfDaysInWeek) - Constants.minimumInteritemSpacing
return CGSize(width: dimension, height: dimension)
}
For me, Constants.numberOfDaysInWeek is naturally 7, and Constants.minimumInteritemSpacing is equal to 2, but those can be any numbers you desire.

How to display a set number of rows in small Table View with self-sizing cells

I have a custom table view class, which is configured to set the table view height to display only 3 rows. This means if there are 20 rows, table view will be sized to display first 3 rows, and allowing user to scroll.
This code of mine works only if I set the static rowHeight
class CannedRepliesTableView: UITableView {
/// The max visible rows visible in the autocomplete table before the user has to scroll throught them
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
let rows = numberOfRows(inSection: 0) < maxVisibleRows ? numberOfRows(inSection: 0) : maxVisibleRows
return CGSize(width: super.intrinsicContentSize.width, height: (CGFloat(rows) * rowHeight))
}
}
If I set UITableViewAutomaticDimension to the rowHeight, table view is not properly resized. Is there a solution to this?
This would be one way to improve on what you currently have. I don't have experience accessing intrinsicContentSize for this calculations and didn't test this locally (other than syntax), but if previously it worked, this should as well.
Basically you're creating an array with maxVisibleRows number of indexPaths. If you have less, then fetchedIndexesCount prevents an indexOutOfBounds crash. Once you have the array, you iterate for each corresponding cell and fetching its size, finally summing it up.
class CannedRepliesTableView: UITableView {
var focussedSection = 0
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: calculateHeight())
}
private func calculateHeight() -> CGFloat {
guard let indexPaths = startIndexes(firstCount: 3) else {
// Your table view doesn't have any rows. Feel free to return a non optional and remove this check if confident there's always at least a row
return 0
}
return indexPaths.compactMap({ cellForRow(at: $0)?.intrinsicContentSize.height }).reduce(0, +)
}
private func startIndexes(firstCount x: Int) -> [IndexPath]? {
let rowsCount = numberOfRows(inSection: focussedSection)
let fetchedIndexesCount = min(x, rowsCount)
guard fetchedIndexesCount > 0 else {
return nil
}
var result = [IndexPath]()
for i in 0..<fetchedIndexesCount {
result.append(IndexPath(row: i, section: focussedSection))
}
return result
}
}

How to create a minimal console in a Swift app (UILabel, UITextView, scrolling, truncating head?)

I want to create a console in my App in order to display activities or information on the screen. It will begin with a simple string, "Awaiting new messages..." and when different things happen, new messages will be added to that string. When the console fills up, the field will scroll with each addition so that the user is always seeing the most recent message at the bottom of the console, and old messages disappear off the view at the top.
Is there a way with "Truncate Head" and a multi-line UILabel? I attempted to do this with UILabel first, but I could find no way to always be viewing the END of the string. Truncate head actually works line-by-line, so it would show me the first five lines of the string and then the tail of the last visible line. I tried various alignment and wrapping settings but nothing worked... am I missing something? Is there a way to have a UILabel always display the end of a string and let the contents just disappear off the top?
Cut the string to fit each time? Maybe I could just cut the string to the last thousand characters or similar? But I don't know how big the UILabel will be (on different screens)... and even if I did, with fonts being what they are, I doubt I could know exactly the number of characters I should trim the string to. I can't trim it to a given amount of SPACE and get the amount of space in my UILabel, can I?
OR, I could use a UITextView and Scroll Maybe this is what I have to do. I can grab the entire value of the my text view's text and add the new string to it and put it back into the UITextView, and then scroll to the bottom using NSMakeRange and .scrollRangeToBottom.
func updateConsole(switchType: String) {
//unwind the console's text
if let tempString = consoleZe.text {
currentText = tempString
}
consoleZe.text = currentText + "A new message here! Something clever taken from \(switchType).\n"
//Scroll to the bottom
let bottom = NSMakeRange(consoleZe.text.characters.count - 1, 1)
consoleZe.scrollRangeToVisible(bottom)
}
That seems like a lot of work for my little update console. I don't care about scrolling to see past values. I'd even prefer the console didn't scroll... So grabbing, adding, pasting, getting the bottom and then scrolling seems like a lot of extra, unwanted baggage.
All thoughts on implementing a minimal console, using UILabel or UITextView or any other way are welcome, thank you!
I have implemented a “Console View Controller” using a tableview and a “ConsoleBuffer” class as the datasource. A tableview corresponds well to the line-by-line oriented nature of a message-logging console—and makes auto-scrolling easy.
The ConsoleBuffer is a singleton class which holds the console messages in a simple array of strings and some helper functions attached. Please see below the complete ConsoleBufferimplementation:
class ConsoleBuffer {
struct Prefs {
static let defaultLines = 100
static let maxLines = 1000
}
static let shared = ConsoleBuffer()
private var buffer = [String]() {
didSet {
if buffer.count > lines {
buffer.removeFirst(buffer.count - lines)
}
tableView?.reloadData()
NSAnimationContext.runAnimationGroup({ (context) in
if let tableView = self.tableView {
if let scrollView = tableView.enclosingScrollView {
let range = tableView.rows(in: scrollView.contentView.visibleRect)
let lastRow = range.location + range.length
if lastRow == oldValue.count - 1 {
context.allowsImplicitAnimation = true
tableView.scrollRowToVisible(buffer.count - 1)
}
}
}
}, completionHandler: nil)
}
}
var lines = ConsoleBuffer.Prefs.defaultLines {
didSet {
if lines > ConsoleBuffer.Prefs.maxLines {
lines = ConsoleBuffer.Prefs.maxLines
}
}
}
var count: Int {
get {
return buffer.count
}
}
var tableView: NSTableView?
private init() { }
func line(_ n: Int) -> String {
if n >= 0 && n < buffer.count {
return buffer[n]
} else {
return ""
}
}
func add(_ line: String) {
let dateStampedLine = "\(Date()) \(line)"
buffer.append(dateStampedLine)
}
func clear() {
buffer.removeAll()
}
}
These two statements make ConsoleBuffer a singleton:
static let shared = ConsoleBuffer()
private init() { }
Having a singleton makes it easy adding new console lines anywhere in your project without the need of having a reference to an instance of the class. Making init private prevents anyone from calling ConsoleBuffer()—rather you are forced to use its singleton instance: ConsoleBuffer.shared.
The console line strings are held in the buffer array which is private to keep its implementation hidden. When adding new lines to this array, the tableview smoothly scrolls to the last line added but only if previously the last line was displayed. Otherwise the scrolling position remains unchanged.
The datasource is now easy to implement:
func numberOfRows(in tableView: NSTableView) -> Int {
return ConsoleBuffer.shared.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.make(withIdentifier: "ConsoleCell", owner: self) as? NSTableCellView
cell?.textField?.stringValue = ConsoleBuffer.shared.line(row)
return cell
}
In the tableview controller’s viewDidLoad function you need to set the tableView property of ConsoleBuffer to the tableview used. Also, this is the place to set the desired maximum number of lines to store in the buffer array:
ConsoleBuffer.shared.tableView = tableView
ConsoleBuffer.shared.lines = 500
Now you can add new lines to the console like this:
ConsoleBuffer.shared.add("console message")
Hope this gets you going into the right direction.

UICollectionView Custom layout reloading issue in Swift

This is refrence link:
https://www.raywenderlich.com/107439/uicollectionview-custom-layout-tutorial-pinterest
I have implemented load more in UICollectionView at last cell , the data is getting downloaded, after download complete i want to reload collection
collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.reloadData()
let concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(concurrentQueue) {
DataManager.sharedInstance.homeRequest(Dictionary: params, onSuccess: { (dict) in
self.downloadPageIndex += 1
let response = HomeObject(dictionary: dict)
for (_,e) in (response.dataDict?.enumerate())!{
self.dataArray.append(e)
}
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()
}
self.activityIndicatorCell.stopAnimating()
}, onError: { (error) in
self.activityIndicatorCell.stopAnimating()
})
like this collectionview reloadata
Can any one help me out?
Ran into your question and am currently facing the same problem, after some digging i figured it out!
The problem is within the PintrestLayout file
Inside you will find code like (PROBLEM) :
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
Basically, if the attributes that you are applying to the CollectionView are empty (they are at the start), apply the PintrestLayout to it, and thats perfect! BUT If you are reloading the data and adding to the collectionView, you need to apply the layout again, but the cache isnt empty when you try to apply the layout to the collectionView again, thus it returns and doesn't work at all...
SOLUTION :
replace that code with :
guard cache.isEmpty == true || cache.isEmpty == false, let collectionView = collectionView else {
return
}
Now you are saying "i dont care if its empty or not, just apply the damn layout to my collectionView"
And Done! Now reloadData() will update how many cells you got...
This is pretty expensive though, if you wanna make it faster, and trim some memory etc... Then look into invalidateFlowLayout Documentation.

Insert new row into a section of UITableView based on selected row in UIPickerView - Swift

I have a custom inline UIPickerView (essentially just a expanding/collapsing UIPickerView) and there are 3 options (rows) in the picker view: "Weekly", "Monthly" and "Enter Manually". What I am trying to accomplish is that if a user chooses the row "Enter Manually", a new row is inserted below the row containing the custom UIPickerView (which would contain a custom TextInputCell I have created - but thats aside from the matter). The UITableView represents a form where the user can create an "event" so to speak. There are 4 sections and each section has a different amount of rows, some rows have expanding/collapsing UIPickerViews and others have custom UITextFields, and others have expanding/collapsing UIDatePickers. I only want this to happen for 1 particular UIPickerView, but I cannot seem to get it working. I had tried something like this in the didSelectRowAtIndexPath for when the UIPickerView is collapsed from selecting it's row:
tableView.beginUpdates()
if(cell.isKindOfClass(PickerCell)) {
let pickerTableViewCell = cell as! PickerCell
if(!pickerTableViewCell.isExpanded() && pickerTableViewCell.rightLabelText() == numClasses[2]) {
alertOptions.insert("Number of Weeks", atIndex: 1)
numberOfRowsAtSection[2] = 5
tableView.insertRowsAtIndexPaths([
NSIndexPath(forRow: alertOptions.count-3, inSection: 2)
], withRowAnimation: .Automatic)
}
}
tableView.endUpdates()
The numClasses is an array with the UIPickerView row data:
let numClasses = ["Weekly", "Monthly", "Enter Manually"]
All the UITableView data is in separate arrays for each section like follows:
var numberOfRowsAtSection: [Int] = [3, 4, 4, 3]
let mainOptions = ["Client Name", "Invoicing Email", "Invoicing Address"]
let dateOptions = ["All Day", "Starts", "Ends", "Time Zone"]
var alertOptions = ["How Many Classes", "Shown on Calendar As", "Alert by Email", "Alert by Text"]
let billingOptions = ["Amount Charged", "Tax Included", "Billing Date"]
And then I suppose I would just put a condition for everything between tableBeginUpdates() and tableEndUpdates() to test if it was the right UIPickerView like:
if(pickerViewTableCell.pickerView == numClassesPicker.pickerView) { ... }
Is what I'm trying to do possible or am I on the right track? Help!
Here are some images for a better visual:
Prior to selecting "Enter Manually":
During selecting "Enter Manually", UIPickerView is expanded:
And after, with it now collapsed and "Enter Manually" Selected,
What I'm going for here is to now have a new row between "How Many Classes" and "Show on Calendar As":
In alertOptions, just add a new object and reload the tableView after you are finished with the action in the pickerView.
One of the main issues was that I was not using tableView.reloadData() but aside from that, the implementation that I ended up coding was:
tableView.beginUpdates()
if(cell.isKindOfClass(PickerCell)) {
let pickerTableViewCell = cell as! PickerCell
if(pickerTableViewCell.pickerView == numClassesPicker.pickerView && numberOfRowsAtSection[2] < 5) {
let newRowIndex = alertOptions.count - 4
let newNumRows = 5
if(!pickerTableViewCell.isExpanded() && pickerTableViewCell.rightLabelText() == numClasses[2]) {
alertOptions.insert("Number of Weeks", atIndex: 1)
numberOfRowsAtSection[2] = newNumRows
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: newRowIndex, inSection: 2)],
withRowAnimation: .Automatic)
tableView.reloadData()
}
} else if(numberOfRowsAtSection[2] == 5) {
let oldRowIndex = alertOptions.count - 5
let numRows = 4
alertOptions.removeAtIndex(1)
numberOfRowsAtSection[2] = numRows
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: oldRowIndex, inSection: 2)],
withRowAnimation: .Automatic)
tableView.reloadData()
}
}
tableView.endUpdates()
And then in didSelectRowAtIndexPath tableView delegate I have a condition to test how many rows are in numberOfRowsAtSection[2] and then return the type of cell I want accordingly