How to animate UITableView header view - iphone

I need to animate the insertion of a tableview header view. I want that the table rows to slide down while the header view expands its frame.
So far the best result I got is by using the following code:
[self.tableView beginUpdates];
[self.tableView setTableHeaderView:self.someHeaderView];
[self.tableView endUpdates];
The problem with this solution is that the header itself doesn't get animated, its frame doesn't expand.
So the effect I'm getting is that the table rows slide down (as I want) but the header view is immediately shown, and I want it to expand it's frame with animation.
Any ideas?
Thanks.

have the header frame at CGRectZero
and set its frame using animation
[self.tableView beginUpdates];
[self.tableView setTableHeaderView:self.someHeaderView];
[UIView animateWithDuration:.5f animations:^{
CGRect theFrame = someBigger.frame;
someHeaderView.frame = theFrame;
}];
[self.tableView endUpdates];

Here's what I got to work in Swift, with an initial frame height of zero and updating it to its full height in the animations closure:
// Calculate new frame size for the table header
var newFrame = tableView.tableHeaderView!.frame
newFrame.size.height = 42
// Get the reference to the header view
let tableHeaderView = tableView.tableHeaderView
// Animate the height change
UIView.animate(withDuration: 0.6) {
tableHeaderView.frame = newFrame
self.tableView.tableHeaderView = tableHeaderView
})

The selected answer didn't work for me. Had to do the following:
[UIView animateWithDuration: 0.5f
animations: ^{
CGRect frame = self.tableView.tableHeaderView.frame;
frame.size.height = self.headerViewController.preferredHeight;
self.tableView.tableHeaderView.frame = frame;
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}];
The really peculiar part is that this works in one view but not in a subclass of the same view controller / view hierarchy.

I found the definitive and proper way to achieve a real animation on tableHeaderViews!
• First, if you're in Autolayout, keep the Margin system inside your header view. With Xcode 8 its now possible (at last!), just do not set any constraint to any of the header subviews. You'll have to set the correct margins thought.
• Then use beginUpdates and endUpdates, but put endUpdate into the animation block.
// [self.tableView setTableHeaderView:headerView]; // not needed if already there
[self.tableView beginUpdates];
CGRect headerFrame = self.tableView.tableHeaderView.bounds;
headerFrame.size.height = PLAccountHeaderErrorHeight;
[UIView animateWithDuration:0.2 animations:^{
self.tableView.tableHeaderView.bounds = headerFrame;
[self.tableView endUpdates];
}];
Note: I only tried in iOS10, i'll post an update when my iOS9 simulator will be downloaded.
EDIT
Unfortunately, it doesn't work in iOS9 as well as iOS10: the header itself does not change its height, but header subviews seem to move as if the header bounds changed.

SWIFT 4 SOLUTION w/ Different Header
If you want this kind of animation, that is the solution that I have used: (even though in my case I had also to play a bit with the opacity of the objects, since I was loading another completely different header)
fileprivate func transitionExample() {
let oldHeight = tableView.tableHeaderView?.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height ?? 0
let newHeader = YourCustomHeaderView()
newHeader.hideElements() // If you want to play with opacity.
let smallHeaderFrame = CGRect(x: 0, y: 0, width: newHeader.frame.width, height: oldHeight)
let fullHeaderFrame = newHeader.frame
newHeader.frame = smallHeaderFrame
UIView.animate(withDuration: 0.4) { [weak self] in
self?.tableView.beginUpdates()
newHeader.showElements() // Only if have hided the elements before.
self?.tableView.tableHeaderView = newHeader
self?.tableView.tableHeaderView?.frame = fullHeaderFrame
self?.tableView.endUpdates()
}
}
So, basically, it's all about changing the frame inside the .animate closure. The beging/end updates is needed in order to move and update the other elements of the tableView.

Here is a Swift 5 UITableView extension for changing the headerView or changing its height, animated using AutoLayout
extension UITableView {
func animateTableHeader(view: UIView? = nil, frame: CGRect? = nil) {
self.tableHeaderView?.setNeedsUpdateConstraints()
UIView.animate(withDuration: 0.2, animations: {() -> Void in
let hView = view ?? self.tableHeaderView
if frame != nil {
hView?.frame = frame!
}
self.tableHeaderView = hView
self.tableHeaderView?.layoutIfNeeded()
}, completion: nil)
}
}

For me it is animated when using Auto Layout without any explicit animation block or manipulating frames.
// `tableHeaderViewHeight` is a reference to the height constraint of `tableHeaderView`
tableHeaderViewHeight?.constant = .leastNormalMagnitude
tableView.beginUpdates()
tableView.tableHeaderView = tableHeaderView
tableHeaderView.layoutIfNeeded()
tableView.endUpdates()

