Detect fullscreen menu bar movement - objective-c

So right now I have an NSWindow that draws itself like an NSToolbar using INAppStoreWindow, and I was wondering if there was any kind of event or message sent out when the menu bar moves while the app is in full screen, so that I could then move the title bar of the window in response (the way a standard NSToolbar works). Clearly NSToolbar knows something that I don't, and it would save me from making an NSTrackingArea on the upper part of my window.
Here's what it does now:
And here's what I'd like to do:
Unfortunately, KVO'ing a fullScreenAccessoryView does not work as well. Frame events are only generated when entering and exiting fullscreen mode, and not when the toolbar is "moved down" by the status bar.

How about this? Create NSStatusBarItem with a custom NSView having width 0 and then track its window's position using NSWindowWillMoveNotification.
UPDATE: I made a fork of the INAppStoreWindow with a custom toolbar attached to the menubar. Check it out.

One thing you could try is to use set a fake (i.e. empty) toolbar on the window and then give it a fullScreenAccessoryView. This view is, when moving into fullscreen, removed from the view hierarchy and attached below the toolbar. Dunno, however, how this is works when using a custom window class... :/

I was looking to accomplish exactly the same thing. I was able to do it by setting the superview of the titlebar view in a WAYAppStoreWindow (or INAppStoreWindow) to post frame change notifications and then observing frame change notifications on this view. Add the following observer in your setup:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(windowTitleBarFrameDidChange:)
name:NSViewFrameDidChangeNotification
object:window.titleBarView.superview];
And turn on/off frame change notifications when entered/exiting full-screen:
window.titleBarView.superview.postsFrameChangedNotifications = YES;
Then in the implementation of the windowTitleBarFrameWillChange: method, check to see when the titlebar will show by comparing with the last y position of it’s superview’s frame. Here is the code:
// Window titlebar heights.
#define kWindowTitlebarHeightDefault 22.0
#define kWindowTitlebarHeightStandard 37.0
#define kWindowTitlebarHeightExtended 82.0
- (void) windowTitleBarFrameDidChange:(NSNotification *)notification;
{
NSView * titleBarView = [notification object]; // view is actually the titlebar container view
if (NSMinX(_lastTitleBarFrame) == NSMinX(titleBarView.frame) &&
NSWidth(_lastTitleBarFrame) == NSWidth(titleBarView.frame) &&
NSHeight(_lastTitleBarFrame) == NSHeight(titleBarView.frame) &&
NSMinY(_lastTitleBarFrame) == -kWindowTitlebarHeightDefault &&
NSMinY(titleBarView.frame) > -kWindowTitlebarHeightDefault) // titlebar will show
{
[self windowTitleBarWillShow];
}
else if (NSMinX(_lastTitleBarFrame) == NSMinX(titleBarView.frame) &&
NSWidth(_lastTitleBarFrame) == NSWidth(titleBarView.frame) &&
NSHeight(_lastTitleBarFrame) == NSHeight(titleBarView.frame) &&
NSMinY(_lastTitleBarFrame) == 0.0 && NSMinY(titleBarView.frame) < 0.0) // titlebar will hide
{
[self windowTitleBarWillHide:YES];
}
else if (NSWidth(_lastTitleBarFrame) != NSWidth([NSScreen mainScreen].frame) &&
NSWidth(titleBarView.frame) == NSWidth([NSScreen mainScreen].frame)) // just went full-screen
{
[self windowTitleBarWillHide:NO];
}
_lastTitleBarFrame = titleBarView.frame;
}
- (void) windowTitleBarWillHide:(BOOL)animate
{
WAYAppStoreWindow * window = (WAYAppStoreWindow *)[(NSWindowController *)[[self windowControllers] objectAtIndex:0] window];
NSView * themeFrame = window.titleBarView.superview.superview;
if (animate)
[themeFrame animator].alphaValue = 0.0;
else
themeFrame.alphaValue = 0.0;
}
- (void) windowTitleBarWillShow
{
WAYAppStoreWindow * window = (WAYAppStoreWindow *)[(NSWindowController *)[[self windowControllers] objectAtIndex:0] window];
NSView * themeFrame = window.titleBarView.superview.superview;
[themeFrame animator].alphaValue = 1.0;
}
- (void) windowWillEnterFullScreen:(NSNotification *)notification
{
WAYAppStoreWindow * window = [notification object];
[self setUpFullScreenTitleBarForWindow:window];
_lastTitleBarFrame = NSZeroRect;
}
- (void) windowDidEnterFullScreen:(NSNotification *)notification;
{
WAYAppStoreWindow * window = [notification object];
window.titleBarView.superview.postsFrameChangedNotifications = YES;
_fullscreenToolbarView.hidden = NO;
}
- (void) windowWillExitFullScreen:(NSNotification *)notification;
{
WAYAppStoreWindow * window = [notification object];
window.titleBarView.superview.postsFrameChangedNotifications = NO;
[self setUpStandardTitleBarForWindow:window];
}
- (void) setUpNormalTitleBarForWindow:(WAYAppStoreWindow *)window
{
window.appearance = nil;
window.showsTitle = NO;
window.titleBarHeight = kWindowTitlebarHeightExtended;
window.verticalTrafficLightButtons = YES;
window.centerTrafficLightButtons = YES;
window.trafficLightButtonsLeftMargin = 13.0;
_fullscreenToolbarView.hidden = YES;
}
- (void) setUpFullScreenTitleBarForWindow:(WAYAppStoreWindow *)window
{
window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
window.showsTitle = YES;
window.titleBarHeight = kWindowTitlebarHeightStandard;
window.verticalTrafficLightButtons = NO;
window.centerTrafficLightButtons = YES;
window.trafficLightButtonsLeftMargin = 13.0;
window.verticallyCenterTitle = YES;
_fullscreenToolbarView.hidden = YES;
_lastTitleBarFrame = NSZeroRect;
}
NB: I’m using a fork of WAYWindow that adds support for vertically centring the document title in the titlebar.

