How to manage keyboard height when using isSecureTextEntry in iOS UITextField - ios

There is a view with two UITextFields, and the view moves according to the height of the keyboard. However, moving the View is not ideal if at least one textfield has its isSecureTextEntry property set to true. It is noticeable when moving between textfields. View moves down once and returns instantly. This is because the height of the keyboard changes momentarily. I think it's a matter of showing / hiding QuickTypeBar. So how to eliminate that effect and control the view movement?
Below is all the code.
It may not look right in the simulator. Because the QuickType bar is not displayed.
This is happening on the iPhone X iOS13.3.1.
Thanks.
import UIKit
class ViewController: UIViewController {
public var boardView: UIView!
public var usernameTextField: UITextField!
public var passwordTextField: UITextField!
public var boardViewBottomAnchorConstraint: NSLayoutConstraint!
public var boardViewBottomAnchorConstraintConstant: CGFloat = -220
public var offset: CGFloat = 16.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
var constraints = [NSLayoutConstraint]()
let boardView = UIView()
boardView.backgroundColor = .brown
boardView.layer.cornerRadius = 16.0
boardView.translatesAutoresizingMaskIntoConstraints = false
boardViewBottomAnchorConstraint = boardView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: boardViewBottomAnchorConstraintConstant)
constraints.append(boardView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16))
constraints.append(boardViewBottomAnchorConstraint)
constraints.append(boardView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16))
view.addSubview(boardView)
self.boardView = boardView
let usernameTextField = UITextField()
usernameTextField.backgroundColor = .white
usernameTextField.delegate = self
usernameTextField.returnKeyType = .next
usernameTextField.textContentType = .username
usernameTextField.placeholder = "USERNAME TEXTFIELD"
usernameTextField.translatesAutoresizingMaskIntoConstraints = false
constraints.append(usernameTextField.heightAnchor.constraint(equalToConstant: 56))
constraints.append(usernameTextField.topAnchor.constraint(equalTo: boardView.topAnchor, constant: 100))
constraints.append(usernameTextField.leftAnchor.constraint(equalTo: boardView.leftAnchor, constant: 24))
constraints.append(usernameTextField.rightAnchor.constraint(equalTo: boardView.rightAnchor, constant: -24))
boardView.addSubview(usernameTextField)
self.usernameTextField = usernameTextField
let passwordTextField = UITextField()
passwordTextField.backgroundColor = .white
passwordTextField.delegate = self
passwordTextField.returnKeyType = .go
passwordTextField.textContentType = .password
passwordTextField.placeholder = "PASSWORD TEXTFIELD"
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
constraints.append(passwordTextField.heightAnchor.constraint(equalToConstant: 56))
constraints.append(passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 50))
constraints.append(passwordTextField.leftAnchor.constraint(equalTo: boardView.leftAnchor, constant: 24))
constraints.append(passwordTextField.bottomAnchor.constraint(equalTo: boardView.bottomAnchor, constant: -100))
constraints.append(passwordTextField.rightAnchor.constraint(equalTo: boardView.rightAnchor, constant: -24))
boardView.addSubview(passwordTextField)
self.passwordTextField = passwordTextField
NSLayoutConstraint.activate(constraints)
let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing(_:)))
view.addGestureRecognizer(tap)
// false is the ideal move. If true, the view moves violently.
passwordTextField.isSecureTextEntry = true
}
}
extension ViewController {
#objc
public func keyboardWillShow(_ notification: Notification) { print("WILL SHOW")
updateConstraints(notification)
}
#objc
public func keyboardWillHide(_ notification: Notification) { print("WILL HIDE")
boardViewBottomAnchorConstraint.constant = boardViewBottomAnchorConstraintConstant
view.layoutIfNeeded()
}
#objc
public func keyboardWillChangeFrame(_ notification: Notification) { print("WILL CHANGE FRAME")
updateConstraints(notification)
}
private func updateConstraints(_ notification: Notification) {
guard
let toFrame = notification.userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect,
let fromFrame = notification.userInfo?[UIApplication.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
print(fromFrame); print(toFrame);
let constant = -toFrame.size.height - offset
boardViewBottomAnchorConstraint.constant = constant
view.layoutIfNeeded()
}
}
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == usernameTextField {
passwordTextField.becomeFirstResponder()
} else {
passwordTextField.resignFirstResponder()
}
return true
}
}

