Unwanted UIView frame reset after inserting text in UITextField - ios

I'm stuck with some funny problem and ran out of ideas how to solve it.
In one of my controllers I use a simple scheme of adjusting a view frame according to the keyboard appearance.
In UITextFieldDelegate method I initialise the controller's property firstResponder:
func textFieldDidBeginEditing(_ textField: UITextField) {
self.firstResponder = textField
}
Then I use UIKeyboard notifications selectors to change the frame of contentView:
override func keyboardWillShow(_ notifications: Notification) {
super.keyboardWillShow(notifications)
let info = notifications.userInfo
let keyboardFrame:CGRect = (info![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
var bottomY:CGFloat!
if self.firstResponder == self.emailTextField{
bottomY = self.emailBottomLine.frame.origin.y + 80 + self.headerView.frame.height
}
else {
return
}
if bottomY >= keyboardFrame.origin.y {
let offset = bottomY - keyboardFrame.origin.y
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = -offset
})
}else{
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
}
override func keyboardWillHide(_ notifications: Notification) {
super.keyboardWillHide(notifications)
let info = notifications.userInfo
let duration:Double = (info![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animate(withDuration: duration, animations: {
self.contentView.frame.origin.y = self.contentViewOriginY
})
}
And everything works fine until I start typing inside the emailTextField. Each tap on the keyboard causes the contentView reset to its original position without animation.
The question is what really causes this behavior? I'm totally confused and have checked whatever thing is possibly affects this. Please, help!!!

Make sure either:
A. Your contentView does not have any layout constraints attached to it, or else when you set its frame, its frame will be reset on the next layout pass back to what the constraints say the frame should be.
or:
B. Use a constraint to position your contentView's vertical offset relative to the keyboard instead of adjusting its frame.

Related

How do I set variable values back to a previous state in Swift?

Is there a way to "reset" all the values I gave when the keyboard shows? Right now I just hard coded everything but that seems a bit stupid. I hope you can understand my code. I am basically looking for a smarter/faster way to reset all the values, which is in the "else part" of my code.
#objc func keyboardWillChange(notification: Notification) {
guard let keyboardRect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
return
}
if notification.name == UIResponder.keyboardWillShowNotification ||
notification.name == UIResponder.keyboardWillChangeFrameNotification {
// Keyboard shows
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
let heightHelper = self.bottomConstraint.constant
self.bottomConstraint.constant = keyboardRect.height + 20
self.bottomConstraintEmail.constant += keyboardRect.height + 20 - heightHelper
self.anmeldenLabel.alpha = 0
self.logoConstraint.constant = 45
}
}else {
// Keyboard hides
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
self.bottomConstraintEmail.constant = 337
self.bottomConstraint.constant = 255.5
self.logoConstraint.constant = 60
self.anmeldenLabel.alpha = 1
}
}
}
I haven't seen your layout, but I can suggest you two common steps which can make your code look "smarter":
Try to place all your views, which constraints you are trying to animate, onto a separate superview. In this case you'll need to have only one hardcoded constant (the distance from this superview to the keyboard top).
Make a function like this:
func animateConstraint(_ showing: Bool) {
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
if showing {
yourSuperView.bottomConstraint.constant += keyboardRect.height
} else {
yourSuperView.bottomConstraint.constant -= keyboardRect.height
}
}
}
...and then call it twice (in your if and else blocks) with different argument values.

How to update the frameOfPresentedViewInContainerView when using custom modal presentation?

I'm using a custom UIPresentationController to present a view modally. After presenting the view, the first textfield in the presented view becomes the first responder and the keyboard shows up. To ensure that the view is still visible, I move it up. However, when I do this the frameOfPresentedViewInContainerView is not matching the actual frame of the view anymore. Because of this, when I tap on the view it's being dismissed, because there's a tapGestureRecogziner on the backgroundView which is on top of the presentingView. How to notify the presentingController that the frame/position of the presentedView has changed?
In the UIPresentationController:
override var frameOfPresentedViewInContainerView: CGRect {
var frame = CGRect.zero
let safeAreaBottom = self.presentingViewController.view.safeAreaInsets.bottom
guard let height = presentedView?.frame.height else { return frame }
if let containerBounds = containerView?.bounds {
frame = CGRect(x: 0,
y: containerBounds.height - height - safeAreaBottom,
width: containerBounds.width,
height: height + safeAreaBottom)
}
return frame
}
override func presentationTransitionWillBegin() {
if let containerView = self.containerView, let coordinator = presentingViewController.transitionCoordinator {
containerView.addSubview(self.dimmedBackgroundView)
self.dimmedBackgroundView.backgroundColor = .black
self.dimmedBackgroundView.frame = containerView.bounds
self.dimmedBackgroundView.alpha = 0
coordinator.animate(alongsideTransition: { _ in
self.dimmedBackgroundView.alpha = 0.5
}, completion: nil)
}
}
Presenting the view modally:
let overlayVC = CreateEventViewController()
overlayVC.transitioningDelegate = self.transitioningDelegate
overlayVC.modalPresentationStyle = .custom
self.present(overlayVC, animated: true, completion: nil)
Animation when keyboard appears (in the presented view):
#objc func animateWithKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
return
}
// bottomContraint is the constraint that pins content to the bottom of the superview.
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
bottomConstraint.constant = moveUp ? (keyboardHeight) : originalBottomValue
let options = UIView.AnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
From the Apple documentation:
UIKit calls this method multiple times during the course of a
presentation, so your implementation should return the same frame
rectangle each time. Do not use this method to make changes to your
view hierarchy or perform other one-time tasks.
AFAIK, if you specify frame through this variable, it's advised not to change it throughout the course of presentation. If you plan to play around with the frames, don't specify this variable and handle all the changes manually in your animator

