I am trying to make my code more organised and reusable. I have some functions and notification that allows the scroll view to be moved up when keyboard shows up and scroll down when keyboard hides. It is all functioning. However, I would imaging these function will be used in multiple parts of my project that has scrollView inside UIViewcontroller. so I want to create a more resuable code rather than writing the same codes in multiple view controllers.
Currently, inside one of my view controller, I have
var keyboard = CGRect()
override func viewDidLoad() {
// Check notifications of keyboard activity
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PostVC.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PostVC.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
// Tap to hide keyboard
let hideTap = UITapGestureRecognizer(target: self, action: #selector(PostVC.hideKeyboard))
hideTap.numberOfTapsRequired = 1
self.view.userInteractionEnabled = true
self.view.addGestureRecognizer(hideTap)
}
func hideKeyboard() {
self.view.endEditing(true)
}
func keyboardWillShow(notification: NSNotification) {
keyboard = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey]!.CGRectValue())!
UIView.animateWithDuration(0.4) {
self.scrollView.contentSize.height = self.view.frame.size.height + self.keyboard.height / 2 + UITabBarController().tabBar.frame.size.height
}
}
func keyboardWillHide(notification: NSNotification) {
UIView.animateWithDuration(0.4) {
self.scrollView.contentSize.height = 0
}
}
I am abit new to trying to make code more re-usable. I am not sure if I need to create a new class or just create a extension of UIViewcontroller and put it in there. I have tried creating an extension of UIViewcontroller and do something like
func keyboardWillShow(notification: NSNotification, _scrollView: UIScrollView) { }
and pass an instance of the scrollview (#IBOutlet weak var scrollView: UIScrollView!) into the function. However, I then run into trouble with doing #selector(keyboardWillShow(_:, keyboard: keyboard, scrollView: scrollView). It gives me an error saying expected expression in list of expressions (I think it is complaining about _:). I might be on a totally wrong path of doing this. Can anyone please help.
Thanks,
I would recommend creating a protocol and adding the default implementation as a protocol extension. Into the protocol you can add that classes that implement it should have a scrollView and a keyboard. Have in mind that protocol extensions are more flexible than base classes and that's why are often preferred in Swift.
Here is an example
protocol Scrollable : class {
var scrollView: UIScrollView { get }
var keyboardRect: CGRect { get set }
}
extension Scrollable where Self : UIViewController {
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillShowNotification, object: nil, queue: nil, usingBlock: { (notification) in
self.keyboardWillShow(notification)
})
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillHideNotification, object: nil, queue: nil, usingBlock: { (notification) in
self.keyboardWillHide(notification)
})
}
func deregisterForKeyboardNotification() {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillShow(notification: NSNotification) {
self.keyboardRect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey]!.CGRectValue())!
UIView.animateWithDuration(0.4) {
self.scrollView.contentSize.height = self.view.frame.size.height + self.keyboardRect.height / 2 + UITabBarController().tabBar.frame.size.height
}
}
func keyboardWillHide(notification: NSNotification) {
UIView.animateWithDuration(0.4) {
self.scrollView.contentSize.height = 0
}
}
}
Have in mind that I've used addObserverForName instead of addObserver because the later cannot be used with protocol extensions easily. More about that you can read here - Swift make protocol extension a Notification observer
Related
I am able to move the system keyboard above the tab bar by moving its window up when keyboardWillShowNotification is sent. However when the view controller is embedded in a navigation controller and I'm slowly swiping back, the tab bar appears twice. Also the offset resets to normal if the swipe is cancelled (obviously since kyeboardWillShow event is not triggered). Observing keyboardWillChangeFrame also doesn't apply.
Here is some sample code to show exactly what I'm doing:
import UIKit
import Foundation
class ViewController: UIViewController { }
class SecondViewController: UIViewController {
let textField = UITextField(frame: CGRect(x: 150, y: 150, width: 150, height: 150))
override func viewDidLoad() {
super.viewDidLoad()
textField.keyboardType = .decimalPad
view.addSubview(textField)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textField.becomeFirstResponder()
}
}
extension SecondViewController {
private var keyboardOffset: CGFloat {
return -tabBarHeight
}
private var tabBarHeight: CGFloat {
return tabBarController?.tabBar.frame.height ?? 0
}
private var keyboardWindowPredicate: (UIWindow) -> Bool {
return { $0.windowLevel > UIWindow.Level.normal }
}
private var keyboardWindow: UIWindow? {
return UIApplication.shared.windows.last(where: keyboardWindowPredicate)
}
#objc private func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = keyboardOffset
}
}
#objc private func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = 0
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
textField.resignFirstResponder()
}
}
extension Sequence {
func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
return try reversed().first(where: predicate)
}
}
Can anyone explain why does this happen and what can I do to prevent this? I know that moving the keyboard is not something usual in iOS, but it was not my decision and I'm trying to work with it.
Maybe the reason is the following:
The docs to touchesBegan(_:with:) say:
UIKit calls this method when a new touch is detected in a view or
window. Many UIKit classes override this method and use it to handle
the corresponding touch events. The default implementation of this
method forwards the message up the responder chain. When creating your
own subclasses, call super to forward any events that you do not
handle yourself. For example, [super touchesBegan:touches
withEvent:event]; If you override this method without calling super (a
common use pattern), you must also override the other methods for
handling touch events, even if your implementations do nothing.
So the 1st thing I would do is to override, as requested by the docs,
the other methods for handling touch requests, even if your
implementations do nothing
I've been trying to set up my keyboard since a few days ago, but I can't manage to do it. I just want the imageView to move up when I am writing in the bottom text field. In fact, it's working, but it also moves up when I try to write at the topTextField. I don't want that, because when that happens, I can't see the textField at the top, and I can't see what I'm writing.
I'll include my screenshots and my code.
In this image, I pressed the topTextField to write something, but as you can see, the topTextField is lost. I mean the view moves up when I press the topTextField and I don't want that. What I want is so that when I press the topTextField the keyboard should appear but the view should be at the same place.
And in the last one I pressed the textFieldBottom, and as you can see, it works. The view moves up so I can see what I'm writing inside the textFieldBottom.
Here is my code:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.subscribeToKeyboardNotifications()
self.subscribeToKeyboardNotificationsDown()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.unsubscribeToKeyBoardNotifications()
self.unsubscribeToKeyBoardNotificationsDown()
}
func subscribeToKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillShow(_:)) , name: UIKeyboardWillShowNotification, object: nil)
}
func unsubscribeToKeyBoardNotifications() {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
view.frame.origin.y -= getKeyboardHeight(notification)
}
func subscribeToKeyboardNotificationsDown() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
}
func unsubscribeToKeyBoardNotificationsDown() {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillHide(notification: NSNotification) {
view.frame.origin.y += getKeyboardHeight(notification)
}
func getKeyboardHeight(notification:NSNotification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
return keyboardSize.CGRectValue().height
}
// do one thing maintain one globle varible to save current textfield which is on editing mode based on that varible u put condition to move imageview
following example code will help you.
var currentTextField: UITextField
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
currentTextField = textField
return true
}
func keyboardWillShow(notification: NSNotification) {
if currentTextField == textFieldBottom {
view.frame.origin.y -= getKeyboardHeight(notification)
}
}
When topTextField becomes first responder, you're changing view.frame.origin, this is moving your topTextField up. Instead of changing view frame, you can set constraints for bottomTextField and you can change constraints for bottomTextField programmatically in keyboardWillShow method. This way your view frame will not change and topTextField will not move up.
I am creating an app with swift and I subclassed a view controller in which I add a tap gesture recognizer and an NSNotification that listens for the keyboard appearing. I put the selector for the keyboardWillShow in a function in my base view controller. When I subclassed my view controller, however, and I had the keyboard show, my app terminated with an NSException that said that it could not find the selector. Can anyone explain why this happened and how I can fix it?
Here is the functionality in my base view controller:
override func viewDidLoad() {
super.viewDidLoad()
setNotificationListers()
setTapGestureRecognizer()
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func setNotificationListers() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
func setTapGestureRecognizer() {
let tapped = UITapGestureRecognizer(target: self, action: "closeKeyboard")
tapped.numberOfTapsRequired = 1
self.view.addGestureRecognizer(tapped)
}
func closeKeyboard() {
self.view.endEditing(true)
}
func keyboardWillShow() {
self.view.frame.origin.y += CGFloat(keyboardHeight)
}
func keyboardWillHide() {
self.view.frame.origin.y -= CGFloat(keyboardHeight)
}
I did not override anything in my subclass. What will be inherited and what will not?
Thanks in advance for any help!
Your selector declaration require a parameter, but your function does not require a parameter.
Either remove : from your selector declaration
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow"), name: UIKeyboardWillShowNotification, object: nil)
or change your function
func keyboardWillShow(notification: NSNotification) {
self.view.frame.origin.y += CGFloat(keyboardHeight)
}
and do the same for keyboardWillHide.
Note that the Selector method have a :, that means your method need have a parameter.
So, you should change two your methods to this:
func keyboardWillShow(notification: NSNotification) {
self.view.frame.origin.y += CGFloat(keyboardHeight)
}
func keyboardWillHide(notification: NSNotification) {
self.view.frame.origin.y -= CGFloat(keyboardHeight)
}
Anyway, xCode 7.3 has change the way to implement selector. And you can use this great lib https://github.com/hackiftekhar/IQKeyboardManager to handle the keyboard pushing up. Very simple to use.
Hi I am using this code to move my view when a textView is selected, this is to make sure my texView is visible for when the keyboard pops up.
override func viewDidLoad() {
super.viewDidLoad() NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name:UIKeyboardWillShowNotification, object: self.view.window)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name:UIKeyboardWillHideNotification, object: self.view.window)
}
func keyboardWillHide(sender: NSNotification) {
let userInfo: [NSObject : AnyObject] = sender.userInfo!
let keyboardSize: CGSize = userInfo[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue.size
self.view.frame.origin.y += keyboardSize.height
}
func keyboardWillShow(sender: NSNotification) {
let userInfo: [NSObject : AnyObject] = sender.userInfo!
let keyboardSize: CGSize = userInfo[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue.size
let offset: CGSize = userInfo[UIKeyboardFrameEndUserInfoKey]!.CGRectValue.size
if keyboardSize.height == offset.height {
if self.view.frame.origin.y == 0 {
UIView.animateWithDuration(0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardSize.height
})
}
} else {
UIView.animateWithDuration(0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardSize.height - offset.height
})
}
print(self.view.frame.origin.y)
}
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: self.view.window)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: self.view.window)
}
How can i only move the view if the bottom textView is selected? Because currently if you select the uppermost textView it moves half of it off screen.
I appreciate any help, thanks in advance.
In the notification callbacks check for bottomTextView.isFirstResponder() and move the view only if its true. Otherwise don't move the view.
Make your class a UITextViewDelegate like so:
class ViewController: UIViewController, UITextViewDelegate {
Then in viewDidLoad set only the bottom textView to be controlled by the delegate:
bottomTextView.delegate = self
Then you can use these functions, changing the values to suit your needs:
func textViewDidBeginEditing(textView: UITextView) {
self.view.frame.origin.y -= 150
}
func textViewDidEndEditing(textView: UITextView) {
self.view.frame.origin.y += 150
}
Rather than only moving the keyboard for a single text field, I suggest a more flexible approach.
What I do is to keep track of which text field is being selected and do some calculations to move the view controller's content view up just enough to expose the text field that is becoming active.
To do that you need to set up your view controller to be the text fields' delegates.
I have a development blog post that explains this in detail:
Shifting views to make room for the keyboard
The code in that post is in Objective-C, but the concepts are identical. I'm not sure I have the same code in Swift that I can share (The only code I've found is in a project I did for a client.)
That blog post references a project called RandomBlobs on Github.
I would suggest using a delegate. For example, you can set you bottom textView's delegate to self (add UITextViewDelegate first of course) and implement the
textViewDidBeginEditing(_:) method. When that's triggered, you will know that the user started editing this particular view.
I was first having problems with textfields on the bottom of the screen because the keyboard would cover them. Now that I fixed that problem I have a new one. When I try to enter text in a textfield that is at the top of the screen, the screen rises and does not let me see what I'm typing.
I think what I would ideally like to do is change how much the keyboard pushes the screen up. Below is the code I used for the initial change.
I just started learning to develop two weeks ago so I'm still getting familiar with all the syntax and functions.
var kbHeight: CGFloat!
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated:Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
kbHeight = keyboardSize.height
self.animateTextField(true)
}
}
}
func keyboardWillHide(notification: NSNotification) {
self.animateTextField(false)
}
func animateTextField(up: Bool) {
var movement = (up ? -kbHeight : kbHeight)
UIView.animateWithDuration(0.3, animations: {
self.view.frame = CGRectOffset(self.view.frame, 0, movement)
})
}
It's a suggestion
Instead of setting View's frame. put the view inside a UIScrollView.
then inside your keyboardWillShow and keyboardWillHide methods adjust the scrollview frame accordingly.
and set the scrollview's content offset using the "scrollRectToVisible" inside the the textfield delegate method.
- (void) textFieldDidBeginEditing:(UITextField *)textField
{
CGRect rectText = textField.frame;
self.scrollVIew scrollRectToVisible:rectText animated:TRUE];
}