try using github.com/hackiftekhar/IQKeyboardManager from cocoapods. inside documentation you have to add few lines in appdelegate class and you are good to go.
IQKeyboardmanager is avaiable both through cocoapods and IOS swift dependancy manager.

if UIApplication.shared.isKeyboardPresented {
print("Keyboard presented")
} else {
print("Keyboard is not presented")
}
Use this code in combination with your code. Determine when the keyboad is active and do your actions accordingly
edit ----
YOu need to ctrl + drag from each of your text fields into your code and create a #IBAction
#IBAction func textField1() {
//when a user taps on text field 1. this code block is triggered.
if UIApplication.shared.isKeyboardPresented {
print("Keyboard presented")
//if the keyboard is already presented do whatever other action is required
//inside here the keyboard is already active
} else {
print("Keyboard is not presented")
//if the keyboard is not currently active do any required action here
//Inside here the keyboards will become active
}
}
For all your other textfields and all other views you need to do the same thing and customize your code based on the actions
EDI ----
Below is how you can achieve the same functionality without using the interface builder
let textField = UITextField()
self.textField.addTarget(self, action: #selector(targetFunc), for: .touchUpInside)
#objc func targetFunc() {
// same actions as above in here
}

Related

How can I reuse part of code (recognizer, toolbar) applied to a textview?

