NSUserDefaults key becomes nil when sent between view controllers - ios

I need to send a username between two view controllers so that the second view controller knows who to send a message to. I have tried prepareForSegue, however I have found that the variable passed cannot be dynamically altered. I decided to use NSUserDefaults, which worked very well for the length of my development process. Today, it stopped working. I do not think I deleted anything or made an changes, but nevertheless NSUserDefaults is no longer reliably carrying the value between the two view controllers. Every once in a while (maybe 20% of the time?) the value will be correctly passed. The rest of the time, nothing comes through.Code:
Set key:
func chooseFriend(sender: UIButton) {
let requestIndex = sender.tag
let friendChosen = self.friends.objectAtIndex(requestIndex) as! String
NSUserDefaults.standardUserDefaults().setValue("thisisatest", forKey: "testKey")
NSUserDefaults.standardUserDefaults().synchronize()
self.performSegueWithIdentifier("toChat", sender: self)
}
Note: In the viewDidLoad I set testKey = ""
Retrieve key on new view controller:
override func viewDidLoad() {
super.viewDidLoad()
let theKey = NSUserDefaults.standardUserDefaults().valueForKey("testKey")
print("The Key: \(theKey)")
refreshTable()
let swipe: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "dismissKeyboard")
swipe.direction = UISwipeGestureRecognizerDirection.Down
self.view.addGestureRecognizer(swipe)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
testLabel.text = ""
self.messages.addObject("Loading......")
}
Every time, the default comes up empty: The Key: Optional()I can successfully print the default after setting it, however it disappears once I am segued to the next view controller...If anyone else has experienced this problem please let me know.Thanks
Randy's code:
func chooseFriend(sender: UIButton) {
let requestIndex = sender.tag
let friendChosen = self.friends.objectAtIndex(requestIndex) as! String
// Instantiate the second view controller via t's identifier in the storyboard
if let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ChatVC") as? chatViewController {
// Set the chosen friend
secondViewController.friendChosen = friendChosen
self.presentViewController(secondViewController, animated: true, completion: nil)
}
}
Added this to destinationviewcontroller:
var friendChosen: String!

The methods for NSUserDefaults are setObject:forKey: and objectForKey:, not setValue:forKey: (Or look at the special methods for specific object types, like setBool:forKey: or stringForKey: (I don't think there's a custom set method for strings.))
The methods with "value" in their names are KVC methods.
But, as Randy says, using your app's model is a better way to go, or passing the information directly to a property in the destination view controller in prepareForSegue. Using NSUserDefaults would not be my first, or my second, choice in this situation.

It looks like you're using storyboards already so it should be pretty easy to pass information using prepareForSegue like this.
class DestinationVC : UIViewController {
var destName : String!
override func viewWillAppear(animated: Bool) {
//configure UI with the destName
self.label.text = destName
}
}
class PresentingVC : UIViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destinationVC = segue.destinationViewController as? DestinationVC {
destinationVC.destName = "Some String to Pass"
}
}
}
As already mentioned NSUserDefaults is not ideal. You will also be loosing type safety and relying on string matching with keys in NSUserDefaults rather than autocompleting and compiler checking with a var on the destinationVC. It's also good practice to limit where your data is kept and where it could be altered. Storing something in NSUserDefaults when the use case is quite confined will make it more difficult to write focussed tests and make it vulnerable to change from any class anywhere in the app. It may be an edge case but starting a pattern like this in your app could expose you to all sorts of side effect bugs in the future.

Ultimately, this type of information should be passed from view controller to view controller in a model via a delegate. That would be the "appropriate" way to achieve this behavior via a true MVC pattern.
Having said that; I think the quickest fix for you would be not to use segues and to avoid NSUserDefaults all together.
Try the following...
func chooseFriend(sender: UIButton) {
let requestIndex = sender.tag
let friendChosen = self.friends.objectAtIndex(requestIndex) as! String
// Instantiate the second view controller via it's identifier in the storyboard
if let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier("SecondViewControllerIdentifier") as? SecondViewController {
// Set the chosen friend
secondViewController.friendChosen = friendChosen
self.presentViewController(secondViewController, animated: true, completion: nil)
}
}
And in the SecondViewController add the following property.
var friendChosen: String!
Please make sure the value is not nil prior to passing it to the destination view controller

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
}

