I have a UIViewController that contains a UIScrollView which itself contains both a UITableView and another UIScrollView. Inside the nested UIScrollView is another UITableView.
When the in ViewDidAppear of the UIViewController I calculate how big the tables should be (much bigger than the screen) and set their sizes and then set the content size of the UIScrollView to match the tables they contain (the UIScrollView will provide the scrolling rather than the table itself). This all works fine in the simulator, but on an actual device, it grinds to a halt, pegging the processor at ~100% for tens of seconds. This is obviously unacceptable. Does anybody have any idea why? Or how I can work around it?
The code looks something like this:
OuterScrollView.ContentSize = new SizeF (View.Frame.Width, tableHeight);
InnerScrollView.ContentSize = new SizeF (InnerTable.Frame.Width, tableHeight);
InnerScrollView.Frame = new RectangleF(InnerScrollView.Frame.Location,
new SizeF(InnerScrollView.Frame.Width, tableHeight));
// So far so good
OuterTable.Frame = new RectangleF(OuterTable.Frame.Location,
new SizeF(OuterTable.Frame.Width, tableHeight)); // this slows everything down!!
InnerTable.Frame = new RectangleF(InnerTable.Frame.Location,
new SizeF(InnerTable.Frame.Width, tableHeight)); // and so does this
Removing both of the table .Frame setting statements and everything works quickly enough, but with them in it's very slow. And the slowness doesn't come directly here, but somewhere after the call to the base ViewDidAppear.
Update: I had an epiphany and thought if resizing the table is the problem, just make the tables big enough that they don't need resizing. In the setup I have, the scrolling is being handled by the scroll view not the table itself, so I could just set the table as really big and let the scroll views ContentSize take care of clipping the blank part of the table. This works, in the sense that it displays how I want it to, but it's actually even slower! So my conclusion is that it's not the table resize that is the problem, but rather the presence of really long (in this case I set the height to 4,000 - the resize was setting it to 2,354) tables.
Background: To add a little more to what I'm trying to do. Since Apple, in their wisdom, decided that nobody needs a grid like control, I'm trying to set up a situation where I have a grid-like view where the left most columns stay in place, but you can horizontally scroll the right-most columns, and, when you scroll vertically, everything will stay in sync. After some searching, I came across a solution (sorry, can't remember exactly where), which with a little tweaking works (in the simulator). Basically, you embed tables in a scroll view so that the scroll view can handle the scrolling. The layout looks something like this:
+-------------------------Outer Scroll-------------------------+
| +---------------Inner Scroll---------------+|
|+--Fixed Table--+ |+---------Scrolling Table----------------+||
|| | || |||
|| | || |||
|| | || |||
|| | || |||
|| | || |||
|| | || |||
|+---------------+ |+----------------------------------------+||
| +------------------------------------------+|
+--------------------------------------------------------------+
The fixed table has a custom cell with a couple of columns while the scrolling table has another custom cell (wider than the screen) with the rest of the columns. You can scroll the scrolling table horizontally (thanks to the inner scroll view) and you can scroll everything vertically thanks to the outer scroll view.
Another Update: So it seems the problem is with the table not reusing cells when you set it large. I guess the cell reuse logic only extends to determining if the cell is within the frame of the table and not if that part of the table is actually visible. So with 50 items, instead of displaying 6-7 and then re-using those cells, it creates all 50 regardless. So I abandoned my earlier attempt and tried to synchronize the scrolling between two tables like this:
OuterTable.Scrolled += (sender, arg) =>
{
InnerTable.ContentOffset = new PointF(InnerTable.ContentOffset.X,
OuterTable.ContentOffset.Y);
};
InnerTable.Scrolled += (sender, arg) =>
{
OuterTable.ContentOffset = new PointF(0, InnerTable.ContentOffset.Y);
};
This almost works, except iOS doesn't like horizontal scrolling for tables, so the inner table still needs to be wrapped in a scroll view (set with the ContentSize to cover the width I need to scroll). At this point it almost works. It will usually keep seemlessly in sync, but with a bit of messing around you can get, for example, the inner table to scroll diagonally (despite setting direction lock on everybody) and in some cases it's possible to get them out-of-sync, which looks really stupid. So it doesn't quite work as well visually, but at least it doesn't hang up the UI thread as badly.
Have you tried doing this with autolayout? There you are not responsible for when the views actually get resized, just setup the constraints in viewDidLoad and let the rest be taken care of by the system.
But in the terms of manual layout - I don't see why this would actually happen. When you profiled it with Instruments, what was the slow portion in Time Profiler?
From the documentation:
Important: You should not embed UIWebView or UITableView objects in
UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
Your interface might appear slow because touch events are getting handled by the wrong objects. This can make scroll views appear to jump around, act laggy, or not respond at all.
Can you describe the behavior you're going for? Can you achieve what you want using only a UITableView with a UIScrollView in it, or perhaps a UICollectionView?
It appears that the only way for your proposed implementation to work is to load ALL the UI at initialization, which eliminates the performance benefit of using a UITableView at all. It will be considerably more efficient (from a memory perspective) for you to have a top level UITableView that scrolls normally, and contain a horizontally scrolling UIScrollview within each cell.
+-------------------------Table View---------------------------+
|+-----------------------Table View Cell---------------------+|
||+--Fixed Section--+ +---------Synched Scroll View---------+||
||| | | |||
||| | | |||
||+-----------------+ +-------------------------------------+||
|+-----------------------------------------------------------+|
|+-----------------------Table View Cell---------------------+|
||+--Fixed Section--+ +---------Synched Scroll View---------+||
||| | | |||
||| | | |||
||+-----------------+ +-------------------------------------+||
|+-----------------------------------------------------------+|
|+-----------------------Table View Cell---------------------+|
||+--Fixed Section--+ +---------Synched Scroll View---------+||
||| | | |||
||| | | |||
||+-----------------+ +-------------------------------------+||
|+-----------------------------------------------------------+|
+--------------------------------------------------------------+
The important step is synching the horizontal scroll views, which you can do by sending NSNotifications or custom delegate messages on -scrollViewDidScroll:
It may feel wasteful to be creating 10+ scrollviews within table view cells, but in practice it ends up being fairly efficient.
Related
When creating a UITableViewController there are two situations that create an "ugly" UX
calling/open it without any data in it --> shows an empty table (i.e. empty rows,UITableViewCell, as many as fit in the window)
calling/open it with fewer rows of content that fit the window --> show the full rows followed by empty rows
I wish to receive the following result:
if there is no data show a picture or view with text - there isn't any data yet or something like that
show only the full lines and no more rows (blank or background image)
Is there a way to achieve that?
To add these effects, you will probably have to make your own UITableViewController from a regular UIViewController, and even subclass UITableView. If you have a regular UIViewController with your filler image/text as the background, you can place a UITableView on top and hook up the delegate/datasource. Now, in your code, detect when there is no data available and set the hidden property of the UITableView accordingly.
As for the following empty rows, you will either have to turn off the row separators (in IB), or subclass a UITableView (can't help you there). Good luck!
I have a UICollectionView which shows images retrieved from the web. They are downloaded asynchronous.
When user scrolls fast, they see placeholders until the cell loads. It seems UICollectionView only loads what is visible.
Is there a way to say "collection view, load 20 cells more above and below" so chance is higher that it loaded more cells while user was looking at content without scrolling?
The idea is to have the VC recognize when a remote load might be required and start it. The only tricky part is keeping the right state so you don't trigger too much.
Let's say your collection is vertical, the condition you want to know about is when:
BOOL topLoad = scrollView.contentOffset.y < M * scrollView.bounds.size.height
or when
BOOL bottomLoad = scrollView.contentOffset.y > scrollView.contentSize.height - M * scrollView.bounds.size.height
in other words, when we are M "pages" from the edge of the content. In practice though, this condition will be over-triggered, like when you're first loading, or if you're testing it on scrollViewDidScroll, you don't want to generate web requests for every pixel of user scrolling.
Getting it right, therefore, requires additional state in the view controller. The vc can have a pair of BOOLs, like topLoadEnabled, bottomLoadEnabled, that are NO until the view is ready. Then, scroll delegate code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// compute topLoad and bottomLoad conditions
if (topLoad && self.topLoadEnabled) [self startTopLoad];
similarly for bottom. The load code looks like this, abstractly:
self.topLoadEnabled = NO; // don't trigger more loading until we're done
[self.model getMoreTopStuff:^(NSArray *newStuff, NSError *error) {
// do view inserts, e.g. tableView.beginUpdates
self.topLoadEnabled = YES;
}];
Same idea for bottom. We expect the model to fetch for itself (maybe the model has image urls) and cache the result (then the model has images). As the datasource for the view, the view controller gets called upon to configure view cells. I can just naively ask the model for images. The model should answer either fetched images or placeholders.
Hope that makes sense.
In my opinion you are making the wrong assumption: cells are just views so you shouldn't treat them as model objects. UICollectionView and UITableView are very efficient because they constantly recycle cells so you should think in therms of pre loading content in the business side of things. Create interactor or viewmodel objects and populate your data source with those, then you'll be able to ask those objects to preload images, if you still wish to do so.
A BOOL flag seldom is the answer. I'd rather go for estimating a reasonable page size and fetching images as needed from the cellForItemAtIndePath method.
I have a scence with a bunch of labels and buttons. When you tap on a button a view slides up from the bottom with controls in it, a kind of keyboard so to say. It "looks" like this:
-----------------------------
| |
| [Button 1] [ Slider 1 ] |
| |
| [Button 2] [ Slider 2 ] |
| |
-----------------------------
This "keyboard" is created at the very beginning when the view loads and the animation is done switching its heigth from 0 to its instrinsic content size. This escene only supports landscape mode and it took me quite a while to keep the "keyboard" on the view when the device rotates 180 degrees.
The problem I see pops up with either of the two following situations:
The device rotates 180 degrees.
The "keyboard" is called.
This problem as follows:
Unable to simultaneously satisfy constraints...
.
.
.
.
(
"<NSLayoutConstraint:0x718c6c0 UIButton:0x717e0d0.centerY == UISlider:0x717d9d0.centerY>",
"<NSLayoutConstraint:0x7190a00 UIButton:0x717e0d0.centerY == UISlider:0x717d9d0.centerY>"
)
The error log gives me this error twice, once for each set of button-slider.
What I think is weird is that the conflicting constraints are exactly the same. I thought I did some copy-paste mistake and added the same constraint twice, but it's not the case.
I'm guessing it has something to do with updateViewConstraints being called upon rotation and also whe I perform the animation, but I cannot see why only these constraints are affected since there are some more in this "keyboard" view.
All and all, this Autolayout is beign quite more difficult than Apples whants to claim. In my opinion, of course.
Any ideas?
EDIT: the constrains are set all in code using mainly the visual language format. The constrains of the controls inside the "keyboard" are added into the "keyboard" view which is the normal thing to do, I believe.
Just to try it out, I changed the problematic constraints and, instead to adding them to the "keyboard" subview, I added them to the self.view ("keyboard" superview). All the sudden, no more errors are shown.
Despite of that, I'd really like some discussion on the matter because I still don't know what's wrong and I just had a lucky shot. I'd really like to undestand it.
The fact that the conflicting constraints are exactly the same is in fact the error. With Auto Layout, you can't have a constraint twice. That will generate the error you see above.
Definitely, you have added the constraint twice. You can see this from the memory addresses. You have two different NSLayoutConstraint instances, 0x718c6c0 and 0x7190a00. However, the instances each refers to are both the same. That being that the vertical center centerY of your UIButton instance 0x717e0d0 should be in the middle of the UISlider 0x717d9d0.
Possibly your updateConstraints method has been called and you haven't checked to see if a constraint already exists before adding it again.
I don't know if this may help you but I just used this tutorial for setting up constraints to buttons and labels in my app
I just inherited code which hides/shows rows a UITableView by using the delegate heightForRowAtIndexPath method and returning height 0 for "hidden rows".
The code works, but it has me concerned there might be fraught with unforeseen complications. Can someone either ease my concerns or give me good reasons why this could cause problems (I couldn't find any issues with initial testing).
The table is fairly small <10 rows total and would require custom row heights even without this hidden row solution.
I do the same thing in the code I just worked on. I am not happy with different behaviour for different table view settings.
The alternative in my case is more complex (a model that adapts to what is visible or not).
For now, I put a //HACK comment on it and document a few peculiarities.
This is what I have found (iOS 5.0 tested):
Set tableView.rowHeight = 1; Zero will give a cell with zero height (as returned by tableView:tableView heightForRowAtIndexPath:) some default height.
You must have a cell separator. If none is selected, then a default height is assigned to zero height rows. The height of 1 is included with the separator.
If your code works in a different way, it would be interesting to know how it is set up.
It would be cleaner to add and remove the rows between two beginUpdates and endUpdates calls, but I don't see why this 0-height method should not work.
If there are no UI-artifacts, that is (e.g. the Delete button showing up overflowing to the next cell).
I use this method of setting hidden cell heights to 0. It works well and also means I can animate the inclusion of new cells by expanding the cell height (such as adding a DatePicker Cell like the calendar app does).
A few things I have had to watch out for in iOS 7.1 are that very squashed text does still appear even when a cell height is = 0 so I've needed to remove cell text in that case. Also, I have change the size of the cell's separatorInset as that was appearing as well.
I'm struggling with this problem, although I'm close to the solution, but I guessing I'm missing something.
Here is the situation :
I have UITableView with 30 cells, and one section header (if it's helpful..).
The table size is exactly the size of 3 rows.
The mission : to let the user scroll 3 rows everytime, exactly 3.
I set the pagingEnabled=YES.
What happens is :
"page 1" - 3 rows - ok (rows 0-2)
swiping to "page 2" - next 3 rows - ok (row 3-5)
swiping to "page 3" - and the paging is not good, either skiping row 6 and showing row 7-9
or the page is stopped in the middle of the cell of 6 (also tried to move the scroll to complete cell visibilty with scolling end event, but it skips some of the rows on some pages)
Any ideas how to fix the situation, every page = 3 rows only ,without skipping or showing half of cell ?
I don't mind if the way will be without pagingEnabled=YES.
I arrived at the conclusion this is basic thing I will use in the future , and I think a lot of others will use the solution that other friends will give here.
Thanks.
Never thought of using this property in a Table View.
Since it is a subclass of UIScrollView, try instead to use the UIScrollViewDelegate methods and calculate where to stop.
A good place to start can be in scrollViewWillEndDragging:withVelocity:targetContentOffset:. and return the offset yourself.
P.S.
from the documentation:
This method is not called when the value of the scroll view’s
pagingEnabled property is YES. Your application can change the value
of the targetContentOffset parameter to adjust where the scrollview
finishes its scrolling animation.