Swift: Programmatically Navigate to ViewController and Pass Data - ios

I recently started to learn swift and it has been pretty good so far. Currently I'm having an issue trying to pass data between view controllers. I managed to figure out how to programmatically navigate between two view controllers using a navigation controller. Only problem is now I'm having a hard time trying to figure out how to pass three string entered by the user (for json api) to the next view.
Here's my current attempt. Any help is much appreciated!
ViewController:
/* Get the status code of the connection attempt */
func connection(connection:NSURLConnection, didReceiveResponse response: NSURLResponse){
let status = (response as! NSHTTPURLResponse).statusCode
//println("status code is \(status)")
if(status == 200){
var next = self.storyboard?.instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
self.presentViewController(next, animated: false, completion: nil)
}
else{
RKDropdownAlert.title("Error", message:"Please enter valid credentials.", backgroundColor:UIColor.redColor(), textColor:UIColor.whiteColor(), time:3)
drawErrorBorder(usernameField);
usernameField.text = "";
drawErrorBorder(passwordField);
passwordField.text = "";
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let navigationController = segue.destinationViewController as! UINavigationController
let newProjectVC = navigationController.topViewController as! SecondViewController
newProjectVC.ip = ipAddressField.text
newProjectVC.username = usernameField.text
newProjectVC.password = passwordField.text
}
SecondViewController:
import UIKit
class SecondViewController: UIViewController {
var ip:NSString!
var username:NSString!
var password:NSString!
override func viewDidLoad() {
super.viewDidLoad()
println("\(ip):\(username):\(password)")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

The method prepareForSegue is called when your app's storyboard performs a segue (a connection that you create in storyboards with Interface Builder). In the code above though you are presenting the controller yourself with presentViewController. In this case, prepareForSegue is not fired. You can do your setup right before presenting the controller:
let next = self.storyboard?.instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
next.ip = ipAddressField.text
next.username = usernameField.text
next.password = passwordField.text
self.presentViewController(next, animated: false, completion: nil)
You can read more about segue here

Updated syntax for Swift 3:
let next = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController
next.ip = ipAddressField.text
next.username = usernameField.text
next.password = passwordField.text
self.present(next, animated: true, completion: nil)

Related

Why self delegate is nil?

I want to make a weather application by adding a city name with openweathermap api. But I could not send the city I added in AddCityViewController back to HomeViewController. Because, self?.delegate is nil, in AddCityViewController.swift
#objc private func didTapSaveButton() {
print("clicked save button")
if let city = cityTextfield.text {
let weatherURL = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(city)&APPID=b4251cb51691654da529bccf471596bc&units=imperial")!
let weatherResource = Resource<WeatherViewModel>(url: weatherURL) { data in
let weatherVM = try? JSONDecoder().decode(WeatherViewModel.self, from: data)
return weatherVM
}
Webservice().load(resource: weatherResource) { [weak self] result in
if let weatherVM = result {
if let delegate = self?.delegate {
delegate.addWeatherDidSave(vm: weatherVM)
self?.dismiss(animated: true, completion: nil)
}
}
}
}
}
When I debug the prepare function in HomeViewController.swift was not getting called.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let nav = segue.destination as? UINavigationController else {
fatalError("NavigationController not found")
}
guard let addWeatherCityVC = nav.viewControllers.first as? AddCityViewController else {
fatalError("AddWeatherCityController not found")
}
addWeatherCityVC.delegate = self
}
What I want is, I want to pass the city name back to HomeViewController when user press the save button.
extension HomeViewController: AddWeatherDelegate {
func addWeatherDidSave(vm: WeatherViewModel) {
print(vm.name)
}
}
Source code in GitHub
You are not using segue for navigation, so the prepareForSegue method won't get triggered. In your code, you are manually initialising an instance of AddCityViewController and presenting it. So to fix the issue, you have to set delegate to that instance.
#objc private func didTapAddButton() {
let vc = AddCityViewController()
vc.title = "Add City"
vc.delegate = self
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
}
Or else you can use segue for navigation.

ViewController Pushing Swift From One VC to Another VC And Returning back