Related

Disabling NSView fade animation for NSView `setHidden:`

I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.

How to re-size UITextView when keyboard shown with iOS 7

I have a view controller which contains a full-screen UITextView. When the keyboard is shown I would like to resize the text view so that it is not hidden under the keyboard.
This is a fairly standard approach with iOS, as described in this question:
How to resize UITextView on iOS when a keyboard appears?
However, with iOS 7, if the user taps on the text view in the bottom half of the screen, when the text view resizes, the cursor remains offscreen. The text view only scrolls to bring the cursor into view if when the user hits enter.
I read the docs which talk about this very topic. I translated it into Swift and it worked absolutely beautifully for me.
This is used for a full page UITextView like iMessage.
I am using iOS 8.2 and Swift on XCode 6.2 and here's my code. Just call this setupKeyboardNotifications from your viewDidLoad or other initialization method.
func setupKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWasShown:"), name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillBeHidden:"), name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWasShown(aNotification:NSNotification) {
let info = aNotification.userInfo
let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as NSValue
let kbSize = infoNSValue.CGRectValue().size
let contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0)
codeTextView.contentInset = contentInsets
codeTextView.scrollIndicatorInsets = contentInsets
}
func keyboardWillBeHidden(aNotification:NSNotification) {
let contentInsets = UIEdgeInsetsZero
codeTextView.contentInset = contentInsets
codeTextView.scrollIndicatorInsets = contentInsets
}
Also if you are having issues with the caret being in the right place when rotated check for the orientation change and scroll to the right position.
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
scrollToCaretInTextView(codeTextView, animated: true)
}
func scrollToCaretInTextView(textView:UITextView, animated:Bool) {
var rect = textView.caretRectForPosition(textView.selectedTextRange?.end)
rect.size.height += textView.textContainerInset.bottom
textView.scrollRectToVisible(rect, animated: animated)
}
Swift 3:
func configureKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(aNotification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(aNotification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWasShown(aNotification:NSNotification) {
let info = aNotification.userInfo
let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as! NSValue
let kbSize = infoNSValue.cgRectValue.size
let contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0)
textView.contentInset = contentInsets
textView.scrollIndicatorInsets = contentInsets
}
func keyboardWillBeHidden(aNotification:NSNotification) {
let contentInsets = UIEdgeInsets.zero
textView.contentInset = contentInsets
textView.scrollIndicatorInsets = contentInsets
}
Swift 4 & 5:
func setupKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_ :)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyboardWillShow(_ notification:NSNotification) {
let d = notification.userInfo!
var r = (d[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
r = self.textView.convert(r, from:nil)
self.textView.contentInset.bottom = r.size.height
self.textView.verticalScrollIndicatorInsets.bottom = r.size.height
}
#objc func keyboardWillHide(_ notification:NSNotification) {
let contentInsets = UIEdgeInsets.zero
self.textView.contentInset = contentInsets
self.textView.verticalScrollIndicatorInsets = contentInsets
}
With Auto Layout, it's much easier (provided you understand Auto Layout) to handle:
Instead of trying to identify and resize the affected views, you simply create a parent frame for all your view's contents. Then, if the kbd appears, you resize the frame, and if you've set up the constraints properly, the view will re-arrange all its child views nicely. No need to fiddle with lots of hard-to-read code for this.
In fact, in a similar question I found a link to this excellent tutorial about this technique.
Also, the other examples here that do use textViewDidBeginEditing instead of the UIKeyboardWillShowNotification have one big issue:
If the user has an external bluetooth keyboard attached then the control would still get pushed up even though no on-screen keyboard appears. That's not good.
So, to summarize:
Use Auto Layout
Use the UIKeyboardWillShowNotification notification,
not the TextEditField's events for deciding when to resize your
views.
Alternatively, check out LeoNatan's reply. That might even be a cleaner and simpler solution (I've not tried myself yet).
Do not resize the text view. Instead, set the contentInset and scrollIndicatorInsets bottom to the keyboard height.
See my answer here:
https://stackoverflow.com/a/18585788/983912
Edit
I made the following changes to your sample project:
- (void)textViewDidBeginEditing:(UITextView *)textView
{
_caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:#selector(_scrollCaretToVisible) userInfo:nil repeats:YES];
}
- (void)_scrollCaretToVisible
{
//This is where the cursor is at.
CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
if(CGRectEqualToRect(caretRect, _oldRect))
return;
_oldRect = caretRect;
//This is the visible rect of the textview.
CGRect visibleRect = self.textView.bounds;
visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
visibleRect.origin.y = self.textView.contentOffset.y;
//We will scroll only if the caret falls outside of the visible rect.
if(!CGRectContainsRect(visibleRect, caretRect))
{
CGPoint newOffset = self.textView.contentOffset;
newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 5, 0);
[self.textView setContentOffset:newOffset animated:NO];
}
}
Removed setting old caret position at first, as well as disabled animation. Now seems to work well.
Whilst the answer given by #Divya lead me to the correct solution (so I awarded the bounty), it is not a terribly clear answer! Here it is in detail:
The standard approach to ensuring that a text view is not hidden by the on-screen keyboard is to update its frame when the keyboard is shown, as detailed in this question:
How to resize UITextView on iOS when a keyboard appears?
However, with iOS 7, if you change the text view frame within your handler for the UIKeyboardWillShowNotification notification, the cursor will remain off screen as described in this question.
The fix for this issue is to change the text view frame in response to the textViewDidBeginEditing delegate method instead:
#implementation ViewController {
CGSize _keyboardSize;
UITextView* textView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
textView = [[UITextView alloc] initWithFrame:CGRectInset(self.view.bounds, 20.0, 20.0)]; textView.delegate = self;
textView.returnKeyType = UIReturnKeyDone;
textView.backgroundColor = [UIColor greenColor];
textView.textColor = [UIColor blackColor];
[self.view addSubview:textView];
NSMutableString *textString = [NSMutableString new];
for (int i=0; i<100; i++) {
[textString appendString:#"cheese\rpizza\rchips\r"];
}
textView.text = textString;
}
- (void)textViewDidBeginEditing:(UITextView *)textView1 {
CGRect textViewFrame = CGRectInset(self.view.bounds, 20.0, 20.0);
textViewFrame.size.height -= 216;
textView.frame = textViewFrame;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
CGRect textViewFrame = CGRectInset(self.view.bounds, 20.0, 20.0);
textView.frame = textViewFrame;
[textView endEditing:YES];
[super touchesBegan:touches withEvent:event];
}
#end
NOTE: unfortunately textViewDidBeginEdting fires before the UIKeyboardWillShowNotification notification, hence the need to hard-code the keyboard height.
Following on is working for me :
.h file
#interface ViewController : UIViewController <UITextViewDelegate> {
UITextView *textView ;
}
#property(nonatomic,strong)IBOutlet UITextView *textView;
#end
.m file
#implementation ViewController
#synthesize textView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 424.0f);
//UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame];
textView.frame = textViewFrame;
textView.delegate = self;
textView.returnKeyType = UIReturnKeyDone;
textView.backgroundColor = [UIColor greenColor];
textView.textColor = [UIColor blackColor];
[self.view addSubview:textView];
}
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView{
NSLog(#"textViewShouldBeginEditing:");
return YES;
}
- (void)textViewDidBeginEditing:(UITextView *)textView1 {
NSLog(#"textViewDidBeginEditing:");
CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 224.0f);
textView1.frame = textViewFrame;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView{
NSLog(#"textViewShouldEndEditing:");
return YES;
}
- (void)textViewDidEndEditing:(UITextView *)textView{
NSLog(#"textViewDidEndEditing:");
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
return YES;
}
- (void)textViewDidChange:(UITextView *)textView{
NSLog(#"textViewDidChange:");
}
- (void)textViewDidChangeSelection:(UITextView *)textView{
NSLog(#"textViewDidChangeSelection:");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(#"touchesBegan:withEvent:");
CGRect textViewFrame = CGRectMake(20.0f, 20.0f, 280.0f, 424.0f);
textView.frame = textViewFrame;
[self.view endEditing:YES];
[super touchesBegan:touches withEvent:event];
}
#end
i had done it and its work completely.
#define k_KEYBOARD_OFFSET 95.0
-(void)keyboardWillAppear {
// Move current view up / down with Animation
if (self.view.frame.origin.y >= 0)
{
[self moveViewUp:NO];
}
else if (self.view.frame.origin.y < 0)
{
[self moveViewUp:YES];
}
}
-(void)keyboardWillDisappear {
if (self.view.frame.origin.y >= 0)
{
[self moveViewUp:YES];
}
else if (self.view.frame.origin.y < 0)
{
[self moveViewUp:NO];
}
}
-(void)textFieldDidBeginEditing:(UITextField *)sender
{
//if ([sender isEqual:_txtPassword])
// {
//move the main view up, so the keyboard will not hide it.
if (self.view.frame.origin.y >= 0)
{
[self moveViewUp:YES];
}
//}
}
//Custom method to move the view up/down whenever the keyboard is appeared / disappeared
-(void)moveViewUp:(BOOL)bMovedUp
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.4]; // to slide the view up
CGRect rect = self.view.frame;
if (bMovedUp) {
// 1. move the origin of view up so that the text field will come above the keyboard
rect.origin.y -= k_KEYBOARD_OFFSET;
// 2. increase the height of the view to cover up the area behind the keyboard
rect.size.height += k_KEYBOARD_OFFSET;
} else {
// revert to normal state of the view.
rect.origin.y += k_KEYBOARD_OFFSET;
rect.size.height -= k_KEYBOARD_OFFSET;
}
self.view.frame = rect;
[UIView commitAnimations];
}
- (void)viewWillAppear:(BOOL)animated
{
// register keyboard notifications to appear / disappear the keyboard
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillAppear)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillDisappear)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
// unregister for keyboard notifications while moving to the other screen.
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
}
This is my solution, July 2015 using Swift 1.2 on Xcode 6.4 targeting iOS 7.1 - a combination of several approaches. Borrowed Johnston's keyboard handing Swift code. Its a bit of a hack, but its simple and it works.
I have a vanilla UITextView inside a single View.
I did not want to embed it inside a UIScrollView as per Apple's documentation. I just wanted the UITextView re-sized when software keyboard appeared, and resized to original when keyboard was dismissed.
These are the basic steps:
Set up keyboard notifications
Set up layout constraint in "Interface Builder" (TextView to bottom edge in my case)
Create an IBOutlet for this constraint in the relevant code file so you can adjust it programmatically
Use keyboard notifications to intercept events and get keyboard size
Programmatically adjust constraint IBOutlet using keyboard size to re-size TextView.
Put everything back when keyboard is dismissed.
So, onto the code.
I've set up constraint outlet at the top of the code file via the usual drag-drop in interface builder: #IBOutlet weak var myUITextViewBottomConstraint: NSLayoutConstraint!
I also set up a global variable where I can back up the state of affairs before the keyboard come up: var myUITextViewBottomConstraintBackup: CGFloat = 0
Implement keyboard notifications, call this function in viewDidLoad or any other startup/setup section:
func setupKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWasShown:"), name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillBeHidden:"), name: UIKeyboardWillHideNotification, object: nil)
}
Then these two functions will be called automatically when keyboard is shown/dismissed:
func keyboardWasShown(aNotification:NSNotification) {
let info = aNotification.userInfo
let infoNSValue = info![UIKeyboardFrameBeginUserInfoKey] as! NSValue
let kbSize = infoNSValue.CGRectValue().size
let newHeight = kbSize.height
//backup old constraint size
myUITextViewBottomConstraintOld = myUITextViewBottomConstraint.constant
// I subtract 50 because otherwise it leaves a gap between keyboard and text view. I'm sure this could be improved on.
myUITextViewBottomConstraint.constant = newHeight - 50
func keyboardWillBeHidden(aNotification:NSNotification) {
//restore to whatever AutoLayout set it before you messed with it
myUITextViewBottomConstraint.constant = myUITextViewBottomConstraintOld
}
The code works, with a minor issue:
It's not responsive to the predictive text ribbon above the keyboard opening/closing. I.e. it will take the state of it into account when the keyboard is called up, but if you were to slide it up or down while keyboard is shown the constraint will not be adjusted. It is a separate event that needs to be handled. Its not enough of a functionality hit for me to bother with.
#Johnston found a good solution. Here's a variation using UIKeyboardWillChangeFrameNotification which correctly accounts for keyboard size changes (i.e. showing/hiding the QuickType bar). It also correctly handles the case where the text view is embedded in a navigation controller (i.e. where the contentInset isn't otherwise zero). It's also written in Swift 2.
override func viewDidLoad() {
:
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillChangeFrameNotification, object: nil, queue: nil) { (notification) -> Void in
guard let userInfo = notification.userInfo,
let keyboardFrameEndValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue
else { return }
let windowCoordinatesKeyboardFrameEnd = keyboardFrameEndValue.CGRectValue() // window coordinates
let keyboardFrameEnd = self.view.convertRect(windowCoordinatesKeyboardFrameEnd, fromView: nil) // view coordinates
var inset = self.textView.contentInset
inset.bottom = CGRectGetMaxY(self.textView.frame) - CGRectGetMinY(keyboardFrameEnd) // bottom inset is the bottom of textView minus top of keyboard
self.textView.contentInset = inset
self.textView.scrollIndicatorInsets = inset
}
}