I have a class called ThemeVC which has a textview (connected with an IBoutlet) and functionalities applied to it (it has a recognizer that detects the tapped words).
My goal here is that I would like to extract that piece of functionality, and put it maybe in its own class or create a delegate so I could reuse that functionality on other textviews.
Anyone knows how?
I pasted my code below.
(HERE comments, are functions that should be called from any view controller)
import UIKit
class ThemeVC: UIViewController, UITextViewDelegate, UINavigationControllerDelegate {
#IBOutlet weak var themeTextView: UITextView!
var tB = UIBarButtonItem()
// Move away from ThemeVC ... ->
var selectionDict = [String:Int]()
var viewTagCount = Int()
var tap = UIGestureRecognizer()
var firstTimeGrouped = false
// -> ... Move away from ThemeVC
override func viewDidLoad() {
super.viewDidLoad()
themeTextView.delegate = self
loadbuttons ()
//HERE
addTagSelectorToolBar ()
}
func loadbuttons () {
tB = UIBarButtonItem(image: UIImage(systemName: "hand.point.up.left"), style: .plain, target: self, action: #selector(getTag(sender:)))
navigationItem.rightBarButtonItems = [tB]
}
#objc func getTag(sender: AnyObject) {
themeTextView.resignFirstResponder()
//HERE
startTagSelection()
}
}
// Move away from ThemeVC ... ->
extension ThemeVC {
func startTagSelection () {
navigationController?.setToolbarHidden(false, animated: false)
tap.isEnabled = true
tB.isEnabled = false
themeTextView.isEditable = false
themeTextView.isSelectable = false
}
}
extension ThemeVC {
#objc func doneTagSelection(){
navigationController?.setToolbarHidden(true, animated: false)
tap.isEnabled = false
tB.isEnabled = true
themeTextView.isEditable = true
themeTextView.isSelectable = true
firstTimeGrouped = false
}
}
extension ThemeVC {
func addTagSelectorToolBar (){
addTappedTagRecognizer()
tap.isEnabled = false
let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTagSelection))
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
toolbarItems = [spacer, done]
}
}
extension ThemeVC {
func addTappedTagRecognizer () {
tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(recognizer:)))
tap.delegate = self as? UIGestureRecognizerDelegate
themeTextView.addGestureRecognizer(tap)
}
#objc private func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: themeTextView)
let position: CGPoint = CGPoint(x:location.x, y:location.y)
let tapPosition: UITextPosition? = themeTextView.closestPosition(to:position)
if tapPosition != nil {
let textRange: UITextRange? = themeTextView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: UITextDirection(rawValue: 1))
if textRange != nil
{
let tappedWord: String? = themeTextView.text(in:textRange!)
print(tappedWord ?? "Unable to get word")
}
}
}
}
// ... -> Move away from ThemeVC
How to test my code:
Create a new project with a storyboard
On the left hand side rename viewcontroller with themeVC, and replace
its code with the code I gave.
On the storyboard, embed the controller in a navigation controller, on right side, change in identity inspector class from view controller to themeVC
add a textview and link it to the IBoutlet
Looking at the parts you want to move away from ThemeVC, I would have to say not everything should be moved away from ThemeVC.
For example, you marked startTagSelection as something you want to move away, but you reference the navigationController which belongs to the view controller so it should ideally not be the responsibility of your UITextView to update your UINavigationBar.
So the two ideas discussed in the comments was using SubClasses and Protocols.
Protocols was the suggestion of Ptit Xav so I will show one way that could be used, Ptit Xav could add an answer if something else was in mind.
I start with creating a protocol
// Name the protocol as you see appropriate
// I add #objc so it can be accessible from Storyboard
// This will be used to `hand over` responsibility of
// a certain action / event
#objc
protocol CustomTextViewTagDelegate: class {
func customTextViewDidStartSelection(_ textView: CustomTextView)
func customTextViewDidFinishSelection(_ textView: CustomTextView)
}
Next I subclass a UITextView to add my own customization
#IBDesignable
class CustomTextView: UITextView {
var selectionDict = [String:Int]()
var viewTagCount = Int()
var tap = UIGestureRecognizer()
var firstTimeGrouped = false
// Name it as you wish
// #IBInspectable added for storyboard accessibility
// You could also make it an IBOutlet if your prefer
// that interaction
#IBInspectable
weak var tagDelegate: CustomTextViewTagDelegate?
func startTagSelection () {
// Remove the commented lines as this should the responsibility of
// the view controller, manage in the view controller using the delegate
// navigationController?.setToolbarHidden(false, animated: false)
// tB.isEnabled = false
tap.isEnabled = true
isEditable = false
isSelectable = false
// Hand over responsibility of this action back whatever
// has subscribed as the delegate to implement anything else
// for this action
tagDelegate?.customTextViewDidStartSelection(self)
}
func addTappedTagRecognizer () {
tap = UITapGestureRecognizer(target: self,
action: #selector(tapResponse(recognizer:)))
tap.delegate = self as? UIGestureRecognizerDelegate
addGestureRecognizer(tap)
}
#objc private func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: self)
let position: CGPoint = CGPoint(x:location.x,
y: location.y)
let tapPosition: UITextPosition? = closestPosition(to:position)
if tapPosition != nil {
let textRange: UITextRange? = tokenizer.rangeEnclosingPosition(tapPosition!,
with: UITextGranularity.word,
inDirection: UITextDirection(rawValue: 1))
if textRange != nil
{
let tappedWord: String? = text(in:textRange!)
print(tappedWord ?? "Unable to get word")
}
}
}
#objc func doneTagSelection() {
// This is not the text view's responsibility, manage in the
// view controller using the delegate
// navigationController?.setToolbarHidden(true, animated: false)
// tB.isEnabled = true
tap.isEnabled = false
isEditable = true
isSelectable = true
firstTimeGrouped = false
// Hand over responsibility of this action back whatever
// has subscribed as the delegate to implement anything else
// for this action
tagDelegate?.customTextViewDidFinishSelection(self)
}
}
And finally use it like so
class ThemeVC: UIViewController {
// Change UITextView to CustomTextView
#IBOutlet weak var themeTextView: CustomTextView!
var tB = UIBarButtonItem()
// If you do not set up the delegate in your
// storyboard, you need to it in your code
// call this function from didLoad or something
// if needed
private func configureTextView() {
themeTextView.tagDelegate = self
}
// All your other implementation
}
extension ThemeVC: CustomTextViewTagDelegate {
func customTextViewDidStartSelection(_ textView: CustomTextView) {
navigationController?.setToolbarHidden(false,
animated: false)
tB.isEnabled = false
}
func customTextViewDidFinishSelection(_ textView: CustomTextView) {
navigationController?.setToolbarHidden(true,
animated: false)
tB.isEnabled = true
}
}
I did not add addTagSelectorToolBar as part of the CustomTextView implementation as this is not a good candidate to be part of that module as all of its code is related to the view controller so i don't recommend making a part of the CustomTextView implementation.

