6
\$\begingroup\$

This is something I put together recently, and I thought I would see if anyone had any recommendations for modification of this short implementation of a tip calculator.

The calculator uses a UISegmentedControl to switch between tip percentages of 10%, 15%, and 20%. I have also added images of the interface under the source code.

Please let me know what I'm doing right and wrong, and if there is anything I can do to improve my code.


class TipViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var amountTextField: UITextField!
    @IBOutlet var tipLabel: UILabel!
    @IBOutlet var totalLabel: UILabel!
    @IBOutlet var segmentedControl: UISegmentedControl!
    @IBOutlet var segmentedControlCenterYConstraint: NSLayoutConstraint!
    @IBOutlet var tipLabelBottomConstraint: NSLayoutConstraint!
    @IBOutlet var tapGesture: UITapGestureRecognizer!

    // Whenever the value of subtotal has been assigned, call updateTotalAndTipLabels().
    var subtotal: Double? {
        didSet {
            updateTotalAndTipLabels()
        }
    }

    // Based on the segementControl's selectedSegmentIndex, assign the appropriate tip percentage.
    var tipPercentage: Double {
        switch segmentedControl.selectedSegmentIndex {
        case 0:
            return 0.10
        case 1:
            return 0.15
        default:
            return 0.20
        }
    }

    // A decimal styled NumberFormatter that allows up to 2 decimal places.
    let numberFormatter: NumberFormatter = {
        let nf = NumberFormatter()
        nf.numberStyle = .decimal
        nf.minimumFractionDigits = 0
        nf.maximumFractionDigits = 2
        return nf
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        tapGesture.addTarget(self, action: #selector(dismissKeyboard))
    }

    func updateTotalAndTipLabels() {
        if let subtotal = subtotal {
            // Calculates the appropriate tip based on the selectedSegmentIndex of the segmentedControl.
            let tip = subtotal * tipPercentage

            // Updates the tipLabel.
            tipLabel?.text = numberFormatter.string(from: NSNumber(floatLiteral: tip))

            // Updates the totalLabel to include the subtotal + the tip amount.
            totalLabel?.text = numberFormatter.string(from: NSNumber(floatLiteral: subtotal + tip))
        } else {
            // If subtotal is nil, set the text of both the tipLabel and textLabel to their initial states, "0.00."
            tipLabel?.text = "0.00"
            totalLabel?.text = "0.00"
        }
    }

    // Whenever the keyboard resigns first responder, animate a couple constraints so that the segmentedControl and the tip Label and TextField is shifted into their inital states.
    func dismissKeyboard() {
        UIView.animate(withDuration: 2.0) {
            self.segmentedControlCenterYConstraint.constant = -8
            self.tipLabelBottomConstraint.constant = 16
        }

        amountTextField?.resignFirstResponder()
    }

    @IBAction func subtotalFieldEditingChanged(_ textField: UITextField) {
        // If the value passed is convertible to Double, set it to subtotal, otherwise assign 'nil' to subtotal.
        if let text = textField.text, let value = Double(text) {
            subtotal = value
        } else {
            subtotal = nil
        }
    }

    // Whenever a segment is tapped, call updateTotalAndTipLabels() to assign the appropriate tip and total amounts.
    @IBAction func calculateTip(_ sender: UISegmentedControl) {
        updateTotalAndTipLabels()
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // Return false if user attempts to input invalid characters.
        guard string.rangeOfCharacter(from: CharacterSet(charactersIn: "1234567890.").inverted) == nil else { return false }

        // Allow user to proceed with their input request if nil is returned when attempting to find the range of the string "."
        // in amountTextField?.text and the attempted input is not "."
        return textField.text?.range(of: ".") == nil && string.range(of: ".") == nil ? true : false
    }

    // Whenever the keyboard becomes the first responder, animate a couple constraints so that the segmentedControl and the tip Label and TextField is shifted vertically up.
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        UIView.animate(withDuration: 2.0) {
            self.segmentedControlCenterYConstraint.constant = -72
            self.tipLabelBottomConstraint.constant = 72
        }

        return true
    }
}


PICTURES

note: These images are only provided for a better understanding of the actual implementation.

Picture of the initial interface

enter image description here

Picture of the interface with amountTextField as firstResponder and sample input data

enter image description here

Picture of the interface without amountTextField as firstResponder and sample input data

enter image description here

\$\endgroup\$
1
  • \$\begingroup\$ if you respect SOLID then your more clean . \$\endgroup\$ Commented May 10, 2017 at 8:11

1 Answer 1

1
\$\begingroup\$
  • Add access level annotations (private/public)
  • UIView.animate(withDuration: 2.0) { self.segmentedControlCenterYConstraint.constant = -8 self.tipLabelBottomConstraint.constant = 16 } This won't animate. You shall call self.view.layoutIfNeeded() inside animation block.
  • You can abstract away tip calculation in separate class/struct. And make available tips as an input for this view controller.
  • You can improve NumberFormatter with nf.numberStyle = .currency
  • You can use Keyboard notifications instead of textFieldShouldBeginEditing to change screen layout. This will allow you to use animation options the same as keyboard uses (see UIKeyboardWillShow UIKeyboardWillHide and UIKeyboardFrameBeginUserInfoKey, UIKeyboardAnimationCurveUserInfoKey etc.).
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.