User defaults not saving, not retrieving data - swift
I am successfully using user defaults on other pages of my app, it works like you would expect. In this specific controller I've called a method from a struct with parameters and assigned that as a constant. As far as I can tell, for some reason this configuration will not save or retrieve data from user defaults. I am sure there is a way but I don't know the proper way. Some guidance would help a lot here.
So, I'm trying to save textfields to user defaults and call textfield data as well as method once app reloads so that user has all old data back in tact. Right now nothing happens, I can't even print to troubleshoot if anything gets saved because I really don't know how to print saved state. Noob here.
So far I have tried moving the saving point to various places, before method call on button press, after call, tried inserting user defaults into struct method also, all no go! I've also tried various retrieval methods in view did load but no go so far.
Here is where my code sits currently, non-operational user default saving:
override func viewDidLoad() {
area.delegate = self
volume.delegate = self
height.delegate = self
// Check to see if there is a saved state, if there is then use those numbers and run method for even gauge
let savedArea = StateManager.retrieveClearCalcValue(key: StateManager.areaKey) as? Double
let savedVolume = StateManager.retrieveClearCalcValue(key: StateManager.volumeKey) as? Double
let savedHeight = StateManager.retrieveClearCalcValue(key: StateManager.lengthKey) as? Double
// If there is data in saved states, set text fields to saved data and call calculate
if (savedArea != nil) && (savedVolume != nil) && (savedHeight != nil) {
area.text = String(savedArea!)
volume.text = String(savedVolume!)
height.text = String(savedHeight!)
let result = zoneCalc.clearCalc(area: Double(area.text!), volume: Double(volume.text!), height: Double(height!))
areaBorder.text = String("\(result.0) mm")
areaBorderLabel.text = "Cut result:"
}
}
Button:
#IBAction func calcButtonPress(_ sender: Any) {
// Resigns keyboard once button pressed
self.view.endEditing(true)
// State save attempt
StateManager.saveClearZone(area: Double(area.text!), volume: Double(volume.text!), height: Double(height!))
let result = clearCalc.clearZoneCalc(area: Double(area.text!) ?? 1.0, volume: Double(volume.text!) ?? 1.0, height: Double(height!))
areaBorder.text = String("\(result.0) mm")
areaBorderLabel.text = "Cut result:"
}
EDIT (adding save struct):
struct StateManager {
static var unitAreaKey = "UnitArea"
static var unitVolumeKey = "UnitVolume"
static var unitHeightKey = "UnitHeight"
// Saving user data
static func saveClearCalcState(area: Any, volume: Any, height: Any) {
// Call reference to user defaults
let defaults = UserDefaults.standard
// Save state data
defaults.set(area, forKey: unitAreaKey)
defaults.set(volume, forKey: unitVolumeKey)
defaults.set(height, forKey: unitHeightKey)
}
// Retrieve user data
static func retrieveClearCalcValue(key: String) -> Any? {
//Call reference to user defaults
let defaults = UserDefaults.standard
return defaults.value(forKey: key)
}
}
My best guess would be that conversion from String to Double sometimes fails on this line (assuming that instead of saveClearZone you meant to write saveClearCalcState):
StateManager.saveClearZone(area: Double(area.text!), volume: Double(volume.text!), height: Double(height!))
The Double's initializer from a string is failable, meaning it can return nil if conversion from a string to a number fails. It can fail because of a wide range of issues like a trailing space or a non-strict format.
Below are a few recommendations how to fix it.
Use strictly typed function arguments and avoid Any
Currently, your save method accepts Any but if you know that you only want to save your data when the user typed in all values correctly, then use Double instead:
static func saveClearCalcState(area: Double, volume: Double, height: Double)
If you change the argument types to Double, your code will no longer compile because initializers like Double(area.text!) return Double? and you'll have to unwrap the optionals first to check that the values are valid, which is a good opportunity to let the user know if their input can't be processed.
If your intention is to just save whatever the user typed in just to preserve the state between launches, you may choose String? instead of Any. Whatever your choose instead of Any will declare the usage of the method much clearer.
Use NumberFormatter to convert strings to numbers
The NumberFormatter class provides much more flexibility for this task. For example, you can convert the numbers that the user typed in in their current locale. Say, if your user is in a country where a comma is used as a decimal separator, NumberFormatter will let them use their preferred way of typing numbers while the Double initializer always requires a full stop symbol as a decimal separator.
Use optional chaining instead of force unwrapping
Your code contains a lot of force unwrapping (the exclamation point operators).
Remember that an attempt to force-unwrap a nil will crash your app.
It may be fine in some cases, but it wouldn't be nice to crash the app just because a text field doesn't have any text in it.
Prefer optional chaining to force unwrapping.
Example of graceful nil handling
#IBAction func calcButtonPress(_ sender: Any) {
// Resigns keyboard once button pressed
self.view.endEditing(true)
guard let areaText = area.text, !areaText.isEmpty else {
// TODO: alert the user that area can't be empty
return
}
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale.current
guard let areaValue = numberFormatter.number(from: areaText)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
//...
//... do the same checks for volume and height
//...
// State save attempt
StateManager.saveClearZone(area: areaValue, volume: volumeValue, height: heightValue)
let result = clearCalc.clearZoneCalc(area: areaValue, volume: volumeValue, height: heightValue)
areaBorder.text = String("\(result.0) mm")
areaBorderLabel.text = "Cut result:"
}
#Vadim, firstly thanks again for the detailed post. Learned a lot again and applied your optional handling, I will be using these methods from now on. Very succinct!
With regards to the problems with user defaults... STILL HAVING THEM. This is driving me NUTS. I have tried so many different things I can barely list them all but I will try here:
I have:
Re-wrote the State manager struct 3 times and changed many different things and attempted all sort of different data type handling
Moved the save state point around in the code many times and to many different points
Downloaded a cocopod "Defaults" which ended up having exactly the same problem as UserDefaults
I've changed my view did load method AT LEAST 10 times with many different configurations
Tried all sorts of optional nil handling as of now
What is going wrong:
No matter what configurations of code I get the same weird data retrieval, because I'm writing the values to the text fields on reboot if not nil I can see values (also printing them). And consistently the 1st textfield is correct but the second and third are incorrect. Sometimes the 2nd and 3rd will show the last data i entered but not most recent reboot data. Like it lags behind one reboot. Example:
Attempt one:
Field 1: 12345
Field 2: 1234
Field 3: 123
Reboot--
Load:
Show-
Field 1: 12345
Field 2:
Field 3:
New data entry attempt:
Field 1: 54321
Field 2: 5432
Field 3: 543
Reboot--
Show-
Field 1: 54321
Field 2: 1234
Field 3: 123
WTF?
Also I must state that this exact state works perfectly for 6 other view controllers and using the exact same struct methods for saving and retrieval. Only difference is in this VC I made a class for the calculator and the others the calculator is small so its in the VC declared and called as a function with no parameters...
Anyway here is my FULL code for the VC, unaltered, if you care to help I would be much obliged and will push forward the favour in the future when I am knowledgable:
// Copyright © 2020 Roberto B. All rights reserved.
//
import UIKit
class CutCalculatorViewController: UIViewController {
// Length cut user inputs
#IBOutlet weak var overallLength: UITextField!
#IBOutlet weak var unitLength: UITextField!
#IBOutlet weak var headJointSize: UITextField!
#IBOutlet weak var metricImperialLengthSelector: UISegmentedControl!
// Length cut labels
#IBOutlet weak var buttonSelectorLabel: UILabel!
#IBOutlet weak var courseOneCut: UILabel!
#IBOutlet weak var courseOneCutLabel: UILabel!
#IBOutlet weak var courseTwoCut: UILabel!
#IBOutlet weak var courseTwoCutLabel: UILabel!
// Height cut user inputs
#IBOutlet weak var overallHeight: UITextField!
#IBOutlet weak var unitHeight: UITextField!
#IBOutlet weak var bedJointSize: UITextField!
#IBOutlet weak var metricImperialHeightSelector: UISegmentedControl!
// Height cut label
#IBOutlet weak var heightCutResult: UILabel!
override func viewDidLoad() {
if StateManager.retrieveCutCalcValue(key: StateManager.overallLengthKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.headJointSizeKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthSelector) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthSelector) as? Int == 0 {
let savedOverallLength = StateManager.retrieveCutCalcValue(key: StateManager.overallLengthKey) as? Double
let savedUnitLength = StateManager.retrieveCutCalcValue(key: StateManager.unitLengthKey) as? Double
let savedHeadJoint = StateManager.retrieveCutCalcValue(key: StateManager.headJointSizeKey) as? Double
metricImperialLengthSelector.selectedSegmentIndex = 0
print(savedOverallLength!)
print(savedUnitLength!)
print(savedHeadJoint!)
print(metricImperialLengthSelector.selectedSegmentIndex)
overallLength.text = String(savedOverallLength!)
unitLength.text = String(savedUnitLength!)
headJointSize.text = String(savedHeadJoint!)
let result = cutCalc.metricRunningBondCalc(overallLength: savedOverallLength!, unitLength: savedUnitLength!, headJointSize: savedHeadJoint!)
courseOneCut.text = String("\(result.0) mm")
courseTwoCut.text = "N/A"
courseTwoCut.textColor = UIColor.clear
courseTwoCutLabel.textColor = UIColor.clear
buttonSelectorLabel.text = "Stack bond cut result"
courseOneCutLabel.text = "Cut result:"
} else if StateManager.retrieveCutCalcValue(key: StateManager.overallLengthKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.headJointSizeKey) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthSelector) != nil && StateManager.retrieveCutCalcValue(key: StateManager.unitLengthSelector) as? Int == 1 {
let savedOverallLength = StateManager.retrieveCutCalcValue(key: StateManager.overallLengthKey)
let savedUnitLength = StateManager.retrieveCutCalcValue(key: StateManager.unitLengthKey)
let savedHeadJoint = StateManager.retrieveCutCalcValue(key: StateManager.headJointSizeKey)
metricImperialLengthSelector.selectedSegmentIndex = 1
print(savedOverallLength!)
print(savedUnitLength!)
print(savedHeadJoint!)
print(metricImperialLengthSelector.selectedSegmentIndex)
overallLength.text = savedOverallLength as? String
unitLength.text = savedUnitLength as? String
headJointSize.text = savedHeadJoint as? String
let result = cutCalc.imperialRunningBondCalc(overallLength: savedOverallLength as! Double, unitLength: savedUnitLength as! Double, headJointSize: savedHeadJoint as! Double)
courseOneCut.text = String("\(result.0) inches")
courseTwoCut.text = "N/A"
courseTwoCut.textColor = UIColor.clear
courseTwoCutLabel.textColor = UIColor.clear
buttonSelectorLabel.text = "Stack bond cut result"
courseOneCutLabel.text = "Cut result:"
}
super.viewDidLoad()
}
// Initialize cut calculator structure
let cutCalc = CutCalculator()
// Initialize metric imperial segments
var unitLengthMeasurement: Bool = true
var unitHeightMeasurement: Bool = true
// Length cut buttons
// Stack bond- DONE
#IBAction func stackBondLength(_ sender: Any) {
// Resigns keyboard once button pressed
self.view.endEditing(true)
// Activate methods for cut calculation metric/imperial & label changes
if unitLengthMeasurement == true {
guard let overallLength = overallLength.text, !overallLength.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
guard let unitLength = unitLength.text, !unitLength.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
guard let headJoint = headJointSize.text, !headJoint.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale.current
guard let overallLengthValue = numberFormatter.number(from: overallLength)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
guard let unitLengthValue = numberFormatter.number(from: unitLength)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
guard let headJointValue = numberFormatter.number(from: headJoint)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
let unitSelectorValue = metricImperialLengthSelector.selectedSegmentIndex
StateManager.saveLengthCutCalcState(overallLength: overallLengthValue, unitLength: unitLengthValue, headJointSize: headJointValue, measurementLengthSelector: unitSelectorValue)
let result = cutCalc.metricRunningBondCalc(overallLength: overallLengthValue, unitLength: unitLengthValue, headJointSize: headJointValue)
courseOneCut.text = "\(result.0) mm"
courseTwoCut.text = "N/A"
courseTwoCut.textColor = UIColor.clear
courseTwoCutLabel.textColor = UIColor.clear
buttonSelectorLabel.text = "Stack bond cut result"
courseOneCutLabel.text = "Cut result:"
// Popup alert
if result.0 < 36.0 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to open or squeeze head joints to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
} else {
guard let overallLength = overallLength.text, !overallLength.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
guard let unitLength = unitLength.text, !unitLength.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
guard let headJoint = headJointSize.text, !headJoint.isEmpty else {
let alert = UIAlertController(title: "Empty field!", message: "All text fields must contain data.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
self.present(alert, animated: true)
return
}
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.locale = Locale.current
guard let overallLengthValue = numberFormatter.number(from: overallLength)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
guard let unitLengthValue = numberFormatter.number(from: unitLength)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
guard let headJointValue = numberFormatter.number(from: headJoint)?.doubleValue else {
// TODO: alert the user that the text they entered can't be recognized as a numeric value
return
}
let unitSelectorValue = metricImperialLengthSelector.selectedSegmentIndex
StateManager.saveLengthCutCalcState(overallLength: overallLengthValue, unitLength: unitLengthValue, headJointSize: headJointValue, measurementLengthSelector: unitSelectorValue)
let result = cutCalc.imperialRunningBondCalc(overallLength: overallLengthValue, unitLength: unitLengthValue, headJointSize: headJointValue)
courseOneCut.text = "\(result.0) inches"
courseTwoCut.text = "N/A"
courseTwoCut.textColor = UIColor.clear
courseTwoCutLabel.textColor = UIColor.clear
buttonSelectorLabel.text = "Stack bond cut result"
courseOneCutLabel.text = "Cut result:"
// Popup alert
if result.2 < 2.5 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to open or squeeze head joints to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
}
}
// Running bond- DONE
#IBAction func runningBondLength(_ sender: Any) {
// Resigns keyboard once button pressed
self.view.endEditing(true)
// Activate methods for cut calculation metric/imperial & label changes
if unitLengthMeasurement == true {
let result = cutCalc.metricRunningBondCalc(overallLength: Double(overallLength.text!) ?? 1.0, unitLength: Double(unitLength.text!) ?? 1.0, headJointSize: Double(headJointSize.text!) ?? 1.0)
courseOneCut.text = String("\(result.0) mm")
courseTwoCut.text = String("\(result.1) mm")
buttonSelectorLabel.text = "Running bond cut results"
courseTwoCut.textColor = #colorLiteral(red: 0.7333333333, green: 0.8823529412, blue: 0.9803921569, alpha: 1)
courseTwoCutLabel.textColor = #colorLiteral(red: 0.7333333333, green: 0.8823529412, blue: 0.9803921569, alpha: 1)
courseOneCutLabel.text = "Course 1 cut:"
courseTwoCutLabel.text = "Course 2 cut:"
// Popup alert
if result.0 < 36 || result.1 < 36 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to open or squeeze head joints to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
} else {
let result = cutCalc.imperialRunningBondCalc(overallLength: Double(overallLength.text!) ?? 1.0, unitLength: Double(unitLength.text!) ?? 1.0, headJointSize: Double(headJointSize.text!) ?? 1.0)
courseOneCut.text = String("\(result.0) inches")
courseTwoCut.text = String("\(result.1) inches")
buttonSelectorLabel.text = "Running bond cut results"
courseTwoCut.textColor = #colorLiteral(red: 0.7333333333, green: 0.8823529412, blue: 0.9803921569, alpha: 1)
courseTwoCutLabel.textColor = #colorLiteral(red: 0.7333333333, green: 0.8823529412, blue: 0.9803921569, alpha: 1)
courseOneCutLabel.text = "Course 1 cut:"
courseTwoCutLabel.text = "Course 2 cut:"
// Popup alert
if result.2 < 2.5 || result.3 < 2.5 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to open or squeeze head joints to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
}
}
// Height cut button- DONE
#IBAction func heightCut(_ sender: Any) {
// Resigns keyboard once button pressed
self.view.endEditing(true)
// Activate methods for cut calculation metric/imperial & label changes
if unitHeightMeasurement == true {
let result = cutCalc.metricHeightCutCalc(overallHeight: Double(overallHeight.text!) ?? 1.0, unitHeight: Double(unitHeight.text!) ?? 1.0, beadJointSize: Double(bedJointSize.text!) ?? 1.0)
heightCutResult.text = String("\(result) mm")
// Popup alert
if result < 30.5 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to gain or squeeze to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
} else {
let result = cutCalc.imperialHeightCutCalc(overallHeight: Double(overallHeight.text!) ?? 1.0, unitHeight: Double(unitHeight.text!) ?? 1.0, beadJointSize: Double(bedJointSize.text!) ?? 1.0)
heightCutResult.text = String("\(result) inches")
// Popup alert
if result.1 < 2.5 {
// First instantiate the UIAlertController
let alert = UIAlertController(title: "Small cut!", message: "Depending on conditions you may be able to gain or squeeze to eliminate this small cut.", preferredStyle: .alert)
// Add action buttons to it and attach handler functions if you want to
alert.addAction(UIAlertAction(title: "Got it!", style: .cancel, handler: nil))
// Show the alert by presenting it
self.present(alert, animated: true)
}
}
}
// Length cut calculator metric imperial selector- DONE
#IBAction func lengthUnitSelector(_ sender: UISegmentedControl) {
if (metricImperialLengthSelector.selectedSegmentIndex == 0) {
unitLengthMeasurement = true
} else {
unitLengthMeasurement = false
}
}
// Height cut calculator metric imperial selector- DONE
#IBAction func heightUnitSelector(_ sender: UISegmentedControl) {
if (metricImperialHeightSelector.selectedSegmentIndex == 0) {
unitHeightMeasurement = true
} else {
unitHeightMeasurement = false
}
}
// Acts to dismiss number keyboard when user taps outside
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
overallLength.resignFirstResponder()
unitLength.resignFirstResponder()
headJointSize.resignFirstResponder()
overallHeight.resignFirstResponder()
unitHeight.resignFirstResponder()
bedJointSize.resignFirstResponder()
}
}
Here is my saving class:
// Cut calculator saved states
static var overallLengthKey = "OverallLength"
static var unitLengthKey = "UnitLength"
static var headJointSizeKey = "HeadJointSize"
static var unitLengthSelector = "LengthUnitSelector"
static var overallHeightKey = "OverallHeight"
static var unitHeightKey = "UnitHeight"
static var bedJointSizeKey = "BedJointSize"
static var unitHeightSelector = "HeightUnitSelector"
// Saving user data
static func saveLengthCutCalcState(overallLength: Double, unitLength: Double, headJointSize: Double, measurementLengthSelector: Int) {
let defaults = UserDefaults.standard
defaults.set(overallLength, forKey: overallLengthKey)
defaults.set(unitLength, forKey: unitLengthKey)
defaults.set(headJointSize, forKey: headJointSizeKey)
defaults.set(measurementLengthSelector, forKey: unitLengthSelector)
defaults.synchronize()
}
// Saving user data
static func saveHeightCutCalcState(overallHeight: Double, unitHeight: Double, bedJointSize: Double, measurementHeightSelector: Int) {
let defaults = UserDefaults.standard
defaults.set(overallHeight, forKey: overallHeightKey)
defaults.set(unitHeight, forKey: unitHeightKey)
defaults.set(bedJointSize, forKey: bedJointSizeKey)
defaults.set(measurementHeightSelector, forKey: unitHeightSelector)
}
// Retrieve user data
static func retrieveCutCalcValue(key: String) -> Any? {
let defaults = UserDefaults.standard
defaults.synchronize()
return defaults.value(forKey: key)
}
// Clear state
static func clearLengthCutCalcState() {
let defaults = UserDefaults.standard
// Clear the saved state data in user defaults
defaults.removeObject(forKey: overallLengthKey)
defaults.removeObject(forKey: unitLengthKey)
defaults.removeObject(forKey: headJointSizeKey)
defaults.removeObject(forKey: unitLengthSelector)
}
static func clearHeightCutCalcState() {
let defaults = UserDefaults.standard
// Clear the saved state data in user defaults
defaults.removeObject(forKey: overallHeightKey)
defaults.removeObject(forKey: unitHeightKey)
defaults.removeObject(forKey: bedJointSizeKey)
defaults.removeObject(forKey: unitHeightSelector)
}
// DONE- Even gauge calcualtor saved states -DONE
Related
How to add checkmark to submenu of UIMenu in Swift Storyboard?
I'm trying to create a filter option for an app in Swift. Currently, the UI will add a check mark if one of the category filters are selected, such as in the image below for "Food". However, if the "Within Radius" filter is selected, the UI doesn't get updated. At the moment, this is the code I've written to add the checkmark: private func updateActionState(actionTitle: String? = nil, menu: UIMenu) -> UIMenu { if let actionTitle = actionTitle { menu.children.forEach { action in guard let action = action as? UIAction else { return } if action.title == actionTitle { if(action.state == .on){ action.state = .off } else{ action.state = .on } } else{ action.state = .off } } } else { let action = menu.children.first as? UIAction action?.state = .on } return menu } I created the menu as follows: private lazy var elements: [UIAction] = [food, clothing, furniture, other] private lazy var menu = UIMenu(title: "Category", children: elements) private lazy var deferredMenu = UIMenu(title: "Distance", options: .displayInline, children: [self.distanceRadius]) override func viewDidLoad() { super.viewDidLoad() mapView.delegate = self loadMap(); menu = menu.replacingChildren([food, clothing, furniture, other, deferredMenu]) navigationItem.leftBarButtonItem?.menu = menu } And the UIAction is declared as: private lazy var distanceRadius = UIAction(title: "Within Radius", attributes: [], state: currFilter == "Within Radius" ? .on : .off){action in var alert = UIAlertController(title: "Radius", message: "Filter within a radius (in miles)", preferredStyle: .alert) //2. Add the text field. You can configure it however you need. alert.addTextField(configurationHandler: { (textField) -> Void in textField.text = "" }) //3. Grab the value from the text field, and print it when the user clicks OK. var radius = 0 alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (action) -> Void in let textField = (alert?.textFields![0])! as UITextField radius = Int(textField.text!) ?? 0 self.toggleFilter(actionTitle: "Within Radius", radius: radius) })) // 4. Present the alert. self.present(alert, animated: true, completion: nil) self.navigationItem.leftBarButtonItem?.menu = self.updateActionState(actionTitle: "Within Radius", menu: self.menu); } However, for the "Within Radius" option, the action.title is "Distance", as opposed to "Within Radius", which is what the UIAction is created with. Is there any way to cast a UIAction as a UIMenu to access the children within distanceRadius? Or is there another way to get the check mark to appear in the Distance submenu? I've tried re-calling updateActionState on deferredMenu as well but that did not do anything.
How to reload UIView after closing UIAlertController
I try to make BarChart using https://github.com/CoreCharts/CoreCharts I have a button, which call UIAlertController with textField. After I click submit button, new data is added to the array. I need the diagram to reboot. #IBAction func newWeightButton(_ sender: Any) { //updateGraph() let alert = UIAlertController(title: "Enter your new weight", message: nil, preferredStyle: .alert) alert.addTextField() let submitAction = UIAlertAction(title: "Add", style: .default) { [unowned alert] _ in let textField = alert.textFields![0] textField.keyboardType = UIKeyboardType.numberPad textField.becomeFirstResponder() let answer = Double(textField.text!) self.weightArr.append(answer!) print(self.weightArr) self.loadCoreChartData() } alert.addAction(submitAction) present(alert, animated: true) } func loadCoreChartData() -> [CoreChartEntry] { return getWeightList() } func getWeightList()->[CoreChartEntry] { var allResults = [CoreChartEntry]() //let days = ["Mon","Tue","Wed","Thur","Fri"] let plateNumber = [80,75,90,88,84] var count = self.weightArr.count for index in 0..<count { let newEntry = CoreChartEntry(id: "\(self.weightArr[index])", barTitle: "Day", barHeight: Double(self.weightArr[index]), barColor: UIColor(red: 0, green: 122/255, blue: 1, alpha: 1)) allResults.append(newEntry) }
For CoreCharts, call reload() on the chart. Generally, in iOS, setting properties of the UIView is sufficient to get it to update, but with views that use a datasource of some type (like tables, collection views, and core charts), there is usually some kind of reload...() function to tell the view that the datasource changed.
NSUserDefault issue
I am creating a divelog app. When someone select a new dive, they will get an alert with two options: "Blank template" or "Use last dive as template". The issue is with the later, last dive as a template. Each dive I record the data to the NSUserDefaults. So, when someone wants last dive as a template, I just call each of the defaults and populate the tableview. However, I am getting a nil error on the first field, the diveSiteTextfield.text, this baffles me: Here is where the data gets saved to NSUserDefaults As you can see, I added a print command to test to see if the diveSite field is nil, but it displays the correct data. The diveSiteTextfield.text is not nil. import UIKit import Parse var diveUUID = NSUUID().UUIDString class DiveDetails1VC: UIViewController, UITextFieldDelegate, UITableViewDelegate, UIPickerViewDelegate, UIPickerViewDataSource { let userDefault = NSUserDefaults.standardUserDefaults() #IBOutlet weak var ratingControl: DiveSiteRating! #IBOutlet weak var diveSiteTextfield: UITextField! #IBOutlet weak var diveDurationTextfield: UITextField! ... func toDive2NewParse () { let diveObject = PFObject(className: "divelog") diveObject["diveNumber"] = diveNumberInt diveObject["diveSite"] = diveSiteTextfield.text diveObject["diveDuration"] = diveDurationTextfield.text diveObject["maxDepth"] = diveMaxDepthTextfield.text diveObject["diveDate"] = diveDateTextfield.text diveObject["diveTime"] = diveTimeTextfield.text diveObject["bodyOfWater"] = diveBodyOfWaterTextfield.text diveObject["diveBuddy"] = diveBuddyTextfield.text diveObject["diveCity"] = diveCityTextfield.text diveObject["maxDepthSymbol"] = diveMaxDepthSymbolTextField.text diveObject["diveCountry"] = diveCountryTextField.text diveObject["uuid"] = "\(diveUUID)" // diveObject["diveRating"] = ratingControl diveObject["entryFrom"] = "Divelog" if PFUser.currentUser()!.username == nil { diveObject["username"] = "Not Signed in" } else { diveObject["username"] = PFUser.currentUser()!.username } diveObject.saveEventually() userDefault.setObject(diveNumberInt, forKey: "diveNumber") userDefault.setObject(diveSiteTextfield.text, forKey: "diveSite") userDefault.setObject(diveDurationTextfield.text!, forKey: "diveDuration") userDefault.setObject(diveMaxDepthTextfield.text, forKey: "diveDepth") userDefault.setObject(diveMaxDepthSymbolTextField.text, forKey: "symbol_maxDepth") userDefault.setObject(diveDateTextfield.text, forKey: "entryDate") userDefault.setObject(diveTimeTextfield.text, forKey: "entryTime") userDefault.setObject(diveCityTextfield.text, forKey: "diveCity") userDefault.setObject(diveCountryTextField.text, forKey: "diveCountry") userDefault.setObject(diveBodyOfWaterTextfield.text, forKey: "bodyOfWater") userDefault.setObject(diveBuddyTextfield.text, forKey: "diveBuddy") userDefault.setObject(diveUUID, forKey: "uuid") userDefault.synchronize() print("DiveSite is ...\(userDefault.objectForKey("diveSite"))") } Here is the UIAlertAction that retrieves the "last dive": #IBAction func addNewDive (sender: AnyObject) { let alertController = UIAlertController(title: "New Dive", message: "Choose a template", preferredStyle:UIAlertControllerStyle.ActionSheet) let buttonOne = UIAlertAction(title: "Blank template", style: .Default, handler: { (action) -> Void in self.performSegueWithIdentifier("segueNewDive1", sender: self) print("Button One Pressed") }) let buttonTwo = UIAlertAction(title: "Use last dive as template", style: .Default, handler: { (action) -> Void in self.VC1.diveSiteTextfield.text = self.userDefault.stringForKey("diveSite") self.VC1.diveDurationTextfield.text = self.userDefault.stringForKey("diveDuration") self.VC1.diveMaxDepthTextfield.text = self.userDefault.stringForKey("diveDepth") ... self.DNo.textView.text = self.userDefault.objectForKey("diveNotes") as? String; self.userDefault.synchronize() self.performSegueWithIdentifier("segueNewDive1", sender: self) any help on understanding why the nil will be greatly appreciated. Updated with Vladian's comment
How to get text input out of UIAlertcontroller OR how to wait for the input using Swift
I am trying to present an Alertcontroller that prompts the user for a filename and then use the filename elsewhere in the program. I have been trying numerous variations of the following code: import UIKit class ViewController: UIViewController { var shortName: String! #IBAction func saveFile(sender: AnyObject) { //request filename with alert var alertController:UIAlertController? alertController = UIAlertController(title: "Enter File", message: "Enter file name below", preferredStyle: .Alert) alertController!.addTextFieldWithConfigurationHandler( {(textField: UITextField!) in textField.placeholder = "" }) let action = UIAlertAction(title: "Submit", style: UIAlertActionStyle.Default, handler: {[weak self] (paramAction:UIAlertAction!) in if let textFields = alertController?.textFields{ let theTextFields = textFields as [UITextField] let enteredText = theTextFields[0].text self!.shortName = enteredText //trying to get text into shortName print(self!.shortName) // prints } }) alertController?.addAction(action) self.presentViewController(alertController!, animated: true, completion: nil) //do some stuff with the input print(shortName) //THIS RETURNS nil if uncommented. comment out to avoid crash } 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. } } I have thoroughly researched this and can't find how to either: get the string value of shortName out of the UIAlertAction closure and into the shortName (I know that the current "self!.shortName" is unavailable outside the closure - doesn't matter what name I use - can't get it out) If you run the program "as-is", the print(shortName) line will cause a crash due to unwrapping of nil. How can I get the alert to "wait" for input? Most of the posted "solutions" have the same problem - they don't actually get the text input out of the closure and into a variable that can be accessed by the rest of the program. thanks
Of course you get crash, shortName is nil while Submit button isn't pressed. You can try something like this: #IBAction func saveFile(sender: AnyObject) { var alertController:UIAlertController? alertController = UIAlertController(title: "Enter File", message: "Enter file name below", preferredStyle: .Alert) alertController!.addTextFieldWithConfigurationHandler( {(textField: UITextField!) in textField.placeholder = "" }) let action = UIAlertAction(title: "Submit", style: UIAlertActionStyle.Default, handler: {[weak self] (paramAction:UIAlertAction!) in if let textFields = alertController?.textFields{ let theTextFields = textFields as [UITextField] let enteredText = theTextFields[0].text self!.shortName = enteredText //trying to get text into shortName print(self!.shortName) // prints self?.handleText() NSOperationQueue.mainQueue().addOperationWithBlock({ self?.handleTextInMainThread() }) } }) alertController?.addAction(action) self.presentViewController(alertController!, animated: true, completion: nil) } func handleText() { print(self.shortName) } func handleTextInMainThread() { print(self.shortName) } You have use NSOperationQueue if you want to work with UI inside handleTextInMainThread after user's input.
I think it is timing. You tried to print shortName right after presenting the alert. At that time the value is not set yet. You can either use semaphore to wait till it is set or do whatever you want to do in the action closure for "submit".
Black background image using camera with Swift2
I'm trying to display the user camera (back camera) using the AVFoundation, but I must be doing something wrong because It's only showing a black background image. I have checked my Privacy > Camera and there isn't any option regarding the camera with my App, and I am not able to display the .Alert action to ask the user the permission to access the camera. Here is my code, I hope you could help me because this is very weird: import UIKit import AVFoundation class CodigoBarrasViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { #IBOutlet weak var messageLabel:UILabel! #IBOutlet weak var imagenFondo:UIImageView! #IBOutlet weak var BackgroundView:UIView! var string:String! var captureSession:AVCaptureSession? var videoPreviewLayer:AVCaptureVideoPreviewLayer? var qrCodeFrameView:UIView? // Added to support different barcodes let supportedBarCodes = [AVMetadataObjectTypeQRCode, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeCode39Code, AVMetadataObjectTypeCode93Code, AVMetadataObjectTypeUPCECode, AVMetadataObjectTypePDF417Code, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeAztecCode] override func viewDidAppear(animated: Bool) { captureSession?.startRunning() self.qrCodeFrameView?.hidden = true } override func viewDidLoad() { //captureSession?.startRunning() super.viewDidLoad() // Get an instance of the AVCaptureDevice class to initialize a device object and provide the video // as the media type parameter. let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) do { input = try AVCaptureDeviceInput(device: captureDevice) as AVCaptureDeviceInput } catch let error as NSError { print(error) } // Initialize the captureSession object. captureSession = AVCaptureSession() // Set the input device on the capture session. captureSession?.addInput(input) //captureSession?.addInput(input as AVCaptureInput) // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. let captureMetadataOutput = AVCaptureMetadataOutput() captureSession?.addOutput(captureMetadataOutput) // Set delegate and use the default dispatch queue to execute the call back captureMetadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue()) captureMetadataOutput.metadataObjectTypes = supportedBarCodes // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill videoPreviewLayer?.frame = view.layer.bounds view.layer.addSublayer(videoPreviewLayer!) // Start video capture. captureSession?.startRunning() // Move the message label to the top view view.bringSubviewToFront(imagenFondo) view.bringSubviewToFront(messageLabel) view.bringSubviewToFront(BackgroundView) // Initialize QR Code Frame to highlight the QR code qrCodeFrameView = UIView() qrCodeFrameView?.layer.borderColor = UIColor(hex: 0x00B7BB).CGColor qrCodeFrameView?.layer.borderWidth = 2 view.addSubview(qrCodeFrameView!) view.bringSubviewToFront(qrCodeFrameView!) } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) //self.navigationController?.hidesBarsOnSwipe = true self.navigationController?.setNavigationBarHidden(true, animated: false) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { // Check if the metadataObjects array is not nil and it contains at least one object. if metadataObjects == nil || metadataObjects.count == 0 { qrCodeFrameView?.frame = CGRectZero //messageLabel.text = "No QR code is detected" return } else { // Get the metadata object. let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject // Here we use filter method to check if the type of metadataObj is supported // Instead of hardcoding the AVMetadataObjectTypeQRCode, we check if the type // can be found in the array of supported bar codes. if supportedBarCodes.filter({ $0 == metadataObj.type }).count > 0 { // If the found metadata is equal to the QR code metadata then update the status label's text and set the bounds let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as! AVMetadataMachineReadableCodeObject qrCodeFrameView?.frame = barCodeObject.bounds if metadataObj.stringValue != nil { captureSession?.stopRunning() self.qrCodeFrameView?.hidden = false launchApp(metadataObj.stringValue) } } } } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) { if segue.identifier == "seeProduct" { let destinationController = segue.destinationViewController as! ProductoCamViewController let string = (sender as! String!) let backItem = UIBarButtonItem() backItem.title = " " navigationItem.backBarButtonItem = backItem destinationController.ean = string } } func launchApp(decodedURL: String) { let alertPrompt = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) //let alertPrompt = UIAlertController(title: "", message: decodedURL, preferredStyle: .ActionSheet) let confirmAction = UIAlertAction(title: "See product", style: UIAlertActionStyle.Default, handler: { (action) -> Void in self.performSegueWithIdentifier("seeProduct", sender: decodedURL) }) let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default, handler: { (action) -> Void in self.captureSession?.startRunning() self.qrCodeFrameView?.hidden = true }) //let cancelAction = UIAlertAction(title: "Cancelar", style: UIAlertActionStyle.Cancel, handler: nil) alertPrompt.addAction(confirmAction) alertPrompt.addAction(cancelAction) self.presentViewController(alertPrompt, animated: true, completion: nil) } } Thanks in advance, Regards.
I would suggest taking a look at UIImagePickerControllerDelegate if you're wanting to access the camera. Implement this and all of the permission alerts are handled for you