So I basically have a form, consisting of several text fields. The user types into the fields as usual. But the user also has the option of double-tapping a text field, which presents a modal view controller, allowing the user to choose from a number of options relating to that field.
Can I somehow present the modal "over" the keyboard, such that when it is dismissed, the keyboard is still active for the field that had been first responder before I presented the modal?
Right now, the keyboard dismisses while the modal appears, and reappears as the modal is dismissed. It looks clunky to me, and distracting. Would love to streamline it, and reduce the amount of animation onscreen.
Edit: I've updated this answer for iOS 12 and Swift. The revised example project (containing new Swift and updated Objective-C implementations) is here.
You can create a new UIWindow and place that over the default window while hiding the keyboard's window.
I have an example project on Github here, but the basic process is below.
Create a new UIViewController class for your modal view. I called mine OverlayViewController. Set up the corresponding view as you wish. Per your question you need to pass back some options, so I made a delegate protocol OverlayViewController and will make the primary window's root view controller (class ViewController) our delegate.
protocol OverlayViewControllerDelegate: class {
func optionChosen(option: YourOptionsEnum)
}
Add some supporting properties to our original view controller.
class ViewController: UIViewController {
/// The text field that responds to a double-tap.
#IBOutlet private weak var firstField: UITextField!
/// A simple label that shows we received a message back from the overlay.
#IBOutlet private weak var label: UILabel!
/// The window that will appear over our existing one.
private var overlayWindow: UIWindow?
Add a UITapGestureRecognizer to your UITextField.
override func viewDidLoad() {
super.viewDidLoad()
// Set up gesture recognizer
let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
doubleTapRecognizer.numberOfTapsRequired = 2
doubleTapRecognizer.delegate = self
firstField.addGestureRecognizer(doubleTapRecognizer)
firstField.becomeFirstResponder()
}
UITextField has a built-in gesture recognizer, so we need to allow multiple UIGestureRecognizers to operate simultaneously.
extension ViewController: UIGestureRecognizerDelegate {
// Our gesture recognizer clashes with UITextField's.
// Need to allow both to work simultaneously.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
This is the interesting part. When the gesture recognizer is triggered, create the new UIWindow, assign your OverlayViewController as the root view controller, and show it. Note that we set the window level to UIWindowLevelAlert so it will appear in front. However, the keyboard will still be in front despite the alert window level, so we have to manually hide its window, too.
It is important to not set the new UIWindow as key or to change the first responder from the UITextField or the keyboard will be dismissed.
Previously (before iOS 10?) we could get away with overlayWindow.makeKeyAndVisible(), but now setting it as key will dismiss the keyboard. Also, the keyboard's window now has a non-standard UIWindow.Level value that is in front of every publicly defined value. I've worked around that by finding the keyboard's window in the hierarchy and hiding it instead.
#objc func handleDoubleTap() {
// Prepare the overlay window
guard let overlayFrame = view?.window?.frame else { return }
overlayWindow = UIWindow(frame: overlayFrame)
overlayWindow?.windowLevel = .alert
let overlayVC = OverlayViewController.init(nibName: "OverlayViewController", bundle: nil)
overlayWindow?.rootViewController = overlayVC
overlayVC.delegate = self
// The keyboard's window always appears to be the last in the hierarchy.
let keyboardWindow = UIApplication.shared.windows.last
keyboardWindow?.isHidden = true
}
The overlay window is now the original window. The user can now select whatever options you built into the overlay view. After your user selects an option, your delegate should take whatever action you intend and then dismiss the overlay window and show the keyboard again.
func optionChosen(option: YourOptionsEnum) {
// Your code goes here. Take action based on the option chosen.
// ...
// Dismiss the overlay and show the keyboard
overlayWindow = nil;
UIApplication.shared.windows.last?.isHidden = false
}
The overlay window should disappear, and your original window should appear with the keyboard in the same position as before.
I can't try this right now, but have implemented similar for other purposes. In the action for presenting the modal controller, I assume gesture recognizer or delegate method, first take a screenshot and place it in an imageView over the current subviews. Later when returning, simply remove the imageView.
Might sound crazy but I remember having done this for a transition where the keyboard moving during the transition caused similar clunky behavior. It was not difficult to implement at all.
If you have trouble trying it, perhaps someone will provide some code. I can reference my own work later and add an example, but not now.
#Rob Bajorek's answer is excellent.
For iOS 9,10 there are small changes.
Instead of the code:
[self.overlayWindow setWindowLevel:UIWindowLevelAlert];
[self.overlayWindow makeKeyAndVisible];
Put the following code:
NSArray *windows = [[UIApplication sharedApplication] windows];
UIWindow *lastWindow = (UIWindow *)[windows lastObject];
[self.overlayWindow setWindowLevel:lastWindow.windowLevel + 1];
[self.overlayWindow setHidden:NO];
In order to keyboard to visible any of text accepting fields such UITextField or UITextView or UISearchBar should be the first responder and they should be visible in the view. Meaning responding view should be in the top level hierarchy in the window.
If you don't need this effect, Instead of presenting a ViewController you can add ViewController.view as a subview of your self.view with animation.
You have access to the frame of the keyboard in iOS.
You need to implement code to listen to the keyboard notifications (like UIKeyboardWillShowNotification and UIKeyboardWillChangeFrameNotification). The notification will send you informations about the frame of the keyboard.
Giva a look to the description of the "Keyboard Notification User Info Keys" in the windows reference.
You'll find useful for you purpose:
UIKeyboardBoundsUserInfoKey The key for an NSValue object containing a CGRect that identifies the bounds rectangle of the
keyboard in window coordinates. This value is sufficient for obtaining
the size of the keyboard. If you want to get the origin of the
keyboard on the screen (before or after animation) use the values
obtained from the user info dictionary through the
UIKeyboardCenterBeginUserInfoKey or UIKeyboardCenterEndUserInfoKey
constants.
With the information of the keyboard frame you can show there you modal view.
Just add an tap gesture in your textfield and a UITextfield *flagTextfield;
UITapGestureRecognizer* doubleTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(DoubleTapMethod:)];
doubleTap.numberOfTapsRequired = 2;
[self.txtTest addGestureRecognizer:doubleTap];
-(void)DoubleTapMethod:(UITapGestureRecognizer *)gesture
{
[flagTextfield resignFirstResponder];
NSLog(#"DoubleTap detected");
//Set your logic on double tap of Textfield...
//presents a modal view controller
}
- (void)textFieldDidBeginEditing:(UITextField *)textField{
flagTextfield = textfield;
}
Related
I have a modal view controller A presented at the top of the view controllers hierarchy.
Is it possible to make it fullscreen temporarily when a user presses a button? And then go back to standard modal look when used presses another button?
To clarify, I'm not talking about modalPresentationStyle = .fullScreen BEFORE A is presented, I need to present A in a standard way and then stretch it to be fullscreen when needed.
Although if I write pseudo code it would be something like this:
class A: UIViewController {
#objc func goFullScreen(sender: UIButton) {
// pseudo code:
// modalPresentationStyle = .fullScreen
// go fullscreen, block `drag-to-dismiss` gesture, remove `cornerRadius`, set `self.view.frame = UIScreen.main.bounds`
}
#objc func cancelFullScreen(sender: UIButton) {
// pseudo code:
// modalPresentationStyle = .pageSheet
// return to the standard look of a modal view controller
}
}
I found UIAdaptivePresentationControllerDelegate.presentationControllerShouldDismiss method that intercepts drag to close gesture, but I don't see any way to go fullscreen.
Picture of what I am talking about:
ps. I know I can make a custom container view controller with a custom transition delegate but I'd like to know if it's possible to implement such behaviour with system classes because I basically need just to flip modalPresentationStyle property with some interpolation animation...
Also on the right I hide/show some UI elements, but still it's the same view controller
I want to replace the default keyboard of a UITextField with a custom keyboard. So I created a new subclass of a UIViewController with a xib-file (the other way like creating both files seperately and setting the File's Owner doesn't work either).
Then I added a button to the KeyboardView and connected it to an IBAction. After that I set the textfields inputView to the new view like this:
override func viewDidLoad() {
let keyboardVC = KeyboardViewController()
textField.inputView = keyboardVC.view
keyboardVC.view.autoresizingMask = .FlexibleHeight
keyboardVC.delegate = textField
}
It's working and the custom keyboard shows up, but if I touch the button, the IBAction is not called. What's the problem in my setup? (I checked some examples and they all do it the same way).
UPDATE:
I now removed the ViewController and subclassed the UIView. Now the actions are working. Why isn't it working with a ViewController?
Since no one holds the UIViewController- there is no reference to it after the viewDidLoad() ended, it is released from the memory.
When the button is pressed, the view controller that should response to the action is not exist -> you are holding only the view of the view controller as the textField.inputView.
This issue is stemming from my previous question dismiss keyboard on tapping on any UIControl object.
So when user has a keyboard present, I want the user to be able to tap anywhere outside of keyboard or UITextView to close keyboard. This works...But I also want that "first" tap that closes the keyboard to not cause any other actions.
i.e. UIKeyboard is present...user taps anywhere to close and happens to tap on a UIButton. UIKeyboard is closed and UIButton's action is NOT sent.
I tried to place
#IBAction func blahblah(sender: UIButton){
if keyboardShow == false {//UIButtons action here
}
}
But with some println() tests, it's showing that the keyboard is closed(thus keyboardShow is set to false) before UIButton registers the tap.
But I also want that "first" tap that closes the keyboard to not cause any other actions.
You can do it the way you are doing it, but that's very inflexible because you have to deal with all those other controls separately and individually.
The simplest way to do this is simply to put an invisible view on top of your entire "screen" (i.e., add it to your view controller's view, in front of all views) — except that it should be behind the text view.
text view
secret invisible view
all other views (buttons etc.)
Normally, your secret invisible view's userInteractionEnabled is false, so taps just fall through to whatever is behind it, as if the invisible view were not even there. The user can tap buttons and so on.
But when you show the keyboard, you also set your secret invisible view's userInteractionEnabled to true and give it a tap gesture recognizer. Thus, when the user taps outside the text view, the tap gesture recognizer responds, and you dismiss the keyboard.
When you dismiss the keyboard, you reverse all of that: remove the tap gesture recognizer and turn the invisible view's userInteractionEnabled to false.
I use this kind of trick all the time in my apps.
It is also possible to be more sophisticated: put your interfering invisible view in front of everything and override its touch handling (hitTest:). Here's an example of a view that blocks all touches except touches that would land on one particular passthruView located behind it:
class BlockerView: UIView {
weak var passthruView : UIView!
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if let pv = self.passthruView {
let pt = pv.convertPoint(point, fromView: self)
if pv.pointInside(pt, withEvent: event) {
return nil // let the touch fall thru
}
}
return super.hitTest(point, withEvent: event)
}
}
I have a navigation controller that pushes a view-controller (PARENT) that contains an UIPageViewController (PAGES). Now I used pan/swipe gestures to switch between the children of the page-view-controller. However, I can no longer pop the PARENT-view controller using the swipe gesture from the left border of the screen, because it is interpreted as a gesture in PAGES.
Is it possible to accomplish that swipe-to-pop when the left-most view-controller is shown?
Two ideas:
Return nil in pageViewController:viewControllerBeforeViewController -> doesn't work.
Restrict the touch area, as described here.
Or is there a more straightforward way?
I had the same situation as #smallwisdom, but handled it differently.
I have a view controller A that I push on top of my navigation controller's stack.
This view controller A contains a horizontal scroll view that stretches all the way from left side of the screen to the right.
In this scenario, when I wanted to swipe the screen to pop view controller A from navigation controller's stack, all I ended up doing was scrolling this horizontal scroll view.
The solution is pretty simple.
Inside my view controller A, I have code like this:
_contentScrollView = [[UIScrollView alloc] init];
[self.view addSubview:_contentScrollView];
for (UIGestureRecognizer *gestureRecognizer in _contentScrollView.gestureRecognizers) {
[gestureRecognizer requireGestureRecognizerToFail:self.navigationController.interactivePopGestureRecognizer];
}
It works great. What this does?
It is telling the scrollView's gesture recognizers that they have to wait to see if some other gesture recognizer will recognize the current gesture.
If that other fails to recognize, then they will no longer have to wait and they can try to recognize the current gesture.
If that other recognizer succeeds and recognizes the current gesture, then all of the gesture recognizers that have been waiting will automatically fail.
This other gesture recognizer they have to wait is set to be the navigation controller's interactivePopGestureRecognizer. He is in charge for the swipe-to-go-back gestures.
I mostly agree with #ancajic's answer. I would like to provide an extra-case when you set UIPageViewController's transitionStyle to 'Scroll', in which you'll not get gestureRecognizers to be set, the workaround is:
if (self.navigationController?.interactivePopGestureRecognizer != nil)
{
for view in self.pageViewController!.view.subviews
{
if let scrollView = view as? UIScrollView
{
scrollView.panGestureRecognizer.requireGestureRecognizerToFail(self.navigationController!.interactivePopGestureRecognizer!);
}
}
}
I had a similar issue in one of my projects and used the following method. In my case, it was one of those left-side menus that were really popular before iOS 7.
My solution was to set the UINavigationControllerDelegate and then implemented the following:
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated {
// enable the interactive menu gesture only if at root, otherwise enable the pop gesture
BOOL isRoot = (navigationController.viewControllers.firstObject == viewController);
self.panGestureRecognizer.enabled = isRoot;
navigationController.interactivePopGestureRecognizer.enabled = !self.panGestureRecognizer.enabled;
}
EDIT:
Additionally, you need a hook into the UIPageViewController's gesture recognizers. (They aren't returned by the gestureRecognizers property for a scroll view style page view controller.) It's annoying, but the only way I've found to access this is to iterate through the scrollview's gesture recognizers and look for the pan gesture. Then set a pointer to it and enable/disable based on whether or not you are currently displaying the left-most view controller.
If you want to keep the right swipe enabled, then replace the pan gesture with a subclassed pan gesture recognizer of your own that can conditionally recognize based on the direction of the pan gesture.
First, find UIPageViewController's scrollView
extension UIPageViewController {
var bk_scrollView: UIScrollView? {
if let v = view as? UIScrollView {
return v
}
// view.subviews 只有一个元素,其类型是 _UIQueuingScrollView,是 UIScrollView 的子类
// view.subviews have only one item of which the type is _UIQueuingScrollView, which is kind of UIScrollView's subclass.
for v in view.subviews where v is UIScrollView {
return v as? UIScrollView
}
return nil
}
}
Second, set gestureRecognizer's dependence.
override func viewDidLoad() {
super.viewDidLoad()
if let ges = navigationController?.interactivePopGestureRecognizer {
pageViewController.bk_scrollView?.panGestureRecognizer.require(toFail: ges)
}
}
Swift version of #Lcsky's answer:
if let interactivePopGesture = self.navigationController?.interactivePopGestureRecognizer, let pageViewController = self.swipeVC?.pageViewController {
let subView = pageViewController.view.subviews
for view in subView {
if let scrollView = view as? UIScrollView{
scrollView.panGestureRecognizer.require(toFail: interactivePopGesture)
}
}
}
We created a custom numeric keypad for iPad. On our testers iPad this keyboard was suddenly moved out of place and it took us quite a while to figure out that it is possible to move that custom keyboard by start dragging at the location where the "Keyboard Button" would normally be located.
The customers will have very hard times to move it back in case they accidentally moved it. As it makes no sense to move the keyboard on that specific input screen I would rather prefer to prevent the keyboard from moving instead of painting some kind of handle that makes visible to the users that the keyboard can be moved. (This is a special input screen for just editing one single numeric value. The keyboard is like part of the layout and is always visible on this screen.)
I tried hard but could not find a way to prevent the keyboard from moving when dragged on this specific place. Even all my dirty ideas like removing possibly preexisting GestureRecognizers (there were none) or placing my own button in front did not help.
Edit:
The keyboard is even movable in the simplest possible custom keyboard app written in Monotouch I can think of. Did I missed something?
using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
namespace KeyboardTest
{
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
private UIWindow window;
private UIViewController viewController;
private UIViewController keyboardViewController;
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
window = new UIWindow (UIScreen.MainScreen.Bounds);
// Create a red dummy keyboard
keyboardViewController = new UIViewController();
keyboardViewController.View.BackgroundColor = UIColor.Red;
// Create a textfield and assign our beautiful red keyboard as its InputView
UITextField textField = new UITextField();
textField.BorderStyle = UITextBorderStyle.RoundedRect;
textField.Frame = new System.Drawing.RectangleF(44, 44, 200, 44);
textField.InputView = keyboardViewController.View;
// create a rootview controller and add our textfield
viewController = new UIViewController();
viewController.View.AddSubview(textField);
window.RootViewController = viewController;
window.MakeKeyAndVisible ();
return true;
}
}
}
For what you explain, I´m guessing that you have your keyboard as a subview of your main view. Instead, I would set it as a inputView of the UItextFields and then make the first of your textFields be the firs responder on viewDidLoad. Something like:
-(void)viewDidLoad{
[super viewDidLoad];
CustomkeyPad *keypad=[[CustomKeyPad alloc]init]; // initWithFrame would probably be better
self.textField.delegate=self;
self.textField.inputView=keypad;
[self.textField becomeFirstResponder];
}
I´m not in my Mac so I made probably some mistake here, but that´s the idea and That´s how I did it when I did my custom keypad as well and it´s universal (iPad and iPhone) for both landscape and portrait modes and so far it has given me no problems.