Using User Defaults to store data passed from another View Controller - swift

I'm storing an integer using User Defaults, but the number I'm trying to store was transferred via segue (ageFromSettings) and needs to have a default value.
The problem is that the data that was transferred via segue gets stored when presented, but when the app restarts it stores the default value.
Any ideas how to save an item that was passed via segue?
class ViewController: UIViewController {
#IBOutlet weak var youHaveLabel: UILabel!
#IBOutlet weak var daysLeftLabel: UILabel!
#IBOutlet weak var leftToLiveLabel: UILabel!
#IBAction func settingsButtonPressed(_ sender: Any) {
performSegue(withIdentifier: "toSettings", sender: nil)
}
var daysLeft = 0
var ageFromSettings = 36500
override func viewDidAppear(_ animated: Bool) {
UserDefaults.standard.set(ageFromSettings, forKey: "persistedAge")
if let age = UserDefaults.standard.object(forKey: "persistedAge") as? Int{
print("age: \(age)")
daysLeft = age
}
if daysLeft == 36500 {
daysLeftLabel.alpha = 0
youHaveLabel.alpha = 0
leftToLiveLabel.text = "Set your age in Settings"
} else if daysLeft == -1{
leftToLiveLabel.text = "Day On Borrowed Time"
} else if daysLeft == 0{
leftToLiveLabel.text = "Statistically, you died today"
} else if daysLeft == 1{
leftToLiveLabel.text = "Day Left to Live"
} else if daysLeft < 0 {
leftToLiveLabel.text = "Days on Borrowed Time"
}
if daysLeft > -1 {
daysLeftLabel.text = String(daysLeft)
} else {
daysLeftLabel.text = String(daysLeft * -1)
}
}
}