Use below code this works
extension UITableView {
func hideTableHeaderView() -> Void {
self.beginUpdates()
UIView.animate(withDuration: 0.2, animations: {
self.tableHeaderView = nil
})
self.endUpdates()
}
func showTableHeaderView(header: UIView) -> Void {
let headerView = header
self.beginUpdates()
let headerFrame = headerView.frame
headerView.frame = CGRect()
self.tableHeaderView = headerView
UIView.animate(withDuration: 0.2, animations: {
self.tableHeaderView?.frame = headerFrame
self.tableHeaderView?.alpha = 0
self.endUpdates()
}, completion: { (ok) in
self.tableHeaderView?.alpha = 1
})
}
}

Related

UIView.animateWithDuration not animating after initial animation

I have two methods:
private func showPicker() {
UIView.animate(withDuration: 0.3,
animations: {
var pickerFrame = self.vPickerWrapper.frame
pickerFrame.origin.y = self.view.frame.size.height - 300.0
self.vPickerWrapper.frame = pickerFrame
self.bIsShowingPicker = true
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
})
}
private func closeThePicker() {
UIView.animate(withDuration: 0.3,
animations: {
var pickerFrame = self.vPickerWrapper.frame
pickerFrame.origin.y = self.view.frame.size.height + 1
self.vPickerWrapper.frame = pickerFrame
self.bIsShowingPicker = false
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
})
....
the first method is called when the user touches a text field that requires them to populate from the pickerview. The second method is called when either the close or select button is touched within the wrapper.
The wrapper is created in storyboard and has constraints (trailing, leading bottom) to position it off screen. I initially made the bottom constraint a IBOutlet and wanted to change its constant to handle the animation. (Which I do with other objects in this VC). I then tried to work with CGAffineTransform.translate and .identity and finally dropped down to working with the frame. In all three cases the wrapper will display but not close (move back off screen).
Here's the value of vPickerWrapper after I set its frame to pickerFrame:
pickerFrame CGRect (origin = (x = 0, y = 668), size = (width = 375, height = 300))
which is what I am expecting, just not any animation in the sim. The view never moves. Any idea why the display is not showing it in the new coordinates?

Position element to CGRect with Safe Area

I´m declaring a button:
let button = UIButton(frame: CGRect(x: ?, y: ?, width: 50, height: 50))
But I´m note sure how to set x and y relative to the Safe Area so that it works good on both >= iOS 11 and <= iOS 10.
I tried to use self.view.safeAreaInsets but it returns 0.0 for some reason. Any ideas how to solve this?
For iOS 11:
self.view.safeAreaInsets will be zero if you call it on viewDidLoad, viewWillAppear, etc
It should have the value if you call it on viewDidLayoutSubviews, viewDidAppear
This would be far easier if you used autolayout and constraints, but you can get the margins of the safe area this way:
if #available(iOS 11.0, *) {
if let window = UIApplication.shared.keyWindow {
let topMargin = window.safeAreaInsets.top
let leftMargin = window.safeAreaInsets.left
}
}
I have same question when make animations.
Finally solved question, idea:create a frame equal safeArea frame.
(Device Orientaion only Left and Right)
CGRect safeAreaFrame;
if (#available(iOS 11.0, *)) {
UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets;
safeAreaFrame = CGRectMake(safeAreaInsets.left,
safeAreaInsets.top,
self.view.frame.size.width - safeAreaInsets.left - safeAreaInsets.right,
self.view.frame.size.height - safeAreaInsets.top - safeAreaInsets.bottom);
} else {
safeAreaFrame = self.view.frame;
}
then you have safe area frame use, for example:
UIView *view = [[UIView alloc] initWithFrame:safeAreaFrame];
view.backgroundColor = UIColor.redColor;
[self.view addSubview:view];
Note: The safe area insets return (0, 0, 0, 0) in viewDidLoad,
hence they are set later than viewDidLoad,
put into viewSafeAreaInsetsDidChange or viewDidAppear.
I hope this will help you.

How can you make a .xib file fill the whole screen width (Swift)

I want a UIScrollView which contains to other views. One is a UITableView and the other is a MKMapView. To do this I created two xib files. When I want to add these two to my scroll view I do it like this:
func setupScrollView() {
scrollView.isPagingEnabled = true
scrollView.contentSize = CGSize(width: self.view.bounds.width * 2, height: self.view.bounds.height)
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
}
func loadScrollViewViews() {
if let tableView = Bundle.main.loadNibNamed("ViewTableShow", owner: self, options: nil)?.first as? ViewTableShow {
scrollView.addSubview(tableView)
tableView.frame.size.width = self.view.bounds.size.width
tableView.frame.origin.x = 0
}
if let mapView = Bundle.main.loadNibNamed("ViewMapShow", owner: self, options: nil)?.first as? ViewMapShow {
scrollView.addSubview(mapView)
mapView.frame.size.width = self.view.bounds.size.width
mapView.frame.origin.x = self.view.bounds.width
}
}
First I call setupScrollView and then loadScrollViewViews in the viewDidLoad function in my ViewController class. self.view.bounds.size.width gives me the correct size of the screen width but it doesn't seems to set the width for the two views properly.
Here is what it looks like right know. I want to cover the whole screen width. Does anyone know how to do it?
Try adding some constraints to your scrollView

