Wrong cells count for collection view in UI Tests - ios

I have a test for a collection view that works like this:
func testDeleteItem() {
app.collectionViews.staticTexts["Item"].tap()
app.buttons["Delete"].tap()
XCTAssertEqual(app.collectionViews.cells.count, 2)
XCTAssertFalse(app.collectionViews.cells.staticTexts["Item"].exists)
}
After the tap, there is a new screen with the delete button. When the button is tapped, the screen dismisses itself and reloads the collection view. Everything goes as expected in the UI, but I get both asserts failing. In the first count it is still 3 and in the second item it still exists.

I have found the solution, but it's a workaround for wrong API behavior. Collection view is caching cells, that's probably why I have 3 cells, even if I have removed one. Deleted cell is offscreen, so you can test if it is hittable:
XCTAssertFalse(app.cells.staticTexts["Item"].hittable)
To find a count, I have created extension:
extension XCUIElementQuery {
var countForHittables: UInt {
return UInt(allElementsBoundByIndex.filter { $0.hittable }.count)
}
}
and my test looks like this:
func testDeleteItem() {
app.collectionViews.staticTexts["Item"].tap()
app.buttons["Delete"].tap()
XCTAssertEqual(app.collectionViews.cells.countForHittables, 2)
XCTAssertFalse(app.collectionViews.cells.staticTexts["Item"].hittable)
}

I also came across this issue, but in my case, .cells query wasn't evaluating correctly. Instead of .cells, using
XCUIApplication().collectionViews.element.childrenMatchingType(.Cell).count
worked for me and returned the correct count.
Update:
I also found that scrolling the view so that all the cells are dequeued before getting the count fixed the issue. It seems the accessibility framework does not find the other cells until they have been dequeued (I guess that makes sense).
XCUIApplication().collectionViews.element.swipeUp()

I think cells query returns all cells from all the tables currently in view hierarchy. Try to do this app.tables.firstMatch.cells.count, it worked for me.

I ran into this question when I was looking for the same answer, but in Objective-C. For those like me, I adapted #Tomasz's method to count Collection View cells in UI tests:
-(NSInteger)countForHittables:(NSArray<XCUIElement*>*)collectionView{
__block int hittables = 0;
[collectionView enumerateObjectsUsingBlock:^(XCUIElement * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.hittable){
hittables++;
}
}];
return hittables;
}
To call it: [self countForHittables:app.collectionViews.cells.allElementsBoundByIndex];.

I had the same issue. Even if the collection hasn't been populated because it was waiting for the response of an API, cells.count >= 1 was always true.
What I did, based on Tomasz Bąk's answer I created an extension to wait for the collection to be populated:
extension XCTestCase {
func waitForCollectionToBePopulated(_ element: XCUIElement, timeout: TimeInterval) {
let query = element.cells
let p = NSPredicate(format: "countForHittables >= 1")
let e = expectation(for: p, evaluatedWith: query, handler: nil)
wait(for: [e], timeout: timeout)
}
}
And on the caller site will look:
waitForCollectionToBePopulated(collection, timeout: {timeOut})

Related

UITest - UICollectionView scrolling issue with horizontal direction when isPagingEnabled true

I've been trying to scroll UICollectionView with horizontal scroll, to the next page when isPagingEnabled property was set as true. I've been working on it for couple of days and I've made a lot of research, but I couldn't find any case like mine. If you already had this problem and if you already found a solution for it, it would be great sharing your solution way with me. Here is my current case;
func sampleTest() {
let collectionView = app.collectionViews[.sampleCollectionView]
collectionView.waitUntil(.exists)
let totalPageCount = collectionView.cells.count
guard totalPageCount > 0 else {
XCTFail("No pages could find in collection to take snapshot.")
return
}
for currentPage in 1...totalPageCount {
snapshot("Page\(currentPage)")
collectionView.swipeLeft()
}
}
Here, swipeLeft() method of XCUIElement is not working as expected in my case. When I call the method, it is not moving to the next page. It swipes a little bit and turn back due to isPagingEnabled = true statement.
In addition, there is another problem that collectionView.cells.count is calculated wrong. It always returns 1. I assume that the reason of the problem is about reusability. Because the other cells has not dequeued yet. Or collectionView.cells.count is not working as I guess?

Problems with asynchronous data, UITableView, and reloadRowsAt

I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.
For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:
1) Dequeue reusable cell
2) Call configure
func configure(someArguments: ) {
//some checks
process(withArguments: ) { [weak self in] in
if let weakSelf = self {
weakSelf.reloadDelegate.reload(forID: id)
}
}
}
3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy
4) If the async data is NOT in the cache, fetch it, cache it, and call the completion
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
//add to cache
//call completion
}
}
}
5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)
func reload(forID: ) {
tableView.beginUpdates()
tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
tableView.endUpdates()
}
Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).
Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.
If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!
I've tried wrapping various things in DispatchQueue.main.async to no avail.
reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)
Any help would be appreciated!
Reused cells are not "fresh". Clear the cell while waiting for content.
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
// ** reset the content of the cell, clear labels etc **
//add to cache
//call completion
}
}
}

Refresh Control Issue with custom cells - Swift 4 IOS 11