The problem is that you are saying:
UserDefaults.standard.set(ageFromSettings, forKey: "persistedAge")
if let age = UserDefaults.standard.object(forKey: "persistedAge") as? Int{
That's nutty. In the second line, we know that UserDefaults contains a persistedAge key, and we know what its value is — because you just put it there, in the first line. And you're doing that even if you have already set a different persistedAge value on a previous run of the app.
Delete the first line. If your goal is to supply an initial default value, use register(defaults:_). That's what it's for. It will be used only just in case you have not called set(_:forKey:) yet.

In your case, you are setting the value of the persistedAge key every single time in the viewDidAppear(_:) method of your view controller. Right afterwards, you are retrieving the data and trying to read it, which is somewhat redundant. In the latter case, you will also be resetting the value of the persistedAge key every time your view controller appears.

Related

How to get a button state to save after signing out and sign back in?

I have a save button that when tapped, will save the text in a label (quoteLabel) to a user account in Firebase. The button will then hide and the unsave button will no longer be hidden so that the user can unsave if desired. Both of these buttons are able to post and delete data as desired, however, if I sign-out of the app and sign back in, the buttons are reset to their original status. How do I get the button to stay in the last state it was set to? I can't seem to get this to work. Please let me know if more information is needed, I'm still new to all this.
Edit: Updated with suggestions from below. I still can't seem to get this to work properly. When I tap just one button, they all end up being saved as tapped when I sign back in. I added how I assigned a key as well.
import Foundation
import UIKit
import FirebaseDatabase
import FirebaseAuth
class QuotesCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var quoteLabel: UILabel!
#IBOutlet weak var save: UIButton!
#IBOutlet weak var unsave: UIButton!
#IBAction func saveButton(_ sender: UIButton) {
UserDefaults.standard.set(true, forKey: uuid)
save.isHidden = true
unsave.isHidden = false
var ref: DatabaseReference?
ref = Database.database().reference()
if quoteLabel.text == "The best time to plant a tree was 20 years ago - The next best time is today - Unknown"{
guard let user = Auth.auth().currentUser?.uid else { return }
ref!.child("users").child(Auth.auth().currentUser!.uid).child("Quote").child("Quote1").setValue(quoteLabel.text!)
}
}
#IBAction func UnsaveButton(_ sender: UIButton) {
UserDefaults.standard.set(false, forKey: uuid)
save.isHidden = false
unsave.isHidden = true
var ref: DatabaseReference?
ref = Database.database().reference()
if quoteLabel.text == "The best time to plant a tree was 20 years ago - The next best time is today - Unknown"{
ref!.child("users").child(Auth.auth().currentUser!.uid).child("Quote").child("Quote1").removeValue()
}
}
func setup(with quote: Quotes){
let saveButtonShowing = UserDefaults.standard.bool(forKey: uuid)
if(saveButtonShowing){ // Retrieves the state variable from the hard drive and sets the button visibility accordingly
save.isHidden = true
unsave.isHidden = false
} else {
save.isHidden = false
unsave.isHidden = true
}
quoteLabel.text = quote.quote
}
}
This is how I tried to add a key to each quote:
import UIKit
import Foundation
let uuid = UUID().uuidString
struct Quotes {
let quote: String
let identifier: String
}
let quotes: [Quotes] = [
Quotes(quote: "The best time to plant a tree was 20 years ago - The next best time is today - Unknown", identifier: uuid),
Quotes(quote: "Everytime you spend money, you're casting a vote for the type of world you want - Anna Lappe",identifier: uuid),
Quotes(quote: "Buy less, choose well, make it last - Vivienne Westwood", identifier: uuid),
Quotes(quote: "The future depends on what we do in the present - Mahatma Gandhi", identifier: uuid),
]
The problem is that the .isHidden property of the UIButton's is a state variable, meaning it is saved in RAM. This means that once you exit the application and reopen it, your app will startup again with the default state variable settings.
If you wish to persist the state of these variables, you'll need to save the button states to the phone's hard drive. UserDefaults makes this easy.
#IBAction func saveButton(_ sender: UIButton) {
UserDefaults.standard.set(true, forKey: "saveButtonHidden") // Saves the state to the hard drive
save.isHidden = true
unsave.isHidden = false
var ref: DatabaseReference?
ref = Database.database().reference()
if quoteLabel.text == "The best time to plant a tree was 20 years ago - The next best time is today - Unknown"{
guard let user = Auth.auth().currentUser?.uid else { return }
ref!.child("users").child(Auth.auth().currentUser!.uid).child("Quote").chi.ld("Quote1").setValue(quoteLabel.text!)
}
}
#IBAction func UnsaveButton(_ sender: UIButton) {
UserDefaults.standard.set(false, forKey: "saveButtonHidden") // Saves the state to the hard drive
save.isHidden = false
unsave.isHidden = true
var ref: DatabaseReference?
ref = Database.database().reference()
if quoteLabel.text == "The best time to plant a tree was 20 years ago - The next best time is today - Unknown"{
ref!.child("users").child(Auth.auth().currentUser!.uid).child("Quote").child("Quote1").removeValue()
}
if quoteLabel.text as! NSObject == ref!.child("users").child(Auth.auth().currentUser!.uid).child("Quote").child("Quote1") {
save.isHidden = true
unsave.isHidden = false
}
}
func setup(with quote: Quotes){
let saveButtonShowing = UserDefault.standard.bool(forKey: "saveButtonHidden")
if(saveButtonHidden){ // Retrieves the state variable from the hard drive and sets the button visibility accordingly
save.isHidden = true
unsave.isHidden = false
} else {
save.isHidden = false
unsave.isHidden = true
}
quoteLabel.text = quote.quote
}
You need to store the info on local device.
the best way is to store the info on firebase according to the user info.
I think it's no problem to show the code here.

Control (UIDatePicker) doesn't trigger change event when it starts out hidden