Having trouble with LargeTitle and a segmented control with a table view

Sample project can be found at https://github.com/SRowley90/LargeTitleIssueTestiOS
I am trying to position a segmented control below the Large title in an iOS app. I have a UIToolbar which contains the segmented control inside.
When scrolling up the title and toolbar behave as expected.
When scrolling down the navigation bar is correct, but it doesn't push the UITabBar or the UITableView down, meaning the title goes above the segmented control as can be seen in the images below.
I'm pretty sure it's something to do with the constraints I have set, but I can't figure out what.
The TabBar is fixed to the top, left and right.
The TableView is fixed to the bottom, left and right.
The tableView is fixed vertically to the TabBar
I have the position UITabBarDelegate method set:
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
Take the delegation of the tableView somewhere:
tableView.delegate = self
Override the scrollViewDidScroll and update toolbar position appearance (since the real position should not change according to have that nice bounce effect.
extension ViewController: UIScrollViewDelegate {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var verticalOffset = scrollView.contentOffset.y + defaultNavigationBarHeight
if scrollView.refreshControl?.isRefreshing ?? false {
verticalOffset += 60 // After is refreshing changes its value the toolbar goes 60 points down
print(toolbar.frame.origin.y)
}
if verticalOffset >= 0 {
toolbar.transform = .identity
} else {
toolbar.transform = CGAffineTransform(translationX: 0, y: -verticalOffset)
}
}
}
You can use the following check before applying transformation to make it more reliable and natural to default iOS style:
if #available(iOS 11.0, *) {
guard let navigationController = navigationController else { return }
guard navigationController.navigationBar.prefersLargeTitles else { return }
guard navigationController.navigationItem.largeTitleDisplayMode != .never else { return }
}
Using UIScrollViewDelegate didn't work well with CollectionView and toolbar for me. So, I did:
final class CollectionViewController: UICollectionViewController {
private var observesBag: [NSKeyValueObservation] = []
private let toolbar = UIToolbar()
override func viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let defaultNavigationBarHeight = statusBarHeight + navigationBarHeight
let observation = navigationController!
.navigationBar
.observe(\.center, options: NSKeyValueObservingOptions.new) { [weak self] navBar, _ in
guard let self = self else { return }
let newNavigatonBarHeight = navBar.frame.height + statusBarHeight
let yTranslantion = newNavigatonBarHeight - defaultNavigationBarHeight
if yTranslantion > 0 {
self.toolbar.transform = CGAffineTransform(
translationX: 0,
y: yTranslantion
)
} else {
self.toolbar.transform = .identity
}
}
observesBag.append(observation)
}
}
Observe the "center" of the navigationBar for changes and then translate the toolbar in the y-axis.
Even though it worked fine when I tried to use this solution with UIRefreshControl and Large Titles it didn't work well.
I set up the refresh control like:
private func setupRefreshControl() {
let refreshControl = UIRefreshControl()
self.webView.scrollView.refreshControl = refreshControl
}
the height of the UINavigationBar is changed after the complete refresh triggers.

Button disappears after animating it

I'm using UIView Extension for button to slide it up with keyboard.
extension UIView {
func bindToKeyboard() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardWillChange(_ notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
let curve = notification.userInfo![UIKeyboardAnimationCurveUserInfoKey] as! UInt
let startingFrame = (notification.userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let endingFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let deltaY = endingFrame.origin.y - startingFrame.origin.y
let options = UIViewAnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
self.frame.origin.y += deltaY
self.layoutIfNeeded()
}, completion: nil)
}
}
Then in ViewController just using :
func setUpView() {
okayButton.bindToKeyboard()
self.isHeroEnabled = true
}
But the problem is when I press other button on the screen:
Save button disappears after tapping on other button, when it is in the "upper position", and appears when it's on the bottom. What am I doing wrong? How to prevent/fix it?
Edit: There is no action on any of these buttons! (+,-,save)
Thanks!
You don't necessarily need to update self.view . What you can do is create a IBOutlet bottom spacing for the save button.
#IBOutlet weak var saveButtonBottomSpacing: NSLayoutConstraint!
When keyboard is open, set bottom spacing constant to keyboard's height.
When keyboard is dismissed, restore the bottom spacing. May be 0 or your desired value.
You can make this changes within UIView animation block.
Hide (resign) your keyboard upon successful action on 'Save' button.
Here is sample code you need to update in your Save button action.
#IBAction func btnSave(sender: Any){
// add this line in your upon, successful action on save button
yourTextView.resignFirstResponder()
}