How to make scrollView scrolling only if keyboard appears in swift

I am using scrolview for view with height 1000, initially i don't want my scrolView to scroll. if i tap on any textField then i want my scrolview to scroll and if i return keyboard then i don't want my scrollview to scroll.
Here i am able to textfield up when when keyboard appears but i am unable to return textfield to its orginal position when i return keyboard and when i return keyboard i dont want my view to scroll,
Please help me in the code.
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var scrolView: UIScrollView!
#IBOutlet weak var upTFLD: UITextField!
var activeTextField = UITextField()
#IBOutlet weak var downTFLD: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
upTFLD.delegate = self
downTFLD.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardAppear(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardDisappear(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
}
#objc func onKeyboardAppear(_ notification: NSNotification) {
let info = notification.userInfo!
let rect: CGRect = info[UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
let kbSize = rect.size
let insets = UIEdgeInsets(top: 0, left: 0, bottom: kbSize.height+20, right: 0)
self.scrolView.contentInset = insets
self.scrolView.scrollIndicatorInsets = insets
var visibleRect: CGRect = self.scrolView.convert(self.scrolView.bounds, to: self.view)
visibleRect.size.height -= rect.size.height;
let inputRect: CGRect = self.activeTextField.convert(self.activeTextField.bounds, to: self.scrolView)
if (visibleRect.contains(inputRect)) {
self.scrolView.scrollRectToVisible(inputRect, animated: true)
}
}
#objc func onKeyboardDisappear(_ notification: NSNotification) {
self.scrolView.contentInset = UIEdgeInsets.zero
self.scrolView.scrollIndicatorInsets = UIEdgeInsets.zero
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
activeTextField = textField
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
activeTextField.resignFirstResponder()
return true
}
}
initially i dont want scrolling, and if i return keyboard i need textfield come to its original position and no scrolling again.
Only keyboard appears then only i need scrolling. please help me in the code.
You just need to set the contentOffset of your ScrollView right after the keyboard is hidden.
Create a variable to store offsetBeforeShowKeyboard
var offsetBeforeShowKeyboard: CGFloat?
When view is initially loaded:
self.scrollView.isScrollEnabled = false
When select any TextField:
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.scrollView.isScrollEnabled = true
if (self.offsetBeforeShowKeyboard == nil) {
self.offsetBeforeShowKeyboard = self.scrollView.contentOffset
}
}
When keyboard is hidden
#objc func onKeyboardDisappear(_ notification: NSNotification) {
self.scrollView.isScrollEnabled = false
if let offset = self.offsetBeforeShowKeyboard {
self.scrolView.setContentOffset(offset, animated: true)
}
self.offsetBeforeShowKeyboard = nil
}
In viewWillAppear
yourScrollview.isScrollEnabled = false
After Keyboard appears make it true
yourScrollview.isScrollEnabled = true
Alternatively you can use IQKeyboard manager to take care of textfields.Checkout: IQKeyboardManager

User Input Data does not remain after viewing other ViewControllers