I'm new to iOS development, but I'm having an issue with one of the views that I'm working on. I have a UIDatePicker that can either be hidden or visible depending on the state of a UISwitch. It seems that the associated #IBAction does not trigger when the view starts out hidden. It does work when the date picker starts out visible, so the IBAction is working.
Here's a simplified version of my code:
import UIKit
class StatusEditorViewController: UIViewController {
#IBOutlet var expiryPicker: UIDatePicker!
#IBOutlet var enableExpirySwitch: UISwitch!
var editingObject: StoredStatus?
private var pickerIsVisible = false
private var expiresIn: TimeInterval?
override func viewDidLoad() {
// Set a default value
expiryPicker.countDownDuration = TimeInterval(3600)
// If this view got passed an object to edit, use that for expiresIn
if let status = editingObject {
if let expires = status.expiresIn.value {
expiresIn = TimeInterval(expires)
}
}
// Hide the picker and turn off the "enable expiry" switch if we don't
// have a value yet. We'll show the picker once the switch has been pressed
pickerIsVisible = expiresIn != nil
enableExpirySwitch.isOn = expiresIn != nil
updatePicker()
}
func updatePicker() {
expiryPicker?.isHidden = !pickerIsVisible
}
#IBAction func expiryDidUpdate(_ sender: UIDatePicker) {
expiresIn = sender.countDownDuration
print(expiresIn!)
}
#IBAction func expirySwitchDidUpdate(_ sender: UISwitch) {
pickerIsVisible = sender.isOn
updatePicker()
// If the user just turned on the switch, we want to make sure we store the
// initial value already, in case the user navigated away
if (sender.isOn && expiresIn == nil) {
expiresIn = expiryPicker.countDownDuration
}
}
}
I'm not sure what's going wrong. I tried manually attaching a target (e.g. self.expiryPicker.addTarget(self, action: #selector(setExpiryValue), for: .allEditingEvents))
once the view becomes available, but that didn't work either.
I hope someone can tell me what I'm doing wrong. I'm guessing there's something fundamental that I'm doing wrong, but so far no search on Google or SO has led me to the answer.
Thanks in advance
f.w.i.w, I'm running XCode 11.7, with Swift 5, with a deployment target of iOS 13.7

Xcode finding a nil variable where none should be when methods is called from another ViewController class

I got midway through creating a MacOS app (my first app ever that isn't part of a "Your First App" tutorial) involving a lot of user options in the main window and became fed up with the fact that my ViewController file had become an unwieldy mess that was not going to be maintainable in the long run.
I decided to break it input multiple view controllers in smaller chunks to make it more manageable using container views in UIBuilder for embedding views, but all the tutorials I found were either for outdated versions of Xcode/Swift, or were about managing multiple views in iOS, so I had to extrapolate a little, and I may have done it wrong.
Now I'm getting an error on a method in one ViewController when the method is called by another ViewController, even though that method works find when called by its own view controller.
Either I'm missing something obvious, or I set things up wrong.
Global variables:
var inputPathUrl = URL?
var outputExtension: String = ""
#IBOutlets and local properties for the InOutViewController class:
#IBOutlet weak var inputTextDisplay: NSTextField!
#IBOutlet weak var outputTextDisplay: NSTextField!
#IBOutlet weak var inputBrowseButton: NSButton!
#IBOutlet weak var outputBrowseButton: NSButton!
var outputDirectoryUrl: URL?
var inputFilePath: String = ""
#IBOutlets for the OptionsViewController class
#IBOutlet weak var Button1: NSButton!
#IBOutlet weak var Button2: NSButton!
#IBOutlet weak var Button3: NSButton!
#IBOutlet weak var Button4: NSButton!
#IBOutlet weak var Button5: NSButton!
Methods for the InOutViewController class:
#IBAction func InputBrowseClicked(\_ sender: Any) {
let inputPanel = NSOpenPanel()
inputPanel.canChooseFiles = true
inputPanel.canChooseDirectories = false
inputPanel.allowsMultipleSelection = false
inputPanel.allowedFileTypes = \["aax"\]
let userChoice = inputPanel.runModal()
switch userChoice{
case .OK :
if let inputFileChosen = inputPanel.url {
inputFileUrl = inputFileChosen // define global variable that will be called by other methods in other classes to check if an input file has been chosen
updateInputText() // call methods to display path strings in text fields
updateOutputText()
}
case .cancel :
print("user cancelled")
default :
break
}
}
#IBAction func outputBrowseClicked(_ sender: Any) {
let outputPanel = NSOpenPanel()
outputPanel.canChooseFiles = false
outputPanel.canChooseDirectories = true
outputPanel.allowsMultipleSelection = false
let userChoice = outputPanel.runModal()
switch userChoice{
case .OK :
if let outputUrl = outputPanel.url {
outputDirectoryUrl = outputUrl
updateOutputText()
}
case .cancel :
print("user cancelled")
default:
break
}
}
func updateInputText() {
// call getOutputOption method to see which radio button is selected
OptionsViewController().getOutputOption()
if inputFileUrl != nil {
inputFilePath = inputFileUrl!.path
inputTextDisplay.stringValue = inputFilePath
}
}
func updateOutputText() {
// derive output file path and name from input if no output location is chosen
if inputFileUrl != nil && outputDirectoryUrl == nil {
let outputDirectory = inputFileUrl!.deletingPathExtension()
let outputDirectoryPath = outputDirectory.path
let outputPath = outputDirectoryPath + "(outputExtension)"
outputTextDisplay.stringValue = outputPath
} else if inputFileUrl != nil && outputDirectoryUrl != nil {
// derive default file name from input but use selected output path if one is chosen
let outputDirectoryPath = outputDirectoryUrl!.path
let outputFile = inputFileUrl!.deletingPathExtension()
let outputFilename = outputFile.lastPathComponent
// derive file extension from getOutputOption method of OptionsViewController class
let outputPath = outputDirectoryPath + "/" + outputFilename + "(outputExtension)"
outputTextDisplay.stringValue = outputPath
}
}
That last line (outputTextDisplay.stringValue = outputPath) is what I'm getting the fatal error on, but ONLY when I call this method from the #IBAction for the output format radio buttons in OptionsViewController to update the output display when a different file extension is chosen. When I call the method from the actions methods in InOutViewController it works fine.
Here are the #IBAction method and getOutputOption methods from the OptionsViewController class:
#IBAction func radioButtonClicked(_ sender: Any) {
getOutputOption()
// update display with new file extension
InOutViewController().updateOutputText()
}
func getOutputOption() {
// make sure an input file has been chosen
if inputFileUrl != nil {
// check which radio button is selected and derive output file format based on selection
// not sure why I need to specify the button isn't nil, since one is ALWAYS selected, but I was getting a fatal error without doing so
if (Button1 != nil) && Button1.state == .on {
outputExtension = ".extA"
} else if (Button2 != nil) && Button2.state == .on {
outputExtension = ".extB"
} else if (Button3 != nil) && Button3.state == .on {
outputExtension = ".extC"
} else if (Button4 != nil) && Button4.state == .on {
outputExtension = ".extD"
} else {
outputExtension = ".extE"
}
}
}
I'm sure I'm missing something obvious but like I said, it's my first time working with multiple view controllers and I'm not sure I've implemented them properly, and I've only been coding for a few weeks, so I can't spot where I'm going wrong.
My guess is that outputTextDisplay is an IBOutlet in InOutViewController, and it is declared as implicitly unwrapped. (Something like this: )
var outputTextDisplay: UITextField!
If you reference such a variable from one of the IBActions in your InOutViewController, all is well because at that point your view controller's views are loaded.
If, on the other hand, you call updateOutputText() from another view controller, your InOutViewController's views may not have been loaded yet, so the outlet for outputTextDisplay is still nil. When a variable is declared as implicitly unwrapped (using a ! at the end of the type) then any time you reference it, the compiler force-unwraps it, and if it's nil, you crash.
You should change your updateOutputText() to use ? to unwrap the variable. That stops implicitly unwrapped optionals from crashing. Something like this:
func updateOutputText() {
// derive output file path and name from input if no output location is chosen
if inputFileUrl != nil && outputDirectoryUrl == nil {
let outputDirectory = inputFileUrl!.deletingPathExtension()
let outputDirectoryPath = outputDirectory.path
let outputPath = outputDirectoryPath + "(outputExtension)"
outputTextDisplay?.stringValue = outputPath //Note the `?`
} else if inputFileUrl != nil && outputDirectoryUrl != nil {
// derive default file name from input but use selected output path if one is chosen
let outputDirectoryPath = outputDirectoryUrl!.path
let outputFile = inputFileUrl!.deletingPathExtension()
let outputFilename = outputFile.lastPathComponent
// derive file extension from getOutputOption method of OptionsViewController class
let outputPath = outputDirectoryPath + "/" + outputFilename + "(outputExtension)"
outputTextDisplay?.stringValue = outputPath //Note the `?`
}
}
}
The code outputTextDisplay?.stringValue = outputPath uses "optional chaining", which causes the compiler to check if outputTextDisplay is nil, and stop executing the code if it is.
Note that if you make that change, your string value won't get installed into your outputTextDisplay field when you call the updateOutputText() function. You'll need to install the value after viewDidLoad() is called. (Perhaps in your viewDidLoad or in viewWillAppear.)
Edit:
This code:
#IBAction func radioButtonClicked(_ sender: Any) {
getOutputOption()
// update display with new file extension
InOutViewController().updateOutputText()
}
Is very wrong. In response to the user clicking a radio button, the InOutViewController() bit creates a throw-away instance of an InOutViewController, tries to call its updateOutputText() method, and then forgets about the newly created InOutViewController. Don't do that.
You need a way to keep track fo the child view controllers that are on-screen. To show you how to do that you'll need to explain how your various view controllers are being created. Are you using embed segues?

