Beginner iOS Developer here.
What I have is a UITableVIewController that has 1 section with 3 static grouped cells. Under that I have a UIView that has some buttons and text fields, when pressing one of the buttons, the UIView height increases. My problem is I cant scroll down to see the content that is at the bottom of the UIView
Screenshots:
When the green plus button is pressed, these elements are moved down making room for some new elements to be inserted (which I haven't implemented yet as I am stuck on this issue)
EDIT
Here is an example project: https://www.dropbox.com/s/vamxr12n6rrsam7/resizeSample.zip
You problem is that in your method to change the invoice. You only change the constraint of the invoiceBottomView, but you are not changing the size of invoiceBottomView's superview, that is the invoicePickerViewsContainer. So your tableView won't know you want to use more space of the tableFooterView (your invoicePickerViewsContainer).
You can change your code for this:
- (IBAction)invoiceLinesAddLine:(id)sender {
[UIView animateWithDuration:.25 animations:^{
CGRect invoiceBottomViewFrame = self.invoiceBottomView.frame;
NSInteger invoiceBottomViewFrameHeight = invoiceBottomViewFrame.size.height;
NSInteger invoiceLinesTopConstraintValue = invoiceLinesControlsViewTopConstraint.constant;
NSInteger invoiceLinesBottomConstraintValue = invoiceLinesControlsViewBottomConstraint.constant;
invoiceBottomViewFrameHeight = invoiceBottomViewFrameHeight + 40;
invoiceLinesTopConstraintValue = invoiceLinesTopConstraintValue + 40;
//invoiceLinesBottomConstraintValue = invoiceLinesBottomConstraintValue - 40;
invoiceBottomViewFrame.size.height = invoiceBottomViewFrameHeight;
invoiceLinesControlsViewTopConstraint.constant = invoiceLinesTopConstraintValue;
invoiceLinesControlsViewBottomConstraint.constant = invoiceLinesBottomConstraintValue;
CGRect invoicePickerViewsContainerFrame = self.invoicePickerViewsContainer.frame;
invoicePickerViewsContainerFrame.size.height += 40;
self.invoicePickerViewsContainer.frame = invoicePickerViewsContainerFrame;
self.tableView.tableFooterView = self.invoicePickerViewsContainer;
[self.view layoutIfNeeded];
} completion:^ (BOOL finished){
if (finished) {
//actions when animation is finished
}
}];
}
You don't need to change the invoiceLinesBottomConstraintValue, because you will increment the invoicePickerViewsContainer's height.
Then you need to add again the tableFooterView, so the table view will know that the size change, and the table will change the contentSize
Now, you have problems with the pickers... But I don't understand why you have the pickers in this view. If you want to preserve those pickers inside that view, you will need to change the constraint of those or its frame.
The way you setup the - (IBAction)invoiceLinesAddLine:(id)sender {} and the view within the tableview does not update the self.tableView.contentsize as you add more items. so add these lines to the completion function of the animation function within invoiceLinesAddLines and then twice the incremental height addition (i picked 80 arbitrarily)
completion:^ (BOOL finished){
if (finished) {
//actions when animation is finished
CGSize size=self.tableView.contentSize;
size.height+=80;
self.tableView.contentSize=size;
}
}];
Related
I would like to emulate the swipe to left to show next picture seen in the photos app, among other places so that the new picture slides in from right to left using animation.
However, in my case, the view has more than one photo. It has a caption as well.
I am able to do this without animation by just updating the photo and text upon swipe. This just changes the photo and caption on screen, however, without the right to left animation technique.
Is there a way to show the old view moving to the left while the new updated view moves in from the right.
The following moves the view to the left but renders the screen black as nothing is put in its place.
//In handleSwipe called by gesture recognizer
CGRect topFrame = self.view.frame;
topFrame.origin.x = -320;
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.view.frame = topFrame; _myImage=newImage;
_mycaption=newcaption;} completion:^(BOOL finished){ }];
Thanks for any suggestions.
Edit:
The view in question is a detail view that you get to from a tableview through a segue. It currently already uses a scrollview for vertical scrolling as the content can exceed a single screen in the vertical dimension.
I have found a way to get the old photo and caption to move left and the new to come in from right using a completion block, however, it is less than satisfactory, because there is a gap between the two actions when the screen is black. This seems endemic to completion approach because new image does not start to move until old image has completed moving.
- (IBAction)swipedLeft:(id)sender
{
CGRect initialViewFrame = self.view.frame;
CGRect movedToLeftViewFrame = CGRectMake(initialViewFrame.origin.x - 375, initialViewFrame.origin.y, initialViewFrame.size.width, initialViewFrame.size.height);
CGRect movedToRightViewFrame = CGRectMake(initialViewFrame.origin.x + 375, initialViewFrame.origin.y, initialViewFrame.size.width, initialViewFrame.size.height);
[UIView animateWithDuration:0.1
animations:^{self.view.frame = movedToLeftViewFrame;
//above moves old image out of view.
}
completion:^(BOOL finished)
{
self.view.frame = movedToRightViewFrame;
[self loadNextItem];//changes pic and text.
[UIView animateWithDuration:0.1
animations:^{self.view.frame = initialViewFrame;
//moves new one in
}
completion:nil];
}];
}
I suggest using a UIPageViewController, and manage each image/set of images as a separate view controller.
A page view controller supports either a page curl style transition or a slide transition. You'd want the slide transition.
Three ways to build this using existing components:
UIPageViewController (see DuncanC's answer).
UICollectionView with pagingEnabled set to true. Use a full-screen-sized cell and a UICollectionViewFlowLayout with scrollDirection set to horizontal.
UICollectionView as above, but instead of setting pagingEnabled to true, implement scrollViewWillEndDragging:withVelocity:targetContentOffset: in your delegate. This allows you to have a gutter between each cell while still having each cell fill the screen. See this answer.
It's ScrollView+PageControl
I hope it can help you.
- (void)viewDidLoad {
[super viewDidLoad];
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.delegate =self;
self.scrollView.translatesAutoresizingMaskIntoConstraints =NO;
self.scrollView.pagingEnabled =YES;
self.scrollView.contentSize =CGSizeMake(ViewWidth*VIewCount, ViewHeight);
[self.view addSubview:self.scrollView];
self.pageControl = [[UIPageControl alloc] init];
self. pageControl.translatesAutoresizingMaskIntoConstraints =NO;
[self.view addSubview:self.pageControl];
self. pageControl.numberOfPages = VIewCount;
self.pageControl.currentPage = 0;
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[self.scrollView(width)]"
options:0
metrics:#{#"width":#(ViewWidth)}
views:NSDictionaryOfVariableBindings(self.scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[self.scrollView(height)]"
options:0
metrics:#{#"height":#(HEIGHT_VIEW)}
views:NSDictionaryOfVariableBindings(self.scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_scrollView]-5-[pageControl(15)]"
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:NSDictionaryOfVariableBindings(self.scrollView, self.pageControl)]];
//[imgArr addObject:[UIImage imageNamed:...]];
for (NSInteger index = 0; index<ViewCount; index++) {
UIImageView *addSubview = [[UIImageView alloc] initWithFrame:CGRectMake(index*ViewWidth, 0, ViewWidth, ViewHeight)];
// addSubView.image = [imgArr objectAtIndex:index];
[self.scrollView addSubview:addSubview];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSInteger currentOffset = scrollView.contentOffset.x;
NSInteger index = currentOffset / ViewWidth;
if (currentOffset % ViewWidth == 0) {
pageControl.currentPage = index;
}
}
When a view controller's view is first shown I want to run an animation in which all elements in the view controller slide from outside the bottom of the screen to their natural positions. To achieve this, I do subview.frame.origin.y += self.view.frame.size.height in viewDidLayoutSubviews. I also tried viewWillAppear, but it doesn't work at all. Then I animate them up to their natural positions with subview.frame.origin.y -= self.view.frame.size.height in viewDidAppear.
The problem is that viewDidLayoutSubviews is called several times throughout the view controller's lifespan. As such, when things like showing the keyboard happen all my content gets replaced outside the view again.
Is there a better method for doing this? Do I need to add some sort of flag to check whether the animation has already run?
EDIT: here's the code. Here I'm calling prepareAppearance in viewDidLayoutSubviews, which works, but viewDidLayoutSubviews is called multiple times throughout the controller's life span.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self prepareAppearance];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self animateAppearance];
}
- (NSArray *)animatableViews
{
return #[self.createAccountButton, self.facebookButton, self.linkedInButton, self.loginButton];
}
- (void)prepareAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
UIView * view = [views objectAtIndex:it];
CGRect frame = view.frame;
// Move the views outside the screen, to the bottom
frame.origin.y += self.view.frame.size.height;
[view setFrame:frame];
}
}
- (void)animateAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
__weak UIView * weakView = [views objectAtIndex:it];
CGRect referenceFrame = self.view.frame;
[UIView animateWithDuration:0.4f
delay:0.05f * it
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = weakView.frame;
frame.origin.y -= referenceFrame.size.height;
[weakView setFrame:frame];
}
completion:^(BOOL finished) {
}];
}
}
If you need to animate something when view will appear and then not touch subviews later, I would suggest the following:
Don't change/touch viewDidLayoutSubviews
Add logic to move elements outside the screen (to their initial position before animation) in viewWillAppear
Add logic to animate elements into their proper position in viewDidAppear
UPDATE:
If you're using auto-layout (which is very good thing), you can't animate views by changing their frames directly (because auto-layout would ignore that and change them again). What you need to do is to expose outlets to constraints responsible for Y-position (in your case) and change that constraints rather then setting frames.
Also don't forget to include call to [weakView layoutIfNeeded] after you update constraints in the animation method.
I did both things in viewDidAppear:. It seems that when viewDidAppear: is called, the view is not actually visible, but about to. So the UI elements never show in their initial position if they are replaced there.
I'm trying supplement a UIRefreshControl with a UIView containing a message (statusMessageView) that appears "above" a UITableViewController after the app launches. This view will inform the user that the UITableViewController is already refreshed and does not need refreshing.
Here is a breakdown of what I am trying to accomplish:
The app is launched, the UITableViewController appears normal then scrolls down 44px to reveal statusMessageView (with a height of 44px).
statusMessageView stays visible for 2 seconds
The UITableViewController animates a scroll up to it's original position, effectively "tucking" statusMessageView away. (like UIRefreshControl, but animated with code)
Note: This statusMessageView will be used in conjunction with a UIRefreshControl, so it needs to "go away" after it is displayed so that the UIRefreshControl can be used normally
I have looked at other 3rd party "pull to refresh" controllers, but I think that is overkill due to the fact I am using UIRefreshControl
It seems like the other answers here are providing solutions for a notification that drops down below the UINavigationBar. If you're still looking for a solution that sits in the scrollview of the UITableView, then I would add a custom table header (not section header) to the table view.
Here are the rough steps necessary to accomplish this:
1. Create the initial header view on load
I typically use a UIViewController subclass that owns a UITableView instance variable (instead of using a UITableViewController), but you should be able to accomplish this with either set up. In your tableview set up code (probably in viewDidLoad), where you set things like backgroundColor, contentInset, separatorStyle, etc, create a UILabel that will become your header. Then set this UILabel to be the tableHeaderView of your tableView. Of course, if you're looking to make something a bit more complicated for this "notification section", feel free to make it a UIView with a nested UILabel + something else. So something like:
UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.tableView.bounds.size.width, 44.0f)];
headerLabel.backgroundColor = [UIColor clearColor]; // Just in case you have some fancy background/color in your table view
headerLabel.textAlignment = UITextAlignmentCenter;
headerLabel.font = ....
headerLabel.textColor = ....
headerLabel.text = #"You are an awesome user!";
self.tableView.tableHeaderView = headerLabel;
2. Set your tableview to load "normally" (ie. not show header)
Again, inside viewDidLoad, you need to set your tableview's contentOffset and alwaysBounceVertical properly to hide this header view on load. contentOffset set to the height of the header will start the tableview's y coordinate right below the header. alwaysBounceVertical set to YES will allow your tableview to behave correctly even if your tableview's contentsize is less than your screen size. So something like:
self.tableView.contentOffset = (CGPoint){0.0f, 44.0f};
self.tableView.alwaysBounceVertical = YES;
3. Add the slide down and slide away
Ok, now there are a couple of ways you can do this. On viewDidAppear, you can create a chained UIView animation where the first animation slides the tableview down (ie. sets contentOffset to {0.0f, 0.0f}) is delayed by one second and the second animation slides the tableview back up (ie. sets contentOffset to {0.0f, 44.0f}) is delayed by two seconds. Or you can use GCD and schedule the two animations as async + delayed blocks. Either way is fine (and there are probably two or three other good ways to accomplish this), but just to get the idea across... you can chain the animation like this:
__weak MyCustomViewController *me = self;
[UIView
animateWithDuration:0.4f
delay:1.0f
options:UIViewAnimationOptionAllowUserInteraction
animations:^
{
me.tableView.contentOffset = (CGPoint){0.0f, 0.0f};
}
completion:^(BOOL finished)
{
if (me.tableView)
{
[UIView
animateWithDuration:0.4f
delay:2.0f
options:UIViewAnimationOptionAllowUserInteraction
animations:^
{
me.tableView.contentOffset = (CGPoint){0.0f, 44.0f};
}
completion:^(BOOL finished)
{
if (me.tableView)
{
me.tableView.tableHeaderView = nil; // If you want to completely get rid of this notification header
me.tableView.contentOffset = (CGPoint){0.0f, 0.0f}; // I'm unsure if this will cause the tableview to jump... if it does, you can animate the headerview away instead of animating the tableview up to hide the header, then setting header to nil and reseting the contentOffset
// or
me.tableView.tableHeaderView.hidden = YES; // If you want to just hide the header
}
}];
}
}];
I wrote the sample code without testing any of it... hehe, so it probably won't work as it. But happy to help you flesh this out if you need more help! Good luck.
Update: Allowing user scrolling to cancel animation
I'm not too sure what you want the user interaction on the table to do to the animation. If you just want the animation to be canceled when the user starts scrolling, then I would use GCD (see code below). But I can see other ways you can have the animation work with user touch, so it depends on what you're looking for. In any case, let's say any user touch should disable the next scheduled animation then it could be achieved using two functions like:
- (void)scheduleShowHeaderAnimation
{
__weak MyCustomViewController *me = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0f * NSEC_PER_SEC), dispatch_get_main_queue(), ^ // Start animating after 1 sec delay
{
if (me.tableView) // Make sure the tableview is still around
{
if (! me.tableView.tracking && // Don't show animation if user has begun to touch contentview
! me.tableView.dragging && // Don't show animation if user has begun to drag contentview
! me.tableView.decelerating) // Don't show animation if dragging happened and scrollview is starting to decelerate
{
[UIView
animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionAllowAnimatedContent // This will make sure user can interact with tableview while animation is going on
animations:^
{
me.tableView.contentOffset = (CGPoint){0.0f, 0.0f};
}
completion:^(BOOL finished)
{
[me scheduleHideHeaderAnimation];
}];
}
}
});
}
- (void)scheduleHideHeaderAnimation
{
__weak MyCustomViewController *me = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0f * NSEC_PER_SEC), dispatch_get_main_queue(), ^ // Start animating after 2 secs delay
{
if (me.tableView) // Make sure the tableview is still around
{
if (! me.tableView.tracking && // Don't show animation if user has begun to touch contentview
! me.tableView.dragging && // Don't show animation if user has begun to drag contentview
! me.tableView.decelerating) // Don't show animation if dragging happened and scrollview is starting to decelerate
{
[UIView
animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionAllowAnimatedContent // This will make sure user can interact with tableview while animation is going on
animations:^
{
me.tableView.contentOffset = (CGPoint){0.0f, 44.0f};
}
completion:^(BOOL finished)
{
if (me.tableView)
{
me.tableView.tableHeaderView = nil; // If you want to completely get rid of this notification header
me.tableView.contentOffset = (CGPoint){0.0f, 0.0f}; // I'm unsure if this will cause the tableview to jump... if it does, you can animate the headerview away instead of animating the tableview up to hide the header, then setting header to nil and reseting the contentOffset
// or
me.tableView.tableHeaderView.hidden = YES; // If you want to just hide the header
}
}];
}
}
});
}
I would call scheduleShowHeaderAnimation in the viewDidAppear.
Then to support hiding the header when the user has already scrolled the tableview down, I would implement either - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate or - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView of UIScrollViewDelegate and add something like:
if (self.tableView.tableHeaderView != nil)
{
self.tableView.tableHeaderView = nil;
}
// or
if (self.tableView.tableHeaderView.hidden == NO)
{
self.tableView.tableHeaderView.hidden = YES;
}
If there's more complicated interactions you want to support, or if you want the animation to respond to the user touch in different ways, you might need to override other UIScrollViewDelegate methods and when the user begins to interact with the scrollview (which is the parent class of the table view), then change the behavior of the animation.
Does this get you closer to what you're looking for?
besides the answer of #Khawar , there is another choice:
https://github.com/toursprung/TSMessages
You can use following library for this purpose. You can set the autohide time duration as per your needs. You can also customise its appearance.
https://github.com/tciuro/NoticeView
It can be done in a very simple way: you just create and insert a subview into your refresh control:
- (void)viewDidLoad {
[super viewDidLoad];
...
UIView *smView = [self statusMessageView];
[self.refreshControl insertSubview: smView atIndex: 0];
// to show and hide the view inserted in refresh control
// With this transparent loader color even the middle area is not covered
// so you can have all the space to show your message
self.refreshControl.tintColor = [UIColor colorWithWhite:1.0 alpha:0.0];
[self.refreshControl beginRefreshing];
[self afterDelay: 2.0 performBlock: ^(){
[self.refreshControl endRefreshing];
// this delay is needed to prevent loader animation becoming visible
// while the refresh control is being hidden
[self afterDelay: 0.25 performBlock: ^(){
self.refreshControl.tintColor = nil;
});
});
...
}
-(void)afterDelay: (float)seconds performBlock: (void (^)())block {
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), block);
}
-(UIView)statusMessageView {
...
return view;
}
View size you need is 320x43px (if it is higher the bottom of it will be visible with refresh control hidden), the middle area (approximately 35px) will be covered by the loader animation (but not when you initially show the message).
I use UIImageView to show application logo there (it's even simpler: see here).
I would probably prefer (as a user) not to see this message before I start pulling. When I start pulling I will see that no update is needed before refresh starts (it starts when you pull down approximately twice more than the height of this subview, so user will have time to see what's there and if refresh is needed). If there are unread items in the table (tweets, posts, whatever), you can show the number of unread items in that view.
But it's a matter of personal preference, the way it is done it seems to achieve exactly what you want in a very simple way. I tested, it all works.
I have a UITableView cell that has a custom label inside to handle the variable height. There is also an UIImage on the right and left.
When the table is toggled into edit mode, I want to inform and change each cell in the table, to format properly for being shifted to the right. And, when the user presses the small -, I want to further optimize to make room for the delete button.
Looking for a pattern that works for the above, assuming there is custom content in the cell that I have control over.
Thanks in advance!
Normaly all you need to do is set the springs and struts correctly and your content will slide correctly. If you create your sub-views in code then you need to make sure you call addSubview on the cell.contentView and not the cell.
To hide and / or resize the sub-views you need to override willTransitionToState:
- (void)willTransitionToState:(UITableViewCellStateMask)state
{
UIView *imageView = self.rightImageView;
UIView *labelView = self.centerTextLabel;
CGRect labelFrame = labelView.frame;
if (state & UITableViewCellStateShowingDeleteConfirmationMask) {
labelFrame.size.width += 52;
// Animating the fade while the image is sliding to the left
// is more jarring then just making it go away immediately
imageView.alpha = 0.0;
[UIView animateWithDuration:0.3 animations:^{
labelView.frame = labelFrame;
}];
} else if (!self.rightImageView.alpha) {
labelFrame.size.width -= 52;
[UIView animateWithDuration:0.3 animations:^{
imageView.alpha = 1.0;
labelView.frame = labelFrame;
}];
}
[super willTransitionToState:state];
}
I created a quick sample app up on GitHub that demos an iOS 4.3 app using a nib or code just uncomment //#define USE_NIB_TABLEVIEWCELL to use the nib
https://github.com/GayleDDS/TestTableCell.git
I personally prefer to create the table view cell in a nib file and only after profiling the App, replace the nib with code.
I am trying to simulate the keyboard appear animation, only using a custom subview that will show the user three buttons. Is there any way I can accomplish this with storyboard (i.e. without having to programmatically create a subview)?
Quick Answer
Yes, although you will programmatically have to set some of the subviews properties. What you want to do is have your UIViewController call:
[UIView animateWithDuration:animations:completion:]
Detailed Example
in side of whatever method should bring up the keyboard try the following:
CGFloat windowWidth = self.mainView.frame.size.width;
CGFloat windowHeight = self.mainView.frame.size.height;
// center myCustomSubview along the x direction, and put myCustomSubview just below the screen when UIViewController initially gets onto the screen
CGPoint offScreenBelow = CGPointMake(windowWidth/2, windowHeight + (myCustomView.frame.size.y/2));
CGPoint onScreen = CGPointMake(windowWidth/2,windowHeight/2);
// change the second argument of the CGPointMake function to alter the final height of myCustomSubview
// start myCustomSubview offscreen
myCustomSubview.center = offScreenBelow;
// make sure to add myCustomSubview to the UIViewController's view's subviews
[self.view addSubview:myCustomSubview];
float duration = 1.0; // change this value to make your animation slower or faster. (units in seconds)
// animate myCustomSubview onto the screen
[UIView animateWithDuration:duration
animations:^{
myCustomSubview.center = onScreen;
}
completion:^(BOOL finished){
// add anything you want to be done as soon as the animation is finished here
}];
make sure you method is being called after 'viewDidAppear:' or inside of it.
when you want to get animate myCustomSubview back down off screen make sure to do the following in your UIViewController:
// set offscreen position same way as above
CGFloat windowWidth = self.mainView.frame.size.width;
CGFloat windowHeight = self.mainView.frame.size.height;
CGPoint offScreenBelow = CGPointMake(windowWidth/2, windowHeight + (myCustomView.frame.size.y/2));
// myCustomSubview is on screen already. time to animate it off screen
[UIView animateWithDuration:duration // remember you can change this for animation speed
animations:^{
myCustomSubview.center = offScreenBelow;
}
completion:^(BOOL finished){
[myCustomSubview removeFromSuperView];
}];
If Your Subview Is Not Showing Up
As always when dealing with subviews, make sure the frame is properly set, the subview has been added to a superview with addSubview:, the subview is not nil (and that it is properly initialized), and that neither the alpha nor opacity properties of the subview are set to 0.