如何为一个简单的计算器应用程序集成 NumberFormatter?

How to integrate NumberFormatter for a simple calculator app?

提问人:es88mph 提问时间:8/26/2023 最后编辑:es88mph 更新时间:8/28/2023 访问量:57

问:

我正在尝试更新现有代码以使用逗号分组分隔符(即 10,000,000 而不是 10000000) 我怎样才能用简单的方法做到这一点?我正在发布现有代码。

我想要的只是在活动计算寡妇 (textFieldMain) 和历史窗口 (textViewHistory) 中显示带有逗号的格式化数字

import UIKit

class ViewController: UIViewController {

    // MARK: Properties
    @IBOutlet weak var textFieldMain: UITextField!
    @IBOutlet weak var button0: UIButton!
    @IBOutlet weak var button1: UIButton!
    @IBOutlet weak var button2: UIButton!
    @IBOutlet weak var button3: UIButton!
    @IBOutlet weak var button4: UIButton!
    @IBOutlet weak var button5: UIButton!
    @IBOutlet weak var button6: UIButton!
    @IBOutlet weak var button7: UIButton!
    @IBOutlet weak var button8: UIButton!
    @IBOutlet weak var button9: UIButton!
    @IBOutlet weak var buttonPeriod: UIButton!
    @IBOutlet weak var buttonPlusMinus: UIButton!
    @IBOutlet weak var buttonEqual: UIButton!
    @IBOutlet weak var buttonAddition: UIButton!
    @IBOutlet weak var buttonSubstraction: UIButton!
    @IBOutlet weak var buttonMultiplication: UIButton!
    @IBOutlet weak var buttonDivision: UIButton!
    @IBOutlet weak var buttonAllCancel: UIButton!
    @IBOutlet weak var textViewHistory: UITextView!
    
    var valueA: Double = 0
    var valueB: Double = 0
    var currentOperator: String = ""
    var refreshTextField: Bool = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    // MARK: Actions
    @IBAction func allCancel(_ sender: UIButton) {
        // reset all variables
        valueA = 0
        valueB = 0
        currentOperator = ""
        refreshTextField = true
        
        // update text field
        textFieldMain.text = "0"
    }
    @IBAction func clearHistory(_ sender: UIButton) {
           // reset all variables
         
           textViewHistory.text = ""
       }
    @IBAction func numberPress(_ sender: UIButton) {
        // find text to be added
        var value = sender.currentTitle!
        // get current text
        var currentText = ""
        
        // value shourld be refreshed?
        if textFieldMain.text != "0" && (!refreshTextField  || value == "±") {
            currentText = textFieldMain.text!
        }
        
        // zero
        if currentText == "0" && value == "0" {
            value = ""
        }
        
        // period
        if value == "." {
            if currentText == "" {
                value = "0."
            } else if currentText.range(of: ".") != nil {
                value = ""
            }
        }
        
        // plus/minus
        if value == "±" {
            value = ""
            if currentText != "" {
                if currentText.hasPrefix("-") {
                    let start = currentText.startIndex
                    let end = currentText.index(after: start)
                    currentText = currentText.replacingOccurrences(of: "-", with: "", range: start..<end)
                } else {
                    currentText = "-\(currentText)"
                }
            }
        }
        
        // update text field
        refreshTextField = false
        textFieldMain.text = "\(currentText)\(value)"
    }
    
