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.
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
I'm trying to figure out how to keep the Action Button disabled until the user enters some text in, at which point the button would be enabled again. I've been searching around, and some people are suggesting to use Observers? Would that be the best way to go?
Cheers!
#objc func addExercise() {
var textField = UITextField()
let alert = UIAlertController(title: "New Exercise", message: "Please name your Exercise...", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .default) { (UIAlertAction) in
alert.dismiss(animated: true, completion: nil)
}
let addAction = UIAlertAction(title: "Add Exercise", style: .default) { (UIAlertAction) in
//Add Exercise to database.
//Append exercise to selected workout object.
let exercise = Exercises()
exercise.exerciseName = textField.text!
try! self.realm.write {
self.selectedWorkout?.exercise.append(exercise)
self.loadExercises()
}
}
alert.addTextField { (alertTextField1) in
alertTextField1.delegate = self
alertTextField1.placeholder = "Bench Press"
alertTextField1.text = textField.text
textField = alertTextField1
}
alert.addAction(addAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
Using notification is a way but long work instead you can use simple delegate method or action method of textfield which is much easier as follow:
weak var buttonActionToEnable: UIAlertAction?
alert.addTextField { (alertTextField1) in
alertTextField1.delegate = self
alertTextField1.placeholder = "Bench Press"
alertTextField1.text = textField.text
alertTextField1.addTarget(self, action: #selector(self.textFieldChanged), for: .editingChanged)
}
self.buttonActionToEnable = addAction
addAction.isEnabled = false
#objc func textFieldChanged(_ sender: Any) {
let textfield = sender as UITextField
self.buttonActionToEnable?.isEnabled = textfield.text.count > 0
}
I need to change font of buttons in UIAlertController, I've used the code below, but I doesn't work properly, because when the user touches the buttons of UIAlertController, the font will be change to it's default.
extension UIAlertController {
private func changeFont(view:UIView,font:UIFont) {
for item in view.subviews {
if let col = item as? UICollectionView {
for row in col.subviews{
changeFont(view: row, font: font)
}
}
if let label = item as? UILabel {
label.font = font
} else {
changeFont(view: item, font: font)
}
}
}
//To set font for any UILabels in action sheet
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let font = MyCustomFont
changeFont(view: self.view, font: font! )
}
}
and here is the usage of this extension:
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let action = UIAlertAction(title: STRING_OF_ACTION, style: .default) { _ in
//some action code
}
optionMenu.addAction(action)
//in UIViewController
self.present(optionMenu, animated: true, completion: nil)
It's hard to change it because the iOS system doesn't provide the API for us to do it. So we'd better use third party libraries like: SCLAlertView-Swift or PopupDialog if we need some customization.
I am using MWPhotoBrowser for my app. I need to give delete functionality to my users. Is there any way we can implement delete a particular photo or multiple photos functionality?
Quick help needed.
I did this in swift by adding this extension from outside of the library:
extension MWPhotoBrowser {
public override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let _ = valueForKey("_gridController") as? MWGridViewController {
let leftButtonItem = editButtonItem()
//leftButtonItem.tintColor = QorumColors.ThemeWhite
navigationItem.leftBarButtonItem = leftButtonItem
} else {
navigationItem.leftBarButtonItem = nil
}
}
public override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if editing {
navigationItem.leftBarButtonItem?.title = "Delete"
//navigationItem.leftBarButtonItem?.tintColor = QorumColors.Nickname
displaySelectionButtons = true
title = "Delete Photos"
let gridController = valueForKey("_gridController") as! MWGridViewController
gridController.selectionMode = displaySelectionButtons
gridController.collectionView!.reloadData()
} else {
let nav = self.navigationController as! TempPresentVC
let photosToDelete = nav.selectedPhotos
let afterButtonPress = {
//self.navigationItem.leftBarButtonItem?.tintColor = QorumColors.ThemeWhite
self.displaySelectionButtons = false
self.updateNavigation()
let gridController = self.valueForKey("_gridController") as! MWGridViewController
gridController.selectionMode = self.displaySelectionButtons
gridController.collectionView!.reloadData()
}
guard photosToDelete.count > 0 else {
afterButtonPress()
return
}
let title = "Delete Photo"
let message = "Are you sure you want to delete these photos?"
let action = "Delete"
let cancelAction = "Cancel"
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
let declineButton = UIAlertAction(title: cancelAction, style: .Default, handler: { (action: UIAlertAction) in
afterButtonPress()
})
let acceptButton = UIAlertAction(title: action, style: .Default, handler: { (action: UIAlertAction) in
afterButtonPress()
})
alert.addAction(declineButton)
alert.addAction(acceptButton)
UIApplication.topMostController().presentVC(alert) //private lib
}
}
}
class TempPresentVC: UINavigationController, MWPhotoBrowserDelegate {
var selectedPhotos = [Int]()
func photoBrowser(photoBrowser: MWPhotoBrowser!, photoAtIndex index: UInt, selectedChanged selected: Bool) {
if selected {
selectedPhotos.append(index.toInt)
} else {
selectedPhotos.removeObject(index.toInt)
}
}
}
This doesn't allow you to delete a photo in a single photo view mode, but starting from this code base that part would be easy to implement.
Beware, some of the stuff I use inside are private functions I wrote in my other classes, what they do should be clear though.