I am trying to calculate the position to move a UITextField along with its parent UIView if the keyboard is overlapping the field and move back to its original position after keyboard is closed.
I have already tried https://github.com/hackiftekhar/IQKeyboardManager and it does not work in my particular case.
To explain the problem, please refer two attached screenshot, one when keyboard is opened and another when it is closed.
As you can see, on keyboard open, the text field is overlapping the keyboard, I want to move the text field along with popup view to readjust and sit above the keyboard.
Here is what I tried.
func textFieldDidBeginEditing(_ textField: UITextField) {
self.startOriginY = self.frame.origin.y
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
}
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let screenHeight = self.backgroundView.frame.height
let viewHeight = self.frame.height
let diffHeight = screenHeight - viewHeight - keyboardHeight
if diffHeight < 0 {
self.frame.origin.y = -self.textField.frame.height
}
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.frame.origin.y = self.startOriginY
}
This code moves the view to incorrect position. I am trying to figure out how to calculate the correct position to move the view and remove keyboard overlap.
What is the best way to go about solving this problem?
Thank you.
Basically you need to embedded your view inside a scroll view and use Apple's example to handle the adjustment by altering the bottom content inset of the scroll view:
Embedded inside a scroll view
Register for keyboard notifications
Implement logic to handle the notifications by altering the bottom content inset
I have converted Apple's code snipe to Swift.
Keyboard will show:
if let info = notification.userInfo,
let size = (info[UIKeyboardFrameEndUserInfoKey] as AnyObject?) {
let newSize = size.cgRectValue.size
let contentInset = UIEdgeInsetsMake(0.0, 0.0, newSize.height, 0.0)
scrollView.contentInset = contentInset
}
Keyboard will hide:
let contentInset = UIEdgeInsets.zero
scrollView.contentInset = contentInset
scrollView.scrollIndicatorInsets = contentInset
You can also add hard-coded offset to the newSize.height i.e: newSize.height + 20
Related
FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.
I am trying to animate a UIView shrinking and moving to a different center point. It starts when the user taps a certain UICollectionViewCell, a new UIView is made, centred on the cell, and starts expanding until it fills the entire screen. This works fine. I store the original center point of the cell in the new UIView (custom class with property originCenter).
let expandingCellView = SlideOverView(frame: cell.bounds)
expandingCellView.center = collectionView.convert(cell.center, to: self)
expandingCellView.textLabel.text = cell.textLabel.text
expandingCellView.originWidth = cell.bounds.width
expandingCellView.originHeight = cell.bounds.height
expandingCellView.originCenter = self.convert(expandingCellView.center, to: self)
expandingCellView.originView = self
self.addSubview(expandingCellView)
expandingCellView.widthConstraint.constant = self.frame.width
expandingCellView.heightConstraint.constant = self.frame.height
UIView.animate(withDuration: 0.3, animations: {
expandingCellView.center = self.center
expandingCellView.layoutIfNeeded()
})
This code works perfectly fine. Now I have added a button to that expanded view, which executes the following code:
widthConstraint.constant = originWidth
heightConstraint.constant = originHeight
print(self.convert(self.originCenter, to: nil))
UIView.animate(withDuration: 0.5, animations: {
self.center = self.convert(self.originCenter, to: nil)
self.layoutIfNeeded()
}, completion: { (done) in
// self.removeFromSuperview()
})
The shrinking of the view to its original size works fine. The only thing that doesn't work is the centering. I take the original cell centerpoint, and convert it to this views coordinate system. This produces coordinates which I think are correct. However all the views just move to the top left of the screen.
Below is a link to a screen recording. The first UIView prints its new centerpoint as (208.75, 411.75) and the second UIView I open prints its center as (567.25, 411.75). These values seem correct to me, however they don't move to this point, as you can see in the video. Any way I can fix this?
Even when setting the new center to for instance CGPoint(x: 500, y: 500), the view still moves to x = 149.5 and y = 149.5
Video: https://streamable.com/pxemc
Create a var that has a weak reference to the cell
weak var selectedCell: UICollectionViewCell!
assign the selected cell in CollectionView delegate didSelectItemAt call
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItemAtIndexPath(indexPath)
}
after add expandingCellView as subview do this to make it go from cell size to full screen
// make expanding view the same size as cell
expandingCellView.frame = selectedCell.frame
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.view.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
just reverse it to make it small again like the cell size
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.selectedCell.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
frame use global position.
frame vs bounds
I am trying to re-create a Facebook sign up page for practice; the set up of my viewController is as follows:
1)A profile image container at the top,
2)Email textfield
3)Password Textfield
4)Confirm Password textfield
To solve the issue of the keyboard blocking the "Confirm Password" field, I have used listeners as below.
The issue is that the very first time a user clicks on a textfield to type (no matter which one), the screen moves way too far up, such that the email textfield at the top ends up going beyond the screen size on top. However, when I dismiss the keyboard and re-click on any textfield, it goes up as intended: Only until the top textfield reaches the top of the screen.
I can find no other reason for this behaviour, it persisted even after I re-wrote some of my code.
My ViewController set up:
class ViewController: UIViewController {
#objc func dismissKeyboard() {
view.endEditing(true)
}
let imageContainer: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
return v
}()
let emailTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "EMAIL"
return tf
}()
let passwordTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "Password"
return tf
}()
let confirmPasswordTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "Confirm password"
return tf
}()
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0 {
self.view.frame.origin.y -= keyboardSize.height
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if self.view.frame.origin.y != 0 {
self.view.frame.origin.y = 0
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
view.addSubview(imageContainer)
view.addSubview(emailTF)
view.addSubview(passwordTF)
view.addSubview(confirmPasswordTF)
NSLayoutConstraint.activate([
imageContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageContainer.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.275),
imageContainer.widthAnchor.constraint(equalTo: view.heightAnchor),
imageContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
emailTF.topAnchor.constraint(equalTo: imageContainer.bottomAnchor),
emailTF.heightAnchor.constraint(equalToConstant: 100),
emailTF.widthAnchor.constraint(equalToConstant: 200),
emailTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
passwordTF.topAnchor.constraint(equalTo: emailTF.bottomAnchor, constant: 20),
passwordTF.heightAnchor.constraint(equalToConstant: 100),
passwordTF.widthAnchor.constraint(equalToConstant: 200),
passwordTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
confirmPasswordTF.topAnchor.constraint(equalTo: passwordTF.bottomAnchor, constant: 20),
confirmPasswordTF.heightAnchor.constraint(equalToConstant: 100),
confirmPasswordTF.widthAnchor.constraint(equalToConstant: 200),
confirmPasswordTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
let tapToDismissKeyboard: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
view.addGestureRecognizer(tapToDismissKeyboard)
}
}
I'd really appreciate some help as to what I can do to correct this issue.
Seems like this is a known issue raised here and other SO questions too..
Two points to make:-
1) Use UIResponder.keyboardFrameEndUserInfoKey instead of UIResponder.keyboardFrameBeginUserInfoKey at:
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
in keyboardWillShow()
2) Doing so, you will get the total height of the keyboard including the toolbar. Which means:
However, when I dismiss the keyboard and re-click on any textfield, it
goes up as intended: Only until the top textfield reaches the top of
the screen.
This is the buggy behaviour where you get an incorrect height excluding the toolbar and:
The issue is that the very first time a user clicks on a textfield to
type (no matter which one), the screen moves way too far up, such that
the email textfield at the top ends up going beyond the screen size on
top.
This is actually the intended behaviour with actual height. You could calculate the height of the toolbar and reduce it from this height to achieve your goal! If your VC is embedded in a navigation controller, you could use this
Hope this helps.
When I call my keyboard it will slide up into place, then the rest of the view will slide up to make room for it. Similarly when I dismiss the keyboard, the keyboard will slide out, then the rest of the view will slide down. Is it possible to have the keyboard and the view to slide at the same time?
Here is the code I currently have
// MARK: Animated Keyboard
#objc func keyboardWillShow(notification: NSNotification) {
// check to see if a keyboard exists
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
print("Show")
// Check to see if the information for the size of the keyboard exists
guard let userInfo = notification.userInfo
else {return}
// get the size of the keyboard
guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
else {return}
// get the duration of the keyboard transition
let duration = notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
// get the type and speed of the keyboard transition
let curve = notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as! UInt
// move the frame by the size of the keyboard
let keyboardFrame = keyboardSize.cgRectValue
UIView.animateKeyframes(withDuration: duration, delay: -0.2, options: UIView.KeyframeAnimationOptions(rawValue: curve), animations: {
if self.view.frame.origin.y == 0 {
self.view.frame.origin.y -= keyboardFrame.height
}
}, completion: nil)
}
}
This is not causing any errors, it compiles and runs without issue. It just looks bad.
From your description is sounds like you have actually registered for keyboardDidShowNotification instead of keyboardWillShowNotification.
Make sure you have registered for the correct notification.
You should also use the UIView.animate method and not UIView.animateKeyframes. This of course also means you need to replace UIView.KeyframeAnimationOptions with UIView.AnimationOptions.
Lastly, don't attempt to use a negative delay. UIKit has a lot of power but time travel isn't one of them.
Since Apple introduced safe area insets and adjusted content insets the already working UI layouting code becomes broken. In my case UIScrollView bottom inset extends when the keyboard appears:
func keyboardWillResize(_ notification: Notification) {
let info: [AnyHashable: Any] = notification.userInfo!
let keyboardTop = self.view.frameOnScreen.maxY - (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.y
UIView.animate(withDuration: 0.3, animations: {
self.tableView.contentInset.bottom = keyboardTop
self.tableView.scrollIndicatorInsets = self.tableView.contentInset
})
}
Within iOS 11 this code produces extra inset when keyboard appears, equal to tab bar height. It's obvious because now contentInset represents only user-defined insets, and real insets are represented by adjustedContentInset introduced in iOS 11.
So my question is how to deal with this case in good manner? There is option to write
self.tableView.contentInset.bottom = keyboardTop - self.tableView.adjustedContentInset.bottom
but it looks so ugly. Maybe there is built-in method to extend insets behind the keyboard?
Obviously, the answer is in official documentation. Instead of manually adjust content insets, we should delegate this stuff to view controller and deal with it's safe area insets. So, here is working code:
func keyboardWillResize(_ notification: Notification) {
let info: [AnyHashable: Any] = notification.userInfo!
let keyboardTop = self.view.frameOnScreen.maxY - (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.y
UIView.animate(withDuration: 0.3, animations: {
self.additionalSafeAreaInsets.bottom = max(keyboardTop - self.view.safeAreaInsets.bottom, 0)
})
}