    @IBAction func operatorPress(_ sender: UIButton) {
        // ignore if text field is empty
        if textFieldMain.text != "" {
            // get new operator
            let newOperator = sender.titleLabel?.text!
            // refresh text field
            refreshTextField = true
            
            if currentOperator == "" && newOperator != "=" {
                // copy current value
                valueA = Double(textFieldMain.text!)!
                
                // update operator
                currentOperator = newOperator!
            } else {
                // copy current value
                valueB = Double(textFieldMain.text!)!
                
                // perform operation
                var result: Double = 0
                switch currentOperator {
                case "+":
                    result = valueA + valueB
                case "-":
                    result = valueA - valueB
                case "×":
                    result = valueA * valueB
                case "÷":
                    result = valueA / valueB
                default:
                    result = valueB
                }
                
                if currentOperator != "" {
                    // construct history entry
                    let historyEntry: String = "\(valueA) \(currentOperator) \(valueB) = \(result)\n"
                    // update history
                    textViewHistory.text! += historyEntry
                }
                
                // update operator & values
                currentOperator = ""
                valueA = 0
                valueB = 0
                
                if newOperator != "=" {
                    valueA = result
                    currentOperator = newOperator!
                }
                
                // update text field
                textFieldMain.text = String(result)
            }
        }
    }
    
}

所以我尝试了这些格式规则......

    private let auxFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    let locale = Locale.current
    formatter.groupingSeparator = ""
    formatter.decimalSeparator = locale.decimalSeparator
    formatter.numberStyle = .decimal
    formatter.maximumIntegerDigits = 100
    formatter.minimumFractionDigits = 0
    formatter.maximumFractionDigits = 100
    return formatter
}()
private let auxTotalFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    formatter.groupingSeparator = ""
    formatter.decimalSeparator = ""
    formatter.numberStyle = .decimal
    formatter.maximumIntegerDigits = 100
    formatter.minimumFractionDigits = 0
    formatter.maximumFractionDigits = 100
    return formatter
}()

private let printFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    let locale = Locale.current
    formatter.groupingSeparator = locale.groupingSeparator
    formatter.decimalSeparator = locale.decimalSeparator
    formatter.numberStyle = .decimal
    formatter.maximumIntegerDigits = 9
    formatter.minimumFractionDigits = 0
    formatter.maximumFractionDigits = 8
    return formatter
}() 

对代码进行了以下修改

@IBAction func numberPress(_ sender: UIButton) {
    var value = sender.currentTitle!
    
    if textFieldMain.text != "0" && (!refreshTextField || value == "±") {
        value = textFieldMain.text!
    }
    
    if value == "." {
        if textFieldMain.text == "" {
            value = "0."
        } else if textFieldMain.text!.contains(".") {
            return
        }
    }
    
    if value == "±" {
        if textFieldMain.text != "" {
            if textFieldMain.text!.hasPrefix("-") {
                textFieldMain.text!.remove(at: textFieldMain.text!.startIndex)
            } else {
                textFieldMain.text = "-" + textFieldMain.text!
            }
        }
        return
    }
    
    if refreshTextField {
        textFieldMain.text = value
    } else {
        textFieldMain.text! += value
    }
    
    refreshTextField = false
}

@IBAction func operatorPress(_ sender: UIButton) {
    if textFieldMain.text != "" {
        let newOperator = sender.titleLabel?.text!
        refreshTextField = true
        
        if currentOperator == "" && newOperator != "=" {
            valueA = auxFormatter.number(from: textFieldMain.text!)?.doubleValue ?? 0
            currentOperator = newOperator!
        } else {
            valueB = auxFormatter.number(from: textFieldMain.text!)?.doubleValue ?? 0
            
            var result: Double = 0
            switch currentOperator {
            case "+":
                result = valueA + valueB
            case "-":
                result = valueA - valueB
            case "×":
                result = valueA * valueB
            case "÷":
                result = valueA / valueB
            default:
                result = valueB
            }
            
            if currentOperator != "" {
                let historyEntry = "\(auxTotalFormatter.string(from: NSNumber(value: valueA))!) \(currentOperator) \(auxTotalFormatter.string(from: NSNumber(value: valueB))!) = \(printFormatter.string(from: NSNumber(value: result))!)\n"
                textViewHistory.text! += historyEntry
            }
            
            currentOperator = ""
            valueA = 0
            valueB = 0
            
            if newOperator != "=" {
                valueA = result
                currentOperator = newOperator!
            }
            
            textFieldMain.text = printFormatter.string(from: NSNumber(value: result))
        }
    }
}