Move view when keyboard displayed over UITextField

I have looked around and found this post about moving a view when a keyboard appears. It works great and moves the keyboard anytime I click in a UITextField. The issue is that I have three UITextFields, and the keyboard should only move when it is going to present over a UITextField. I looked at the Apple documentation on this as well, and found some useful information but I am still not getting the desired functionality.
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
var aRect = self.view.frame;
aRect.size.height -= keyboardSize.size.height
if self.view.frame.origin.y == 0{
if aRect.contains(activeField.frame.origin){
self.view.frame.origin.y -= keyboardSize.height
}
}
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
activeField = textField
}
func textFieldDidEndEditing(_ textField: UITextField) {
activeField = nil
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
}
From the Apple documentation I just took the piece where I create the aRect, and then check if the points intersect with the contains function. I would expect this to then make the view move only when the keyboard were to overlap with a textfield, and keep the view in place otherwise. For some reason that I don't fully understand, this is not the case. The keyboard will move the view in the case where any textfield is clicked (even though for some it shouldn't). I have played around with it a bit now and tried debugging but have been unsuccessful. Any ideas?
EDIT: I did a little debugging and it seems that the aRect.contains(...) is returning true for when all textfields are clicked, but in reality it should not. Is contains the right method to be using?
I followed this way to manage such issue in TableView same way you can manage in your view Here is step by step code:
within viewDidLoad added registerForKeyboardNotifications()
Here is the method
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWasShown:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}
Again define other method :
func keyboardWasShown(aNotification: NSNotification) {
let info = aNotification.userInfo as! [String: AnyObject],
kbSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue().size,
contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height, right: 0)
electricalViewListTableview.contentInset = contentInsets
electricalViewListTableview.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
var aRect = self.view.frame
aRect.size.height -= kbSize.height
if let activeTF = activeField {
if !CGRectContainsPoint(aRect, activeTF.frame.origin) {
electricalViewListTableview.scrollRectToVisible(activeTF.frame, animated: true)
}
}
}
Keyboard Hiding Method :
func keyboardWillBeHidden(aNotification: NSNotification) {
let contentInsets = UIEdgeInsetsZero
electricalViewListTableview.contentInset = contentInsets
electricalViewListTableview.scrollIndicatorInsets = contentInsets
}
After this use UITextFieldDelegates method to keep track active textfield :
var activeField: UITextField?
func textFieldDidBeginEditing(textField: UITextField) {
self.activeField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
self.activeField = textField
}
Hope it helps!
You have two main issues with your keyboardWillShow code.
You are using the wrong key to get the keyboard frame. You need UIKeyboardFrameEndUserInfoKey, not UIKeyboardFrameBeginUserInfoKey. You want to know where the keyboard will end up, not where it starts from.
Once you get the keyboard's frame, you need to convert it to local coordinates. It is given to you in screen coordinates.
Your updated code would be:
func keyboardWillShow(notification: NSNotification) {
if let keyboardScreenFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardLocalFrame = self.view.convert(keyboardScreenFrame, from: nil)
var aRect = self.view.frame;
aRect.size.height -= keyboardLocalFrame.size.height
if self.view.frame.origin.y == 0 {
if aRect.contains(activeField.frame.origin) {
self.view.frame.origin.y -= keyboardSize.height
}
}
}
}
You also have a big problem with your keyboardWillHide method. Since you keyboardWillShow method always shortens your view's frame, your keyboardWillHide method also needs to always restore the view's frame height.
If I were you, I wouldn't change the view's frame height in either method. Just adjust its origin as needed to make the text field visible.
Try IQKeyboardManager . It automatically manages text fields to make them visible. You just need to add it to your project, and no need to write even one line of code. A piece from it's documentation:
Often while developing an app, We ran into an issues where the iPhone
keyboard slide up and cover the UITextField/UITextView.
IQKeyboardManager allows you to prevent issues of the keyboard sliding
up and cover UITextField/UITextView without needing you to enter any
code and no additional setup required. To use IQKeyboardManager you
simply need to add source files to your project.
EDIT: In addition to Rmaddy's answer, I can say you should consider changing if aRect.contains(activeField.frame.origin) to if !aRect.contains(activeField.frame), because the first check will return true even if the top of your textfield is in the frame of the view, and also, you should be checking if it doesn't contain the frame of your textfield, then change the frame of the view.
And, I'm not totally sure, but, maybe it would be better if you move your activeField = textField code to the textFieldShouldBeginEditing delegate method.

Resources