How to grab other local tag data in Swift?

I am working on saving data process,
because my data is small, so I want to save in local data storage.
I want a make time schedule.
Because I have single 35 buttons and label(each buttons and label are paired), So I am using UIButton tag now. And I have a problem on using other local data "tag".
I tired to make new func on global swift file(same swift file). but it is not able to call tag, because tag is only can use when UIbutton pressed.
I want to use tag property on viewDidLoad(), not only in UIbuttonPressed.
override func viewDidLoad() {
if let items = UserDefaults.standard.array(forKey: "SubjectList") as? [String] {
subjectArray = items
UITextLabel**1**.text = subjectArray**[1-1]**
}
}
#IBAction func buttonPressed(_ sender: UIButton) {
var textField = UITextField()
let newItem = textField.text!
if sender.tag == 1{
self.subjectArray[sender.tag-1] = newItem
print("1")
}
UserDefaults.standard.set(self.subjectArray, forKey: "SubjectList")
}
Create outlet collections for all the paired labels and buttons
#IBOutlet weak var allLbls:[UILabel]!
#IBOutlet weak var allBts:[UIButton]!
In viewDidLoad
if let items = UserDefaults.standard.array(forKey: "SubjectList") as? [String] {
subjectArray = items
subjectArray.indices?.forEach {
allLbls[$0].text = subjectArray[$0]
allBts[$0].tag = $0
}
}