但现在所有输入的行为都是这样的...... 如果我按这三个数字 7 > 8 > 9 >,它会显示77777777(如果我按顺序分解,则为 7 77 777 7777)

Swift UIVieviceController UIKit

评论

0赞 Joakim Danielson 8/26/2023
您应该在发布之前尝试自己解决问题。
0赞 es88mph 8/26/2023
谢谢Joakim Danielson,我将添加有关我所做的事情和未工作的更多信息!
0赞 DonMag 8/27/2023
@es88mph - 是否允许用户插入数字?那么,如果我按 和 get ,我可以在中间插入 a 来获得吗?还是“标准”计算器风格的输入,每个按钮按下“附加”数字?12123132
0赞 es88mph 8/27/2023
@DonMag这是一种标准的计算器风格,您只能获得 1、3、2 才能获得 132

答:

0赞 DonMag 8/28/2023 #1

有很多方法可以解决这个问题 -- 你可能想搜索现有的例子。

不过有一些建议......

看起来您在值和字符串之间转换,并过多地评估字符串。

最好保持值,并且仅在需要显示时才设置它们的格式。

因此,首先,我们可以沿着这些思路思考。

假设我们已经检查了按钮并将其转换为...var curVal: Double.currentTitlebtnVal: Double

    curVal = 0
    
    // user taps "5"
    curVal *= 10.0
    curVal += btnVal
    // curVal now equals 5
    
    // user taps "3"
    curVal *= 10.0
    curVal += btnVal
    // curVal now equals 53

    // user taps "7"
    curVal *= 10.0
    curVal += btnVal
    // curVal now equals 537
    

因此,每次我们更改 的值时,我们都会使用更新“显示”标签。curValNumberFormatter

如果我们输入一个负数,我们需要做一些不同的操作。为此,我们将减去 if is less-than-Zero:curValbtnValcurVal

    // user taps "5"
    curVal *= 10.0
    curVal += curVal >= 0 ? btnVal : -btnVal
    // curVal now equals 5
    
    // user taps "+/-"
    curVal *= -1.0
    // curVal now equals -5
    
    // user taps "3"
    curVal *= 10.0
    curVal += curVal >= 0 ? btnVal : -btnVal
    // curVal now equals -53
    
    // user taps "7"
    curVal *= 10.0
    curVal += curVal >= 0 ? btnVal : -btnVal
    // curVal now equals -537
    
    // user taps "+/-" again
    curVal *= -1.0
    // curVal now equals 537
    

NumberFormatter将在需要时自动添加“-”...因此,无需再看字符串是否以“-”开头或需要插入“-”。

当然,如果我们尝试在小数点后输入数字,这将不起作用!

因此,我们需要跟踪“十进制计数”并将其除以 10、100、1000 等,然后再将其添加到 .btnValcurVal

让我们看看我们的按钮点击处理程序可能开始是什么样子的:

@objc func btnTapped(_ sender: UIButton) {
    
    guard let title = sender.currentTitle else { return }
    
    // if it's a number button
    if let val = Double(title) {
        var p: Double = 0
        var btnVal: Double = val

        // if "decimal count" is greater-than-Zero,
        //  we need to divide the input value by 10, 100, 1000, etc
        if decCount > 0 {
            p = pow(10.0, Double(decCount))
            btnVal /= p
            decCount += 1
        } else {
            currentValue *= 10.0
        }
        currentValue += currentValue >= 0 ? btnVal : -btnVal

        // if we have decimal values, we need to round the result
        //  due to floating-point precision
        //  e.g. if we enter "0" "." "1" "2"
        //  the actual value is 0.12000000000000002048
        //  so we round to the current number of decimal places
        if p > 0 {
            currentValue = round(currentValue * p) / p
        }
    }

    // if decimal button tapped
    if title == decimalSym {
        if decCount == 0 {
            decCount = 1
            decJustTapped = true
        }
    }
    
    if title == plusMinusSym {
        // we don't want to display "-0"
        //  so only do this if...
        if currentValue != 0 {
            currentValue *= -1
        }
    }

    // clear the current value and
    //  reset decimal count
    if title == "C" {
        currentValue = 0
        decCount = 0
    }
    
    if operators.contains(title) {
        // perform operation...
    }
    
    udpateEntryLabel()
}