Consider two view controller Controller1 and Controller2, I have created a form of many UITextField in controller 1, in that when a user clicks a particular UITextField it moves to Controller2 and he selects the data there.
After selecting the data in Controller2 it automatically moves to Controller1, while returning from controller2 to controller1 other UITextfield data got cleared and only the selected data from controller2 is found. I need all the data to be found in the UITextfield after selecting.
Here is the code for returning from Controller2 to Controller1
if(Constants.SelectedComplexName != nil)
{
let storyBoard: UIStoryboard = UIStoryboard(name: "NewUserLogin", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "NewUser") as! NewUserRegistrationViewController
self.present(newViewController, animated: true, completion: nil)
}
To pass messages you need to implement Delegate.
protocol SecondViewControllerDelegate: NSObjectProtocol {
func didUpdateData(controller: SecondViewController, data: YourDataModel)
}
//This is your Data Model and suppose it contain 'name', 'email', 'phoneNumber'
class YourDataModel: NSObject {
var name: String? //
var phoneNumber: String?
var email: String?
}
class FirstViewController: UIViewController, SecondViewControllerDelegate {
var data: YourDataModel?
var nameTextField: UITextField?
var phoneNumberTextField: UITextField?
var emailTextField: UITextField?
override func viewDidLoad() {
super.viewDidLoad()
callWebApi()
}
func callWebApi() {
//After Success Fully Getting Data From Api
//Set this data to your global object and then call setDataToTextField()
//self.data = apiResponseData
self.setDataToTextField()
}
func setDataToTextField() {
self.nameTextField?.text = data?.name
self.phoneNumberTextField?.text = data?.phoneNumber
self.emailTextField?.text = data?.email
}
func openNextScreen() {
let vc2 = SecondViewController()//Or initialize it from storyboard.instantiate method
vc2.delegate = self//tell second vc to call didUpdateData of this class.
self.navigationController?.pushViewController(vc2, animated: true)
}
//This didUpdateData method will call automatically from second view controller when the data is change
func didUpdateData(controller: SecondViewController, data: YourDataModel) {
}
}
class SecondViewController: UIViewController {
var delegate: SecondViewControllerDelegate?
func setThisData(d: YourDataModel) {
self.navigationController?.popViewController(animated: true)
//Right After Going Back tell your previous screen that data is updated.
//To do this you need to call didUpdate method from the delegate object.
if let del = self.delegate {
del.didUpdateData(controller: self, data: d)
}
}
}
push your view controller instead of a present like this
if(Constants.SelectedComplexName != nil)
{
let storyBoard: UIStoryboard = UIStoryboard(name: "NewUserLogin", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "NewUser") as! NewUserRegistrationViewController
self.navigationController?.pushViewController(newViewController, animated: true)
}
and then pop after selecting your data from vc2 like this
self.navigationController?.popViewController(animated: true)
and if you are not using navigation controller then you can simply call Dismiss method
self.dismiss(animated: true) {
print("updaae your data")
}
There are a few ways to do it, but it usually depends on how you move from VC#1 to VC#2 and back.
(1) The code you posted implies you have a Storyboard with both view controllers. In this case create a segue from VC#1 to VC#2 and an "unwind" segue back. Both are fairly easy to do. The link provided in the comments does a good job of showing you, but, depending on (1) how much data you wish to pass back to VC#1 and (2) if you wish to execute a function on VC#2, you could also do this:
VC#1:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowVC2" {
if let vc = segue.destination as? VC2ViewController {
vc.VC1 = self
}
}
}
VC#2:
weak var VC1:VC1ViewController!
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParentViewController {
VC1.executeSomeFunction()
}
}
Basically you are passing the entire instance of VC1 and therefore have access to everything that isn't marked private.
(2) If you are presenting/dismissing VC#2 from VC#1, use the delegate style as described by one of the answers.
VC#1:
var VC2 = VC2ViewController()
extension VC1ViewController: VC2ControlllerDelegate {
func showVC2() {
VC2.delegate = self
VC2.someData = someData
present(VC2, animated: true, completion: nil)
}
function somethingChanged(sender: VC2ViewController) {
// you'll find your data in sender.someData, do what you need
}
}
VC#2:
protocol VC2Delegate {
func somethingChanged(sender: VC2ViewController) {
delegate.somethingChanged(sender: self)
}
}
class DefineViewController: UIViewController {
var delegate:DefineVCDelegate! = nil
var someData:Any!
func dismissMe() {
delegate.somethingChanged(sender: self)
dismiss(animated: true, completion: nil)
}
}
}
Basically, you are making VC#1 be a delegate to VC2. I prefer the declaration syntax in VC#2 for `delegate because if you forget to set VC#1 to be a delegate for VC#2, you test will force an error at runtime.

Protocols and delegation in swift 3 not sending values between viewcontrollers

