This is a problem stumping me and my team at work.
We have a header view we are using in one section that is not always drawing. It's inconsistent, though appears to be slightly more frequent on fresh installs.
I will first preempt by stating we are not registering it to the table view and thus not actually dequeueing it. I tried that but the boss says since we are never actually re-using it he is adamant against doing so. Therefore we have it like this:
class DashboardViewController {
...
var trendsHeader: TrendsHeader?
...
override func viewDidLoad() {
super.viewDidLoad()
setupTrendsHeader()
....
}
...
func setupTrendsHeader() {
trendsHeader = Bundle(for: TrendsHeader.self).loadNibNamed(TrendsHeader.identifier, owner: view, options: nil)?.first as? TrendsHeader
trendsHeader?.delegate = self
trendsHeader?.datasource = self
trendsHeader?.leftDropdown.selectRowWithAction(at: 0)
}
...
// in heightForHeaderInSection
return TrendsHeader.cellHeight //Height is returned properly, empty space is of the right height
...
// in viewForHeaderInSection
return trendsHeader ?? UIView()//Should only return UIView if trendsHeader is nil
}
I have tried modifying viewForHeaderInSection as such:
//in viewForHeaderInSection
if trendsHeader == nil {
setupTrendsHeader()//Breakpoint inserted here
}
return = trendsHeader ?? UIView()
And with a breakpoint in the if statement so I should know if the trendsHeader is nil. It doesn't hit the breakpoint but still doesn't draw that header. If I wait anywhere from 5–30 seconds the header will show up, or if I scroll down the header will show up once it redraws the section. But I need it to show up initially as well, which it still does in most runs of the app but sometimes just doesn't. Honestly the most frustrating part is the inconsistency.
Any insight as to why this is occurring and/or a resolution that doesn't involve convincing my boss to register a view for re-use that isn't going to be re-used?
We found it. Long story short, the section in question is tied to 2 service calls, one for each option on the header's drop down menu. Those services had delegates who reloaded the section upon the callback functions, and they were getting them in quick succession causing the glitch.
We replaced the reload section code with reload rows code instead, so the header does not get refreshed.
Related
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?
I have a collection view with three different cells. Each of the cells contains a table view. So, there are three table views. I've set tags for each of them (from 1 to 3).
Now, on my view controller (I set it as the table view's data source when I dequeue collection view's cells) I call table view's data source method for the number of rows. To distinguish table views I check each one's tag. Here is the code for that:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch tableView.tag {
case 1:
if unitItems1 != nil {
return unitItems1!.count
} else {
return 0
}
case 2:
if unitItems2 != nil {
return unitItems2!.count
} else {
return 4
}
case 3:
if unitItems3 != nil {
return unitItems3!.count
} else {
return 4
}
default:
return 0
}
}
The problem is, when the first cell of the collection view is shown (with the first table view) it works fine. But when I scroll to the second cell, BOTH case 2 and case 3 get executed. After that, the second table view shows data as expected, but when I scroll to the third one, the method doesn't get called.
I can't figure out why two case statements get called one after another, while everything works fine for the first cell. If you have any ideas why this happens (or maybe, you could suggested a better way of checking table view's), I would appreciate your help.
Actually, the solution is quite simple. The reason of the problem was that collectionView's data source method was dequeueing all the cells one after another, even when they weren't on the screen. Consequently, tableView's inside of each cell were getting set, too. So, their data source method was getting called, hence the problem.
UICollectionView has a property called isPrefetchingEnabled. According to the documentation it denotes whether cells and data prefetching is enabled.
The documentation says:
When true, the collection view requests cells in advance of when they will be displayed, spreading the rendering over multiple layout passes. When false, the cells are requested as they are needed for display, often with multiple cells being requested in the same render loop. Setting this property to false also disables data prefetching. The default value of this property is true.
So, to solve the problem, described in the question, I set it to false as soon as my collectionView gets set.
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
}
}
}
I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!
I have a UITableViewController in my app with a UIRefreshControl added to it. Sometimes however (I'm not sure how to reproduce this, it happens every now and then), I get some extra whitespace at the top of the table view with the refresh control being offset even below that.
This is what it looks like (idle on the left, being pulled down on the right):
I don't have any clue what could be causing this. In my viewdidload I'm only instantiating the refresh control and calling an update function that sets the attributed title. I've moved adding the refresh control to the table view into the viewDidAppear as I've read elsewhere. This is what that code looks like:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
updateData()
}
override func viewDidAppear(animated: Bool) {
refreshControl!.addTarget(self, action: "updateData", forControlEvents: UIControlEvents.ValueChanged)
tableView.insertSubview(self.refreshControl!, atIndex: 0)
}
func updateData() {
//...
ServerController.sendParkinglotDataRequest() {
(sections, plotList, updateError) in
//...
// Reload the tableView on the main thread, otherwise it will only update once the user interacts with it
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
// Update the displayed "Last update: " time in the UIRefreshControl
let formatter = NSDateFormatter()
formatter.dateFormat = "dd.MM. HH:mm"
let updateString = NSLocalizedString("LAST_UPDATE", comment: "Last update:")
let title = "\(updateString) \(formatter.stringFromDate(NSDate()))"
let attributedTitle = NSAttributedString(string: title, attributes: nil)
self.refreshControl!.attributedTitle = attributedTitle
})
}
}
Do you need to add the refresh control as a subview of the tableView? I think all you need to do is assign self.refreshControl. According to the documentation:
The default value of this property is nil.
Assigning a refresh control to this property adds the control to the
view controller’s associated interface. You do not need to set the
frame of the refresh control before associating it with the view
controller. The view controller updates the control’s height and width
and sets its position appropriately.
Adding a subview in viewDidAppear could get executed more than once. If you push a controller from a cell and pop back this will get called again. It could be that insertSubview checks if the refresh already has a parent and removes it first, so might not be your issue. You should only do the insert when the controller appears for the first time.
updateData could also be getting added multiple times.
So I think you only need to assign self.refreshControl and then add a handler for the refresh action as you do now using addTarget but this time do it on self.refreshControl.
You can also do all this from storyboard. In storyboard you select the UITableViewController and on the attribute inspector simply set the Refreshing attribute to enabled. This adds a UIRefreshControl into the table and you can see it on the view hierarchy. You can then simply CTRL drag as normal from the refresh control into the .h file and add an action for valueChange which will be fired when you pull down on the refresh control in the table.
Well, I believe that your described behavior might not necessarily be caused by the refresh control.
According to the fact that you don't have any other subviews below your table view I would recommend you to try to place a "fake"-view below your table view. I usually prefer an empty label with 0 side length.
I had similar issues like yours where my table view insets were broken in some cases. And as soon as I used this "fake" subview the problems disappeared. I've read about this issue in some other threads, too. And the solution was this. Seems to be an odd behavior/bug.
Give it a try :)