Stop Keyboard From Hiding UITextField When Open - ios

Looking for a solution that stops a UITextField from being hidden by the keyboard when it opens regardless of where the UITextField is in the view hierarchy and regardless of other UI elements.

There are plenty of solutions online for adjusting your view so that a UITextField isn't hidden behind the keyboard when it appears however, I couldn't find a "one size fits all" kind of solution so had to adapt a lot of answers and make my own. This is the solution I came up with. Hopefully it helps someone :)
First of all, this solution assumes that your "primary parent" or "top level" view is a UIScrollView or descendant (e.g. UITableView or UICollectionView). This will not work if you don't have a UIScrollView in your hierarchy as this is what is used to scroll the UITextField into position. Other solutions show you how to scroll any UIView but when you think about it, if you need to scroll your UITextField into position, chances are your view should be scrollable anyway to stop it from going off the screen.
Create the extension method below. This searches for subviews of subviews (etc.) that match the given query and returns them as an array. We'll use this later.
public static UIView[] Find(this UIView view, Func<UIView, bool> query)
{
if (view == null)
return null;
var views = new List<UIView>();
if (query.Invoke(view))
views.Add(view);
foreach (var subview in view.Subviews)
{
var foundViews = subview.Find(query);
if (foundViews != null)
views.AddRange(foundViews);
}
return views.ToArray();
}
Add the below method to your UIViewController. This finds the top level UIScrollView from the view hierarchy. There should only be one result.
private UIScrollView FindAdjustmentScrollView()
{
var scrollViews = View.Find(v => v is UIScrollView && v.FindSuperviewOfType(View, typeof(UIScrollView)) == null);
return scrollViews.Length > 0 ? scrollViews[0] as UIScrollView : null;
}
Add the below event handlers. We'll register these with observers next.
private void Keyboard_Appear(NSNotification notification)
{
var firstResponder = View.Find(v => v.IsFirstResponder).FirstOrDefault();
var scrollView = FindAdjustmentScrollView();
if (firstResponder == null || scrollView == null || !(notification.UserInfo[UIKeyboard.FrameEndUserInfoKey] is NSValue value))
return;
var keyboardBounds = value.CGRectValue;
var firstResponderAbsoluteFrame = firstResponder.Superview.ConvertRectToView(firstResponder.Frame, View);
// This is how much of a gap you would like there to be between the bottom of the UITextField
// and the top of the keyboard. Not mandatory but a nice touch in my experience.
var offset = 8;
var bottom = firstResponderAbsoluteFrame.Y + firstResponderAbsoluteFrame.Height + offset;
var scrollAmount = keyboardBounds.Height - (scrollView.Frame.Size.Height - bottom);
if (scrollAmount > 0)
scrollView.SetContentOffset(0, scrollView.ContentOffset.Y + scrollAmount);
}
private void Keyboard_Disappear(NSNotification notification)
{
var firstResponder = View.Find(v => v.IsFirstResponder).FirstOrDefault();
var scrollView = FindAdjustmentScrollView();
if (firstResponder == null || scrollView == null)
return;
scrollView.SetContentOffset(0, 0);
}
Add these 2 fields to your UIViewController. By keeping an object reference to the observers we can unregister them later when the UIViewController is no longer visible.
private NSObject _keyboardWillShowObserver, _keyboardWillHideObserver;
In ViewWillAppear (or wherever you register your event handlers) add these 2 lines. These register the observers and keep an object reference so we can unregister later.
_keyboardWillShowObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, Keyboard_Appear);
_keyboardWillHideObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, Keyboard_Disappear);
In ViewWillDisappear (or wherever you unregister your event handlers) add these 2 lines. These unregister the observers from the UIViewController so they no longer respond to events.
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardWillShowObserver);
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardWillHideObserver);
To clarify a few things:
This solution works when your UITextField is a subview many layers down (even
a UITableViewCell or UICollectionViewCell).
This solution does take into account any modifications made to the keyboard view. For example: if you add a "done" button to the top of your keyboard this will be calculated.
The scroll adjustment is animated.
This solution does work in different orientations.

Related

Animating UIStackView subviews causes layout issues