I have two viewcontrollers(SignInViewcontroller.swift and ProfilePage.swift)
I want to pass the string from SignInViewcontroller to ProfilePage viewcontroller.
I created a protocol in SignInViewcontroller.And I delegate the method in ProfilePage controller.When I send the string through protocols I didn't receive that string in ProfilePage viewcontroller Where I am wrong.please help me to solve.
Here is my code:
SignInViewController.swift
protocol sendTokenDelegate: class {
func sendToken(login:String)
}
class SignInViewController: UIViewController {
weak var delegateToken:sendTokenDelegate?
func loginAzure(email: String, password: String) {
token = "abcdefgh"
self.delegateToken?.sendToken(login: token)
}
}
ProfilePage.swift
class ProfilePage: UIViewController, UITableViewDelegate, UITableViewDataSource, sendTokenDelegate {
override func viewDidLoad() {
let signInVC = SignInViewController()
signInVC.delegateToken = self
}
func sendToken(login: String) {
self.logInToken = login
print("Login Token in Profile Page is \(login)")
}
}
In that case, you need object that will store token from SignInViewController, until ProfilePage is requesting it.
class TokenStorage {
static let shared = TokenStorage()
public var token: String = ""
}
then you receive token call:
TokenStorage.shared.token = receivedToken
and in ProfilePage request it:
print(TokenStorage.shared.token)
If you are coming from SignUpViewController to ProfilePageViewController, you can pass the string values upon navigation after getting the singIn token you want from your logingAzure() I assume:
If you navigate using segues -> self.performSegue(withIdentifier: "signUpToProfile", sender: self)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "signUpToProfile" {
if let profileVC : ProfilePageViewController =
segue.destination as? ProfilePageViewController {
profileVC.loginToken = token
}
}
}
If you are using self.navigationController?.pushViewController
let storyboard = UIStoryboard(name: "Profile", bundle: Bundle.main)
if let profileVC = storyboard.instantiateViewController(withIdentifier:
"ProfilePageViewController") as? ProfilePageViewController {
profileVC.loginToken = token
}
EDIT
If you are not going to profilePage directly from SignUpViewController,
then just save the token in your Keychain OR UserDefaults.
Do this by creating a SessionManager singleton to handle tokens and other resources when logging in or signing up
Just use "prepare for segue"
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ProfileSegue" {
if let vc = segue.destination as? ProfilePage {
vc.token = self.token
}
}
}
When you are going to NextVC that doesn't exist in the navigation controller then you have to bind data with the instance of the class when you are pushing just like that:
let vc = UIStoryboard(name: "ProfilePage", bundle: nil).instantiateInitialViewController() as! ProfilePage
vc. logInToken = "abcdefgh"
self.navigationController?.pushViewController(vc, animated: true)
class ProfilePage: UIViewController {
var logInToken = "" // you will receive the token in this variable.
}
In you case, it seems like ProfilePage doesn't exist.
NOTE:- Delegate will use just opposite case when you want to pass the value from ProfilePage to SignInViewController.
OR
If your all API wants the token so you can declare at the class level or save it to UserDefauls:
1)
var logInToken = "" // your variable visible the entire application classes
class ProfilePage: UIViewController {
}
func loginAzure(email: String, password: String) {
logInToken = "abcdefgh" //Just assign and use it
}
2)
UserDefaults.standard.set("aasdfa", forKey: "token")
let token = UserDefaults.standard.value(forKey: "token"
Also, you are doing the bad coding you have to understand the OOP's
let signInVC = SignInViewController()
signInVC.delegateToken = self
This will reperensent the seprate instance in the memory and every
object has its own properties and behavior.
Try using:
protocol sendTokenDelegate: class {
func sendToken(login:String)
}
class SignInViewController: UIViewController {
weak var delegateToken:sendTokenDelegate?
func loginAzure(email: String, password: String) {
token = "abcdefgh"
if self.delegateToken != nil{
self.delegateToken?.sendToken(login: token)
}
}
}
class ProfilePage: UIViewController, UITableViewDelegate, UITableViewDataSource, sendTokenDelegate {
override func viewDidLoad() {
//get your instantiateViewController from storyboard
let signInVC = self.storyboard?.instantiateViewController(withIdentifier: "SignInViewControllerIdentifire") as! SignInViewController
signInVC.delegateToken = self
}
func sendToken(login: String) {
self.logInToken = login
print("Login Token in Profile Page is \(login)")
}
}

Change the screen in swift

Following is the storyboard of my app:
The app establishes a connection with the server on screen1 and all the communication with the server is performed in the code of this screen only. We make a request on screen4 and send it to server through the code of screen1 and app receives the response from the sever. If app gets successful response then app should show screen5, where I get error.
I wrote different lines of code but failed. Following are line of code which I am using now:
import UIKit
class logoViewController: UIViewController {
#IBOutlet weak var act: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
self.act.startAnimating()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// syncreq function is called from the connectionViewController.
// connectionViewController is the common class for connecting to the remote server
func syncreq (JSONdata: AnyObject) { // Proceesing for PRMS response
// Getting the value from the JSON
var Successful = self.getIntFromJSON(JSONdata as NSDictionary, key: "Successful")
println("Value of Successful : \(Successful)")
if (Successful == 0){
//Method1 not worked
// let adduser = regVC()
// self.presentViewController(adducer, animated: true, completion: nil)
//Method2 not worked
//let adducer = self.storyboard?.instantiateViewControllerWithIdentifier("registrationID") as regVC
//self.navigationController?.pushViewController(adducer, animated: true)
//Method3 not worked
//let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier("registrationID") as regVC
//self.navigationController?.pushViewController(secondViewController, animated: true)
//performSegueWithIdentifier("registrationID", sender: self)
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("registrationID") as RegisterViewController
self.presentViewController(setViewController, animated: false, completion: nil)
}
else if (Successful == 1){
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("mnuID") as menuViewController
self.presentViewController(setViewController, animated: false, completion: nil)
}
}
func getIntFromJSON(data: NSDictionary, key: String) -> Int {
let info : AnyObject? = data[key]
// println("Value of data[key] : \(key)")
if let info = data[key] as? Int {
println("Value of value for \(key) : \(info)")
return info
}
else {
return 0
}
}
}
I got the following error:
Warning: Attempt to present <project.screen5: 0x7a094790> on <project.ViewController: 0x79639d90> whose view is not in the window hierarchy!
screenshot of error:
Your problem is that view of the ViewController 1 is not in the window hierarchy, thus ViewController 1 cannot present modal VC.
Cleanest fix would be to change your app architecture design - having one view controller perform all the network requests may cause more complications than just this one.
However, for 'just make it work' solution you can present modal VC from navigation controller, i.e.
self.navigationController?.presentViewController(setViewController, animated: false, completion: nil)

Protocols and Delegates in Swift

I have two View Controllers: "DiscoverViewController" and "LocationRequestModalViewController".
The first time a user opens the "DiscoverViewController", I overlay "LocationRequestModalViewController" which contains a little blurb about accessing the users location data and how it can help them.
On the "LocationRequestModalViewController" there are two buttons: "No thanks" and "Use location". I need to send the response from the user back to the "DiscoverViewController"
I have done some research and found that delegates/protocols are the best way to do it, so I followed a guide to get that working, but I'm left with 2 errors and can't figure them out.
The errors are:
On DiscoverViewController
'DiscoverViewController' is not convertible to 'LocationRequestModalViewController'
On LocationRequestModalViewController
'LocationRequestModalViewController' does not have a member name 'sendBackUserLocationDataChoice'
I've marked where the errors are happen in the following files:
DiscoverViewController.swift
class DiscoverViewController: UIViewController, UITextFieldDelegate, CLLocationManagerDelegate, LocationRequestModalViewControllerDelegate {
func showLocationRequestModal() {
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var locationRequestVC: AnyObject! = storyboard.instantiateViewControllerWithIdentifier("locationRequestVC")
self.presentingViewController?.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.tabBarController?.presentViewController(locationRequestVC as UIViewController, animated: true, completion: nil)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let vc = segue.destinationViewController as LocationRequestModalViewController
vc.delegate = self //This is where error 1 happens
}
func sendBackUserLocationDataChoice(controller: LocationRequestModalViewController, useData: Bool) {
var enableData = useData
controller.navigationController?.popViewControllerAnimated(true)
}
override func viewDidLoad() {
super.viewDidLoad()
showLocationRequestModal()
}
}
LocationRequestModalViewController
protocol LocationRequestModalViewControllerDelegate {
func sendBackUserLocationDataChoice(controller:LocationRequestModalViewController,useData:Bool)
}
class LocationRequestModalViewController: UIViewController {
var delegate:LocationRequestModalViewController? = nil
#IBAction func dontUseLocationData(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func useLocationData(sender: AnyObject) {
delegate?.sendBackUserLocationDataChoice(self, useData: true) // This is where error #2 happens
}
override func viewDidLoad() {
super.viewDidLoad()
//Modal appearance stuff here...
}
}
The answer is in your question itself. Both errors tells the exact reason.
Issue 1
let vc = segue.destinationViewController as LocationRequestModalViewController
vc.delegate = self //This is where error 1 happens
The self is of type DiscoverViewController
But you declared the delegate as:
var delegate:LocationRequestModalViewController? = nil
You need to change that to:
var delegate:DiscoverViewController? = nil
Issue 2
The same reason, LocationRequestModalViewController does not confirm to the LocationRequestModalViewControllerDelegate, change the delegate declaration.
You have defined your delegate as having type LocationRequestModalViewController which does not conform to LocationRequestModalViewControllerDelegate.

Resources