The user input is typed into the TextFields and TextViews on ViewController1, but when I select a button to show either ViewController2 or ViewController3 via a segue, on my return to ViewController1 (also via another segue), the input is no longer there, as if the app was just reopened.
How do I make the initial user input remain in the text fields and text views while the user switches to a different view and also until the user hits the "Send Email" button back on ViewController1?
Below is my code
ViewController1
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var DateTextField: UITextField!
#IBOutlet weak var ScrollView: UIScrollView!
#IBOutlet weak var FirstTextView: UITextField!
#IBOutlet weak var FirstName: UITextField!
#IBOutlet weak var OtherDetailsField: UITextView!
lazy var datePicker: UIDatePicker = {
let picker = UIDatePicker()
picker.datePickerMode = .date
picker.addTarget(self, action: #selector(datePickerChanged(_:)), for: .valueChanged)
return picker
}()
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
// Adjust Scroll for Keyboard ------------------
#objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
ScrollView.contentInset = .zero
} else {
ScrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
ScrollView.scrollIndicatorInsets = ScrollView.contentInset
// let selectedRange = OtherDetailsField.selectedRange
// OtherDetailsField.scrollRangeToVisible(selectedRange)
}
override func viewDidLoad(){
super.viewDidLoad()
// Adjust Scroll for Keyboard ---------------
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
// Date Picker ---------------
DateTextField.inputView = datePicker}
#objc func datePickerChanged(_ sender: UIDatePicker){
DateTextField.text = dateFormatter.string(from: sender.date)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?){view.endEditing(true)
}
// Dismiss Keyboard ------------------
func setupKeyboardDismissRecognizer(){
let tapRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(
target: self, action: #selector(ViewController.dismissKeyboard))
tapRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapRecognizer)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
// Add Done Button to keypad toolbar -----------------
extension UITextField{
#IBInspectable var doneAccessory: Bool{
get{
return self.doneAccessory
}
set (hasDone) {
if hasDone{
addDoneButtonOnKeyboard()
}
}
}
func addDoneButtonOnKeyboard() {
let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
doneToolbar.barStyle = .default
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneButtonAction))
let items = [flexSpace, done]
doneToolbar.items = items
doneToolbar.sizeToFit()
self.inputAccessoryView = doneToolbar
}
#objc func doneButtonAction()
{
self.resignFirstResponder()
}
}
extension UITextView{
#IBInspectable var doneAccessory: Bool{
get{
return self.doneAccessory
}
set (hasDone) {
if hasDone{
addDoneButtonOnKeyboard()
}
}
}
func addDoneButtonOnKeyboard() {
let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
doneToolbar.barStyle = .default
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.doneButtonAction))
let items = [flexSpace, done]
doneToolbar.items = items
doneToolbar.sizeToFit()
self.inputAccessoryView = doneToolbar
}
#objc func doneButtonAction() {
self.resignFirstResponder()
}
}
ViewController2
import UIKit
class ViewController2: UIViewController {
#IBOutlet weak var ScrollView: UIScrollView!
// Adjust Scroll for Keyboard ------------------
#objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
ScrollView.contentInset = .zero
} else {
ScrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
ScrollView.scrollIndicatorInsets = ScrollView.contentInset
//let selectedRange = yourTextView.selectedRange
//yourTextView.scrollRangeToVisible(selectedRange)
}
override func viewDidLoad() {
super.viewDidLoad()
// Adjust Scroll for Keyboard ---------------
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
// Dismiss Keyboard ------------------
func setupKeyboardDismissRecognizer(){
let tapRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(
target: self, action: #selector(ViewController.dismissKeyboard))
tapRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapRecognizer)
}
func dismissKeyboard()
{
view.endEditing(true)
}
}
ViewController3
import UIKit
class ViewController3: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
short answer: backwards segues that You used create new instances, You could use 'dismiss' method instead or use Tab bar controller.
long answer:
Firstly, I assume You created Single View Controller when creating project in Xcode, and all segues You used are of "show" type.
Lets say You create segue 'ViewController1->ViewController2' and button 'NEXT' to execute it.
When You click on 'NEXT' You create a new instance of ViewController2, something kinda "new copy".
Next You create another segue to go back:
'ViewController2->ViewController1' and button 'BACK' to execute.
when You click on 'BACK' button (which triggers segue), You're NOT going back to the original ViewController1, but You're creating a new instance of ViewController1.
So as You can see, using segues this way can create future memory problems when users could go back and forth, every time creating new instance, stacking windows on top of each other.
The easiest solution is to delete segues that point backwards and instead put 'dismiss' method inside your 'BACK' button codeblock:
self.dismiss(animated: true, completion: nil)
Every time You use 'dismiss' method, You close the actual ViewController and go back to the previous one.
If You want something like 'HOME' button to go from any ViewController back to the first one (root), You can use this code:
self.view.window!.rootViewController?.dismiss(animated: true,completion: nil)
But remember - If You don't write some code to save Your data before dismissing, You will still lose the ViewController2 and ViewController3 data when You dismiss them.
Finally, if You need to keep Your data displayed when user switches between ViewControllers, I would use Tab bar controller. You can create it using template 'Tabbed App' when You create Xcode project.
Hope this helps! And don't take it as 100%, I'm still a Swift Rookie :)