How do I use two actions in a UIPanGestureRecognizer?

I am working with two subviews. Each will be unique and have it's own "action".
Subview 1 = User can drag around the view, rotate, and zoom it
Subview 2 = When user moves finger across their screen an image is added at each point their finger touches.
I have both of these completed by using UIPanGestureRecognizer. My question is, how can I separate these two actions? I want to be able to add one subview, do what is required, and then when I add the other subview, prevent the previous actions from occurring.
Here is what I have tried, this is done in my panGesture method:
for (UIView * subview in imageView.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
if (subview == _aImageView)
{
CGPoint translation = [panRecognizer translationInView:self.view];
CGPoint imageViewPosition = _aImageView.center;
imageViewPosition.x += translation.x;
imageViewPosition.y += translation.y;
_aImageView.center = imageViewPosition;
[panRecognizer setTranslation:CGPointZero inView:self.view];
}
else if (subview == _bImageView)
{
currentTouch = [panRecognizer locationInView:self.view];
CGFloat distance = [self distanceFromPoint:currentTouch ToPoint:prev_touchPoint];
accumulatedDistance += distance;
CGFloat fixedDistance = 60;
if ([self distanceFromPoint:currentTouch ToPoint:prev_touchPoint] > fixedDistance)
{
[self addbImage];
prev_touchPoint = currentTouch;
}
}
}
}
If you want different gesture recognition in two different views, put separate recognizers on each view.
Usually, you want to have your view controller own and manage gesture recognizers, e.g.
- (void)viewDidLoad {
self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
self.panGesture.delegate = self;
[self.viewX addGestureRecognizer:self.panGesture];
// repeat with other recognisers...
}
Note that setting your controller as delegate of the gestureRecognizer is important: this enables you to handle the following delegate method from the view controller (which was the main question):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// handle your logic, which gestureRecognizer should proceed...
return NO;
}
The handler method is the same is this example, but you can set up your own handlers as you like:
- (void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
// handle gesture (usually sorted by state), e.g.
// if(gesture.state == UIGestureRecognizerStateEnded) { ... }
}

