I'm trying to get my app to move the view when the keyboard appears, and so far the results have been... mixed to say the least. I can get it to move, thing is it's either hard coded or only works partially.
I have multiple Textfields in my view, when I tap on them, sometimes depending on where my scroll is it get's hidden by the keyboard.
Now what I need my app to do is to move the view to see the textfield only if the active textfield is hidden by the keyboard.
My Hierarchy for the view goes like this :
So I have a Scroll view, and in the scroll View I have a UIView named ContentView, in the ContentView I have all my textfields and labels.
thing is, I can't hard code it since My app is universal, I need to have the keyboard move the view only if it hides the textfield. Because in a situation where the user is on an iPad, the View will likely never have to move
I used the following Stack overflow answers with no results :
Swift: Scroll View only when a TextField or Button is hidden by the Keyboard
Move view with keyboard using Swift
here's my code that actually comes from one of those answers :
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(NewPaxController.keyboardWillShow), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(NewPaxController.keyboardWillHide), name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification:NSNotification) {
if keyboardIsPresent == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.ContentView.frame.origin.y -= keyboardSize.height
keyboardIsPresent = true
}
}
}
func keyboardWillHide(notification:NSNotification) {
if keyboardIsPresent == true {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.ContentView.frame.origin.y += keyboardSize.height
keyboardIsPresent = false
}
}
}
I'm almost 100% sure all my error come from the fact the I have a ContentView... but I need it in my case. Thanks in advance for your help
You should not modify frames when keyboard shows up.
Instead you need to set the bottom inset of the scrollview scrollView.contentInset.bottom from zero to keyboard height. When the keyboard disappears, you set the inset back to zero.
This whole problem is solvable by mere 10 lines of code.
literally get the keyboard height from notification, store it ion a local variable in the class while KB is showing, and use the value to set insets in delegate callback/event handler methods.
The trick here is that setting nonzero insets will effectively scroll the scrollview together with the content for you up by and that will push the current textfield up as well.
You should first try to locate the UITextField instance by using the following code.
extension UIView {
func firstResponder() -> UIView? {
if self.isFirstResponder() {
return self
}
for subview in self.subviews {
if subview.isFirstResponder() {
return subview
}
}
return nil
}
}
In the keyboardWillShow: function decide if the textfield is visible or not if the keyboard comes up.
func keyboardWillShow(notification:NSNotification) {
if keyboardIsPresent == false {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue(),
let inputView = self.ContentView.firstResponder()
where inputView.frame.maxY > self.ContentView.frame.size.height - keyboardSize.height {
self.ContentView.frame.origin.y -= keyboardSize.height
keyboardIsPresent = true
}
}
}
Than only move the view back in the hide function if it was moved away.
Here is the code for it, It is pretty straight forward, but if you still need help with it, I will explain it further.
#pragma mark - Keyboard Observer events
-(void)keyboardWillShow:(NSNotification*)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
keyboardHeight = kbSize.height;
[self updateScrollViewPosition];
}
-(void)keyboardDidChange:(NSNotification *)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSizeBegin = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGSize kbSizeEnd = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
if (kbSizeBegin.height != kbSizeEnd.height) {
keyboardHeight = kbSizeEnd.height;
if (activeTextField && [activeTextField isFirstResponder]) {
[self updateScrollViewPosition];
}
}
}
-(void)keyboardWillHide:(NSNotification*)notification {
keyboardHeight = 0;
activeTextField = nil;
[self resignAllTextFields];
}
#pragma mark - UITextFieldDelegate Methods
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
activeTextField = textField;
return YES;
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
activeTextField = textField;
[self updateScrollViewPosition];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
keyboardHeight = 0;
activeTextField = nil;
[textField resignFirstResponder];
return YES;
}
#pragma mark - Update Method
-(void)updateScrollViewPosition {
if (keyboardHeight > 0 && activeTextField) {
CGRect frame = activeTextField.frame;
CGFloat yPoint = scrollView.frame.origin.y+frame.origin.y+frame.size.height+8.0;
CGFloat height = self.view.frame.size.height-keyboardHeight;
CGFloat diff = yPoint-height;
if (diff > 0.0) {
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
else {
CGFloat diff = scrollView.contentSize.height-scrollView.contentOffset.y;
if (diff<scrollView.frame.size.height) {
diff = scrollView.contentSize.height-scrollView.frame.size.height;
if (diff < 0) {
diff = 0.0;
}
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
}
}
else {
CGFloat diff = scrollView.contentSize.height-scrollView.contentOffset.y;
if (diff<scrollView.frame.size.height) {
diff = scrollView.contentSize.height-scrollView.frame.size.height;
if (diff < 0) {
diff = 0.0;
}
[scrollView setContentOffset:CGPointMake(0, diff) animated:YES];
}
}
}
#pragma mark
Edit: A simple resignAllTextFields method as requested. containerView is the view which contains all the UITextField.
-(void)resignAllTextFields {
for (UIView *view in containerView.subviews) {
if ([view isKindOfClass:[UITextField class]]) {
UITextField *textField = (UITextField*)view;
[textField resignFirstResponder];
}
}
[self updateScrollViewPosition];
}
Related
I need to adjust a 'message bar' when the keyboard is shown and am having issues when using iPad modal view controller. The 'message bar' should sit right on top of the keyboard when it is shown, and then at the bottom of the modal when the keyboard is hidden (a la Messages app style).
The problem is I need to get the top most point of the keyboard's frame in terms of the Modal's coordinate system.
I found this answer that theoretically seems correct, but doesn't work (How can I find portion of my view which isn't covered by the keyboard (UIModalPresenationStyleFormSheet)?):
FYI When the keyboard is shown, the keyboard's frame in the main window (i.e. 'keyboardFrame') = (-84.0, 526.0, 768.0, 264.0). The keyboard's frame when translated for the modal view controller coordinate system (i.e. 'newSize') = (-168.0, 292.0, 768.0, 264.0)*
// Convert the keyboard rect into the view controller's coordinate system
// The fromView: nil means keyboardRect is in the main window's coordinate system
let newSize = self.view.convertRect(keyboardFrame, fromView: nil)
// And then this is the part that gets covered!
let keyboardCoveredHeight = self.view.bounds.height - newSize.origin.y
self.messageToolbar.bottomConstraint.constant = -keyboardCoveredHeight
I ended up using a familiar category to get the proper keyboard frame in a iPad modal view:
- (CGFloat)heightCoveredByKeyboardOfSize:(CGSize)keyboardSize
{
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
CGRect frameInWindow = [self convertRect:self.bounds toView:nil];
CGRect windowBounds = self.window.bounds;
CGFloat keyboardTop;
CGFloat heightCoveredByKeyboard;
//Determine height of the view covered by the keyboard relative to current rotation
switch (orientation)
{
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
keyboardTop = windowBounds.size.height - keyboardSize.height;
heightCoveredByKeyboard = windowBounds.size.height - frameInWindow.origin.y - keyboardTop;
break;
case UIInterfaceOrientationPortraitUpsideDown:
default:
keyboardTop = windowBounds.size.height - keyboardSize.height;
heightCoveredByKeyboard = CGRectGetMaxY(frameInWindow) - keyboardTop;
break;
}
return MAX(0.0f,heightCoveredByKeyboard);
}
along with DAKeyboardControl (for panning):
// Use [unowned self] in closure for weak reference
self.view.addKeyboardPanningWithFrameBasedActionHandler(nil, constraintBasedActionHandler: { [unowned self] (keyboardFrameInView, opening, closing) -> Void in
if opening
{
if UIDevice.isIpad()
{
// iPad requires a different method due to use of modal view
self.messageToolbar.bottomConstraint.constant = -self.view.heightCoveredByKeyboardOfSize(keyboardFrameInView.size)
}
else
{
self.messageToolbar.bottomConstraint.constant = -keyboardFrameInView.size.height
}
}
if closing
{
self.messageToolbar.bottomConstraint.constant = 0
}
self.view.updateConstraintsIfNeeded()
self.view.layoutIfNeeded()
})
It is possible to calculate exact overlap this way, on keyboardDidShowNotification
let parentRelativeFrame = view.convert(view.frame, to: nil)
let viewBottomVerticalOffset = parentRelativeFrame.origin.y + parentRelativeFrame.height
let overlap = viewBottomVerticalOffset - keyboardFrame.origin.y
I think you're running into one of two problems (maybe both):
In iOS 8, you have to convert the keyboard's rect from the screen's coordinate space
On iPad, the sheet views sometimes move up a bit when the keyboard is displayed, which happens after the keyboard is shown, so you need to calculate the results again in viewWillLayoutSubviews
Here's a sample project that does what you want.
For posterity, here's the interesting source code:
class TypingViewController: UIViewController {
var keyboardHoverView: UIView! // your keyboard toolbar or whatever
var lastKeyboardFrame : CGRect! // whenever you get a frame update, store it here
override func viewDidLoad() {
super.viewDidLoad()
createKeyboardHoverView()
observeKeyboard()
}
func createKeyboardHoverView() {
// make your keyboard toolbar
keyboardHoverView = view
}
func observeKeyboard() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification : NSNotification) {
let dict = notification.userInfo!
let frameValue = dict[UIKeyboardFrameEndUserInfoKey] as! NSValue
lastKeyboardFrame = frameValue.CGRectValue()
moveKeyboardView()
}
func keyboardWillHide(notification : NSNotification) {
moveKeyboardView()
}
override func viewWillLayoutSubviews() {
// this moves the view again, in case the modal view has shifted up
moveKeyboardView()
}
func moveKeyboardView() {
if lastKeyboardFrame != nil {
// this conversion is used in iOS 8 and differs from most online tutorials
let frameInThisView = self.view.convertRect(lastKeyboardFrame, fromCoordinateSpace: view.window!.screen.coordinateSpace)
var newRect = keyboardHoverView.frame
newRect.origin.y = frameInThisView.origin.y - CGRectGetHeight(newRect)
keyboardHoverView.frame = newRect
println("Setting rect: \(newRect)")
}
}
}
I know this looks like a simple question one can simply say:
webview.scrollView.scrollEnabled = NO;
webview.scrollView.panGestureRecognizer.enabled = NO;
webview.scrollView.bounces = NO;
or even
for (UIView* subview in webview.subviews) {
if ([subview respondsToSelector:#selector(setScrollEnabled:)]) {
[(id)subview setScrollEnabled:enabled];
}
if ([subview respondsToSelector:#selector(panGestureRecognizer)]) {
[[(id)subview panGestureRecognizer] setEnabled:enabled];
}
}
but while it does prevent scolling (in the contentOffset meaning) inside the WKWebviewit doesn't prevent it from receiving pan gesture events involving scrolling.
So articles like those of the Huffington Post, which have javascript included to automatically change articles when the user scrolls left or right still get that behavior.
How can I prevent this ?
Before Swift 3
You can simply disable scroll on its implicit scrollView
webView.scrollView.scrollEnabled = false
Swift 3
webView.scrollView.isScrollEnabled = false
Took me a while but I figured out a way of doing this.
I had to remove a private gesture recognizer within a private subview of the WKWebView.
I had a category on WKWebView to do so:
#implementation WKWebView (Scrolling)
- (void)setScrollEnabled:(BOOL)enabled {
self.scrollView.scrollEnabled = enabled;
self.scrollView.panGestureRecognizer.enabled = enabled;
self.scrollView.bounces = enabled;
// There is one subview as of iOS 8.1 of class WKScrollView
for (UIView* subview in self.subviews) {
if ([subview respondsToSelector:#selector(setScrollEnabled:)]) {
[(id)subview setScrollEnabled:enabled];
}
if ([subview respondsToSelector:#selector(setBounces:)]) {
[(id)subview setBounces:enabled];
}
if ([subview respondsToSelector:#selector(panGestureRecognizer)]) {
[[(id)subview panGestureRecognizer] setEnabled:enabled];
}
// here comes the tricky part, desabling
for (UIView* subScrollView in subview.subviews) {
if ([subScrollView isKindOfClass:NSClassFromString(#"WKContentView")]) {
for (id gesture in [subScrollView gestureRecognizers]) {
if ([gesture isKindOfClass:NSClassFromString(#"UIWebTouchEventsGestureRecognizer")])
[subScrollView removeGestureRecognizer:gesture];
}
}
}
}
}
#end
Hope this helps anyone some day.
Credit and many thanks to apouche for the Obj-C code. In case anybody else has the same problem, here is the working solution adapted for Swift 2
extension WKWebView {
func setScrollEnabled(enabled: Bool) {
self.scrollView.scrollEnabled = enabled
self.scrollView.panGestureRecognizer.enabled = enabled
self.scrollView.bounces = enabled
for subview in self.subviews {
if let subview = subview as? UIScrollView {
subview.scrollEnabled = enabled
subview.bounces = enabled
subview.panGestureRecognizer.enabled = enabled
}
for subScrollView in subview.subviews {
if subScrollView.dynamicType == NSClassFromString("WKContentView")! {
for gesture in subScrollView.gestureRecognizers! {
subScrollView.removeGestureRecognizer(gesture)
}
}
}
}
}
}
finally
self.webView.scrollView.userInteractionEnabled = NO
Here is a Swift 3 version:
extension WKWebView {
func setScrollEnabled(enabled: Bool) {
self.scrollView.isScrollEnabled = enabled
self.scrollView.panGestureRecognizer.isEnabled = enabled
self.scrollView.bounces = enabled
for subview in self.subviews {
if let subview = subview as? UIScrollView {
subview.isScrollEnabled = enabled
subview.bounces = enabled
subview.panGestureRecognizer.isEnabled = enabled
}
for subScrollView in subview.subviews {
if type(of: subScrollView) == NSClassFromString("WKContentView")! {
for gesture in subScrollView.gestureRecognizers! {
subScrollView.removeGestureRecognizer(gesture)
}
}
}
}
}
}
I found that I had to make my view controller a UIScrollViewDelegate then add this function to prevent scrolling.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
}
Here is a C# extension for WKWebView based on alain.s's swift solution (based on apouche's solution) for those of us using Xamarin. I am using this in my app.
Notable differences is that I check if subviews exist before looping and instead of dynamically looking for a "WKContentView" (something I'm not sure is even possible in Xamarin) I simply check if each subview has GestureRecognizers and remove them. This will obviously disable all types of gestures so consider this if you expect any user interaction with the web content.
public static class WKWebViewExtension
{
public static void DisableScroll(this WebKit.WKWebView webView)
{
webView.ScrollView.ScrollEnabled = false;
webView.ScrollView.PanGestureRecognizer.Enabled = false;
webView.ScrollView.Bounces = false;
if (webView.Subviews != null)
{
foreach (var subView in webView.Subviews)
{
if (subView is UIScrollView)
{
UIScrollView subScrollView = (UIScrollView)subView;
subScrollView.ScrollEnabled = false;
subScrollView.Bounces = false;
subScrollView.PanGestureRecognizer.Enabled = false;
}
if (subView.Subviews != null)
{
foreach (var subScrollView in subView.Subviews)
{
if (subScrollView.GestureRecognizers != null)
{
foreach (var gesture in subScrollView.GestureRecognizers)
{
subScrollView.RemoveGestureRecognizer(gesture);
}
}
}
}
}
}
}
}
Here's a swift version if anyone's still having trouble with this issue
let subviews = self.theWebView.scrollView.subviews
for subview in subviews{
if(subview.isKindOfClass(NSClassFromString("WKContentView"))){
if let recognizers = subview.gestureRecognizers {
for recognizer in recognizers! {
if recognizer.isKindOfClass(NSClassFromString("UIWebTouchEventsGestureRecognizer")){
subview.removeGestureRecognizer(recognizer as! UIGestureRecognizer)
}
}
}
}
}
Swift 5
disableScrollView(self.webView)
func disableScrollView(_ view: UIView) {
(view as? UIScrollView)?.isScrollEnabled = false
view.subviews.forEach { disableScrollView($0) }
}
Try to disable scrollView zoom in this way:
CGFloat zoomScale = webview.scrollView.zoomScale;
webview.scrollView.maximumZoomScale = zoomScale;
webview.scrollView.minimumZoomScale = zoomScale;
I was using this code to determine what is the size of the keyboard :
- (void)keyboardWillChange:(NSNotification *)notification {
NSDictionary* keyboardInfo = [notification userInfo];
NSValue* keyboardFrameBegin = [keyboardInfo valueForKey:UIKeyboardFrameBeginUserInfoKey];
CGRect keyboardFrameBeginRect = [keyboardFrameBegin CGRectValue];
}
I'm running this in the simulator.
The problem is since iOS 8 this will not give the correct value, if the keyboard suggestions is up or if I push them down I get different (not correct) values.
How can I get the exact size of the keyboard including the keyboard suggestions?
With the introduction of custom keyboards in iOS, this problem becomes a tad more complex.
In short, the UIKeyboardWillShowNotification can get called multiple times by custom keyboard implementations:
When the Apple system keyboard is opened (in portrait)
UIKeyboardWillShowNotification is sent with a keyboard height of 224
When the Swype keyboard is opened (in portrait):
UIKeyboardWillShowNotification is sent with a keyboard height of 0
UIKeyboardWillShowNotification is sent with a keyboard height of 216
UIKeyboardWillShowNotification is sent with a keyboard height of 256
When the SwiftKey keyboard is opened (in portrait):
UIKeyboardWillShowNotification is sent with a keyboard height of 0
UIKeyboardWillShowNotification is sent with a keyboard height of 216
UIKeyboardWillShowNotification is sent with a keyboard height of 259
In order to handle these scenarios properly in one code-line, you need to:
Register observers against the UIKeyboardWillShowNotification and UIKeyboardWillHideNotification notifications:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
Create a global variable to track the current keyboard height:
CGFloat _currentKeyboardHeight = 0.0f;
Implement keyboardWillShow to react to the current change in keyboard height:
- (void)keyboardWillShow:(NSNotification*)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
CGFloat deltaHeight = kbSize.height - _currentKeyboardHeight;
// Write code to adjust views accordingly using deltaHeight
_currentKeyboardHeight = kbSize.height;
}
NOTE: You may wish to animate the offsetting of views. The info dictionary contains a value keyed by UIKeyboardAnimationDurationUserInfoKey. This value can be used to animate your changes at the same speed as the keyboard being displayed.
Implement keyboardWillHide to the reset _currentKeyboardHeight and react to the keyboard being dismissed:
- (void)keyboardWillHide:(NSNotification*)notification {
NSDictionary *info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
// Write code to adjust views accordingly using kbSize.height
_currentKeyboardHeight = 0.0f;
}
Use
NSValue* keyboardFrameBegin = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
I also had this issue, until I came across this StackOverflow article:
Convert UIKeyboardFrameEndUserInfoKey
This shows you how to use the convertRect function, to convert the keyboard's size into something usable, but on the screen orientation.
NSDictionary* d = [notification userInfo];
CGRect r = [d[UIKeyboardFrameEndUserInfoKey] CGRectValue];
r = [myView convertRect:r fromView:nil];
Previously, I had an iPad app which used UIKeyboardFrameEndUserInfoKey but didn't use convertRect, and it worked fine.
But with iOS 8, it no longer worked properly. Suddenly, it would report that my keyboard, running on an iPad in landscape mode, was 1024 pixels high.
So now, with iOS 8, it's essential that you use this convertRect function.
The similar to dgangsta's solution written in Swift 2.0:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillShow(notification: NSNotification) {
guard let kbSizeValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
guard let kbDurationNumber = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber else { return }
animateToKeyboardHeight(kbSizeValue.CGRectValue().height, duration: kbDurationNumber.doubleValue)
}
func keyboardWillHide(notification: NSNotification) {
guard let kbDurationNumber = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber else { return }
animateToKeyboardHeight(0, duration: kbDurationNumber.doubleValue)
}
func animateToKeyboardHeight(kbHeight: CGFloat, duration: Double) {
// your custom code here
}
I make extension for UIViewController
extension UIViewController {
func keyboardWillChangeFrameNotification(notification: NSNotification, scrollBottomConstant: NSLayoutConstraint) {
let duration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber
let curve = notification.userInfo?[UIKeyboardAnimationCurveUserInfoKey] as! NSNumber
let keyboardBeginFrame = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
let keyboardEndFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let screenHeight = UIScreen.mainScreen().bounds.height
let isBeginOrEnd = keyboardBeginFrame.origin.y == screenHeight || keyboardEndFrame.origin.y == screenHeight
let heightOffset = keyboardBeginFrame.origin.y - keyboardEndFrame.origin.y - (isBeginOrEnd ? bottomLayoutGuide.length : 0)
UIView.animateWithDuration(duration.doubleValue,
delay: 0,
options: UIViewAnimationOptions(rawValue: UInt(curve.integerValue << 16)),
animations: { () in
scrollBottomConstant.constant = scrollBottomConstant.constant + heightOffset
self.view.layoutIfNeeded()
},
completion: nil
)
}
}
You can use like this :
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillChangeFrameNotification:", name: UIKeyboardWillChangeFrameNotification, object: nil)
}
...
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillChangeFrameNotification(notification: NSNotification) {
self.keyboardWillChangeFrameNotification(notification, scrollBottomConstant: inputContainerBottom)
// Write more to here if you want.
}
There are times when devs need to know the keyboard height before it's actually shown, allowing them pre-layout the interface appropriately.
If that's the case, here's an inclusive spec:
This includes the quick type bar at the top, since that is on by default in all current versions of iOS.
Here is the swift 3 notification setup I used to test this, if anyone needs it:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: .UIKeyboardWillShow, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
guard let keyboardSize = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
print("\(keyboardSize)")
}
Only one string for swift:
let keyboardSize = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue().size
UIKeyboardFrameEndUserInfoKey always stores NSValue, so no need to check it.
I noticed an issue showing when switching between default keyboard and a custom (UIPickerView) keyboard - the custom keyboard would show a 253 height instead of 162, after switching from the default keyboard.
What worked in this case was setting autocorrectionType = UITextAutocorrectionTypeNo; for the input field with the custom keyboard.
The issue only occured in iOS 8 (tested on simulator only). It doesn't occur in iOS 9 (simulator or device).
In Swift, not in one line...
self.keyboardDidShowObserver = NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: NSOperationQueue.mainQueue(), usingBlock: { (notification) in
if let userInfo = notification.userInfo, let keyboardFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRect = keyboardFrameValue.CGRectValue()
// keyboardRect.height gives the height of the keyboard
// your additional code here...
}
})
[notificationCenter addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification * _Nonnull note) {
float keyboardHeight = [[note.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
}];
I would like to receive updates from the uipageviewcontroller during the page scrolling process. I want to know the transitionProgress in %. (This value should update when the user move the finger in order to get to another page). I'm interested in the animation progress from one page to another, not the progress through the total number of pages.
What I have found so far:
There is a class called UICollectionViewTransitionLayout that have the property corresponding to what I am looking for, "transitionProgress". Probably uipageviewcontroller implement this method somehow?
I can call the following method on the uipagecontroller but I only get 0 as result!
CGFloat percentComplete = [self.pageViewController.transitionCoordinator percentComplete];
in SWIFT to copy paste ;) works perfect for me
extension UIPageViewController: UIScrollViewDelegate {
public override func viewDidLoad() {
super.viewDidLoad()
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
scrollView.delegate = self
}
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = abs(point.x - view.frame.size.width)/view.frame.size.width
print("percentComplete: ",percentComplete)
}
}
At last I found out a solution, even if it is probably not the best way to do it:
I first add an observer on the scrollview like this:
// Get Notified at update of scrollview progress
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
[sW addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
And when the observer is called:
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
CGPoint point = sW.contentOffset;
float percentComplete;
//iPhone 5
if([ [ UIScreen mainScreen ] bounds ].size.height == 568){
percentComplete = fabs(point.x - 568)/568;
} else{
//iphone 4
percentComplete = fabs(point.x - 480)/480;
}
NSLog(#"percentComplete: %f", percentComplete);
I'm very happy that I found this :-)
Since I thought that the functionality of scrolling would stay forever, but that the internal implementation may change to something other than a scroll view, I found the solution below (I haven't tested this very much, but still)
NSUInteger offset = 0;
UIViewController * firstVisibleViewController;
while([(firstVisibleViewController = [self viewControllerForPage:offset]).view superview] == nil) {
++offset;
}
CGRect rect = [[firstVisibleViewController.view superview] convertRect:firstVisibleViewController.view.frame fromView:self.view];
CGFloat absolutePosition = rect.origin.x / self.view.frame.size.width;
absolutePosition += (CGFloat)offset;
(self is the UIPageViewController here, and [-viewControllerForPage:] is a method that returns the view controller at the given page)
If absolutePosition is 0.0f, then the first view controller is shown, if it's equal to 1.0f, the second one is shown, etc... This can be called repeatedly in a CADisplayLink along with the delegate methods and/or UIPanGestureRecognizer to effectively know the status of the current progress of the UIPageViewController.
EDIT: Made it work for any number of view controllers
Use this -
for (UIView *v in self.pageViewController.view.subviews) {
if ([v isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)v).delegate = self;
}
}
to implement this protocol : -(void)scrollViewDidScroll:(UIScrollView *)scrollView
and then use #xhist's code (modified) in this way
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint point = scrollView.contentOffset;
float percentComplete;
percentComplete = fabs(point.x - self.view.frame.size.width)/self.view.frame.size.width;
NSLog(#"percentComplete: %f", percentComplete);
}
Based on Appgix solution, I'm adding this directly on my 'UIPageViewController' subclass. (Since I only need it on this one)
For Swift 3:
class MYPageViewControllerSubclass: UIPageViewController, UIScrollViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
for subView in view.subviews {
if subView is UIScrollView {
(subView as! UIScrollView).delegate = self
}
}
}
// MARK: - Scroll View Delegate
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
NSLog("percentComplete: %f", percentComplete)
}
// OTHER CODE GOES HERE...
}
While Appgix' solution seemed to work at first, I noticed that when the user pans in a UIPageViewController, lifts the finger shortly and then immediately starts dragging again while the "snap-back" animation is NOT YET finished and then lifts his finger again (which will again "snap-back"), the scrollViewDidScroll method is only called when the page view controller finished the animation.
For the progress calculation this means the second pan produces continuous values like 0.11, 0.13, 0.16 but when the scroll view snaps back the next progress value will be 1.0 which causes my other scroll view to be out of sync.
To fight this I'm now listening to the scroll view's contentOffset key, which is still updated continuously in this situation.
KVO approach for Swift 4
var myContext = 0
override func viewDidLoad() {
for view in self.view.subviews {
if view is UIScrollView {
view.addObserver(self, forKeyPath: "contentOffset", options: .new, context: &introPagingViewControllerContext)
}
}
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
guard let change = change else { return }
if context != &myContext {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == "contentOffset" {
if let contentOffset = change[NSKeyValueChangeKey.newKey] as? CGPoint {
let screenWidth = UIScreen.main.bounds.width
let percent = abs((contentOffset.x - screenWidth) / screenWidth)
print(percent)
}
}
}
I have a button in a toolbar. How can I grab its frame? Do UIBarButtonItems not have a frame property?
Try this one;
UIBarButtonItem *item = ... ;
UIView *view = [item valueForKey:#"view"];
CGFloat width;
if(view){
width=[view frame].size.width;
}
else{
width=(CGFloat)0.0 ;
}
This way works best for me:
UIView *targetView = (UIView *)[yourBarButton performSelector:#selector(view)];
CGRect rect = targetView.frame;
With Swift, if you needs to often work with bar button items, you should implement an extension like this:
extension UIBarButtonItem {
var frame: CGRect? {
guard let view = self.value(forKey: "view") as? UIView else {
return nil
}
return view.frame
}
}
Then in your code you can access easily:
if let frame = self.navigationItem.rightBarButtonItems?.first?.frame {
// do whatever with frame
}
Oof, lots of rough answers in this thread. Here's the right way to do it:
import UIKit
class ViewController: UIViewController {
let customButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
customButton.setImage(UIImage(named: "myImage"), for: .normal)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: customButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(self.customButton.convert(self.customButton.frame, to: nil))
}
}
Thanks to Anoop Vaidya for the suggested answer. An alternative could be (providing you know the position of the button in the toolbar)
UIView *view= (UIView *)[self.toolbar.subviews objectAtIndex:0]; // 0 for the first item
CGRect viewframe = view.frame;
Here's what I'm using in iOS 11 & Swift 4. It could be a little cleaner without the optional but I'm playing it safe:
extension UIBarButtonItem {
var view: UIView? {
return perform(#selector(getter: UIViewController.view)).takeRetainedValue() as? UIView
}
}
And usage:
if let barButtonFrame = myBarButtonItem.view?.frame {
// etc...
}
Edit: I don't recommend using this anymore. I ended up changing my implementation to use UIBarButtonItems with custom views, like Dan's answer
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
UIView *view = [item valueForKey:#"view"];
return [view frame];
}
You can create a UIBarButtonItem with a custom view, which is a UIButton, then you can do whatever you want. :]
in Swift 4.2 and inspired with luca
extension UIBarButtonItem {
var frame:CGRect?{
return (value(forKey: "view") as? UIView)?.frame
}
}
guard let frame = self.navigationItem.rightBarButtonItems?.first?.frame else{ return }
You can roughly calculate it by using properties like layoutMargins and frame on the navigationBar, combined with icon size guides from Human Interface Guidelines and take into count the current device orientation:
- (CGRect)rightBarButtonFrame {
CGFloat imageWidth = 28.0;
CGFloat imageHeight = UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeLeft || UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeRight ? 18.0 : 28.0;
UIEdgeInsets navigationBarLayoutMargins = self.navigationController.navigationBar.layoutMargins;
CGRect navigationBarFrame = self.navigationController.navigationBar.frame;
return CGRectMake(navigationBarFrame.size.width-(navigationBarLayoutMargins.right + imageWidth), navigationBarFrame.origin.y + navigationBarLayoutMargins.top, imageWidth, imageHeight);
}
Try this implementation:
#implementation UIBarButtonItem(Extras)
- (CGRect)frameInView:(UIView *)v {
UIView *theView = self.customView;
if (!theView.superview && [self respondsToSelector:#selector(view)]) {
theView = [self performSelector:#selector(view)];
}
UIView *parentView = theView.superview;
NSArray *subviews = parentView.subviews;
NSUInteger indexOfView = [subviews indexOfObject:theView];
NSUInteger subviewCount = subviews.count;
if (subviewCount > 0 && indexOfView != NSNotFound) {
UIView *button = [parentView.subviews objectAtIndex:indexOfView];
return [button convertRect:button.bounds toView:v];
} else {
return CGRectZero;
}
}
#end
You should do a loop over the subviews and check their type or their contents for identifying.
It is not safe to access view by kvo and you cannot be sure about the index.
Check out this answer: How to apply borders and corner radius to UIBarButtonItem? which explains how to loop over subviews to find the frame of a button.
I used a view on the bar button item with a tag on the view:
for view in bottomToolbar.subviews {
if let stackView = view.subviews.filter({$0 is UIStackView}).first {
//target view has tag = 88
if let targetView = stackView.subviews.filter({$0.viewWithTag(88) != nil}).first {
//do something with target view
}
}
}
Swift 4 up The current best way to do it is to access its frame from :
self.navigationItem.rightBarButtonItems by
let customView = navigationItem.rightBarButtonItems?.first?.customView // access the first added customView
Accessing this way is safer than accessing private api.
check out the answer in this :
After Add a CustomView to navigationItem, CustomView always return nil