Can someone please give me a hint on how to recreate the scrolling effect used in the UltraVisual iPhone app? Here's a gif to illustrate the effect:
The first "cell" is full height while the other displayed cells are regular sized. While the user scrolls up, the first cell slowly animates to the regular height, while the next one slowly gets bigger. Do they use an UITableView? Or an UIScrollView? I have no idea how it's made...
Ha, you made my day! I actually wrote that view :)
This is actually very straightforward. This view uses UICollectionView with a custom UICollectionViewLayout.
The general principle is this. I make up a 'drag interval' – that is the required distance to drag between each cell. This value is arbitrary but affects how much the user has to drag to switch cells. The total height of the collection view is the 'drag interval' * the number of items in the view. Then I set the layout to automatically paginate to the nearest drag interval (which gives it the snapping behavior). This is very similar to how coverflow works. From this you can calculate the index of the 'top cell' by dividing the contentOffset.y by the height.
With the 'top cell' index you can generate the frames for each cell pretty easily. The top cell's frame is { 0, contentOffset.y, 320, 176 }, and from there you can calculate the next cells frame and so forth.
Then the last trick is calculating the interpolation of the page index. This is basically the decimal part of the current cell index. This will give a number between 0 and 1 that can be used to calculate the interpolation between the top frame and the frame below.
Every 'prepareLayout' calculates the frames of the cells on screen, and then in layoutAttributesForElementsInRect:, generate all the layoutAttributes based on the generated frames.
Using this trick you can create all sorts of complicated layouts. UICollectionView can be a powerful beast, but definitely takes a bit to wrap your head around it.
It's very cool! We made a fairly simple to use control like this that can be found here:
https://github.com/RobotsAndPencils/RPSlidingMenu
Related
Nowadays fortunately it's trivial to have an iOS table where every cell has a dynamic height. So in the cell vertical constraints ..
---- top of content view
- vertical constraint to
-- UILabel, with, .lines set to zero
- vertical constraint to
---- bottom of content view
Assume the UILabel texts vary greatly one word, 20 words, 100 words,
In the table set
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200 // say
and you're done, these days it works perfectly of course.
However, I had the common situation where you load the table, imagine ten cells.
I populate the UILabel with "Loading..."
Only then - say, a second or two later - do we get the information for the text content. It arrives say a second later and the cell changes the text to "Some long text .. with many lines".
In fact I was surprised to learn it seems UITableView does NOT handle this. The cell in question gets stuck on the original short height.
So, after the larger text is set, I tried all permutations of the usual:
maintext.sizeToFit()
contentView.layoutSubviews()
contentView.layoutIfNeeded()
on the cell, doesn't work.
I tried sending a setNeedsLayout and/or layoutIfNeeded to the table itself, doesn't work.
I thought about .reloadData() on the table itself but - doh - that would again trigger the content being drawn from the server and loaded again, so that's not ideal.
Please note that:
Obviously there are any number of workarounds for the specific example such as not using dynamic data
I am completely aware how to manually animate the height of one cell (like when you "expand" one to show something else when the user taps)
This question is about autolayout and table view - which, thanks Apple, nowadays flawlessly handles completely dynamic cell heights involving UILabels with lines zero.
But what about if the text in such a label changes?
It seems that the table view system does NOT handle this.
Surely there's a way?
When the content of a cell changes the layout (in this case, the height) you must inform the table view that the layout has changed.
This is commonly done with either:
tableView.beginUpdates()
tableView.endUpdates()
or:
tableView.performBatchUpdates(_:completion:)
Why is that not triggered automatically?
I suppose it could be to allow you to do your own animation, or you may want to delay the update, or some other reason that doesn't come to mind at the moment.
Or, it may be due to maintaining backward compatibility?
I don't know. I imagine Apple could tell us...
I have a UICollectionView that has two layout's (list style like uitableview and gallery style which is a full screen horizontal item scroller).
The cell contains a single image and two labels (title & description). In list layout the image is on the left and labels on the right. In gallery layout the image is full width & 50% height with labels beneath. Like this:
The default animation when switching between these two layouts is too simply resize and move the cells to their new positions in the layout. However I want to make a much more specific animation.
Specifically these are the rough steps I want the animation to take when going from list to gallery view:
Fade out all but the first visible cell (aka cell 1)
Fade out the labels in cell 1
Grow the width of the image in cell 1 to 100%
Grow height of cell 1 to full screen (with the image height following the cell growing - to 40% height)
Fade back in the labels for cell 1
Show the other cells (although they should be off screen - so this may not be neccessary)
The steps would pretty much reverse for gallery -> list layout change.
The UICollectionViewFlowLayout is perfect for the layouts that i want to achieve, I just want to have a much finer control over the animation so that I can produce this "stepped animation" (or would this be like keyframe animation?)
It probably doesn't matter much - but i'm using auto layout constraints (via SnapKit) wherever possible and i'd like to maintain that where i can.
Of course I don't expect someone to program all this for me, however Google isn't helping me work out where to start.
Could someone please point me in the right direction of which methods I need to be implementing/overriding on which classes, and maybe a hint of pseudo-code?
I'm building an app where I really need to display a list like the one seen in the iOS calendar app. I need to create a collectionView where I can have cells that expand enough to cover their respective hours, like so:
I've tried various things, including this project at Github which I didn't understand how to use in another project
As well as this project I quickly made with a UITableViewController while exploring different methods:
But I'm not really getting where I want to. I need to have the design seen in the first picture and was wondering if anyone could point me in the right direction to achieve that?
Thank you so much for help!!
Update:
Shouldn't the actual cell frame represent the start of an hour, 10:00 in this example? (Rather than the custom-made separator)
Update
Why does the hierarchy look like this:
This kind of positioning appears to be "blocking" the touch of the cells and the didSelectItemAtIndexPath method doesn't get called. This applies to the cells that appear further back than the other ones.
You should indeed use a UICollectionView along with a custom layout. Just provide supplementary views to build the underlying daily schedule (one view per hour), and then use the cells to lay down your events.
When subclassing UICollectionViewLayout, you need to implement a few methods:
collectionViewContentSize should return a height equal to the number of hours (24) multiplied by the height of a supplementary view representing an hour.
prepareLayout does almost everything. In that method, you calculate every layoutAttributes you will need to use. By getting the time of the event and its duration, you are able to compute the frame of every event. The supplementary view frames (every single "hour" block) are pretty straightforward too, since their height is fixed (origin.y = fixed height multiplied by the hour).
layoutAttributesForElementsInRect: simply iterates through your previously prepared layoutAttributes and returns all whose frame intersect the provided frame.
layoutAttributesForItemAtIndexPath: finds and returns the cell's layoutAttributes that match the provided indexPath.
layoutAttributesForSupplementaryViewOfKind:atIndexPath finds and returns the supplementary view's layoutAttributes that math the provided indexPath.
Next, you give an instance of that layout to a collection view:
self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:myCustomLayout]
Then, you only need to provide the required views for the supplementary views (a.k.a the hour blocks) and the cells, via the UICollectionView's delegate & dataSource. Feel free to create a custom delegate in your custom layout if you need the UIViewController to provide more info that you have with the standard delegate/dataSource!
Note: Since the line that indicate the start of an hour is a few pixels below the cell's top border, you need to shift every time of event by the same number of pixels. Say you have 6 pixels above every line and every hour block has a height of 60 pixels, then if an even start at 2AM, you will set its origin.y = 2 * 60 + 6 (2 hours * 60 pixels per hour + 6 pixels padding). You will also need to adjust you last cell block to be 6 pixels taller since it won't have another cell below.
I recommend you to read the official documentation about creating custom layouts.
To help you: I made a quick sample project since making your own layout can sometime be troublesome. Go check the CalendarViewLayout class, I added a few comments to explain how I dealt with the padding. Here is was it looks like:
I don't think this is so hard to do.
You just need a different perspective, than what you tried.
Make all the CollectionView cells the same size. They represent 1h time, so they should have equal size.
Then have another view in each cell. This is the one that show that there is an event. This view you will give a color, and place it in the cell according to the time when the event start or finish.
So in the cellForRowAtIndexPath, you would check your events and if this cell have any event then you are going to show this event view, color it, and position it. If you make the cells height 60, it gets super easy to position the view. For example if the event start at 1:20, at the cell for hour 1, you will size the event view (0, 20, 320, 40).
As per discussion in comments on the question, using the Custom Collection View Layout library linked in the question is suitable for the mentioned problem which can be easily integrated using CocoaPods.
UICollectionView is highly customizable using custom UICollectionViewLayout. See the official apple documentation for creating custom layouts.
To be frank, Apple's documentation is too overwhelming for most of us. So, you can look up (google) custom UICollectionViewLayout tutorial, go through any one you find the easiest and go on to use the library to your specific needs.
I am working on an iOS app that has a UICollectionView. I would like to make a simple custom layout in which the middle cell on each row is slightly lower than the one on the right and left. I've been looking over tutorials and the apple documentation but most of it is for a much more complicated set up.
Is there is a quick way to stagger the middle cell. Thanks for any input!
Currently the collectionView looks like this...
I simply want the center cells shifted down by 40 pixels or so.
Subclass UICollectionViewFlowLayout and alter the response that the superclass gives so that the cells are positioned where you want them.
Here's an example of a UICollectionViewFlowLayout subclass:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch08p466collectionViewFlowLayout2/ch21p748collectionViewFlowLayout2/MyFlowLayout.swift
It shifts the cells left (so that they are left-justified instead of full-justified across the screen). It is not difficult to see how to adapt this to shift certain cells down.
I would like to create a zoomable timeline in my iOS application for a kind of a todo-list. Zooming in would display days and hours and zooming out would trigger the folding out of days or zooming out to months. There would be a scrolling function.
As an example, I would want it to work like this: http://almende.github.com/chap-links-library/js/timeline/doc/
What kind of basic view would be an appropriate starting point for this, keeping in mind that the memory needed should be as low as possible? Would a UITableView, UIScrollView, or something else work for this?
The UICollectionView / UITableView will not work because the cells are almost always the same width/height. Most importantly the cells always have the same spacing in-between each cell. Because of this it is able to easily calculate what the index range is, and query the dataSource for the cell's it needs based on index.
A timeline view on the other hand is much different than these controls. The spacing between cells is different, with the cells sometimes overlapping with each other. If you had a data source sorted by position, the control would still have to guess where to start looking for the range. So finding the correct index range is going to be more expensive - you just have to find the right algoritm to determine this in a shorter amount of time.
You're going to have to build your own control by subclassing a UIScrollView. You shouldn't have to mess with drawRect at all. An important concept, which is used by UITableView and UICollectionView, is dequeue'ing cells. The iOS 5 version of Apple's PhotoScroller demonstrates this concept with paging (the iOS 6 version replaces the custom paging with UIPageViewController). You'll have to download the old documentation to get the old sample code.
I'm currently building a timeline view, which I will open source at some point. It's somewhat based on the UITableView and works in horizontal or vertical direction. It dequeue's cells just like the UITableView. I'm not focusing on labels or scaling, but the concept of having inconsistent spacing in-between cells. To give you a head start, here are my dataSource methods I settled on:
- (NSInteger)numberOfCellsInTimelineView:(TimelineView *)timelineView;
- (CGRect)timelineView:(TimelineView *)timelineView cellFrameForIndex:(NSInteger)index;
- (TimelineViewCell *)timelineView:(TimelineView *)timelineView cellForIndex:(NSInteger)index;
Two of these calls are identical to what UITableView has, but it has a new call called cellFrameForIndex. What's significant about this call is that the TimelineView can guess an index and lookup the frame of the cell and see where it fits in the visible bounds. If it guesses a cell inside the visible bounds, it can simply iterate forward and backward until it finds the edges.
Currently the algoritm I'm using takes round(count * (CGRectGetMidX(timelineView.bounds) / timelineView.contentSize.width)) (for the horizontal direction). Basically what this does is takes the mid-point of the visible bounds of the UIScrollView and gets the percentage of what has been scrolled. Then it multiplies that by the number of cells. This works fairly well. When testing a random data-set with 100,000 records at random spacing the calls to cellFrameForIndex ranged from 8 to 150. I'm able to pull 52-60 FPS with this. I'm still working on it, and trying to find a better/quicker way to determine the index range. I'm trying to keep it down to visible cell count + 10 (max) iterations.
If you have time to wait, I'll update my answer to include a link to my GitHub project when I'm done. This could be a few days, to a week. I may add scaling. But you'll have to fork it and add labels and anything else you want.
Edit:
Here is my Github project: https://github.com/lukescott/TimelineView
UITableView is definitely not suitable. UIScrollView might be better but it is not very well suited for dynamic or very long content.
I believe the easist approach would be to do it everything by yourself - implement it by a UIView subclass with a drawRect. Of course, you should use UIPanRecognizer in the same manner as UIScrollView uses it.