Allow click and dragging a view to drag the window itself?

I'm using a textured window that has a tab bar along the top of it, just below the title bar.
I've used -setContentBorderThickness:forEdge: on the window to make the gradient look right, and to make sure sheets slide out from the right position.
What's not working however, is dragging the window around. It works if I click and drag the area that's actually the title bar, but since the title bar gradient spills into a (potentially/often empty) tab bar, it's really easy to click too low and it feels really frustrating when you try to drag and realise the window is not moving.
I notice NSToolbar, while occupying roughly the same amount of space below the title bar, allows the window to be dragged around when the cursor is over it. How does one implement this?
Thanks.
I tried the mouseDownCanMoveWindow solution (https://stackoverflow.com/a/4564146/901641) but it didn't work for me. I got rid of that method and instead added this to my window subclass:
- (BOOL)isMovableByWindowBackground {
return YES;
}
which worked like a charm.
I found this here:
-(void)mouseDown:(NSEvent *)theEvent {
NSRect windowFrame = [[self window] frame];
initialLocation = [NSEvent mouseLocation];
initialLocation.x -= windowFrame.origin.x;
initialLocation.y -= windowFrame.origin.y;
}
- (void)mouseDragged:(NSEvent *)theEvent {
NSPoint currentLocation;
NSPoint newOrigin;
NSRect screenFrame = [[NSScreen mainScreen] frame];
NSRect windowFrame = [self frame];
currentLocation = [NSEvent mouseLocation];
newOrigin.x = currentLocation.x - initialLocation.x;
newOrigin.y = currentLocation.y - initialLocation.y;
// Don't let window get dragged up under the menu bar
if( (newOrigin.y+windowFrame.size.height) > (screenFrame.origin.y+screenFrame.size.height) ){
newOrigin.y=screenFrame.origin.y + (screenFrame.size.height-windowFrame.size.height);
}
//go ahead and move the window to the new location
[[self window] setFrameOrigin:newOrigin];
}
It works fine, though I'm not 100% sure I'm doing it correctly. There's one bug I've found so far, and that's if the drag begins inside a subview (a tab itself) and then enters the superview (the tab bar). The window jumps around. Some -hitTest: magic, or possibly even just invalidating initialLocation on mouseUp should probably fix that.
As of macOS 10.11, the simplest way to do this is to utilize the new -[NSWindow performWindowDragWithEvent:] method:
#interface MyView () {
BOOL movingWindow;
}
#end
#implementation MyView
...
- (BOOL)mouseDownCanMoveWindow
{
return NO;
}
- (void)mouseDown:(NSEvent *)event
{
movingWindow = NO;
CGPoint point = [self convertPoint:event.locationInWindow
fromView:nil];
// The area in your view where you want the window to move:
CGRect movableRect = CGRectMake(0, 0, 100, 100);
if (self.window.movableByWindowBackground &&
CGRectContainsPoint(movableRect, point)) {
[self.window performWindowDragWithEvent:event];
movingWindow = YES;
return;
}
// Handle the -mouseDown: as usual
}
- (void)mouseDragged:(NSEvent *)event
{
if (movingWindow) return;
// Handle the -mouseDragged: as usual
}
#end
Here, -performWindowDragWithEvent: will handle the correct behavior of not overlapping the menu bar, and will also snap to edges on macOS 10.12 and later. Be sure to include a BOOL movingWindow instance variable with your view's private interface so you can avoid -mouseDragged: events once you determined you don't want to process them.
Here, we are also checking that -[NSWindow movableByWindowBackground] is set to YES so that this view can be used in non-movable-by-window-background windows, but that is optional.
Have you tried overriding the NSView method mouseDownCanMoveWindow to return YES?
It works for me after TWO steps:
Subclass NSView, override the mouseDownCanMoveWindow to return YES.
Subclass NSWindow, override the isMovableByWindowBackground to return YES.
It's quite easy:
override mouseDownCanMoveWindow property
override var mouseDownCanMoveWindow:Bool {
return false
}
If you got a NSTableView in your window, with selection enabled, overriding the mouseDownCanMoveWindow property won't work.
You need instead to create a NSTableView subclass and override the following mouse events (and use the performWindowDragWithEvent: mentioned in Dimitri answer):
#interface WindowDraggableTableView : NSTableView
#end
#implementation WindowDraggableTableView
{
BOOL _draggingWindow;
NSEvent *_mouseDownEvent;
}
- (void)mouseDown:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseDown:event]; // Normal behavior.
return;
}
_draggingWindow = NO;
_mouseDownEvent = event;
}
- (void)mouseDragged:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseDragged:event]; // Normal behavior.
return;
}
assert(_mouseDownEvent);
_draggingWindow = YES;
[self.window performWindowDragWithEvent:_mouseDownEvent];
}
- (void)mouseUp:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseUp:event]; // Normal behavior.
return;
}
if (_draggingWindow == YES) {
_draggingWindow = NO;
return; // Event already handled by `performWindowDragWithEvent`.
}
// Triggers regular table selection.
NSPoint locationInWindow = event.locationInWindow;
NSPoint locationInTable = [self convertPoint:locationInWindow fromView:nil];
NSInteger row = [self rowAtPoint:locationInTable];
if (row >= 0 && [self.delegate tableView:self shouldSelectRow:row])
{
NSIndexSet *rowIndex = [NSIndexSet indexSetWithIndex:row];
[self selectRowIndexes:rowIndex byExtendingSelection:NO];
}
}
#end
Also don't forget to set the corresponding window movableByWindowBackground property as well:
self.window.movableByWindowBackground = YES;
When you set property isMovableByWindowBackground in viewDidLoad, it may not work because the window property of the view is not yet set. In that case, try this:
override func viewDidAppear() {
self.view.window?.isMovableByWindowBackground = true
}
Thank you! Dimitris' answer solved my issue but I needed it in swift 5. Here is what I came up with.
final class BlahField: NSTextView {
var movingWindow = false
override func mouseDown(with event: NSEvent) {
movingWindow = false
let point = self.convert(event.locationInWindow, from: nil)
if (self.window!.isMovableByWindowBackground && self.frame.contains(point)) {
self.window?.performDrag(with: event)
movingWindow = true
return
}
}
override func mouseDragged(with event: NSEvent) {
if (movingWindow) {
return
}
}
}