func udpateEntryLabel() {
    // this will handle entering "." followed by one or more Zeroes
    numFormatter.minimumFractionDigits = decCount - 1
    
    var str = numFormatter.string(from: currentValue as NSNumber) ?? "0"
    
    // this will only be true if the decimal symbol button was tapped
    //  but we do not yet have a fractional value
    if decJustTapped {
        decJustTapped = false
        str += "."
    }
    
    entryLabel.text = str
}

这是一个快速、完整的示例......


简单的按钮子类来获取“圆角”按钮

class PillButton: UIButton {
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = bounds.height * 0.5
    }
}

视图控制器示例

class CalcVC: UIViewController {
    
    let entryLabel = UILabel()
    
    let numFormatter: NumberFormatter = {
        let nf = NumberFormatter()
        nf.numberStyle = .decimal
        // we want ".1" to be displayed as "0.1"
        nf.minimumIntegerDigits = 1
        return nf
    }()

    var currentValue: Double = 0
    var decJustTapped: Bool = false
    var decCount: Int = 0

    // for clarity (tough to read division character and plus character)
    //  and, you'll probably want to localize the decimal character
    let plusMinusSym: String = "±"
    let pctSym: String = "%"
    let divideSym: String = "+"
    let multiplySym: String = "X"
    let minusSym: String = "-"
    let plusSym: String = "+"
    let decimalSym: String = "."
    let equalsSym: String = "="
    
