Custom UIViewController transition where UITableViewCell grows to full screen and back? - ios

I haven't been able to find any examples of this online. How can I achieve the following effect? Instead of the standard slide-to-left effect when tapping a table row, I'd like the view controller transition animation to look like the following:
User taps a cell in the table
The cell starts growing to fill the screen, pushing other rows above and below it "offscreen".
As the cell grows, cells elements (text, images, etc.) cross-fade into the new view's contents until the new view completely fills the screen.
I'd like to also be able to interactively transition back into the table view by dragging up from the bottom edge, such that the reverse of the above is achieved. i.e. view starts shrinking back into a normal table view cell as the "offscreen" cells animate back into position.
I've thought about taking a snapshot of the table and splitting it up at the points above and below the cell, and animating these snapshots offscreen as part of a custom view controller transition. Is there a better way? Ideally I'd like to not take snapshots, as I may want to have animations, etc., still happening in the table view rows as they fade offscreen.

First thing first, i have seen your post today and his is written in swift programming language.
the follwing code is to expand the selected cell to the full screen, where the subviews will fade out and background image will expands.
First complete cellForRowAtIndexPath, then in didSelectRowAtIndexPath,
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
isSelected = true
selectedCellIndex = indexPath.row
tableView.beginUpdates()
var cellSelected: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cellSelected.frame = tableCities.rectForRowAtIndexPath(indexPath)
println("cell frame: \(cellSelected) and center: \(cellSelected.center)")
let newCell = cellSelected.frame.origin.y - tableOffset
println("new cell origin: \(newCell)")
var tempFrame: CGRect = cellSelected.frame
variableHeight = cellSelected.frame.origin.y - tableOffset
tempFrame.size.height = self.view.frame.height
println("contentoffset: \(tableView.contentOffset)")
let offset = tableView.contentOffset.y
println("cell tag: \(cellSelected.contentView.tag)")
let viewCell: UIView? = cellSelected.contentView.viewWithTag(cellSelected.contentView.tag)
println("label: \(viewCell)")
viewCell!.alpha = 1
UIView.animateWithDuration(5.0, delay: 0.1, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
tableView.setContentOffset(CGPointMake(0, offset + self.variableHeight), animated: false)
tableView.contentInset = UIEdgeInsetsMake(0, 0, self.variableHeight, 0)
tableView.endUpdates()
cellSelected.frame = tempFrame
viewCell!.alpha = 0
println("contentoffset: \(tableView.contentOffset)")
}, completion: nil)
}
then update your heightForRowAtIndexPath, as
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if isSelected && (selectedCellIndex == indexPath.row) {
return self.view.frame.height
}else {
return 100
}
}
Ignore this if already solved. and this might be helpful to others.

Related

How to manually load UITableView

I kind of have a weird layout here, it's kind of like this (also see pics):
-UITableViewCell 1
----UIView 2
--------UITableView 3
The controller the the UITableView (1) is like that:
//mainTableView (1) controller
var cellHeights = [CGFloat]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let card = CardSource.orangeCards[indexPath.row]
cell.configureCardInCell(card)
cellHeights.insert(card.frame.height + 15, at: indexPath.row)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row]
}
but the problem is that when the screen first loads, the UIViews overlap because the cells are too small because the smaller table view (the one in the UIView) hasn't loaded yet and it's height isn't defined. Proof of this is that when I scroll to the bottom of the main table view then scroll back up cellForRowAt is called again and the two views don't overlap anymore (see pics). So what I basically want is a way to load the small table view and define it's height before the bigger table view loads (or if you have any other solutions, that'd be welcome too)
I know my question isn't very clear, I'm not really good at explaining stuff, so don't hesitate to ask me questions in the comments.
Many thanks!
When the view first loads
After scrolling down then back up
EDIT:
I found this:
static var pharmacyOrangeCard: CardView {
let view = Bundle.main.loadNibNamed("Pharmacy Orange Card", owner: self, options: nil)?.first as! PharmacyTableCardView
print(view.frame.height)
return view
}
prints the correct height. But then, when I try to access it from the controller above, it gives me a smaller number! In the meanwhile, I applied these constraints:
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
card.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
card.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
But I don't think that affects height, does it?
EDIT 2:
Okay, so I've changed this constraint:
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
to this:
card.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 17.5).isActive = true
card.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -17.5).isActive = true
So these constraints seem to play a role because now I have this:
enter image description here
By the way, I don't know if that matters but I'm using XIB files for each of these "cards", and the height isn't constrained, so maybe that plays a role?
SOLVING EDIT:
I solved the problem by doing:
override func viewDidAppear(_ animated: Bool) {
mainTableView.reloadData()
}
Once a cell loaded on the screen, you cannot change height for that cell for better UI-Experience,
and in hierarchy heightForRowAt get called before cellForRowAt.
So you had 2 options to choose for a solution to your problem
first:: get your heights values ready before your table view try to loads cells in it (get heights array ready before setting delegate and datasource values to your tableView)
second:: whenever you need to update your tableView cells to re-established with respect to new height values, call this each time after you have updated your height values
yourTableView.reloadData()