I have a UIStackView with several arranged views inside, which can be shown and hidden using buttons.
I animate the changes by:
- initially setting the alpha to 0 and isHidden = true of some subviews
- in an animate block, switch which subview has alpha = 1 and isHidden = false
I created a playground to show the issue: https://gist.github.com/krummler/d0e8db8cb037ae7202f7d801d3114111
In short, this works fine for two views: switching between ANY two subviews works fine. When pressing a third, the view collapses and refuses to come back. After that, showing subviews becomes a mess. Interestly, it does not show this behaviour when animations are commented out.
My questions:
- Am I missing something or did I hit some bug in UIKit?
- How can I work around this or is there a better wat to achieve what I am trying to do?
It is hard to say how UIStackView is implemented, but it may be attempting to update its layout when isHidden is modified even though the value is not actually changing.
Perhaps this is a UIKit bug, but as a workaround you can modify your resetSubviews(to:) implementation so that it only sets isHidden when the state is actually changing.
private func resetSubviews(to view: UIView) {
view1.alpha = view == view1 ? 1 : 0
view2.alpha = view == view2 ? 1 : 0
view3.alpha = view == view3 ? 1 : 0
view4.alpha = view == view4 ? 1 : 0
let updateIsHiddenForView = { (viewToUpdate: UIView) in
let isHidden = view != viewToUpdate
if isHidden != viewToUpdate.isHidden {
viewToUpdate.isHidden = isHidden
}
}
updateIsHiddenForView(view1)
updateIsHiddenForView(view2)
updateIsHiddenForView(view3)
updateIsHiddenForView(view4)
}

Adding and removing UIStackViews messing up my UIScrollView