How programmatically move a UIScrollView to focus in a control above keyboard?

I have 6 UITextFields on my UIScrollView. Now, I can scroll by user request. But when the keyboard appear, some textfields are hidden.
That is not user-friendly.
How scroll programmatically the view so I get sure the keyboard not hide the textfield?
Here's what worked for me. Having an instance variable that holds the value of the UIScrollView's offset before the view is adjusted for the keyboard so you can restore the previous state after the UITextField returns:
//header
#interface TheViewController : UIViewController <UITextFieldDelegate> {
CGPoint svos;
}
//implementation
- (void)textFieldDidBeginEditing:(UITextField *)textField {
svos = scrollView.contentOffset;
CGPoint pt;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:scrollView];
pt = rc.origin;
pt.x = 0;
pt.y -= 60;
[scrollView setContentOffset:pt animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[scrollView setContentOffset:svos animated:YES];
[textField resignFirstResponder];
return YES;
}
Finally, a simple fix:
UIScrollView* v = (UIScrollView*) self.view ;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:v];
rc.origin.x = 0 ;
rc.origin.y -= 60 ;
rc.size.height = 400;
[self.scroll scrollRectToVisible:rc animated:YES];
Now I think is only combine this with the link above and is set!
I've put together a universal, drop-in UIScrollView and UITableView subclass that takes care of moving all text fields within it out of the way of the keyboard.
When the keyboard is about to appear, the subclass will find the subview that's about to be edited, and adjust its frame and content offset to make sure that view is visible, with an animation to match the keyboard pop-up. When the keyboard disappears, it restores its prior size.
It should work with basically any setup, either a UITableView-based interface, or one consisting of views placed manually.
Here it is.
(For google: TPKeyboardAvoiding, TPKeyboardAvoidingScrollView, TPKeyboardAvoidingCollectionView.)
Editor's note: TPKeyboardAvoiding seems to be continually updated and fresh, as of 2014.
If you set the delegate of your text fields to a controller object in your program, you can have that object implement the textFieldDidBeginEditing: and textFieldShouldReturn: methods. The first method can then be used to scroll to your text field and the second method can be used to scroll back.
You can find code I have used for this in my blog: Sliding UITextViews around to avoid the keyboard. I didn't test this code for text views in a UIScrollView but it should work.
simple and best
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
// self.scrlViewUI.contentOffset = CGPointMake(0, textField.frame.origin.y);
[_scrlViewUI setContentOffset:CGPointMake(0,textField.center.y-90) animated:YES];
tes=YES;
[self viewDidLayoutSubviews];
}
The answers posted so far didn't work for me as I've a quite deep nested structure of UIViews. Also, the I had the problem that some of those answers were working only on certain device orientations.
Here's my solution, which will hopefully make you waste some less time on this.
My UIViewTextView derives from UIView, is a UITextView delegate and adds a UITextView after having read some parameters from an XML file for that UITextView (that XML part is left out here for clarity).
Here's the private interface definition:
#import "UIViewTextView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreGraphics/CGColor.h>
#interface UIViewTextView (/**/) {
#private
UITextView *tf;
/*
* Current content scroll view
* position and frame
*/
CGFloat currentScrollViewPosition;
CGFloat currentScrollViewHeight;
CGFloat kbHeight;
CGFloat kbTop;
/*
* contentScrollView is the UIScrollView
* that contains ourselves.
*/
UIScrollView contentScrollView;
}
#end
In the init method I have to register the event handlers:
#implementation UIViewTextView
- (id) initWithScrollView:(UIScrollView*)scrollView {
self = [super init];
if (self) {
contentScrollView = scrollView;
// ...
tf = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 241, 31)];
// ... configure tf and fetch data for it ...
tf.delegate = self;
// ...
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(keyboardWasShown:) name: UIKeyboardWillShowNotification object:nil];
[nc addObserver:self selector:#selector(keyboardWasHidden:) name: UIKeyboardWillHideNotification object:nil];
[self addSubview:tf];
}
return(self);
}
Once that's done, we need to handle the keyboard show event. This gets called before the textViewBeginEditing is called, so we can use it to find out some properties of the keyboard. In essence, we want to know the height of the keyboard. This, unfortunately, needs to be taken from its width property in landscape mode:
-(void)keyboardWasShown:(NSNotification*)aNotification {
NSDictionary* info = [aNotification userInfo];
CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGSize kbSize = kbRect.size;
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat sWidth = screenRect.size.width;
CGFloat sHeight = screenRect.size.height;
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if ((orientation == UIDeviceOrientationPortrait)
||(orientation == UIDeviceOrientationPortraitUpsideDown)) {
kbHeight = kbSize.height;
kbTop = sHeight - kbHeight;
} else {
//Note that the keyboard size is not oriented
//so use width property instead
kbHeight = kbSize.width;
kbTop = sWidth - kbHeight;
}
Next, we need to actually scroll around when we start editing. We do this here:
- (void) textViewDidBeginEditing:(UITextView *)textView {
/*
* Memorize the current scroll position
*/
currentScrollViewPosition = contentScrollView.contentOffset.y;
/*
* Memorize the current scroll view height
*/
currentScrollViewHeight = contentScrollView.frame.size.height;
// My top position
CGFloat myTop = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow.rootViewController.view].y;
// My height
CGFloat myHeight = self.frame.size.height;
// My bottom
CGFloat myBottom = myTop + myHeight;
// Eventual overlap
CGFloat overlap = myBottom - kbTop;
/*
* If there's no overlap, there's nothing to do.
*/
if (overlap < 0) {
return;
}
/*
* Calculate the new height
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight + overlap);
/*
* Set the new height
*/
[contentScrollView setFrame:nrect];
/*
* Set the new scroll position
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = contentScrollView.contentOffset.y + overlap;
[contentScrollView setContentOffset:npos animated:NO];
}
When we end editing, we do this to reset the scroll position:
- (void) textViewDidEndEditing:(UITextView *)textView {
/*
* Reset the scroll view position
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight);
[contentScrollView setFrame:nrect];
/*
* Reset the scroll view height
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = currentScrollViewPosition;
[contentScrollView setContentOffset:npos animated:YES];
[tf resignFirstResponder];
// ... do something with your data ...
}
There's nothing left to do in the keyboard was hidden event handler; we leave it in anyway:
-(void)keyboardWasHidden:(NSNotification*)aNotification {
}
And that's it.
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end
I know this is old, but still none of the solutions above had all the fancy positioning stuff required for that "perfect" bug-free, backwards compatible and flicker-free animation.
Let me share my solution (assuming you have set up UIKeyboardWill(Show|Hide)Notification):
// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardFrameInWindow;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];
// the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];
CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);
// this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
_scrollView.contentInset = newContentInsets;
_scrollView.scrollIndicatorInsets = newContentInsets;
/*
* Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
* that should be visible, e.g. a purchase button below an amount text field
* it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
*/
if (_focusedControl) {
CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.
CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;
// this is the visible part of the scroll view that is not hidden by the keyboard
CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;
if (controlVisualBottom > scrollViewVisibleHeight) { // check if the keyboard will hide the control in question
// scroll up until the control is in place
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);
// make sure we don't set an impossible offset caused by the "nice visual offset"
// if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
} else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) {
// if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y = controlFrameInScrollView.origin.y;
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
}
}
[UIView commitAnimations];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = notification.userInfo;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
// undo all that keyboardWillShow-magic
// the scroll view will adjust its contentOffset apropriately
_scrollView.contentInset = UIEdgeInsetsZero;
_scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
[UIView commitAnimations];
}
You may check it out: https://github.com/michaeltyson/TPKeyboardAvoiding (I used that sample for my apps). It is working so well. I hope that helps you.
Actually, here's a full tutorial on using TPKeyboardAvoiding, which may help someone
(1) download the zip file from the github link. add these four files to your Xcode project:
(2) build your beautiful form in IB. add a UIScrollView. sit the form items INSIDE the scroll view. (Note - extremely useful tip regarding interface builder: https://stackoverflow.com/a/16952902/294884)
(3) click on the scroll view. then at the top right, third button, you'll see the word "UIScrollView". using copy and paste, change it to "TPKeyboardAvoidingScrollView"
(4) that's it. put the app in the app store, and bill your client.
(Also, just click on the Inspector tab of the scroll view. You may prefer to turn on or off bouncing and the scroll bars - your preference.)
Personal comment - I strongly recommend using scroll view (or collection view) for input forms, in almost all cases. do not use a table view. it's problematic for many reasons. and quite simply, it's incredibly easier to use a scroll view. just lay it out any way you want. it is 100% wysiwyg in interface builder. hope it helps
This is my code, hope it will help you. It work ok in case you have many textfield
CGPoint contentOffset;
bool isScroll;
- (void)textFieldDidBeginEditing:(UITextField *)textField {
contentOffset = self.myScroll.contentOffset;
CGPoint newOffset;
newOffset.x = contentOffset.x;
newOffset.y = contentOffset.y;
//check push return in keyboar
if(!isScroll){
//180 is height of keyboar
newOffset.y += 180;
isScroll=YES;
}
[self.myScroll setContentOffset:newOffset animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
//reset offset of content
isScroll = NO;
[self.myScroll setContentOffset:contentOffset animated:YES];
[textField endEditing:true];
return true;
}
we have a point contentOffset to save contentoffset of scrollview before keyboar show. Then we will scroll content for y about 180 (height of keyboar). when you touch return in keyboar, we will scroll content to old point(it is contentOffset). If you have many textfield, you don't touch return in keyboar but you touch another textfield, it will +180 . So we have check touch return
Use any of these,
CGPoint bottomOffset = CGPointMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height);
[self.MainScrollView setContentOffset:bottomOffset animated:YES];
or
[self.MainScrollView scrollRectToVisible:CGRectMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height-30, MainScrollView.frame.size.width, MainScrollView.frame.size.height) animated:YES];
I think it's better use keyboard notifications because you don't know if the first responder (the control with focus on) is a textField or a textView (or whatever). So juste create a category to find the first responder :
#import "UIResponder+FirstResponder.h"
static __weak id currentFirstResponder;
#implementation UIResponder (FirstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:#selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
#end
then
-(void)keyboardWillShowNotification:(NSNotification*)aNotification{
contentScrollView.delegate=nil;
contentScrollView.scrollEnabled=NO;
contentScrollViewOriginalOffset = contentScrollView.contentOffset;
UIResponder *lc_firstResponder = [UIResponder currentFirstResponder];
if([lc_firstResponder isKindOfClass:[UIView class]]){
UIView *lc_view = (UIView *)lc_firstResponder;
CGRect lc_frame = [lc_view convertRect:lc_view.bounds toView:contentScrollView];
CGPoint lc_point = CGPointMake(0, lc_frame.origin.y-lc_frame.size.height);
[contentScrollView setContentOffset:lc_point animated:YES];
}
}
Eventually disable the scroll and set the delegate to nil then restore it to avoid some actions during the edition of the first responder. Like james_womack said, keep the original offset to restore it in a keyboardWillHideNotification method.
-(void)keyboardWillHideNotification:(NSNotification*)aNotification{
contentScrollView.delegate=self;
contentScrollView.scrollEnabled=YES;
[contentScrollView setContentOffset:contentScrollViewOriginalOffset animated:YES];
}
In Swift 1.2+ do something like this:
class YourViewController: UIViewController, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
_yourTextField.delegate = self //make sure you have the delegate set to this view controller for each of your textFields so textFieldDidBeginEditing can be called for each one
...
}
func textFieldDidBeginEditing(textField: UITextField) {
var point = textField.convertPoint(textField.frame.origin, toView: _yourScrollView)
point.x = 0.0 //if your textField does not have an origin at 0 for x and you don't want your scrollView to shift left and right but rather just up and down
_yourScrollView.setContentOffset(point, animated: true)
}
func textFieldDidEndEditing(textField: UITextField) {
//Reset scrollview once done editing
scrollView.setContentOffset(CGPoint.zero, animated: true)
}
}