How to read ble peripheral value correctly in swift?

I am trying to read a byte array from my one view controller to another, please find my code below.
From my First View
class First: UIViewController {
var myByteArray = [UInt8](repeating:0, count: 20)
viewDidLoad(){
......}
Few statements later hers where I read my data in a function
func passThis(){
let ReceiveData = rxCharacteristic?.value
if let ReceiveData = ReceiveData {
let ReceivedNoOfBytes = ReceiveData.count
myByteArray = [UInt8](repeating: 0, count: ReceivedNoOfBytes)
(ReceiveData as NSData).getBytes(&myByteArray, length: ReceivedNoOfBytes)
print("Data Received ",myByteArray)
}
This is my Second View that I'm trying to read my array from First View
class Second: UIViewController {
var myByteArray2 = [UInt8](repeating: 0, count: 20)
viewDidLoad(){
super.viewDidLoad()
let fvc = First()
myByteArray2 = fvc.myByteArray
print(myByteArray2)
}
Now I have [11,12,13,14,15,16,17,18,19,20] from myByteArray
but have [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] from myByteArray2 ?
Can somebody help?
Also how do I clear the readValue buffer in rxCharacterstic before writing and reading new values?
Any help/comments appreciated.
Thanks
EDIT -> How my passing is done
From BLECentral
class BLECentral: ...
var centralManager: CBCentralManager!
//After Scanning and connecting
func centralManager(_central: CBCentralManager, didConnect peripheral: CBPeripheral){
peripheral.delegate = self
peripheral.discoverServices([BLEUUID])
//Once connected, move to new view controller to manager incoming and outgoing data
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let firstVC = storyboard.instantiateViewController(withIdentifier: "First") as! First
firstVC.peripheral = peripheral
navigationController?.pushViewController(firstVC, animated: true)
}
Now in my First under prepare for segue block I'm passing the peripheral like this
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is Second
{
let vc2 = segue.destination as? Second
vc2.periperal = blePeripheral
}
}
You are creating a new First view controller from your Second view controller, instead of accessing the one already created.
//Create a new View Controller
let fvc = First()
So you have two First that are made now. What you want to do is access the already created First view controller. Assuming there is no other way, you want to have a "Singleton". This is a very bad way to handle this, as I'll explain later, and there is most likely a better way, but I'm going to give a solution to access First from Second if they never communicate, but First is already created.
If you declare in First a piece of code like:
static let shared = First()
Then that singleton can be accessed via
let first = First.shared
That being said, that's a really bad way of handling communication between view controllers. If you call Second from First, you should pass the data from First to Second (or you could pass a reference of First to Second so Second can access First).
There is generally good ways to pass data between view controllers in the
func prepare(for segue: UIStoryboardSegue, sender: Any?)
method before you navigate. Whenever, whatever, makes the Second view controller should pass it the data it needs.
Lastly, another reason the Singleton view controller is a terrible idea is that it gets away from the MVC concept. So if you can't pass the proper data to Second, you probably need a new class that handles the data for you that both First and Second can work with which is the Model in MVC.

Passing data between view controllers through segue

I have a MapViewController with a prepareForSegue(_:sender:)method, which I intend to use to send data to LandmarkTableViewController, and is called when a button is pressed.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationvc = segue.destinationViewController
if let landmarkvc = destinationvc as? LandmarkTableViewController {
if let identifier = segue.identifier {
let library = Landmark(name: "Run Run Shaw Library", properties: ["Chinese Kanji", "Gray", "Green Plants"])
let bank = Landmark(name: "Hang Seng Bank", properties: ["Chinese Kanji", "Green"])
switch identifier {
case "showLibrary" : landmarkvc.passedLandmark = library // pass data to LandmarkTableViewController
case "showBank" : landmarkvc.passedLandmark = bank // pass data to LandmarkTableViewController
default : break
}
}
}
}
The LandmarkTableViewController is properly set up to display the String array properties, with one String on each row. So what I intend to do is pass the appropriate data for the table to properties according to which button was pressed, and let LandmarkTableViewController display the corresponding properties.
class LandmarkTableViewController: UITableViewController {
var properties = [String]()
var passedLandmark = Landmark(name: "temp", properties: ["temp"]) // initially set to default value
override func viewDidLoad() {
super.viewDidLoad()
loadSampleProperties()
}
func loadSampleProperties() {
self.properties = passedLandmark!.properties
}
// other methods....
}
class Landmark {
var name: String
var properties: [String]
init?(name: String, properties: [String]) {
self.name = name
self.properties = properties
// Initialization should fail if there is no name or if there is no property.
if name.isEmpty || properties.isEmpty {
return nil
}
}
However, when I run the code, only temp is displayed in the table view. I've been stuck on this for a long time now, so any help is much appreciated!
Edit: loadData() inside of viewDidLoad() is changed to the correct loadSampleProperties(). I made an error while posting the code to the question.
I think this should solve your problem if not double check your identifiers
and you can make sure to data passing with adding print(passedLandmark) to viewDidLoad() or breakpoint to make sure you getting the data
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationvc = segue.destinationViewController
if let landmarkvc = destinationvc as? LandmarkTableViewController {
if segue.identifier == "showLibrary" {
let library = Landmark(name: "Run Run Shaw Library", properties: ["Chinese Kanji", "Gray", "Green Plants"])
landmarkvc.passedLandmark = library
}
if segue.identifier == "showBank" {
let bank = Landmark(name: "Hang Seng Bank", properties: ["Chinese Kanji", "Green"])
landmarkvc.passedLandmark = bank
}
}
}
Hope this will helps
Code is missing from your quote, so I can't be sure, but I assume your loadData() method is the one that reloads the table view data with Landmark you've passed in prepareForSegue. If that is the case:
viewDidLoad() is called before prepareForSegue, so that all the views and elements of the destinationViewController are loaded and ready to use. Thus, in your case, the table view is loaded with your "temp" data and nothing makes it reload when you set the proper one.
You have two options:
You could call loadData()/reloadData() in viewWillAppear for example, which is called after prepareForSegue(). Bare in mind that viewWillAppear will possibly be called again in some other navigation.
Otherwise, you could instantiate and present/push the new controller in your parent view controller, instead of using the segue.

Passing data to another ViewController in Swift

Before I begin, let me say that I have taken a look at a popular post on the matter: Passing Data between View Controllers
My project is on github https://github.com/model3volution/TipMe
I am inside of a UINavigationController, thus using a pushsegue.
I have verified that my IBAction methods are properly linked up and that segue.identifier corresponds to the segue's identifier in the storyboard.
If I take out the prepareForSegue: method then the segue occurs, but obviously without any data updating.
My specific error message is: Could not cast value of type 'TipMe.FacesViewController' (0x10de38) to 'UINavigationController' (0x1892e1c).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
if segue.identifier == "toFacesVC" {
let navController:UINavigationController = segue.destinationViewController as! UINavigationController
let facesVC = navController.topViewController as! FacesViewController
facesVC.balanceLabel.text = "Balance before tip: $\(balanceDouble)"
}
}
Below is a screenshot with the code and error.
side notes: using Xcode 6.3, Swift 1.2
A couple of things:
1: change your prepareForSegue to
if segue.identifier == "toFacesVC" {
let facesVC = segue.destinationViewController as! FacesViewController
facesVC.text = "Balance before tip: $\(balanceDouble)"
}
2: add a string variable to your FacesViewController
var text:String!
3: change the FacesViewController viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
balanceLabel.text = text
}
The reasons for all the changes: the segue destinationViewController is the actual FacesViewController you transition to -> no need for the navigationController shenanigans. That alone will remove the "case error", but another will occur due to unwrapping a nil value because you try to access the balanceLabel which will not have been set yet. Therefore you need to create a string variable to hold the string you actually want to assign and then assign that text in the viewDidLoad - at the point where the UILabel is actually assigned.
Proof that it works:
4: If you want display two decimal places for the balance you might change the String creation to something like (following https://stackoverflow.com/a/24102844/2442804):
facesVC.text = String(format: "Balance before tip: $%.2f", balanceDouble)
resulting in:

Resources