    lazy var operators: [String] = [
        pctSym, divideSym, multiplySym, minusSym, plusSym, equalsSym
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue

        let btn = UIButton()
        let btnVal: Double = 0
        
        var curVal: Double = 0
        
        // user taps "5"
        curVal *= 10.0
        curVal += btnVal
        // cVal now equals 5
        
        // user taps "3"
        curVal *= 10.0
        curVal += btnVal
        // cVal now equals 53

        // user taps "7"
        curVal *= 10.0
        curVal += btnVal
        // cVal now equals 537
        
        // user taps "5"
        curVal *= 10.0
        curVal += btnVal
        // cVal now equals 5
        
        // user taps "+/-"
        curVal *= -1.0
        // cVal now equals -5
        
        // user taps "3"
        curVal *= 10.0
        curVal += curVal >= 0 ? btnVal : -btnVal
        // cVal now equals -53
        
        // user taps "7"
        curVal *= 10.0
        curVal += curVal >= 0 ? btnVal : -btnVal
        // cVal now equals -537
        
        // user taps "+/-" again
        curVal *= -1.0
        // cVal now equals 537
        

        let titles: [[String]] = [
            ["C", plusMinusSym, pctSym, divideSym],
            ["7", "8", "9", multiplySym],
            ["4", "5", "6", minusSym],
            ["1", "2", "3", plusSym],
            ["0", decimalSym, equalsSym],
        ]
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 6

        stackView.addArrangedSubview(entryLabel)
        
        for (iRow, rowTitles) in titles.enumerated() {
            let rowStack = UIStackView()
            rowStack.axis = .horizontal
            rowStack.distribution = .fillEqually
            rowStack.spacing = 6
            
            // handle bottom row differently because we have only 3 buttons
            if iRow == titles.count - 1 {
                
                let b = PillButton()
                b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
                b.setTitle(rowTitles[0], for: [])
                b.setTitleColor(.white, for: .normal)
                b.setTitleColor(.lightGray, for: .highlighted)
                b.backgroundColor = .darkGray
                rowStack.addArrangedSubview(b)
                
                let rowStack2 = UIStackView()
                rowStack2.axis = .horizontal
                rowStack2.distribution = .fillEqually
                rowStack2.spacing = 6
                for j in 1..<rowTitles.count {
                    let b = PillButton()
                    b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
                    b.setTitle(rowTitles[j], for: [])
                    b.setTitleColor(.white, for: .normal)
                    b.setTitleColor(.lightGray, for: .highlighted)
                    b.backgroundColor = j == 1 ? .darkGray : .systemOrange
                    b.heightAnchor.constraint(equalTo: b.widthAnchor, multiplier: 1.0).isActive = true
                    rowStack2.addArrangedSubview(b)
                }
                rowStack.addArrangedSubview(rowStack2)
                
            } else {
                rowTitles.forEach { s in
                    let b = PillButton()
                    b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
                    b.setTitle(s, for: [])
                    b.setTitleColor(.white, for: .normal)
                    b.setTitleColor(.lightGray, for: .highlighted)
                    b.backgroundColor = .darkGray
                    b.heightAnchor.constraint(equalTo: b.widthAnchor, multiplier: 1.0).isActive = true
                    rowStack.addArrangedSubview(b)
                }
            }
            stackView.addArrangedSubview(rowStack)
        }
        
        // set background colors for non-darkGray buttons
        for i in 0..<stackView.arrangedSubviews.count - 1 {
            if let sv = stackView.arrangedSubviews[i] as? UIStackView,
               let v = sv.arrangedSubviews.last {
                v.backgroundColor = .systemOrange
            }
        }
        if let sv = stackView.arrangedSubviews[1] as? UIStackView {
            for i in 0..<3 {
                sv.arrangedSubviews[i].backgroundColor = .lightGray
            }
        }
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        let g = view.safeAreaLayoutGuide
    
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
        ])

        // label properties
        entryLabel.font = .monospacedDigitSystemFont(ofSize: 24.0, weight: .regular)
        entryLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        entryLabel.textAlignment = .right
        entryLabel.text = "0"
        
    }
    
    @objc func btnTapped(_ sender: UIButton) {
        
        guard let title = sender.currentTitle else { return }
        
        // if it's a number button
        if let val = Double(title) {
            var p: Double = 0
            var btnVal: Double = val

            // if "decimal count" is greater-than-Zero,
            //  we need to divide the input value by 10, 100, 1000, etc
            if decCount > 0 {
                p = pow(10.0, Double(decCount))
                btnVal /= p
                decCount += 1
            } else {
                currentValue *= 10.0
            }
            currentValue += currentValue >= 0 ? btnVal : -btnVal

            // if we have decimal values, we need to round the result
            //  due to floating-point precision
            //  e.g. if we enter "0" "." "1" "2"
            //  the actual value is 0.12000000000000002048
            //  so we round to the current number of decimal places
            if p > 0 {
                currentValue = round(currentValue * p) / p
            }
        }

        // if decimal button tapped
        if title == decimalSym {
            if decCount == 0 {
                decCount = 1
                decJustTapped = true
            }
        }
        
        if title == plusMinusSym {
            // we don't want to display "-0"
            //  so only do this if...
            if currentValue != 0 {
                currentValue *= -1
            }
        }

        // clear the current value and
        //  reset decimal count
        if title == "C" {
            currentValue = 0
            decCount = 0
        }
        
        if operators.contains(title) {
            // perform operation...
        }
        
        udpateEntryLabel()
    }
    
    func udpateEntryLabel() {
        // this will handle entering "." followed by one or more Zeroes
        numFormatter.minimumFractionDigits = decCount - 1
        
        var str = numFormatter.string(from: currentValue as NSNumber) ?? "0"
        
        // this will only be true if the decimal symbol button was tapped
        //  but we do not yet have a fractional value
        if decJustTapped {
            decJustTapped = false
            str += "."
        }
        
        entryLabel.text = str
    }
}

运行时看起来像这样:

enter image description here

请注意,仅实现了数字、小数、“C”和“+/-”按钮......我将把操作和“历史记录视图”任务留给你:)

评论

0赞 es88mph 8/28/2023
明白了。这种方法作为一个过程听起来要简单得多,而且很有前途。我会尝试消化和实施你的建议,看看会发生什么。非常感谢!