I have a messaging screen I am creating and I am almost done it. I built most of the views with nib files and constraints. I have one small bug however where I can visually see some of the cells laying themselves out when the keyboard dismisses because of the requirement to call [self.view layoutIfNeeded] in an animation block involving a constraint. Here is the problem:
- (void)keyboardWillHide:(NSNotification *)notification
{
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
// adding this line causes the bug but is required for the animation.
[self.view layoutIfNeeded];
} completion:0];
}
Is there any way around directly calling layout if needed on the view since this also causes my collection view to lay itself out which makes the cells layout visually on screen sometimes.
I tried everything I can think of but it I can't find a solution to the bug fix. I have already tried calling [cell setNeedLayout]; in every location possible, nothing happens.
How about this?
In your UITableViewCell implement a custom protocol called MYTableViewCellLayoutDelegate
#protocol MYTableViewCellLayoutDelegate <NSObject>
#required
- (BOOL)shouldLayoutTableViewCell:(MYTableViewCell *)cell;
#end
Create a delegate for this protocol
#property (nonatomic, weak) id layoutDelegate;
Then override layoutSubviews on your UITableViewCell:
- (void)layoutSubviews {
if([self.layoutDelegate shouldLayoutTableViewCell:self]) {
[super layoutSubviews];
}
}
Now, in your UIViewController you can implement the shouldLayoutTableViewCell: callback to control whether the UITableViewCell gets laid out or not.
-(void)shouldLayoutTableViewCell:(UITableViewCell *)cell {
return self.shouldLayoutCells;
}
Modify your keyboardWillHide method to disable cell layout, call layoutIfNeeded, and restore the cell layout ability in the completion block.
- (void)keyboardWillHide:(NSNotification *)notification {
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
self.shouldLayoutCells = NO;
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
[self.view layoutIfNeeded];
} completion:completion:^(BOOL finished) {
self.shouldLayoutCells = NO;
}];
}
I can't really test this since you didn't provide sample code, but hopefully this will put you on the right path.
Related
I have a UIViewController with many subviews like UILabels, UIImages and a UIWebview. With a defined action by the user, the subviews of the UIViewController animate to different sizes and different locations inside of the UIViewController's view. Is it possible that this can be undone with a different defined action by the user? I want to make all the subviews revert back to their previous locations and sizes that they were before the animation was run. I thought of two possible solutions:
Get the properties of the subviews with the view.subviews() method before the animation is run, and then set the subviews after the animation to the properties in this array, or,
Call a method on the UIViewController to tell it to redraw all the subviews according to the properties set in the storyboard file.
Are these the right way of accomplishing what I would like to do? And if so, how would I go about doing this? (I don't know how to programmatically implement either of my ideas.)
Any help is greatly appreciated. Thanks.
Here is the solution.
#interface ViewController ()
#property (strong, nonatomic) NSMutableArray *frames;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Saving initial frames of all subviews
self.frames = [NSMutableArray new];
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
CGRect frame = view.frame;
NSValue *frameValue = [NSValue valueWithCGRect:frame];
[self.frames addObject:frameValue];
}
}
- (NSMutableArray *)allViewsOfView:(UIView *)view
{
NSMutableArray *result = [NSMutableArray new];
[result addObject:view];
for (UIView *subView in view.subviews) {
[result addObjectsFromArray:[self allViewsOfView:subView]];
}
return result;
}
- (void)resetFrames
{
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
NSValue *frameValue = [self.frames objectAtIndex:[allViews indexOfObject:view]];
CGRect frame = [frameValue CGRectValue];
view.frame = frame;
}
}
#end
Call [self resetFrame]; whenever you want to revert view's frames back to their initial values.
You could cache all your subview's frame before changing it and running the animation, in this way you can even cache more than one action. A stack structure will be perfect for this, but there is no way to achieve this in interface builder, you have to reference outlets from IB to code to get their frame.
I added about 500 views to my viewController.view.
This action took about 5 seconds on target.
Now I want the screen to refresh after each subview I'm adding, so the user will see them appears one by one on screen.
I tried this in my viewController:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for(int i=0; i<500; i++)
{
//...Create aView
[self.view addsubview:aView];
[self.view setNeedsDisplay];
}
}
I run it and nothing happened for 5 seconds then all views appeared at once.
I made sure that [self.view setNeedsDisplay] called from the main thread context.
Any idea how to make those subviews appear one by one?
I found a simple solution. I added a new property to my viewController - 'subviewsCount' witch was initialised to 500. then called the following method from viewDidLoad:
-(void) addSubviewsToMotherView
{
self.subviewsCount -=1;
if (self.subviewsCount >= 0)
{
[UIView animateWithDuration:0.0
delay:0.0
options:UIViewAnimationOptionLayoutSubviews
animations:^{
[self methodToAddSubview];
}
completion:^(BOOL finished){
[self addSubviewsToMotherView];
}
];
}
}
I am implementing a view that is in some way similar to what happens in Messages app, so there is a view with UITextView attached to the bottom of the screen and there is also UITableView showing the main content. When it is tapped it slides up with the keyboard and when keyboard is dismissed it slides back to the bottom of the screen.
That part I have and it is working perfectly - I just subscribed to keyboard notifications - will hide and wil show.
The problem is that I have set keyboard dismiss mode on UITableView to interactive and I cannot capture changes to keyboard when it is panning.
The second problem is that this bar with uitextview is covering some part of uitableview. How to fix this? I still want the uitableview to be "under" this bar just like in messages app.
I am using AutoLayout in all places.
Any help will be appreciated!
============
EDIT1:
Here is some code:
View Hierarchy is as follows:
View
- UITableView (this one will contain "messages")
- UIView (this one will slide)
UITableView is has constraints to top, left, right and bottom of parent view so it fills whole screen.
UIView has constraints to left, right and bottom of parent view so it is glued to the bottom - I moved it by adjusting constant on constraint.
In ViewWillAppear method:
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.DidShowNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillChangeFrameNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillHideNotification, OnKeyboardWillHideNotification);
And here are methods:
void OnKeyboardDidShowNotification (NSNotification notification)
{
AdjustViewToKeyboard (Ui.KeyboardHeightFromNotification (notification), notification);
}
void OnKeyboardWillHideNotification (NSNotification notification)
{
AdjustViewToKeyboard (0.0f, notification);
}
void AdjustViewToKeyboard (float offset, NSNotification notification = null)
{
commentEditViewBottomConstraint.Constant = -offset;
if (notification != null) {
UIView.BeginAnimations (null, IntPtr.Zero);
UIView.SetAnimationDuration (Ui.KeyboardAnimationDurationFromNotification (notification));
UIView.SetAnimationCurve ((UIViewAnimationCurve)Ui.KeyboardAnimationCurveFromNotification (notification));
UIView.SetAnimationBeginsFromCurrentState (true);
}
View.LayoutIfNeeded ();
commentEditView.LayoutIfNeeded ();
var insets = commentsListView.ContentInset;
insets.Bottom = offset;
commentsListView.ContentInset = insets;
if (notification != null) {
UIView.CommitAnimations ();
}
}
I'd recommend you to override -inputAccessoryView property of your view controller and have your editable UITextView as its subview.
Also, don't forget to override -canBecomeFirstResponder method to return YES.
- (BOOL)canBecomeFirstResponder
{
if (!RUNNING_ON_IOS7 && !RUNNING_ON_IPAD)
{
//Workaround for iOS6-specific bug
return !(self.viewDisappearing) && (!self.viewAppearing);
}
return !(self.viewDisappearing);
}
With this approach system manages everything.
There are also some workarounds you must know about: for UISplitViewController (UISplitViewController detail-only inputAccessoryView), for deallocation bugs (UIViewController with inputAccessoryView is not deallocated) and so on.
This solution is based on a lot of different answers on SO. It have a lot of benefits:
Compose bar stays on bottom when keyboard is hidden
Compose bas follows keyboard while interactive gesture on UITableView
UITableViewCells are going from bottom to top, like in Messages app
Keyboard do not prevent to see all UITableViewCells
Should work for iOS6, iOS7 and iOS8
This code just works:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = // . . .
// . . .
cell.contentView.transform = CGAffineTransformMakeScale(1,-1);
cell.accessoryView.transform = CGAffineTransformMakeScale(1,-1);
return cell;
}
- (UIView *)inputAccessoryView {
return self.composeBar;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.transform = CGAffineTransformMakeScale(1,-1);
// This code prevent bottom inset animation while appearing view
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.top = CGRectGetMaxY(self.navigationController.navigationBar.frame);
newEdgeInsets.bottom = self.view.bounds.size.height - self.composeBar.frame.origin.y;
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = CGPointMake(0, -newEdgeInsets.bottom);
// This code need to be done if you added compose bar via IB
self.composeBar.delegate = self;
[self.composeBar removeFromSuperview];
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note)
{
NSNumber *duration = note.userInfo[UIKeyboardAnimationDurationUserInfoKey];
NSNumber *options = note.userInfo[UIKeyboardAnimationCurveUserInfoKey];
CGRect beginFrame = [note.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.bottom = self.view.bounds.size.height - endFrame.origin.y;
CGPoint newContentOffset = self.tableView.contentOffset;
newContentOffset.y += endFrame.origin.y - beginFrame.origin.y;
[UIView animateWithDuration:duration.doubleValue
delay:0.0
options:options.integerValue << 16
animations:^{
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = newContentOffset;
} completion:^(BOOL finished) {
;
}];
}];
}
Use for example pod 'PHFComposeBarView' compose bar:
#property (nonatomic, strong) IBOutlet PHFComposeBarView *composeBar;
And use this class for your table view:
#interface InverseTableView : UITableView
#end
#implementation InverseTableView
void swapCGFLoat(CGFloat *a, CGFloat *b) {
CGFloat tmp = *a;
*a = *b;
*b = tmp;
}
- (UIEdgeInsets)contentInset {
UIEdgeInsets insets = [super contentInset];
swapCGFLoat(&insets.top, &insets.bottom);
return insets;
}
- (void)setContentInset:(UIEdgeInsets)contentInset {
swapCGFLoat(&contentInset.top, &contentInset.bottom);
[super setContentInset:contentInset];
}
#end
If you would like keyboard to disappear by tapping on message:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.composeBar.textView resignFirstResponder];
}
Do not call this, this will hide composeBar at all:
[self resignFirstResponder];
UPDATE 2:
NEW SOLUTION for keyboard tracking works much better:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Compose view height growing tracking
[self.composeBar addObserver:self forKeyPath:#"frame" options:0 context:nil];
// iOS 7 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"center" options:0 context:nil];
// iOS 8 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"frame" options:0 context:nil];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.composeBar removeObserver:self forKeyPath:#"frame"];
[self.composeBar.superview removeObserver:self forKeyPath:#"center"];
[self.composeBar.superview removeObserver:self forKeyPath:#"frame"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.composeBar.superview || object == self.composeBar)
{
// Get all values
CGPoint newContentOffset = self.tableView.contentOffset;
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
UIEdgeInsets newScrollIndicartorInsets = self.tableView.scrollIndicatorInsets;
// Update values
CGFloat bottomInset = self.view.bounds.size.height - [self.composeBar convertPoint:CGPointZero toView:self.view].y;
CGFloat diff = newEdgeInsets.bottom - (bottomInset + 7);
newContentOffset.y += diff;
newEdgeInsets.bottom = bottomInset + 7;
newScrollIndicartorInsets.bottom = bottomInset;
// Set all values
if (diff < 0 || diff > 40)
self.tableView.contentOffset = CGPointMake(0, newContentOffset.y);
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
}
}
OK, the interactive keyboard dismissal will send a notification with name UIKeyboardDidChangeFrameNotification.
This can be used to move the text view while the keyboard is being dismissed interactively.
You are already using this but you are sending it to the OnKeyboardDidShow method.
You need a third method called something like keyboardFramedDidChange. This works for the hide and the show.
For the second problem, you should have your vertical constraints like this...
|[theTableView][theTextView (==44)]|
This will tie the bottom of the tableview to the top of the text view.
This doesn't change how any of the animation works it will just make sure that the table view will show all of its contents whether the keyboard is visible or not.
Don't update the content insets of the table view. Use the constraints to make sure the frames do not overlap.
P.S. sort out your naming conventions. Method names start with a lowercase letter.
P.P.S. use block based animations.
I'd try to use an empty, zero-height inputAccessoryView. The trick is to glue your text field's bottom to it when the keyboard appears, so that they'd move together. When the keyboard is gone, you can destroy that constraint and stick to the bottom of the screen once again.
I made an open source lib for exactly this purpose. It works on iOS 7 and 8 and is set up to work as a cocoapod as well.
https://github.com/oseparovic/MessageComposerView
Here's a sample of what it looks like:
You can use a very basic init function as shown below to create it with screen width and default height e.g.:
self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];
There are several other initializers that are also available to allow you to customize the frame, keyboard offset and textview max height as well as some delegates to hook into frame changes and button clicks. See readme for more!
I got the code below from this SO question. I'm trying to slide up textfields when I begin editing (because they otherwise get covered by the iPhone keyboard). However, the log statement reveals that the textFieldDidBeginEditing method is not getting called.
I have the code below in two different subclasses of UIViewController. In one of them, for example, I have a textfield connected from the storyboard to the UIViewController like this
#property (strong, nonatomic) IBOutlet UITextField *mnemonicField;
I moved the textfield up to the top of the view (i.e. not covered by keyboard) so that I could edit it to try to trigger the log statement but it didn't work. The text field otherwise works as expected i.e. the data I enter is getting saved to coreData etc etc.
Can you explain what I might be doing wrong?
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
NSLog(#"The did begin edit method was called");
[self animateTextField: textField up: YES];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[self animateTextField: textField up: NO];
}
- (void) animateTextField: (UITextField*) textField up: (BOOL) up
{
const int movementDistance = 180; // tweak as needed
const float movementDuration = 0.3f; // tweak as needed
int movement = (up ? -movementDistance : movementDistance);
[UIView beginAnimations: #"anim" context: nil];
[UIView setAnimationBeginsFromCurrentState: YES];
[UIView setAnimationDuration: movementDuration];
self.view.frame = CGRectOffset(self.view.frame, 0, movement);
[UIView commitAnimations];
}
You have not assigned your delegate of UITextField in your ViewController class:
In your viewcontroller.m file, In ViewDidLoad method, do this:
self.mnemonicField.delegate=self;
In your viewcontroller.h file, do this:
#interface YourViewController : ViewController<UITextFieldDelegate>
You should set up text field delegate to self.
Add this line to viewDidLoad method:
self.mnemonicField.delegate = self;
and remember to add this line <UITextFieldDelegate> to conform to that protocol.
You can achieve the same effect in storyboard by control drag from desired UITextField to view controller and select delegate.
You created IBOutlet so just drag your textfield to viewController and set delegate
Then in .h add the following
#interface ViewController : ViewController<UITextFieldDelegate>
In other case if you click any other button without moving the focus of Uitextfield then delegate will not be called, for that you need to explicitly call
yourtextfield.resignFirstResponder()
Here's a strange fact :
I have a custom UITableViewCell in which I have an animation (an image moves).
It works fine on the creation of the cell.
But once I scroll to hide the cell and then go back on it, the animation has stopped. That, I can understand. But I'm calling my animation method in my viewWillAppear too. And the wired part is that the method is called, I've put a breakpoint in it, nothing is desallocated...
My UITableViewCell is kept strongly (there's a music player in it, the music continues to play). I really don't get it.
Here's my code :
#property (nonatomic, strong) DWPlayerCellVC *playerView;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"player"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"player"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
NSArray *viewsToRemove = [cell.contentView subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
if(!self.playerView){
self.playerView = [[DWPlayerCellVC alloc] init];
}
[cell.contentView addSubview:self.playerView.view];
self.playerView.model = self.model[indexPath.row];
return cell;
DWPlayerCellVC :
#implementation DWPlayerCellVC
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self zoomIn];
}
- (void)setModel:(id)model
{
_model = model;
// ...
[self zoomIn];
}
- (void)zoomIn
{
UIViewAnimationOptions options =
UIViewAnimationOptionBeginFromCurrentState |
UIViewAnimationOptionCurveEaseInOut |
UIViewAnimationOptionRepeat |
UIViewAnimationOptionAutoreverse;
[UIView
animateWithDuration:10.f
delay:0.f
options:options
animations:^{
CGFloat scale = 1.4f;
self.imageCoverView.transform = CGAffineTransformMakeScale(scale, scale);
} completion:NULL];
}
If you have any ideas...
Thanks a lot !
It looks like the problem is because zoomIn will only ever be called by setModel. This is because UITabelViewCells are not expected to managed displaying UIViewControllers so viewWillAppear: will never be called.
There are two options that I can think of.
The first would be to set up your root view controller to be a containment view controller and add the DWPlayerCellVC as a childViewController. This option does involve some work to get it all running, I suggect reading Creating Custom Container View Controllers to see whets required to get it working.
The second (and the one I would use) would be to create a UITableViewCell subclass that handles running the animation. Then you can just implement the method prepareForReuse to restart the animation. That method is automatically called on a cell when you use dequeueReusableCellWithIdentifier:.
Your UITableViewCell will remember it's state from before. When it's put into the queue (when it scrolls out of view), it doesn't revert back to a pristine state. When you call your ZoomIn method, I think what's happening is that it's zooming in again, but you can't tell because it's already zoomed in.
Try - before you zoom in, ensure that it's zoomed out. So in ViewWillAppear, to a quick "Reset" of the Cell, and then call zoomIn, and see if that fixes the issue.