iOS7 Status Bar like the native weather app

Does anyone know how can I reproduce a similar effect from the native iOS7 weather app?
Basically, the status bar inherits the view's background underneath, but the content doesn't show up.
Also, a 1 pixel line is drawn after the 20 pixels height of the status bar, only if some content is underlayed.
The best thing is to make it through the clipSubview of the view. You put your content into the view and make constraints to left/right/bottom and height. Height on scroll view you check is the cell has minus position and at that time you start to change the height of content (clip) view to get desired effect.
This is a real app you can download and take a look from www.fancyinteractive.com. This functionality will be available soon as next update.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *visibleCells = [convertorsTableView visibleCells];
if (visibleCells.count) {
for (CVConverterTableViewCell *cell in visibleCells) {
CGFloat positionYInView = [convertorsTableView convertRect:cell.frame toView:self.view].origin.y;
[self clipLayoutConstraint:cell.clipHeightLayoutConstraint withPosition:positionYInView defaultHeight:cell.frameHeight];
[cell.converterLabel layoutIfNeeded];
[cell.iconImageView layoutIfNeeded];
}
}
[self checkStatusBarSeperator:scrollView.contentOffset.y];
}
- (void)clipLayoutConstraint:(NSLayoutConstraint *)constraint withPosition:(CGFloat)position defaultHeight:(CGFloat)defaultHeight {
if (position < 0) {
constraint.constant = (defaultHeight - -position - 20 > 10) ? defaultHeight - -position - 20 : 10;
} else
constraint.constant = defaultHeight;
}
You can accomplish this by setting a mask to the table view's layer. You will not be able however to render the animations inside the cells, but you can do those yourself behind the table view, and track their movement with the table view's scrollview delegate methods.
Here is some informations on CALayer masks:
http://evandavis.me/blog/2013/2/13/getting-creative-with-calayer-masks
Swift 5:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visibleCells = tableView.visibleCells as? [TableViewCell] else { return }
let defaultClipHeight: CGFloat = 24
let statusBarHeight: CGFloat = UIApplication.statusBarHeight
if !visibleCells.isEmpty {
for cell in visibleCells {
let topSpace = cell.frame.size.height - defaultClipHeight - cell.clipBottomConstraint.constant
let cellOffsetY = tableView.contentOffset.y - cell.frame.origin.y + statusBarHeight
if cellOffsetY > topSpace {
let clipOffsetY = cellOffsetY - topSpace
let clipHeight = defaultClipHeight - clipOffsetY
cell.clipHeightConstraint.constant = max(clipHeight, 0)
} else {
cell.clipHeightConstraint.constant = defaultClipHeight
}
}
}
}
Starting Page:
Scrolling First Item:
Scrolling Second Item:

viewWillAppear for subviews

I have UIScrollView with multiple UIVIew subviews. I would like to update the data that is displayed by each UIView when they appear in the visible portion of the UIScrollView.
What is the callback that gets triggered? I tried viewWillAppear, but it does not seem to get called.
Thanks. :)
You have to do the calculation yourself. Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.
Swift 3 solution
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = greenView.frame
let container = CGRect(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y, width: scrollView.frame.size.width, height: scrollView.frame.size.height)
// We may have received messages while this tableview is offscreen
if (viewFrame.intersects(container)) {
// Do work here
print("view is visible")
}
else{
print("nope view is not on the screen")
}
}
Above answers are correct if your scrollview is not in the zoomed in state. In case if your scrollview can zoom above calculation won't work as you need to consider zoom too
here is the code
CGRect visibleRect;
visibleRect.origin = self.mapScrollView.contentOffset;
visibleRect.size = self.mapScrollView.bounds.size;
float theScale = 1.0 / self.mapScrollView.zoomScale;
visibleRect.origin.x *= theScale;
visibleRect.origin.y *= theScale;
visibleRect.size.width *= theScale;
visibleRect.size.height *= theScale;
if(CGRectIntersectsRect(visibleRect, btnPin.frame)){
...
}
A slight refinement. I wanted to know the amount of the view that was displayed in the scrollview:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
// Figure out how much of the self.userFeedbackView is displayed.
CGRect frame = CGRectIntersection(self.scrollView.bounds, self.userFeedbackView.frame);
CGFloat proportion = (frame.size.height*frame.size.width)/(self.userFeedbackView.frameWidth*self.userFeedbackView.frameHeight);
NSLog(#"%f; %#", proportion, NSStringFromCGRect(frame));
}
Ole Begemann's answer in swift 5,
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.intersects(subViewFrame){
// do something
}
}
Note: Pls make sure that the subViewFrame is calculated with respect to the scrollView's frame.