Whenever I use the pickerview to switch views from Auto Rent to Schedule Rent it works perfectly. It is when I switch from Schedule Rent to Auto Rent that this black bar appears. I have attached the hierarchy of my content view. I thought it had to do with previous constraints added, so I remove a StackView whenever one view is chosen. For example, if Auto Rent is chosen, then I remove the StackView where the Schedule View is in:
//Holds Temp Stackviews
var stackViewHolder1: UIView?
var stackViewHolder2: UIView?
override func viewDidLoad() {
super.viewDidLoad()
stackViewHolder1 = stackViewMain.arrangedSubviews[0]
stackViewHolder2 = stackViewMain.arrangedSubviews[1]
}
if txtRentType.text == "Auto Rent" {
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView!.isHidden = true
stackViewMain.removeArrangedSubview(tempView!)
if(tempView1!.isHidden == true){
tempView1!.isHidden = false
stackViewMain.addArrangedSubview(tempView1!)
}
else{
let tempView = stackViewHolder1
let tempView1 = stackViewHolder2
tempView1!.isHidden = true
stackViewMain.removeArrangedSubview(tempView1!)
if(tempView!.isHidden == true){
tempView!.isHidden = false
stackViewMain.addArrangedSubview(tempView!)
}
}
I have tried deleting one view and toggling only one view has being hidden and that removes the black bar issue. There is no constraint with the stackViews and Content View.
EDIT:
The screen highlighted is the scrollView. The one after is the contentView. UIWindow goes black in the back.
My Title Bar at the top ends up in the middle somehow.
You can try to modify your stack distribution property
stack.distribution = .equalCentering
After you won't need to use this:
.removeArrangedSubview()
.addArrangedSubview()
When you hide some view, the other view take all space of your stack, you don't need to update your constraints. You can try it on interface builder to see how it works.
are you pinning your scrollview and the content view to the bottom with constraints?
If the content view is a stack view you can pin it to the bottom as well with layout constraints and play with the content distribution.
You don't need to use remove/Add arranged subviews.
when hiding a view in a stackView its automatically removed.
so i think you can just hide or show the stackViewMain.subviews[0] o stackViewMain.subviews[1]
i'm with objc maybe i do a mistake but it would be something like this :
if txtRentType.text == "Auto Rent" {
stackViewMain.arrangedSubviews[0].isHidden = true;
stackViewMain.arrangedSubviews[1].isHidden = false;
}else{
stackViewMain.arrangedSubviews[1].isHidden = true;
stackViewMain.arrangedSubviews[0].isHidden = false;
}

UIButton height not being set correctly on UITableView

I have an UITableView which holds a list of items. For each UITableViewcell in that table, depending on a property from the associated object to that cell, I toggle the visibility of an UIButton by changing the height constraint constant to 0, or to a value defined to make it visible. I've checked the Clip to Bounds option on the Xcode designer for that button.
If I feed the table view a list of items that set some of the buttons visible, and others hidden and scroll, the cells that had the button visible may have it hidden, and vice-versa. This is more noticeable when there's few cells with the button, and the rest without it.
The method that contains the logic to show or hide the UIButton is from within the UITableViewCell custom class for the cells, as it follows:
public partial class UITableViewCellCustom : UITableViewCell
{
public Object obj;
public void SetObject(Object obj)
{
// Do something with obj...
// Do something with the obj that determines if the buttons should be collapsed or not
Boolean collapseButton = ...;
ToggleButtonVisibility(collapseButton);
}
private void ToggleButtonVisibility(Boolean collapse)
{
NSLayoutConstraint uiButtonCancelHeightConstraint = UIButtonCancel.Constraints
.FirstOrDefault(query => query.FirstItem == UIButtonCancel
&& query.FirstAttribute == NSLayoutAttribute.Height);
NSLayoutConstraint uiButtonCancelTopConstraint = this.ContentView.Constraints
.FirstOrDefault(query => query.FirstItem == UIButtonCancel
&& query.FirstAttribute == NSLayoutAttribute.Top);
if (collapse)
{
uiButtonCancelHeightConstraint.Constant = 0;
uiButtonCancelTopConstraint.Constant = 0;
}
else
{
uiButtonCancelHeightConstraint.Constant = 30;
uiButtonCancelTopConstraint.Constant = 10;
}
}
}
The SetObject method is called from the UITableViewSource class that gets the object from the correct index and sets it to the cell ( No problem here ). Then, while some UILabels texts are changed with the values from the object, I check if the button is required or not ( No problem here ). When I call the ToggleButtonVisibility method, and attempt to change the two constraints -- height and top -- the values are applied, the top constraint is visibly changed, but the height constraint seems to be ignored when the cell is reused.
I've tried to force the ClipToBounds to true, force the method in the main thread, but none of them worked. What am I missing here?
Forgot to mention: When the button is pressed, the table view is cleared ( I feed the source an empty list, and reload the data ), an long task is performed, and then a new list is applied to the table, but the cell in question remains with the button bugged.
Notes:
Hiding the button by changing the Alpha to 0 or by set the Hidden to true is not an option, since it will leave an hole within the tableview.
Fixed this problem by wrapping the UIButton on a UIView and then resizing the view depending on my needs.

ScrollView - Gesture Recognizer - Swipe vertically

I have a UIScrollView that works when swiping left or right, however I've reduced the size of the scrollView so, now display area doesn't fully occupy the superview's frame, and swiping works only within the frame of the scroll view.
I would like to be able to scroll vertically even when swiping up and down outside the horizontal bounds of the narrowed scroll view.
It was recommended that I use a gesture recognizer, but that's beyond my current familiarity with iOS and could use more specific advice or a bit more guidance to get started with that.
There is a simpler approach then use a Gesture Recognizer =]
You can setup the superview of the scroll view (which is BIGGER...) to pass the touches to the scroll view. It's working M-A-G-I-C-A-L-Y =]
First, select the view that will pass all it's touches to the scroll view. if your parent view is already ok with that you may use it. otherwise you should consider add a new view in the size that you want that will catch touches.
Now create a new class (I'll use swift for the example)
class TestView: UIView {
#IBOutlet weak var Scroller: UIScrollView!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if (view == self) {
return Scroller
}
return view
}
}
Nice! now as you can see we added an outlet of the scroller. so use interface builder, select the new view and set it's class to "TestView" in the identity inspector (Or to the name that you'll use for your custom class).
After you set the class and your view is still selected go to connections inspector and connect "Scroller" to your scroll view on the storyboard. All connected properly =]
That's it!! no gesture recognizer needed!!
The new view will pass all it's touches to the scroll view and it'll behave just like you pan in it =]
In my answer I used that answer
EDIT: I improved the code now, it wasn't working as expected before, now it catches only when in needs and not every touch in the app as before
Search for a component called SwipeGestureRecognizer :
Grab it and drop it on top of the View (use the hierarchy to make sure
you drop it on it, if you drop it on another element this code will not work):
Select one of the SwipeGestureRecognizer in the hierarchy and go to its attribute page. Change Swipe to Right.
Make sure the other recogniser has the Swipe attribute to Left
Select UIScrollView and uncheck Scrolling enabled
Connect detectSwipe() (see source code below) to both recognizers.
--
#IBAction func detectSwipe (_ sender: UISwipeGestureRecognizer) {
if (currentPage < MAX_PAGE && sender.direction == UISwipeGestureRecognizerDirection.left) {
moveScrollView(direction: 1)
}
if (currentPage > MIN_PAGE && sender.direction == UISwipeGestureRecognizerDirection.right) {
moveScrollView(direction: -1)
}
}
func moveScrollView(direction: Int) {
currentPage = currentPage + direction
let point: CGPoint = CGPoint(x: scrollView.frame.size.width * CGFloat(currentPage), y: 0.0)
scrollView.setContentOffset(point, animated: true)
// Create a animation to increase the actual icon on screen
UIView.animate(withDuration: 0.4) {
self.images[self.currentPage].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.4)
for x in 0 ..< self.images.count {
if (x != self.currentPage) {
self.images[x].transform = CGAffineTransform.identity
}
}
}
}
Refer to https://github.com/alxsnchez/scrollViewSwipeGestureRecognizer for more
I don't have time for detailed answer but:
In storyboard drag a pan gesture recognizer on the scroll view's superview... Connect it's action with your view controller and in this action change the scroll view position by using the properties from the gesture recognizer that you got as parameter
Tip: when connecting the action change parameter type from Any to UIPanGestureRecognizer in the combo box
please don't see this answer as recommendation to use this approach in your problem, I don't know if that's the best way, I'm just helping you to try it