Sync animation for scroll in collection view within table view

I have collection views (in plural) inside a table view of many sections. Just so we're clear, a single table view with many sections with only one row each being that row an individual collection view.
All set up is working just fine, the data is well divided and delegates are all wired up recognizing everything they need to recognize. My problem is kind of simple but difficult at the same time: I want to scroll to specific collection view's position whenever I need to find a specific cell in animated fashion.
So far I'm able to jump with no problem to both table section (indexPath.section) and collection item (indexPath.row). The issue arises when I need to scroll (simultaneously) with animation.
My findings so far
I'm only able to achieve my current goal deactivating scroll animations for UITableView (UICollectionView can perform well with/out it)
Whenever I set UITableView selectRow or scrollToRow animation flags to true then the app crashes (99% sure this happens because I'm trying to access and "invisible" section due to the animation hasn't shown it yet).
Relevant snippets of code
#IBOutlet weak var albumTableView: UITableView!
#IBOutlet weak var stickersCollectionView: UICollectionView!
func locateCell() {
...
let stickerIndex = methodThatReturnsExactIndex()
let sectionIndex = IndexPath(row: 0, section: stickerIndex.section)
albumTableView.selectRow(at: sectionIndex, animated: false, scrollPosition: .top)
let rowIndex = IndexPath(item: stickerIndex.row, section: 0)
stickersCollectionView.scrollToItem(at: rowIndex, at: 0, animated: true)
}
I was thinking in experiment with the UIScrollViewDelegate (detecting when the tableview and the collectionview stopped in order to perform the scrolling) but that would imply spreading global variables around the code and experience tough me that's just racing conditions waiting to happen. Any help will be appreciated.
First Scroll your tableView to that specific index with/without animation. This will make that cell visible now get your cell by providing that indexPath so you could access the collectionView object inside your tableViewCell. Then ask you collectionView to scroll to specific indexPath with/without animation.
Take another global bool to store that tableView is begin scrolling. Also store both indexPath used for collection and tableView and use tab
tableViewIsScrolling = true
let yourSelectedIndexPathForTableView = IndexPath(row: 0, section: 4)//store it globally
let yourSelectedIndexPathForCollectionView = IndexPath(row: 10, section: 0)//store it globally
tableView.scrollToRow(at: yourSelectedIndexPathForTableView, at: .middle, animated: false)
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if tableViewIsScrolling {
tableViewIsScrolling = false
//Perform your scrolling of CollectionView
guard let yourCell = tableView.cellForRow(at: yourSelectedIndexPathForTableView) as? YourCell else {return}
yourCell.collectionView.scrollToItem(at: yourSelectedIndexPathForCollectionView, at: .centeredHorizontally, animated: true)
}
}

Animating cell more than once

I have a uitableview cell and it will animate when the cell is displayed. These cells are in tableviews and these tableviews are tabs of a uitabbarcontroller. But if a user presses one of the four tabs for the first time then the cells will animate, but if a user clicks a new tab and goes back to the previous tab the cells will not animate. How can I make it so that the cells animate everytime the tableview is presented. Here is my code, can someone please show me how i can make the cell animate each time the view is presented and not only when the cell is presented.
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let rotationTransform = CATransform3DScale(CATransform3DIdentity, 10, 10, 0)
cell.layer.transform = rotationTransform
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.1, options: .curveEaseOut, animations: {
cell.layer.transform = CATransform3DIdentity
}, completion: nil)
}
Reload the data on view will appear controller method:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableview.reloadData()
}
Also, you may need to reset to the starting state before you animate again. why? because when you scroll your table view the cells that appears will not animate. they will have the final state after animation (reusable cells).
Another thing that you may scoll and the top cell will disappear before completing the animation. I am not sure what would happen. maybe it will not take its final state. and when you get back to it or that cell get reused that cell will not have the final state. but, if you reset to the first state before you start animating again that will solve all the problems.
reset animation:
cell.layer.transform = nil
// you may need to layout the cell. not sure. after this.
// then the rest of your code that animates the cell.

Scrollview with embedded tableview