I am building an iOS app and I am trying to implement a pull-down refresh control on my project. The data is fetched correctly from an API and displayed on my table. But the problem rises when I do pull down to refresh. The following situations happen:
If I pull down for a long distance from the top, and the tableview.reloadData() function is called, the cells in the non-visible portion of the table come with the default tableview cells on top of them, overlapping...
if I pull down multiple times in quick succession the same issue happens.
I believe that it is because tableview.reloadData() is called multiple times in quick succession. But why are the default cells getting dequeued on top of my custom cells? Here is the section of code in the function to handle the pulldown:
#objc func refreshFunc(){
//let offset = scrollView.contentOffset.y
if myRefreshControl.isRefreshing{
readJson { (activities) in
self.activities = activities
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
}
}
Any help would be appreciated. Thanks in advance
UPDATE:
Changing the code to the code below seems to remove the error, but the problem is that now I need to pull down twice in order to get the results updated on the table:
#objc func refreshFunc(){
readJson { (activities) in
self.activities = activities
}
self.tableView.reloadData()
self.myRefreshControl.endRefreshing()
}
Please note that running the reloadData on the main thread gives the same result, I still need to pull down twice to update.
Please try to give some delay before refresh table view may it resolve your problem.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayTime) { [weak self] in
self?.tableView.reloadData()
}
Hope it works
Cheers :)
DispatchQueue.main.async {
self.tableView.reloadData
}
Try this, always, if u want change UI you have to call on main thread

UICollectionView state restoration: restore all UICollectionViewCells

I searched a lot through Google and SO, so please forgive me, if this question has already been answered!
The problem:
I have a UICollectionView with n UICollectionViewCells. Each cell contains a UIView from a XIB file. The Views are used for data entry, so all cells have a unique reuseIdentifier. Each View has also a unique restorationIdentifier. Everything works in normal usage, but not when it comes to state restoration:
The first 3 or 4 cells are getting restored properly because they are visible on the screen on startup, but the remaining cells, which are not visble, are not getting restored.
Current solution:
So I've discovered so far that a View is only restored if it's added to userinterface at startup.
My current working solution is to set the height of all cells to 1 in the process of restoring. Now every cell is loaded and all views are restored.
When applicationFinishedRestoringState() is called, I reload the CollectionView with the correct height.
Now my question is: I'm not happy with this solution, is there a more clean way to achieve restoring of all the UIViews?
I think you are getting a bit confused between your data model and your views. When first initialised, your table view is constructed from a data model, pulling in stored values in order to populate whatever is in each cell. However, your user does not interact directly with the data model, but with the view on the screen. If the user changes something in the table view, you need to signal that change back up to the view controller so that it can record the change to the data model. This means in turn that if the view needs to be recreated the view controller has the information it needs to rebuild whatever was in the table when your app entered the background.
I have put together a simple gitHub repository here: https://github.com/mpj-chandler/StateManagementDemo
This comprises a CustomTableViewController class which manages a standard UITableView populated with CustomTableViewCells. The custom cells contain three switch buttons, allowing the state of each cell to be represented by an array of Boolean values.
I created a delegate protocol for the cells such that if any of the switches is tripped, a signal is sent back to the view controller:
protocol CustomTableViewCellDelegate {
func stateDidChange(sender: CustomTableViewCell) -> Void
}
// Code in CustomTableViewCell.swift:
#objc fileprivate func switched(sender: UISwitch) -> Void {
guard let index : Int = switches.index(of: sender) else { return }
state[index] = sender.isOn
}
// The cell's state is an observed parameter with the following didSet method:
fileprivate var state : [Bool] = Array(repeating: false, count: 3) {
didSet {
if state != oldValue, let _ = delegate {
delegate!.stateDidChange(sender: self)
}
}
}
CustomTableViewController is registered to the CustomTableViewCellDelegate protocol, so that it can record the change in the model as follows:
// Code in CustomTableViewController.swift
//# MARK:- CustomTableViewCellDelegate methods
internal func stateDidChange(sender: CustomTableViewCell) -> Void {
guard let indexPath : IndexPath = tableView.indexPath(for: sender) else { return }
guard indexPath.row < model.count else { print("Error in \(#function) - cell index larger than model size!") ; return }
print("CHANGING MODEL ROW [\(indexPath.row)] TO: \(sender.getState())")
model[indexPath.row] = sender.getState()
}
You can see here that the function is set up to output model changes to the console.
If you run the project in simulator and exit to the home screen and go back again you will see the state of the tableView cells is preserved, because the model reflects the changes that were made before the app entered the background.
Hope that helps.

How to update UIPageViewController's page control count

I have a UIPageViewController with a page control. In the delegate, I implement the following methods:
presentationCountForPageViewController:
presentationIndexForPageViewController:
This works well, but the total number of pages can change, and the number of dots displayed in the page control doesn't change in this case.
How do I tell the page view controller to call presentationCountForPageViewController: to update the total number of dots when this happens?
So it turns out this was a result of using NSFetchedResultsController as the source of the data for pages. The count for NSFetchedResultsController was not getting updated before I called setViewControllers:direction:animated:completion: on the UIPageViewController.
The fix was to call the following function in controllerDidChangeContent: to force an update to the page control.
func refreshPageController() {
let controllers = pageController.viewControllers
if controllers.count > 0 {
pageController.setViewControllers(controllers, direction: .Forward, animated: false, completion: nil)
}
}
Update:
Though the solution above worked, it broke the animation I was using to scroll to the next page when a page was removed.
A better solution: just wait until the page removal is committed to Core Data before calling setViewControllers:direction:animated:completion:.
I had the same problem, and the workaround I found is to reset the uipageviewcontroller view every time I change my dataSoure and reload the whole pageViewController with the new dataSource. This way I'm able to have the correct number of dots (with the dataSource and delegate functions correctly implemented). I do this with the following function :
func reloadPageViewController() {
if pageContentView.subviews.count > 1 {
for _ in 1...pageContentView.subviews.count - 1 {
pageContentView.subviews.last?.removeFromSuperview()
}
}
guard let pageViewController = storyboard?.instantiateViewController(identifier: String(describing: PageViewController.self)) as? PageViewController else {
return
}
pageViewController.removeFromParent()
self.configurePageViewController()
}
Set the dataSource property again to a new instance of a data source object that has the new number of objects.

Resources