Perform Segue in ViewDidLoad [duplicate] - ios

This question already has answers here:
Perform Segue on ViewDidLoad
(12 answers)
Closed 4 years ago.
I`m trying to perform a segue if its the first time the app is loading.
I can see my print message in the debugger, but the Perform Segue is not working. I don't get any errors.
Can somebody please tell me whats wrong?
import UIKit
import LocalAuthentication
let isFirstLaunch = UserDefaults.isFirstLaunch()
extension UserDefaults {
// check for is first launch - only true on first invocation after app install, false on all further invocations
// Note: Store this value in AppDelegate if you have multiple places where you are checking for this flag
static func isFirstLaunch() -> Bool {
let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag"
let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag)
if (isFirstLaunch) {
UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag)
UserDefaults.standard.synchronize()
}
return isFirstLaunch
}
}
class loginVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if isFirstLaunch == false {
performSegue(withIdentifier: "setPassword", sender: self)
print("testFalse") }
else {
performSegue(withIdentifier: "setPassword", sender: self)
print("testTrue")}
// Do any additional setup after loading the view, typically from a nib.
}

You can't use performSegue() from within viewDidLoad(). Move it to viewDidAppear().
At viewDidLoad() time, the current view isn't even attached to the window yet, so it's not possible to segue yet.

You can also use a different approach - change the main window's rootViewController to the view controller of your choice depending on isFirstLaunchboolean
UIApplication.shared.keyWindow?.rootViewController = setPasswordViewController

Related

Get Data from UIViewController to Another UIViewController

Suppose I have a storyboard like so:
Is it possible for me to get a flag or a boolean data from A back to B? I initially thought of using delegation but most of the tutorials about it talks about sending data between UIViewControllers that are part of 1 NavigationController. In my case, the UIViewController I need to get data is outside of the navigation controller. Is there a way for me to send data from A to B despite not being embedded in the same NavigationController?
If you don't want to use delegate between the classes . One possible way is to create separated file , saved in class and fetch required data any where in navigation .
Useful class for your case would be create singleton class FlowEngine . Use getter / setter method for saving and fetching of data. Code is attached for your reference .
class FlowEngine : NSObject{
private let static shared = FlowEngine()
private var data : String
private init(){
}
func savedData(text : String){
data = text
}
func fetchSavedData() -> String{
return data // add checsk for nil values
}
}
Delegation doesn't require the ViewControllers to be in same navigation stack. You can use the same for your case. However, if you choose to go with NotificationCenter, just remember to remove the observer when appropriate.
Other answers seem to accomplish your requirements but for the sake of completeness you could try to use KVC and KVO for modifying values in A and receiving its changes in B (or any other place)
You could see a detailed explanation of how to use them in here.
You have several ways to go, depending on your needs :
Delegation
Declare a protocol in A, and make B conform to it. Set the delegate of A to B. This could be cumbersome if the navigation stack has too many level, as you would need to pass the reference of B to each ViewController between A & B
Notification / KVO
B subscribe to a notification sent by A, no reference needed, thread safe. Don't forget to unsubscribe when done.
Proxy class
Use a proxy singleton class, that will hold your data. A will write to it, and B will read it in viewWillAppear.
UserDefaults
Same concept as a Proxy Class, but the data will persist during your app life cycle and even after killing the app. It's appropriate if you want to change a flag or a setting for your user, not if you have a lot of data to hold.
Cocoa Touch uses the target-action mechanism for communication between a control and another object. More here... If you would like to use it with UIControl objects like buttons, then you can set it in Interface Builder by sending an action to the FirstResponder object.
Target-Action will start searching a VC which responds to a given method from the current first responder and then will move to the next responder and will terminate a search in a current UIWindow. Once a controller which responds to a method signature is found, the search is terminated.
class AViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func configure(with dictionary: Dictionary<String, Any>) {
print(dictionary)
}
}
class BViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let a = self.targetViewController(forAction: #selector(ViewController.configure(with:)), sender: self) as? ViewController
a?.configure(with: ["firstName": "Alex", "lastName": "Toto"])
}
}
if your A viewController is not huge, In B viewController do this :
class B : UIViewController {
var a : A! = nil
func viewDidLoad() {
super.viewDidLoad()
a = storyboard?.instantiateViewController(withIdentifier: "StoryBoard ID") as? A
if a.booleanValue == true {
// use your booleanValue
a = nil // deallocate after using your value.
}
}
}
Update (better solution)
We've had to edit a few things to the functionality which presented me with the opportunity to refactor this. I used the NSNotification way, which was way cleaner than using closures.
ViewControllerB
override func viewDidLoad() {
super.viewDidLoad()
//Observe for notification from "myIdentifier"
NotificationCenter.default.addObserver(self, selector: #selector(self.processNotification(notification:)), name: Notification.Name("myIdentifier"), object: nil)
}
//function that gets called when notification is received
//the #objc annotation is required!
#objc func processNotification(notification: Notification) {
//Do something
}
ViewControllerA
#IBAction func didTapButton(_ sender: Any) {
//Process something
// ...
//
//Post a notification to those observing "myIdentifier"
NotificationCenter.default.post(name: Notification.Name("myIdentifier"), object: nil)
self.dismiss(animated: true, completion: nil)
}
Old (but working) solution
This might be an unpopular solution but I managed to solve this with callbacks. I was looking into another possible solution which was commented NSNotification but since someone from the team already had experience with using callbacks in this manner, we decided to ultimately use that.
How we made it work:
ViewControllerB is given the actual code implementation through prepare(for segue: UIStoryboardSegue, sender: Any?) while ViewControllerC (This is the middle UIViewController in the picture) has a callback property and ViewControllerA contains the value to pass when it's about to be dismissed.
ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "secondSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[0] as! ViewControllerC
vc.completion = { hasAgreed in
//Do Something
}
}
}
ViewControllerC
class ViewControllerC: UIViewController {
var completion: ((Bool) -> ())?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "thirdSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[1] as! ViewControllerA
vc.middleController = self
}
}
ViewControllerA
class ViewControllerC: UIViewController {
var middleController: ViewControllerC?
#IBAction func didTapButton(_ sender: Any) {
self.dismiss(animated: true, completion: {
middleController?.completion(true)
})
}
}
With this, we got the data we needed from the diagram picture above.
Your best bet is to make use of NotificationCenter to achieve this.
Post notification like this:
NotificationCenter.default.post(name: Notification.Name("NotificationName"), object: nil, userInfo: ["somekey":"somevalue"])
Observe it like this:
NotificationCenter.default.addObserver(self, selector: #selector(self.dataReceived(notification:)), name: Notification.Name("NotificationName"), object: nil)
Use the following method:
#objc func dataReceived(notification: Notification) {}

Cannot take values from other view controller Swift

I want to take user settings details from this view controller and read these details to the previous view controller. I have tried many different ways, but I cannot take values until I visit this view controller
I have tried first method from this page Pass Data Tutorial
This method is also not working. I think it is very simple, but I cannot figure out the right way to do it.
class SetConvViewController: UIViewController {
var engS = "engS"
#IBOutlet weak var swithEnglish: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
if let eng2 = defaults.value(forKey: engS)
{
swithEnglish.isOn = eng2 as! Bool
}
}
let defaults = UserDefaults.standard
#IBAction func switchEng(_ sender: UISwitch) {
defaults.set(sender.isOn, forKey: engS)
}
}
If I understand you correctly from this part - „but I cannot take values until I visit this view controller” - your problem lies with the fact, that until you visit your settings, there is no value for them in UserDefaults.
If you are reading them using getObject(forKey:) method, I’d recommend you to switch to using getBool(forKey:), since it will return false even if the value has not been set yet for that key ( docs )
Anyhow, if you want to set some default/initial values you can do so in your didFinishLaunching method in AppDelegate :
if UserDefaults.standard.object(forKey: „engS”) == nil {
// the value has not been set yet, assign a default value
}
I’ve also noticed in your code that you used value(forKey:) - you should not do that on UserDefaults - this is an excellent answer as to why - What is the difference between object(forKey:) and value(forKey:) in UserDefaults?.
On a side note, if you are using a class from iOS SDK for the first time, I highly recommend looking through its docs - they are well written and will provide you with general understanding as to what is possible.
I would recommend you to store this kind of data as a static field in some object to be able to read it from any place. e.g.
class AppController{
static var userDefaults = UserDefaults.standard
}
and then you can save it in your SetConvViewController like
#IBAction func switchEng(_ sender: UISwitch) {
AppController.userDefaults.set(sender.isOn, forKey: engS)
}
and after that you can just read it from any other view controller just by calling
AppController.userDefaults
Using segues you can set to any destination whether it be next vc or previous:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PreviousVC" {
if let prevVC = segue.destination as? PreviousViewController {
//Your previous vc should have your storage variable.
prevVC.value = self.value
}
}
If you're presenting the view controller:
Destination vc:
//If using storyboard...
let destVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DestinationViewController") as! DestinationViewController
destVC.value = self.value
self.present(destVC, animated: true, completion: nil)
Previous vc:
weak var prevVC = self.presentingViewController as? PreviousViewController
if let prevVC = prevVC {
prevVC.value = self.value
}

Swift: How can I make sure the seque does not run before the http task is finished?

I want the http task to run, setting the globally defined variable jwt. Then and only then do I want to run the seque that passes the jwt to my next activity.
I know that they are running out of order because the print statements are out of order. Capture is at the bottom. I redacted the actual jwt but where the black box is, is the jwt string.
Main view controller
import UIKit
var jwt = ""
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func submitLogin() {
let url = URL(string: "http://example.com")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options:.allowFragments) as? [String:Any] {
jwt = json["jwt"] as! String
print("Checkpoint 1 " + jwt)
}
} catch let err{
print(err.localizedDescription)
}
}
task.resume()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "loginSubmit" {
if let toViewController = segue.destination as? HomeActivity {
print("Checkpoint 2 " + jwt)
toViewController.token = jwt
}
}
}
}
Second View Controller
import UIKit
class HomeActivity: UIViewController {
var token:String!
override func viewDidLoad() {
super.viewDidLoad()
renderInbox()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func renderInbox() {
print("Printing token" + token)
}
}
P.S I have only been doing Swift for less than a week. So any general tips or the like would be welcome. Such as how to pass the jwt to the seque without using a global variable. Or what that is after checkpoint 1 in the output picture.
P.P.S Sorry about the code being in and out of the Stack Overflow code blocks. If you know how to fix it let me know and I'll try
It sounds like you have a single button that triggers both a segue and a URLSession request. Since the request is asynchronous, the segue will always start before the request completes. The button triggers both at the same time, and there is no way that I know of to tell the segue to wait.
If you want the segue to start after the request, you'll need to remove the existing segue from the button (just click it and delete in Interface Builder), create a manual segue (with an identifier), and then present the segue manually using performSegue.
To create a manual segue in Interface Builder, control-drag from the ViewController icon (yellow circle with a white square inside) in the top bar of your source ViewController to anywhere in your destination ViewController.
A menu will pop up and let you select what kind of transition you want. Once it's created, go to the Attributes inspector and set a descriptive identifier.
Now you add code in the URLSession's callback to manually invoke your new segue. Add this line right after "Checkpoint 1":
performSegue(withIdentifier: "Your Identifier Here", sender: self)
Now your segue should happen only after the request is complete. This is also nice because if the request fails or you don't like the response, you can skip performing the segue at all.
No need to use segue here.
In the call back of the task, you can use presentViewController method to present the new view controller (or pushViewController if you have a navigation controller).

First time launch / segue swift

what i am trying to accomplish is once the app is launched it will check for first time use. if it is the first time use it will take you to a view controller to enter credentials, else it will take you to to the main menu of the app. this is what i have so far but every time i launch it will give me a blank page with the error message of "A segue must either have a performHandler or it must override -perform.
" i have both segues linked on storyboard. can any one please steer me in the right direction.
let defaults = UserDefaults.standard
if defaults.string(forKey: "isAppAlreadyLaunchedOnce") != nil{
print("first time")
self.performSegue(withIdentifier: "toToken", sender: nil)
}else{
defaults.set(true, forKey: "isAppAlreadyLaunchedOne")
defaults.synchronize()
print("not first")
self.performSegue(withIdentifier: "toMainMenu", sender: nil)
}
If your segue type is set to be Custom in Storyboard — you have to subclass UIStoryboardSegue with your own logic in order for it to work.
class MySegue: UIStoryboardSegue {
override func perform() {
// your custom transition logic
}
}
Otherwise just use one of the existing presets from iOS SDK.
Your method is very confusing. The way I go about this problem is shown below.
override viewDidLoad() {
super.viewDidLoad
let checkForFirstTimeLaunch = Userdefaults.standard.string(forKey: "Flag")
if checkForFirstTimeLaunch == false {
print("First time launch")
//if user launches app for the first time, it will go here
} else {
//otherwise, it will go here
}
}

Pass Data using Closures

I know that there are multiple approaches to pass data back from one controller to another like Delegates, NSNotifications. I am using another way using Closures to pass data data back. I just want to know is it safe way how I pass any data using blocks like below or should I avoid this approach.
First ViewController (where I make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {(dataReturned) -> ()in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :NSString) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnFirstValue)
}
#IBAction func returnSecondValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: returnSecondValue)
}
}
That's a very good and reasonable approach and much better than notifications.
Looking at the evolution of Cocoa API you will notice that Apple has replaced more and more delegate API with blocks / closures over the years.

Resources