I have 2 controllers inside NavigationController. First pushes the second one to the stack and user can interact with the text field there. Then (in one scenario) user will tap on back button to be taken to the previous screen. Assuming that loading of second one is 'heavy', so I will be keeping only one instance of it once it is needed.
Expected:
I would like to have keyboard hidden once back button is pressed.
Actual:
First responder keeps being restored when I go back to the second for the second time. How to prevent that? Resigning first responder also doesn't do the trick there...
Problem demo:
https://gitlab.com/matrejek/TestApp
Major code parts:
class FirstViewController: UIViewController {
var child: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "child")
return vc
}()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func onButtonTap(_ sender: Any) {
self.navigationController?.pushViewController(child, animated: true)
}
}
class SecondViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
view.endEditing(true)
}
}
This does seem odd --- and it seems like your approach should work.
Apparently (based on quick testing), since you are not allowing the Navigation Controller to release the SecondVC, the text field is remaining "active."
If you add this to SecondViewController, it will prevent the keyboard from "auto re-showing" the next time you navigate to the controller - not sure it will be suitable for you, but it will do the job:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.view.endEditing(true)
}
}
Edit: Jan 25 2020
Based on new comments, yes, this seems to be a bug.
My previous work-around answer worked -- sort of. The result was the keyboard popping up and then disappearing on subsequent pushes of child.
Following is a better work-around. We have SecondViewController conform to UITextFieldDelegate and add a BOOL class var / property that will prevent the text field from becoming first responder. Comments should be clear:
class FirstViewController: UIViewController {
var child: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "child")
return vc
}()
#IBAction func onButtonTap(_ sender: Any) {
self.navigationController?.pushViewController(child, animated: true)
}
}
// conform to UITextFieldDelegate
class SecondViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
// bool var to prevent text field re-becoming first responder
// when VC is pushed a second time
var bResist: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// assign text field delegate
textField.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// view has appeared, so allow text field to become first responder
bResist = false
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return !bResist
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// end editing on this view
view.endEditing(true)
// we want to resist becoming first responder on next push
bResist = true
}
}
Related
any help is appreciated.
I am new to Ios development and I am trying to change a label text which is located in my first initial view controller. I want this text to change as I press a button in the second view controller which is segued to the initial one.
here is my first view controller
import UIKit
protocol gameModeDelegate {
func didTapChoice(test:String)
}
class ViewController2: UIViewController {
var selectionDelegate:gameModeDelegate!
#IBAction func chooseButton(_ sender: Any) {
selectionDelegate.didTapChoice(test: "TEST")
let selectVC = storyboard?.instantiateViewController(withIdentifier: "VC1") as! ViewController
present(selectVC,animated: true,completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
here is what i have done in the second where the label is
override func viewDidLoad() {
super.viewDidLoad()
let selectVC2 = storyboard?.instantiateViewController(withIdentifier: "VC1") as! ViewController2
selectVC2.selectionDelegate = self
winningLabel.isHidden = true
winningLabel.center = CGPoint(x: winningLabel.center.x, y: winningLabel.center.y - 400)
playAgainoutlet.isHidden = true
playAgainoutlet.center = CGPoint(x: playAgainoutlet.center.x, y: playAgainoutlet.center.y + 400)
}
extension ViewController: gameModeDelegate{
func didTapChoice(test: String) {
CommunicationLabel.text = test
}
}
I tried these two methods so far and i keep getting this error.
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
You should not use this approach to achieve the result, you may use two different approaches to achieve the same result.
1- Use a delegate protocol approach:
in secondViewController you should declare a protocol like this
protocol applySelecction {
func applyText(text: String)
}
and in the class declare a variable like this.
var delegate: apply selection?
then in the button action
#IBAction func saveButtom(sender: UIButton){
//print(selected)
delegate?.applySelection(text: text) //text is the value select from UILAbel o the option the user select
self.dismiss(animated: true, completion: nil)
}
then in firstViewController conforms to applySelection protocol like this
class FirstViewController: UIViewController,applySelection{
func applyText(text: String){
//update the UIlabel here
2- Use a closure.
here in secondViewController you should add a new var like this,
var applyText: ((String) -> Void)?
then in
#IBAction func saveButtom(sender: UIButton){
self.applyText(text) //text is your text to update
}
and in firstViewController in prepare for segue rewrite like this.
let vc = segue.destination as! fisrtViewController)
vc.applyText = { [weak self] data in
guard let self = self else {return}
self.text = text //this is assigning the text to self-text supposing text is a UILabel in this viewController
}
You may try one of the two approaches which may seem right for you.
EDIT.
try this.
class ViewController2: UIViewController {
var selectionDelegate:gameModeDelegate!
#IBAction func chooseButton(_ sender: Any) {
selectionDelegate.didTapChoice(test: "TEST")
//if segue is a show segue
self.navigationController?.popViewController(animated: true)
//else is a modal segue.
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
PD. you dont have to present a viewcontroller already present in the view stack, only dissmis it. Good luck
I'm using the Hero library to do a transition between view controllers.
First view controller:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var people = [Person]()
override func viewDidLoad() {
super.viewDidLoad()
hero.isEnabled = true
hero.modalAnimationType = .push(direction: .right)
}
#IBAction func handleButton(){
let view = self.storyboard?.instantiateViewController(withIdentifier: "secondVC") as! UIViewController
present(view, animated: true, completion: nil)
}
}
Second view controller:
class Second: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
hero.isEnabled = true
// Do any additional setup after loading the view.
}
#IBAction func handleBackButton(){
hero.modalAnimationType = .push(direction: .left)
hero.dismissViewController()
}
}
It presents the new view without any animation. When I dismiss the second controller, it will apply the correct animation. How do I present the second view with an animation?
Okay found the solution.
In the first case where I have:
#IBAction func handleButton() {
let view = self.storyboard?.instantiateViewController(withIdentifier: "secondVC") as! UIViewController
present(view, animated: true, completion: nil)
}
I have to set the animation there on the view.
#IBAction func handleButton() {
let view = self.storyboard?.instantiateViewController(withIdentifier: "secondVC") as! UIViewController
view.hero.modalAnimationType = .push(direction: .right)
present(view, animated: true, completion: nil)
}
works
Set the modal animation type by setting the Hero extension heroModalAnimationType:
hero.heroModalAnimationType = .push(direction: .right)
Don't forget to disable the default animation for the transition using the Hero.shared object's methods :
func disableDefaultAnimationForNextTransition()
func setDefaultAnimationForNextTransition(_ animation: HeroDefaultAnimationType)
I have two UIViewController, when I click a button, it goes from the first view controller to the second one. And before that, I animated a UIView to move to another place. After dismissing the second View Controller, I want to move the UIView in the first view controller back to where it originally was. However, when I call a function from the second View Controller to animate the UIview in the first view controller after dismissing the second one, It could not get the UIView's properties, and cannot do anything with it. I think because the first UIViewController is not loaded yet. Is that the problem? And How should I solve this?
There are two solutions you can either use swift closures
class FirstViewController: UIViewController {
#IBAction func start(_ sender: Any) {
guard let secondController = self.storyboard?.instantiateViewController(withIdentifier: "SecondController") as? SecondController else { return }
secondController.callbackClosure = { [weak self] in
print("Do your stuff")
}
self.navigationController?.pushViewController(secondController, animated: true)
}
}
//----------------------------
class SecondController: UIViewController {
var callbackClosure: ((Void) -> Void)?
override func viewWillDisappear(_ animated: Bool) {
callbackClosure?()
}
}
or you can use protocols
class FirstViewController: UIViewController {
#IBAction func start(_ sender: Any) {
guard let secondController = self.storyboard?.instantiateViewController(withIdentifier: "SecondController") as? SecondController else { return }
secondController.delegate = self
self.navigationController?.pushViewController(secondController, animated: true)
}
}
extension ViewController : ViewControllerSecDelegate {
func didBackButtonPressed(){
print("Do your stuff")
}
}
//--------------------------
protocol SecondControllerDelegate : NSObjectProtocol {
func didBackButtonPressed()
}
class SecondController: UIViewController {
weak var delegate: SecondControllerDelegate?
override func viewWillDisappear(_ animated: Bool) {
delegate?.didBackButtonPressed()
}
}
You can try to use a closure. Something like this:
class FirstViewController: UIViewController {
#IBOutlet weak var nextControllerButton: UIButton!
private let animatableView: UIView = UIView()
private func methodsForSomeAnimation() {
/*
perform some animation with 'animatableView'
*/
}
#IBAction func nextControllerButtonAction() {
// you can choose any other way to initialize controller :)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let secondController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else { return }
secondController.callbackClosure = { [weak self] in
self?.methodsForSomeAnimation()
}
present(secondController, animated: true, completion: nil)
}
}
class SecondViewController: UIViewController {
#IBOutlet weak var dismissButton: UIButton!
var callbackClosure: ((Void) -> Void)?
#IBAction func dismissButtonAction() {
callbackClosure?()
dismiss(animated: true, completion: nil)
/*
or you call 'callbackClosure' in dismiss completion
dismiss(animated: true) { [weak self] in
self?.callbackClosure?()
}
*/
}
}
When you present your second view controller you can pass an instance of the first view controller.
The second VC could hold an instance of the first VC like such:
weak var firstViewController: NameOfController?
then when your presenting the second VC make sure you set the value so it's not nil like so:
firstViewController = self
After you've done this you'll be able to access that viewControllers functions.
iOS 11.x Swift 4.0
In calling VC you put this code ...
private struct Constants {
static let ScannerViewController = "Scan VC"
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == Constants.ScannerViewController {
let svc = destination as? ScannerViewController
svc?.firstViewController = self
}
}
Where you have named the segue in my case "Scan VC", this is what it looks like in Xcode panel.
Now in scan VC we got this just under the class declaration
weak var firstViewController: HiddingViewController?
Now later in your code, when your ready to return I simply set my concerned variables in my firstViewController like this ...
self.firstViewController?.globalUUID = code
Which I have setup in the HiddingViewController like this ...
var globalUUID: String? {
didSet {
startScanning()
}
}
So basically when I close the scanning VC I set the variable globalUUID which in term starts the scanning method here.
When you are saying it could not get the UIView's properties it's because you put it as private ? Why you don't replace your UIView in the first controller when it disappears before to go to your secondViewController. I think it's a case where you have to clean up your view controller state before to go further to your second view controller.
Check IOS lifecycle methods : viewWillDisappear or viewDidDisappear through Apple documentation and just do your animation in one of these methods.
Very simple solution actually... Just put your animation in the viewDidAppear method. This method is called every time the view loads.
class firstViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// insert animation here to run when FirstViewController appears...
}
}
i'm very excited about memory leaks and performance problems with iOS. Currently i've learnt that preventing leaks with getting avoid by retain cycles. I have a snippet below which is containts two viewcontrollers and i'm passing data with delegation. But when i equalized delegate var as nil, the deinit of viewcontroller was not called.
import UIKit
class ViewController: UIViewController, Navigator {
func passData(data: String) {
print("Passed data: " + data)
}
override func viewDidLoad() {
super.viewDidLoad()
}
deinit {
print("deinited: " + self.description)
}
#IBAction func goSecond(_ sender: UIButton) {
let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "secondVC") as! SecondVC
secondVC.delegate = self
self.present(secondVC, animated: false, completion: nil)
}
}
//second vc
import UIKit
protocol Navigator: class{
func passData(data:String)
}
class SecondVC: UIViewController {
weak var delegate:Navigator?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func GoFirst(_ sender: UIButton) {
delegate?.passData(data: "I'm second VC and Passing")
self.delegate = nil
}
}
You are misunderstood the deinit method's job. The deinit is supposed to be called when the instance of a view controller has no reference left to it. So, just simply removing the references of the properties of a view controller doesn't do the whole job.
And you have a misconception of making self.delegate = nil in your SecondVC. This should have been done in your first ViewController.
To make sense of everything, I've done a sample project where you can learn how deinits work. The main code goes here:
First View Controller
class FirstViewController: UIViewController, Navigator {
override func viewDidLoad() {
super.viewDidLoad()
}
deinit {
print("First view controller's deinit called")
}
func passData(data: String) {
print("In First view controller: \(data)")
}
#IBAction func gotoSecond(_ sender: UIButton) {
let viewcontroller = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
viewcontroller.delegate = self
show(viewcontroller, sender: self)
}
}
Second View Controller
protocol Navigator {
func passData(data:String)
}
class SecondViewController: UIViewController {
weak var delegate:Navigator?
override func viewDidLoad() {
super.viewDidLoad()
}
deinit {
print("Second view controller's deinit called")
}
#IBAction func closeButton(_ sender: UIButton) {
delegate?.passData(data: "Delegation from second view controller")
dismiss(animated: true, completion: nil) //when this line executes, the instance of this class is de-referenced. This makes the call to deinit method of this class.
}
}
So, when dismiss happens for second view controller, the reference count goes to 0 for second view controller and this does the job for calling deinit method of second view controller.
But you technically don't call the deinit of the first view
controller as you don't actually de-reference the first view
controller.
You can find the whole project here.
Here is my demo project.
I have two view controllers. The main one has the status bar hidden while the second one hasn't.
I created a custom driven transition animation to go from controller one to controller two.
When I'm on the child view controller (the orange one), I start the driven transition by panning from top to bottom. You can see that the status bar is coming back when dragging. And the UIButton "Hello" is moving as well.
I cancel the transition. Then I start it again and you can see the status bar is coming back as well but this time, my button isn't moving, it stays at the same location, as if the status bar was still hidden.
Any idea why it would behave like this once the transition has been cancelled at least once?
(I'm not even talking about the weird thing with the animation that is kind of doubled when cancelling (maybe a bug with the simulator as it doesn't do it on my iphone 6 9.1 and my iphone 5 8.4.)
Add: import Foundation
Then add an outlet:
class ViewController: UIViewController {
#IBOutlet weak var topConstraint: NSLayoutConstraint!
...
}
Then change the value to 0 when the view disappears and then to 20 when it will appear:
override func viewWillAppear(animated: Bool) {
topConstraint.constant = 20.0
}
override func viewWillDisappear(animated: Bool) {
topConstraint.constant = 0.0
}
Full code (make sure to remember to connect the constraint to the outlet):
import UIKit
import Foundation
class ViewController: UIViewController {
#IBOutlet weak var topConstraint: NSLayoutConstraint!
let controllerTransition = InteractiveControllerTransition(gestureType: .Pan)
let controllerTransitionDelegate = ViewController2Transition()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
controllerTransition.delegate = controllerTransitionDelegate
controllerTransition.edge = .Bottom
}
override func viewWillAppear(animated: Bool) {
topConstraint.constant = 20.0
}
override func viewWillDisappear(animated: Bool) {
topConstraint.constant = 0.0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func unwindToViewController(sender: UIStoryboardSegue) { }
override func prefersStatusBarHidden() -> Bool {
return false
}
#IBAction func helloButtonAction(sender: UIButton) {
// let storyBoard = UIStoryboard(name: "Main", bundle: nil)
// let vc = storyBoard.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
//
// vc.transitioningDelegate = controllerTransition
// controllerTransition.toViewController = vc
//
// self.presentViewController(vc, animated: true, completion: nil)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
// let nvc = storyBoard.instantiateViewControllerWithIdentifier("NavigationViewController2") as! UINavigationController
// let vc = nvc.topViewController as! ViewController2
let vc = storyBoard.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
// nvc.transitioningDelegate = controllerTransition
vc.transitioningDelegate = controllerTransition
controllerTransition.toViewController = vc
// self.presentViewController(nvc, animated: true, completion: nil)
self.presentViewController(vc, animated: true, completion: nil)
}
}