Расчет общей суммы:
// the total, not rounded (e.g. if there was one item at a unit price of 0.10 and 3% markup (i.e. 0.03), this `unrounded` will have 0.103)
let unrounded = Double(quantity) * unitPrice * (markUp + 1.0)
// You might then round that value to two decimal places, like so:
let grandTotal = (unrounded * 100.0).rounded(.toNearestOrAwayFromZero) / 100.0
С учетом сказанного я хотел бы предложить несколько других вещей:
Вы можетеобратите внимание, что выше я не ссылаюсь на элементы управления UIKit, такие как текстовые поля и метки.Вы действительно хотите провести различие между «моделью» (цены, количества, итоги и т. Д.) И «видом» (текстовые поля, метки и т. Д.).
Объекты представления обычно, по соглашению, включают суффикс, который указывает тип объекта представления.Таким образом, вы можете иметь markUpTextField
или quantityLabel
.Таким образом, вы не только не перепутаете их с соответствующими значениями модели, но и можете четко сказать, что это за объект.
При обновлении текстового поля вам следуетобновить модель.Например, когда вы изменяете markUpTextField
, вы обновляете объект числовой модели markUp
.
Когда вы вычисляете сумму, вы должны рассчитывать ее только из объектов модели.Вы не должны ссылаться ни на какие UIKit
объекты.
Это не совсем критично, но это очень хорошая привычка, так как это центральный принцип шаблонов программирования MVC (и MVVM и MVP и ...).Преимущества этого действительно проявляются, когда вы в конечном итоге начинаете использовать представления таблиц / коллекций, где ваши элементы управления UIKit повторно используются для видимых элементов и больше не являются надежными источниками информации.Это также будет чрезвычайно полезно, когда вы начнете заниматься модульным тестированием своего кода, и вы извлекаете бизнес-логику из своих контроллеров представления и перемещаете их в некоторый посреднический объект, такой как «модель представления» или что-то еще.
Следует избегать использования String(format:)
для создания строк для пользовательского интерфейса.Вместо этого используйте NumberFormatter
.Это решает две проблемы:
Вы хотите принимать и создавать «локализованные» числа в вашем пользовательском интерфейсе.Например, в Германии они пишут число от одного миллиона до двух десятичных знаков как 1.000.000,00
.В Индии это может быть 10,00,000.00
.И т. Д. Используя NumberFormatter
, вы минимизируете сумму, которую вам необходимо кодировать для обработки всех этих международных форматов.
Если вы используете NumberFormatter
с numberStyle
из.percent
для вашего значения разметки, он сделает необходимое деление для вас 100
.
Вы можете установить delegate
дляUITextField
объекты должны быть вашим контроллером представления (что вы можете сделать либо в IB, либо программно), а затем иметь расширение UITextFieldDelegate
для вашего контроллера представления, чье shouldChangeCharactersIn
примет изменение, только если полученный текст может быть изменен начисло с использованием вышеуказанных форматеров.
Возможно, вам также понадобится textFieldDidEndEditing
, который хорошо форматирует введенное значение, когда пользователь завершит работу.
Отражая вышеприведенноеВ результате наблюдений вы получите что-то вроде:
class CostingsViewController: UIViewController {
// MARK: Outlets
@IBOutlet weak var quantityLabel: UILabel!
@IBOutlet weak var priceTextField: UITextField!
@IBOutlet weak var markUpTextField: UITextField!
@IBOutlet weak var totalLabel: UILabel!
// MARK: Model objects
var quantity: Int? { didSet { updateTotal() } }
var price: Double? { didSet { updateTotal() } }
var markUp: Double? { didSet { updateTotal() } }
var total: Double? { didSet { totalLabel.text = priceFormatter.string(for: total) } }
// MARK: Private formatters
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
return formatter
}()
private var quantityFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 0
return formatter
}()
private var percentFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// I'm going to set these here, but maybe these were supplied by the presenting view controller
quantity = 3
price = 1000
markUp = 0
// update the UI controls
quantityLabel.text = quantityFormatter.string(for: quantity)
priceTextField.text = priceFormatter.string(for: price)
markUpTextField.text = percentFormatter.string(for: markUp)
totalLabel.text = priceFormatter.string(for: total)
}
}
private extension CostingsViewController {
private func updateTotal() {
// calculate total
let quant = quantity ?? 0
let cost = price ?? 0
let percent = markUp ?? 0
let unrounded = Double(quant) * cost * (percent + 1.0)
// round the result
let rounded = (unrounded * 100.0).rounded(.toNearestOrAwayFromZero) / 100.0
// update our model
total = rounded
}
}
extension CostingsViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// some useful constants
let decimalSeparator = priceFormatter.decimalSeparator ?? "."
let percentSymbol = percentFormatter.percentSymbol ?? "%"
// figure out what the string value will be after replacing the characters
let oldText = textField.text ?? ""
let updateRange = Range(range, in: oldText)!
let text = oldText.replacingCharacters(in: updateRange, with: string).filter(("01234567890" + decimalSeparator).contains)
// update the appropriate model object
switch textField {
case priceTextField:
if text == "" {
price = 0
return true
} else if let value = priceFormatter.number(from: text)?.doubleValue {
price = value
return true
} else {
return false
}
case markUpTextField:
if text == "" {
markUp = 0
return true
} else if let value = percentFormatter.number(from: text + percentSymbol)?.doubleValue {
markUp = value
return true
} else {
return false
}
default:
return true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
switch textField {
case priceTextField: textField.text = priceFormatter.string(for: price)
case markUpTextField: textField.text = percentFormatter.string(for: markUp)
default: break
}
}
}
Еще одно уточнение: при создании типов данных для хранения цен я бы рекомендовал не использовать двоичные числа с плавающей запятой, такие как Float
или * 1074.*.Эти типы не могут на самом деле идеально захватить дробные десятичные значения.Я бы использовал Decimal
тип вместо этого.Это поможет избежать проблем округления, которые могут возникнуть, если вы начнете складывать много двоичных значений с плавающей запятой.
Если вы сделаете это, вы получите что-то вроде:
class CostingsViewController: UIViewController {
// MARK: Outlets
@IBOutlet weak var quantityLabel: UILabel!
@IBOutlet weak var priceTextField: UITextField!
@IBOutlet weak var markUpTextField: UITextField!
@IBOutlet weak var totalLabel: UILabel!
// MARK: Model objects
var quantity: Int? { didSet { updateTotal() } }
var price: Decimal? { didSet { updateTotal() } }
var markUp: Decimal? { didSet { updateTotal() } }
var total: Decimal? { didSet { totalLabel.text = priceFormatter.string(for: total) } }
// MARK: Private formatters
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
formatter.generatesDecimalNumbers = true
return formatter
}()
private var quantityFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 0
return formatter
}()
private var percentFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
formatter.generatesDecimalNumbers = true
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// I'm going to set these here, but maybe these were supplied by the presenting view controller
quantity = 3
price = Decimal(1000)
markUp = Decimal(0)
// update the UI controls
quantityLabel.text = quantityFormatter.string(for: quantity)
priceTextField.text = priceFormatter.string(for: price)
markUpTextField.text = percentFormatter.string(for: markUp)
totalLabel.text = priceFormatter.string(for: total)
}
}
private extension CostingsViewController {
private func updateTotal() {
// calculate total
let quant = Decimal(quantity ?? 0)
let cost = price ?? Decimal(0)
let percent = markUp ?? Decimal(0)
var unrounded = quant * cost * (percent + Decimal(1))
// round the result
var rounded = Decimal()
NSDecimalRound(&rounded, &unrounded, 2, .bankers)
// update our model
total = rounded
}
}
extension CostingsViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// some useful constants
let decimalSeparator = priceFormatter.decimalSeparator ?? "."
let percentSymbol = percentFormatter.percentSymbol ?? "%"
// figure out what the string value will be after replacing the characters
let oldText = textField.text ?? ""
let updateRange = Range(range, in: oldText)!
let text = oldText.replacingCharacters(in: updateRange, with: string).filter(("01234567890" + decimalSeparator).contains)
// update the appropriate model object
switch textField {
case priceTextField:
if text == "" {
price = Decimal(0)
return true
} else if let value = priceFormatter.number(from: text)?.decimalValue {
price = value
return true
} else {
return false
}
case markUpTextField:
if text == "" {
markUp = Decimal(0)
return true
} else if let value = percentFormatter.number(from: text + percentSymbol)?.decimalValue {
markUp = value
return true
} else {
return false
}
default:
return true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
switch textField {
case priceTextField: textField.text = priceFormatter.string(for: price)
case markUpTextField: textField.text = percentFormatter.string(for: markUp)
default: break
}
}
}
Наконец, как я упоминал выше, мы обычно хотели бы получить большую часть этого кода из контроллера представления (используя MVVP или MVP или что-то еще).Это выходит за рамки этого вопроса, но я упоминаю его для полноты картины.