if statement executing itself instead of others

I have a code here that, each time I run it, only the if statement which states "All fields are required" works but NOT ONLY when it must be called, it actually runs in place of the others. So whatever I do even when all the fields are complete, I have "All fields are required" as an alert message.
Here is the code, all help is appreciated, thank you in advance.
import UIKit
class RegisterPageViewController: UIViewController {
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
#IBOutlet weak var repeatPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func registerButtonTapped(_ sender: AnyObject) {
let userEmail = ""
let userPassword = ""
let userRepeatPassword = ""
// Check for empty fields
if (userEmail.isEmpty || userPassword.isEmpty ||
userRepeatPassword.isEmpty)
{
// Display Alert Message
displayMyAlertMessage(userMessage:"All fields are required")
return
}
//Check if passwords match
if (userPassword != userRepeatPassword)
{
// Display an alert message
displayMyAlertMessage(userMessage:"Passwords do not match")
return
}
// Store data
UserDefaults.standard.set(userEmail, forKey:"userEmail")
UserDefaults.standard.set(userEmail, forKey:"userPassword")
UserDefaults.standard.synchronize()
// Display alert message with confirmation
_ = UIAlertController(title:"Alert", message:"Registration is
successfull. Thank you!",
preferredStyle:UIAlertControllerStyle.alert);
_ = UIAlertAction(title:"Ok", style:UIAlertActionStyle.default)
{
action in
self.dismiss(animated: true, completion:nil)
}
}
func displayMyAlertMessage(userMessage:String)
{
let myAlert = UIAlertController(title:"Alert", message: userMessage,
preferredStyle:UIAlertControllerStyle.alert);
let okAction = UIAlertAction(title:"Ok",
style:UIAlertActionStyle.default, handler:nil)
myAlert.addAction(okAction)
self.present(myAlert, animated:true, completion:nil)
}
}
Do you ever assign any values to userEmail, userPassword, userRepeatPassword? You initialize them as empty at the start of the function, and it looks like their values never change.
Instead of declaring them in the function, try using class level variables, and linking them to your textfields in Storyboard.
#IBOutlet weak var userEmail: UITextField!
#IBOutlet weak var userPassword: UITextField!
#IBOutlet weak var userRepeatPassword: UITextField!
#IBAction func registerButtonTapped(_ sender: AnyObject) {
// Check for empty fields
if (self.userEmail.text.isEmpty || self.userPassword.text.isEmpty || self.userRepeatPassword.text.isEmpty) {
// Display Alert Message
displayMyAlertMessage(userMessage:"All fields are required")
return
}
...
}
I suspect that this code is not an accurate representation of your implementation. Would you be able to copy and paste the registerButtonTapped(_:) function unedited?
If it is, I would agree with #unmarshalled: It appears that you have declared each of the variables with an empty string as their value. If the code you have posted is implemented exactly as above, that is the cause of your issue.
based on the code you have posted, I would also recommend the following alterations:
get the email, password & repeatPassword from outside the scope of the function: usually, by just pulling it directly from the UI, most commonly from text fields: i.e. userEmailTextField.text
extracting your user defaults keys, and any other string literals you have, into a constants file is good practice and avoid any unnecessary misspelling related bugs.
you don't need to add a handler to a UIAlertAction if all you want it to do is dismiss the alert. UIAlterController will automatically be dismissed automatically: the handler argument has a default value of nil and can be omitted, simply:
let okayButton = UIAlertAction(title: "Ok", style: .default)
Generally speaking, you don't want to store a reference to a shared instance. However, within small local scopes its a little cleaner to do so:
let userDefaults = UserDefaults.standard
userDefaults.set(userEmail, forKey:"userEmail")
userDefaults.set(userEmail, forKey:"userPassword")
userDefaults.synchronize()
Cheers :)
EDIT:
I would suggest extracting the conditional out to a computed property for readability and check if count == 0 rather than isEmpty. The advantage of this is that you can make the computed property more comprehensive, I.e this will check that the strings are not nil or empty. Usually checking the count is enough, but there’s no harm in covering your bases.
As it stands with the current UIKit implementation, UITextField.text can never be nil. That being said, official documentation does not make that guarantee explicitly, so the best way to handle it is to implement it like an optional, below.
So something like:
fileprivate var registrationFormCompleted: Bool {
guard username = usernameTextfield.text,
password = passwordTextField.text,
repeat = repeatPasswordTextField.text,
else {
return false
}
return username.count > 0 &&
password.count > 0 &&
repeat.count > 0
}
In use it would be:
#IBAction func registerButtonTapped(_ sender: AnyObject) {
// Check for empty fields
if !registrationFormCompleted {
// Display Alert Message
displayMyAlertMessage(userMessage:"All fields are required")
return
}
//....
}