This question already has answers here:
How to display UIView over keyboard in iOS
(7 answers)
Closed 5 years ago.
I need to present a help screen that overlays an open keyboard - the help screen should dim the whole view underneath and keep just a small hole with full transparency to "highlight" that piece. The point is to provide some information about several view components while highlighting them. Without a keyboard, I could just put a view at top of the hierarchy, but in this case the UI uses a keyboard with a custom input accessory that needs to be visible.
I tried to insert a new UIWindow and put it above all the UIWindows:
class CustomTextField: UITextField {
override var canResignFirstResponder: Bool {
return false
}
}
class ViewController: UIViewController {
var textField: UITextField = CustomTextField()
override func viewDidAppear(_ animated: Bool) {
view.backgroundColor = .white
super.viewDidAppear(animated)
textField.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
view.addSubview(textField)
textField.backgroundColor = UIColor.gray
textField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) {
self.window.windowLevel = 100000002.0 // based on experiments with UIApplication.shared.windows this should be the top most window
let controller = UIViewController()
controller.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.window.rootViewController = controller
self.window.makeKeyAndVisible()
}
}
let window = UIWindow(frame: UIScreen.main.bounds)
}
But there are two problems with this approach:
The keyboard gets hidden as soon as the window becomes key and visible.
Even when using windowLevel = 100000002.0 it seems that the keyboard is above the window (the keyboard gets animated, so while hiding, I can see that its above my window).
Any ideas how to deal with these two problems? Is it even possible?
OK, as pointed out by #Krunal, this is kind of a duplicate of this question. The trick there is to add the overlay view to the window in which keyboard is (which happens to be the UIApplication.shared.windows.last):
class ViewController: UIViewController {
var textField: UITextField = UITextField()
override func viewDidAppear(_ animated: Bool) {
view.backgroundColor = .white
super.viewDidAppear(animated)
textField.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
view.addSubview(textField)
textField.backgroundColor = UIColor.gray
textField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) {
// this does the trick
let customView = UIView(frame: self.view.bounds)
customView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
customView.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
UIApplication.shared.windows.last?.addSubview(customView)
}
}
}
Related
I recently added some keyboard shortcuts to the app that I work on after reading this article: https://www.swiftbysundell.com/tips/handling-keyup-and-keydown-events/
One of the shortcuts was using the spacebar to toggle play/pause of the currently loaded media, and it worked as expected. You could hit the spacebar on an external keyboard from anywhere in the app and it would play/pause. However I found that when I had an active UITextField or UITextView, the user couldn't type spaces from an external keyboard, and if they tried it would still toggle play/pause. My understanding of the responder chain is that the when a UITextField is the first responder, it should receive those events first. But it doesn't in this case.
Example Code
I verified this behavior by creating a blank app and adding the following code:
import UIKit
class ViewController: UIViewController {
private var isActive: Bool = false
private var textField = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemRed
textField.backgroundColor = .systemBackground
view.addSubview(textField)
}
override func viewDidLayoutSubviews() {
textField.frame = CGRect(x: 20, y: view.safeAreaInsets.top + 20, width: view.frame.width - 40, height: 30)
}
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if presses.first?.key?.keyCode == .keyboardSpacebar {
isActive.toggle()
view.backgroundColor = isActive ? .systemBlue : .systemRed
} else {
super.pressesBegan(presses, with: event)
}
}
}
If you run this you'll notice that the textfield will receive events for everything you type except the spacebar. That indicates to me that this UIViewController is receiving those events before the UITextField.
Is this expected behavior? Is my understanding of the responder chain wrong? Or is this a bug in UIKit?
This may be the simplest thing you can possibly due in Xcode in Swift and for some reason, it is not working properly.
I want to center a label in a view. The only other thing in the view previously was a webView added programatically but for now I have removed that so basically, I have an empty VC in which I'm trying to center a label.
There are umpteen answers on SO about this and I've tried every combination but can't get it to to work.
Can anyone suggest a foolproof way to accomplish the simple task of centering a UILabel?
Below is the code I currently have and steps I've taken along with result:
I created an empty view controller in Storyboard and embedded it in a navigation controller.
I set the View Controller in Storyboard to my swift VC class. I also have already cleaned project, closed and re-opened XCode and also deleted storyboard and recreated it in case it was corrupted. Still nothing works.
myVC.swift
import UIKit
class myVC: UIViewController,WKScriptMessageHandler, WKNavigationDelegate,WKUIDelegate {
var title= "Hello there"
var loadingLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self
webView.uiDelegate = self
loadingLabel.translatesAutoresizingMaskIntoConstraints = false
// loadingLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// loadingLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
// loadingLabel = UILabel(frame: CGRect(x: 0, y: self.view.center.y, width: 290, height: 70))
loadingLabel.center = self.view.center
loadingLabel.textAlignment = .center
loadingLabel.font = UIFont(name: "Halvetica", size: 18.0)
loadingLabel.numberOfLines = 0
loadingLabel.text = "TEXT I WANT TO CENTER"
loadingLabel.lineBreakMode = .byTruncatingTail
loadingLabel.center = self.view.center
self.view.addSubview(loadingLabel)
self.title = title
}
override func loadView() {
super.loadView()
}
}
Add the loadingLabel as subview before adding the constraints.
view.addSubview(loadingLabel)
loadingLabel.translatesAutoresizingMaskIntoConstraints = false
loadingLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
loadingLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
I'd like to update the UIKeyboardAppearance within a ViewController. By this I mean let's say the VC loads with UIKeyboardAppearance.default. If I press a button, I'd like the keyboard to update to .dark and have the keyboard now show in that same VC as .dark.
As far as I can tell, iOS checks the value for UIKeyboardAppearance while loading the VC, and doesn't check again until it loads the VC again. Even if you change the value of UIKeyboardAppearance and hide/show the keyboard.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// creating a simple text box, and making the placeholder text the value of the keyboardAppearance
myTextBox.backgroundColor = UIColor.lightGray
myTextBox.frame = CGRect(x: 30, y: 200, width: 300, height: 50)
view.addSubview(myTextBox)
UITextField.appearance().keyboardAppearance = .dark
myTextBox.becomeFirstResponder()
myTextBox.placeholder = "Keybaoard Appearance is: \(UITextField.appearance().keyboardAppearance.rawValue)"
// a simple button to toggle the keyboardAppearance
toggleButton.frame = CGRect(x: 30, y: 300, width: 300, height: 50)
toggleButton.setTitle("Toggle Keyboard", for: .normal)
toggleButton.backgroundColor = UIColor.red
toggleButton.addTarget(self, action: #selector(toggleButtonFunction), for: .touchUpInside)
view.addSubview(toggleButton)
}
// toggles the keyboardAppearance. Hides the keyboard, and a second later shows it again.
#objc func toggleButtonFunction() {
if UITextField.appearance().keyboardAppearance == .dark {
UITextField.appearance().keyboardAppearance = .default
}
else {
UITextField.appearance().keyboardAppearance = .dark
}
myTextBox.resignFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
self.myTextBox.becomeFirstResponder()
self.myTextBox.placeholder = "Keybaoard Appearance is: \(UITextField.appearance().keyboardAppearance.rawValue)"
})
}
let myTextBox = UITextField()
let toggleButton = UIButton()
}
I was hoping that after changing the UIKeyboardAppearance and hiding/showing the keyboard it would show with the new appearance (.dark or .default), but it continually shows with the same appearance until the VC is loaded again. You can see the value of UIKeyboardAppearance changes, but iOS seems to not check for that update until the VC loads again.
Is there any way to force a recheck within a VC?
Thanks for your help!
You can change the keyboard appearance of all text fields recursively on your screen (the allSubviewsOf(type:) extension is from this great answer by Mohammad Sadiq):
func changeTextFieldKeyboardAppearance() {
UITextField.appearance().keyboardAppearance = .dark
let textFields = view.allSubviewsOf(type: UITextField.self)
let firstResponder = textFields.first { $0.isFirstResponder }
firstResponder?.resignFirstResponder()
textFields.forEach { $0.keyboardAppearance = .dark }
firstResponder?.becomeFirstResponder()
}
[...]
extension UIView {
func allSubviewsOf<T: UIView>(type: T.Type) -> [T] {
var all = [T]()
func getSubview(view: UIView) {
if let aView = view as? T {
all.append(aView)
}
guard !view.subviews.isEmpty else {
return
}
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}
If your view controller is embedded in a UITabBarController, you can trigger an update by changing its selectedIndex and changing it back to the original index immediately:
guard let tabBarController = tabBarController else {
return
}
let selectedIndex = tabBarController.selectedIndex
UITextField.appearance().keyboardAppearance = .dark
tabBarController.selectedIndex = selectedIndex == 1 ? 0 : 1
tabBarController.selectedIndex = selectedIndex
Thanks to Tamás for the answer!
It led me down the path to discover what I needed.
It looks like if you change the keyboardAppearance for UITextField
UITextField.appearance().keyboardAppearance = .dark
the system only checks on VC load. If you change it for each textField
myTextBox.keyboardAppearance = .dark
the system will check each time firstResponder changes and load the correct keyboard.
Thanks again Tamás!
I want to show a custom UIView above my keyboard, when my text field is becoming first responder.
However, it seems that I can show view and keyboard both at the same time?
Is there is a way to overcome it?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
phoneInputTextField.becomeFirstResponder()
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillChangeFrame , object: nil)
}
func handleKeyboardNotification(notification: NSNotification) {
guard let userInfo = notification.userInfo,
let frameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue
else { return }
let customView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: frameValue.cgRectValue.width + 20))
customView.backgroundColor = UIColor.black
phoneInputTextField.inputView = customView
}
That code shows only keyboard.
You can use inputView and inputAccessoryView both.
inputView is used to assign a some custom view in replacement of UIKeyboard , like you can use UIPickerView , UIDatePicker etc while editing the textField
inputAccessoryView is also used to assign a some custom view but without replacing a UIKeyboard , it renders above the keyboard . like you can use UIToolbar above the keyboard and many other View as per your requirements.
In your case you can use inputAccessoryView
example :
yourTxtField.inputAccessoryView = yourCustomView()
In my main UIViewController embed in UINavigationController I have add an UILabel to a navigationBar using that code:
if let navigationBar = self.navigationController?.navigationBar {
let frameDomanda = CGRect(x: navigationBar.frame.width/2 - domandaN.frame.width/2, y: -10, width: domandaN.frame.width, height: navigationBar.frame.height)
domandaN.frame = frameDomanda
let secondLabel = UILabel(frame: secondFrame)
secondLabel.text = "Second"
navigationBar.addSubview(domandaN)
}
But when I change Controller the UILabel is fixed. It doesn't disappear so I've added that code:
override func viewDidDisappear(animated: Bool) {
domandaN.removeFromSuperview()
}
It works but I want it to disappear immediately after the press of the back button. Not like this image:
(The "example" text goes away later)
Just add it in viewWillDisappear instead this:
override func viewWillDisappear(animated: Bool) {
domandaN.removeFromSuperview()
}
For the animation parameter:
If true, the disappearance of the view is being animated.
You can use viewWillDisappear, and don't forget to call super:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// you code here
}