I'm building an iOS app in swift with Xcode 6.
I'm trying to embed a view controller with a table view in a scrollview. When the user drags in the table view, it is suppose to move the table, not the the scrollview that it is embedded in.
I've made this illustration, to clearify my view and view controller hierachy:
The red area is the content size area of the scrollview.
The green and blue areas are different view controllers, embedded in the scrollview.
The yellow area is a Text field in the blue view controller.
The orange area is a table view in the blue view controller.
I have enabled paging in the scrollview, so that it snaps to either the green or blue view controller. How can I pop the Table view to the top of the view hierachy, so that the only way to scroll the scrollview, will be to drag in the text field.
import UIKit
class RootViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var greenViewController: GreenViewController!
var blueViewController: BlueViewController!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: CGRectMake(0, 0, self.view.frame.width, self.view.frame.height))
scrollView.delegate = self
scrollView.pagingEnabled = true
self.greenViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Green View Controller") as! GreenViewController
self.blueViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Blue View Controller") as! BlueViewController
greenViewController.view.frame = CGRectMake(0, 0, view.bounds.width, view.bounds.height)
blueViewController = CGRectMake(0, view.bounds.height, view.bounds.width, view.bounds.height)
scrollView.addSubview(greenViewController.view)
scrollView.addSubview(blueViewController.view)
scrollView.contentSize = CGSizeMake(view.bounds.width, view.bounds.height*2)
self.view.addSubview(scrollView)
}
I hope that I have expressed myself clearly.
EDIT:
I've tried changing the size of the scrollview, when it scrolls. The idea was to change the height of the frame so it matches the height of the textfield when it is scrolled all the way down. But it seems that it also changes the visible part of whats embedded in the scrollview:
func scrollViewDidScroll(scrollView: UIScrollView) {
if self.scrollView.contentOffset.y > textField.View.bounds.height {
self.scrollView.frame.size.height = view.bounds.height - scrollView.contentOffset.y - textField.View.bounds.height
println(self.scrollView.frame)
}
}
Ok it might be bit late now but still i m posting this as a tutorial !
This is a less prefered way to achieve this. Better way would be,
Using table view controller as parent view and then using prototype cells as static cell(In 'n' numbers as per your requirement) as a card view or for any other use
The every different cell used would be considered as a section and no of prototype cells will be equal to no of sections in code as in snippet below
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 2 {
return list.count
}
return 1
}
number of rows in case of section 0 and 1 would be 1 as static part
Whereas No of rows in case of section 2 i.e dynamic part would be equal to count of list.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : CustomTableViewCell.swift = CustomTableViewCell.swift()
switch indexPath.section {
case 0:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell1", forIndexPath: indexPath) as! CustomTableViewCell
break
case 1:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell2", forIndexPath: indexPath) as! CustomTableViewCell
break
case 2:
cell = tableView.dequeueReusableCellWithIdentifier("dynamicCell", forIndexPath: indexPath) as! CustomTableViewCell
break
default:
break
}
return cell;
}
and thats it! Work is done! Party!
I got reference from here
Mixing static and dynamic sections in a grouped table view?
I mocked this up. My View hierarchy looks like this
ViewController's UIView
...UIView (to act as a container view for all the scrollable content)
.......UIView (for the top content) - green
.......UIView (for the bottom content) - blue
............UILabel
............UITableView (with scrollable content - more rows than visible)
I wired #IBOutlets to the UIScrollView (scrollView) and UIView (containerView) for the scrollable area.
in viewDidLoad I added:
scrollView.contentSize = containerView.frame.size
If I click anywhere outside the tableView (top area, text area, etc...) I scrolls the scrollView. If I try to scroll in the table view, the tableView scrolls (the scrollView does not).
Is that what you were trying to achieve?

Place UITableViewCell beneath siblings

I'm trying to build a custom UITableView that acts in a similar way to the reminders app.
Instead of the top-most visible cell scrolling off the display, I want it to be covered by the next cell so the cells appear to stack on top of one another as you scroll.
Currently I am using:
override func scrollViewDidScroll(scrollView: UIScrollView) {
let topIndexPath: NSIndexPath = tableView.indexPathsForVisibleRows()?.first as! NSIndexPath
let topCell = tableView.cellForRowAtIndexPath(topIndexPath)
let frame = topCell!.frame
topCell!.frame = CGRectMake(frame.origin.x, scrollView.contentOffset.y, frame.size.width, frame.size.height)
}
But the top cell is always above the second cell - causing the second cell to scroll under it.
Also if I scroll quickly this seems to misplace all of my cells.
EDIT: Have fixed this. Answer posted below for future reference.
For anybody searching this in the future.
Simply loop through all the visible cells and set their z position to their row number (so each cell stacks above the previous one).
The if statement tells the top cell to remain at the contentOffset of the scrollview and for all other cells to assume their expected position. This stops the other cells being offset if you scroll too quickly.
override func scrollViewDidScroll(scrollView: UIScrollView) {
// Grab visible index paths
let indexPaths: Array = tableView.indexPathsForVisibleRows()!
var i = 0
for path in indexPaths {
let cell = tableView.cellForRowAtIndexPath(path as! NSIndexPath)
// set zPosition to row value (for stacking)
cell?.layer.zPosition = CGFloat(path.row)
// Check if top cell (first in indexPaths)
if (i == 0) {
let frame = cell!.frame
// keep top cell at the contentOffset of the scrollview (top of screen)
cell!.frame = CGRectMake(frame.origin.x,
scrollView.contentOffset.y,
frame.size.width,
frame.size.height)
} else {
// set cell's frame to expected value
cell!.frame = tableView.rectForRowAtIndexPath(path as! NSIndexPath)
}
i++
}
}

Resources