Add a UIView to a ScrollView for paging

How can I have a UIScollView content be the content from a UIView? I want to design the app layout in a UIView and then lay that view into a UIScrollView that is connected to a UIPageControl for pagination. So when the user swipes to the side, the next view is displayed. I have a sort of idea on how I would accomplish this, but I want to get it right without wasting a lot of time.
Heres my DetailViewController:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using UIKit;
using Foundation;
using CoreGraphics;
using CloudKit;
namespace RecordStorePro
{
public partial class DetailViewController : UIViewController
{
public Record DetailRecord { get; set; }
public DetailViewController (IntPtr handle) : base (handle)
{
}
public void SetDetailRecord (Record record)
{
if (DetailRecord != record) {
DetailRecord = record;
// Update the view
ConfigureView ();
}
}
void ConfigureView ()
{
// Update the user interface for the detail item
if (IsViewLoaded && DetailRecord != null) {
//label.Text = DetailRecord.Album;
}
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Perform any additional setup after loading the view, typically from a nib.
NavigationItem.SetLeftBarButtonItem (new UIBarButtonItem(UIBarButtonSystemItem.Stop, (sender, args) => {
NavigationController.PopViewController(true);
}), true);
ConfigureView ();
//this.scrollview.BackgroundColor = UIColor.Gray;
// set pages and content size
scrollview.ContentSize = new SizeF ((float)(scrollview.Frame.Width * 2), (float)(scrollview.Frame.Height));
//this.scrollview.AddSubview ();
this.scrollview.Scrolled += ScrollEvent;
}
private void ScrollEvent(object sender, EventArgs e)
{
this.pagecontrol.CurrentPage = (int)System.Math.Floor(scrollview.ContentOffset.X / this.scrollview.Frame.Size.Width);
}
public override void DidReceiveMemoryWarning ()
{
base.DidReceiveMemoryWarning ();
// Release any cached data, images, etc that aren't in use.
}
}
}
So when I swipe the screen, I want a subview that contains some labels and textfields to come in and replace the original labels and textfields. It works properly so far except I can't figure out how to add the subview, and make it size appropriately to different screen sizes.
EDIT
Heres the problem Im facing now, the views laying in the ScrollView act funny and are about 44f too tall so they let me drag up and down. i tried setting all kinds of constraints as well as manually setting them to -44 smaller with no help. heres a picture of the problem now:
Heres the screenshot of my constraints set.
View A:
View B:
ScrollView:
Nib view:
To do this, you might want to try these steps:
The view that shows the first page will be called View A. The view that shows the second page will be called View B.
Add both views to the Scroll View.
Control-drag from View A to the Scroll View in the sidebar.
Hold down Shift and select Top Space to Superview, Bottom Space to Superview, and Leading Space to Superview. Next, press Return to add those constraints.
Make sure those constraints’ constants are set to 0.
Control-drag from View B to the Scroll View in the sidebar.
Hold down Shift and select Top Space to Superview, Bottom Space to Superview, and Trailing Space to Superview. Next, press Return to add those constraints.
Control-drag from View A to View B in the sidebar.
Select Horizontal Spacing. Make sure its constant is 0 and its Second Item is View A.Trailing and its First Item is View B.Leading.
Control-drag from View A to View B in the sidebar. Select Equal Widths. Make sure the Constant is 0.
Control-drag from View A to the Scroll View in the sidebar. Select Equal Widths. Make sure the constant is set to 0.
In the inspector, check “Paging Enabled.”
Adding subScrollView with no pagingEnabled to each page and controls into subScrollView works! If you add controls directly to scrollview with paging enabled, it seems gesture recogniser's first responder is always scrollview so your controls never gets the event and behave like a disabled!
UIScrollView has a property called “pagingEnabled”
In Interface Builder, resize the scroll view to allow for some space below it for the page control. Next, drag in a UIPageControl from the library, centered below the scroll view. Resize the UIPageControl to take up the full width of the view.

Resources