How to dismiss UIView on tap Swift4

I have a UIView that is showed every time I press a button in another view
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBAction func showView(_ sender: Any) {
view2.isHidden = false
}
What I want is to add a tap gesture that allows me to hide view2 every time I tap outside of the view and, since those views are draggable, I want the second view not to be tappable when hidden ( so that if I touch under my view I don't risk to move it.
This is what I tried:
var gesture : UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(closeView), name: NSNotification.Name("CloseView"), object: nil)
gesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.closeView))
}
#objc func closeView() {
if view2.isHidden == false {
view2.isUserInteractionEnabled = true
view2.isHidden = false
self.view.removeGestureRecognizer(gesture!)
} else {
view2.isHidden = true
view2.isUserInteractionEnabled = true
self.view.addGestureRecognizer(gesture!)
}
}
let closeTapGesture = UITapGestureRecognizer(target: view, action: #selector(getter: view2.isHidden)
view.addGestureRecognizer(closeTapGesture)
None of this work, how can I do?
You need to check if you actually tapped outside of view2:
var gesture : UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(closeView), name: NSNotification.Name("CloseView"), object: nil)
let gesture = UITapGestureRecognizer(target: self, action: #selector(closeView(_:)))
view.addGestureRecognizer(gesture)
self.gesture = gesture
}
#objc private func closeView(_ tapGestureRecognizer: UITapGestureRecognizer) {
let location = tapGestureRecognizer.location(in: view2)
guard view2.isHidden == false,
!view2.bounds.contains(location) else { //We need to have tapped outside of view 2
return
}
view2.isHidden = true
}
Your tap gesture should only handle closeView .
#objc func closeView() {
view2.isHidden = true
view2.isUserInteractionEnabled = false
gesture?.isEnabled = false
}
And the the button click to show your view2 should call this.
func showView() {
view2.isHidden = false
view2.isUserInteractionEnabled = true
gesture?.isEnabled = true
}

Constraint's constant not updating in custom UIView's methods

I am trying to update a button's constraints when the keyboard is shown and hidden by adding/subtracting the keyboard's height from the constraint's constant.
I had this working previously, but after some re-factoring, it's stopped working. Previously, keyboardWillShow: and keyboardWillHide: were implemented exactly as shown below. I've since tried to use setNeedsUpdateConstraints and setNeedsLayout to try to force a refresh, to no avail.
When doing some simple print() debugging, buttonHorizontalConstraint.constant does get updated, but the changes just aren't reflected visually.
RegistrationNameView.swift
class RegistrationNameView: UIView {
let questionLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFontOfSize(21.0)
label.text = "Hey, what's your name?"
label.textAlignment = .Center
label.textColor = UIColor.lightGrayColor()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let nameField: UITextField = {
let field = UITextField()
field.autocorrectionType = .No
field.font = UIFont.boldSystemFontOfSize(28.0)
field.placeholder = "Full name"
field.returnKeyType = .Next
field.textAlignment = .Center
field.translatesAutoresizingMaskIntoConstraints = false
return field
}()
let nextButton: UIButton = {
let button = UIButton()
button.setTitle("Continue", forState: .Normal)
button.setTitleColor(UIColor.whiteColor(), forState: .Normal)
button.titleLabel?.font = UIFont.boldSystemFontOfSize(17.0)
button.backgroundColor = UIColor(red: 0.263, green: 0.910, blue: 0.847, alpha: 1)
button.layer.cornerRadius = Global.UISizes.CornerRadius
button.translatesAutoresizingMaskIntoConstraints = false
button.contentEdgeInsets = UIEdgeInsetsMake(16.0, 0, 16.0, 0)
return button
}()
var buttonHorizontalConstraint = NSLayoutConstraint()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.whiteColor()
// Add subviews
self.addSubview(questionLabel)
self.addSubview(nameField)
self.addSubview(nextButton)
nameField.becomeFirstResponder()
// Constraint helpers
let spacer = Global.UISizes.SpacingUnit
let margins = self.layoutMarginsGuide
let layoutConstraints: [NSLayoutConstraint] = {
var constraints = [NSLayoutConstraint]()
// Title Label Constraints
constraints.append(
questionLabel.bottomAnchor.constraintEqualToAnchor(nameField.topAnchor, constant: -(spacer * 2)))
constraints.append(questionLabel.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
constraints.append(questionLabel.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))
// Description Label Constraints
constraints.append(nameField.topAnchor.constraintEqualToAnchor(margins.centerYAnchor, constant: spacer * -12))
constraints.append(nameField.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
constraints.append(nameField.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))
// Sign Up Button Constraints
self.buttonHorizontalConstraint = nextButton.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor, constant: -(spacer * 2))
constraints.append(self.buttonHorizontalConstraint)
constraints.append(nextButton.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor))
constraints.append(nextButton.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor))
return constraints
}()
NSLayoutConstraint.activateConstraints(layoutConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo! as NSDictionary).objectForKey(UIKeyboardFrameBeginUserInfoKey)?.CGRectValue.size {
self.buttonHorizontalConstraint.constant -= keyboardSize.height
}
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo! as NSDictionary).objectForKey(UIKeyboardFrameBeginUserInfoKey)?.CGRectValue.size {
self.buttonHorizontalConstraint.constant += keyboardSize.height
}
}
}
RegistrationNameViewController.swift
class RegistrationNameViewController: UIViewController {
var nameView: RegistrationNameView! { return self.view as! RegistrationNameView }
override func viewDidLoad() {
super.viewDidLoad()
let nameView = RegistrationNameView(frame: CGRectZero)
nameView.nextButton.addTarget(self, action: "nextStep:", forControlEvents: .TouchUpInside)
self.view = nameView
}
func nextStep(sender: AnyObject) {
print("going to the next step \(sender)")
let credentialsViewController = RegistrationCredentialsViewController()
self.navigationController?.pushViewController(credentialsViewController, animated: true)
}
}
RegistrationNavigationController.swift
class RegistrationNavigationController: UINavigationController, UINavigationControllerDelegate {
var nameViewController: RegistrationNameViewController = RegistrationNameViewController()
var credViewController: RegistrationCredentialsViewController = RegistrationCredentialsViewController()
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
self.delegate = self
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
let type = String(viewController.dynamicType)
switch type {
case "RegistrationNameViewController":
// Add keyboard notifications to RegistrationNameViewController
NSNotificationCenter.defaultCenter().addObserver(nameViewController.view,
selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(nameViewController.view,
selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
case "RegistrationCredentialsViewController":
// Remove keyboard notifications to RegistrationNameViewController before
// registering for new notifications
NSNotificationCenter.defaultCenter().removeObserver(nameViewController.view)
// Add keyboard notifications to RegistrationCredentialsViewController
NSNotificationCenter.defaultCenter().addObserver(credViewController.view,
selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(credViewController.view,
selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
default:
print("Default")
}
}
}
Thank you for the help!
I solved this on my own after some digging.
I believe the problem is that the NSNotificationCenter observer registrations were happening on a background thread, which caused the methods that actually impacted UI to not actually change the UI.
Instead of registering observers in navigationController:willShowViewController:animated:, I register them in viewWillAppear and unregister them in viewWillDisappear. This happens in RegistrationNameViewController instead of RegistrationNavigationController.
RegistrationNameViewController.swift
override func viewWillAppear(animated:
NSNotificationCenter.defaultCenter().addObserver(self.view,
selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self.view,
selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
(self.view as! RegistrationNameView).nameField.becomeFirstResponder()
}
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self.view)
}
This makes navigationController:willShowViewController:animated: unnecessary, and it can be removed from RegistrationNavigationController.swift

Resources