Часть 2 — Бизнес-логика
В сегодняшнем уроке мы собираемся построить бизнес-логику калькулятора в Swift, используя модель MVVM с учетом лучших практик.
Построение представления описано в Часть 1.
Более того, вполне нормально начать отсюда, если вы не заинтересованы в построении представлений. Идите вперед и скачайте стартовый проект.
API калькулятора
Наша цель — сделать модель Calculator
полностью независимой.
Согласно принципу единой ответственности, каждый модуль, класс или функция должны иметь единую ответственность.
Калькулятор выполняет расчеты; поэтому он должен быть сосредоточен только на получении входных данных, вычислении и возврате результата. Он не несет ответственности ни за что, связанное с просмотром.
Это дает нам возможность использовать Calculator
где угодно. Мы можем использовать его в нашем уже существующем CalculatorView
, в модульных тестах, в другом совершенно другом представлении калькулятора, в CLI и т. д.
Чтобы сделать это возможным, нам нужно объявить API. — какие функции и свойства могут быть доступны вне для использования нашего Калькулятора?
Давайте подумаем, какие свойства и функции необходимы извне, чтобы иметь возможность полноценно работать с калькулятором (это будут наши public
свойства и функции). — Что в нашем распоряжении с помощью реального калькулятора?
Для свойств нам нужен способ чтения отображаемого в данный момент числа.
Для функций нам нужно выполнить действие для каждого случая ButtonType
.
Давайте начнем.
Внутри Models
создайте новый файл Swift с именем Calculator
Добавьте следующий шаблонный код:
Модель представления
Прежде чем продолжить с Calculator
, давайте сначала подключим наше представление, чтобы мы могли сразу начать его тестирование.
Ниже CalculatorView.swift
создайте новый файл Swift с именем CalculatorViewModel
:
В CalculatorViewModel.swift
создайте final class
имя ViewModel
, соответствующее протоколу ObservableObject
и находящееся внутри нашего CalculatorView
Наш ViewModel
будет включать в себя заказ buttonTypes
и экземпляр модели Calculator
. Нам также нужно добавить необходимые свойства и функции, чтобы иметь возможность работать с файлом Calculator
.
Вот код для CalculatorViewModel.swift
:
В CalculatorView.swift
добавьте новое частное свойство @EnvironmentObject
для ссылки на наш ViewModel
.
Обязательно обновите компоненты displayText
и buttonPad
, чтобы получить viewModel.displayText
и viewModel.buttonTypes
.
Вот код:
Теперь нам нужно уведомить ViewModel
о нажатии кнопки калькулятора.
В CalculatorButton.swift
добавьте ViewModel как объект среды и добавьте viewModel.performAction(for: buttonType)
для действия кнопки.
Наконец, в CalculatorApp.swift
создайте экземпляр ViewModel
как environmentObject
Теперь наши View
и наши ViewModel
полностью подключены! View
уведомляет ViewModel
о нажатии кнопки, а ViewModel
обновляет отображаемый текст, показанный в View
.
Калькулятор
Теперь мы начнем добавлять бизнес-логику, чтобы заставить Calculator
работать. Мы будем добавлять каждую из наших функций API одну за другой.
Установить цифру
Вернитесь к Calculator.swift
Прежде чем начать с логики установки цифр, нам понадобятся эти свойства, поэтому добавьте их вверху:
Внизу добавьте следующие вспомогательные функции
При установке цифры нам необходимо:
- Проверьте, можете ли вы добавить цифру (01 не должно быть возможно).
- Преобразование десятичного числа
newNumber
в строку - Добавьте
digit
в конец строки, преобразуйте строку обратно в десятичную и присвойте ее новое значениеnewNumber
.
Вот код для установки цифры:
Установить операцию
Нам нужно создать новую структуру с именем ArithmeticExpression
, чтобы упростить вычисление арифметических выражений.
Добавьте эту структуру в вашу модель Calculator
:
Добавьте свойства expression
и result
и обновите вычисляемое свойство number
.
Теперь для настройки операции нам необходимо:
- Проверьте, есть ли номер, который мы можем использовать (
newNumber
или предыдущийresult
), и назначьте его новой переменнойnumber
- Проверьте, есть ли уже
existingExpression
, если есть, оцените его с помощьюnumber
и присвойте результатnumber
- Назначьте новый
ArithmeticExpression
сnumber
иoperation
наexpression
- Сбросить
newNumber
Вот код:
Теперь наш Калькулятор работает для цифр и операций. Давайте просто добавим еще несколько визуальных очередей, чтобы знать, активна ли операция в данный момент.
Мы выделим кнопку операции, если пользователь только что нажал ее.
Добавьте следующую вспомогательную функцию в Calculator.swift
Добавьте следующую вспомогательную функцию в CalculatorViewModel.swift
И, наконец, добавьте следующие вспомогательные функции в CalculatorButton.swift
В том же файле обновите foregroundColor
и backgroundColor
стиля кнопки CalculatorButton
, чтобы получить только что добавленные вспомогательные функции.
Теперь у нас есть подсветка кнопок управления!
Оценивать
Это просто.
Для оценки нам необходимо:
- Развернуть
newNumber
иexpression
(выражение содержит предыдущее число и операцию) - Оцените
expression
с помощьюnewNumber
и назначьтеresult
- Сбросить
expression
иnewNumber
Вот код:
Установить процент
Чтобы установить процент, нам нужно:
- Проверьте, используется ли в настоящее время
newNumber
илиresult
- Разделите на 100 и присвойте новое значение
Вот код:
Переключить знак
toggleSign()
очень похоже на setPercent()
, так как нам нужно проверить, используется ли в настоящее время newNumber
или result
, и применить операцию (в этом случае добавить знак минус).
Тем не менее, есть сложная часть. — Мы не можем добавить знак минус к 0.
Решение?
Мы можем добавить отрицательный знак к строке, в частности к displayText
. Нам просто нужно знать, когда и когда не вставлять отрицательный знак.
Как и в элементарной математике, мы будем использовать переносы (9 + 8 = 17, перенос 1).
Добавьте это свойство вверху под private var result: Decimal?
private var carryingNegative: Bool = false
Таким образом, если существует newNumber
или result
, примените отрицательный знак, в противном случае сделайте свойство carryingNegative
истинным. (Останется истинным, пока не будет установлено новое число)
Добавьте этот код в toggleSign()
Свойство carryingNegative
используется во вспомогательной функции getNumberString()
Помните, когда newNumber
, expression.number,
и result
равны нулю, значение по умолчанию равно 0? Благодаря свойству carryingNegative
мы можем вставить строку отрицательного знака в 0, чтобы вернуть «-0» displayText
так же, как визуальную очередь, что отрицательный знак активен.
Обновите getNumberString()
следующим кодом:
Наконец, нам нужно деактивировать отрицательный перенос, когда установлено newNumber
.
Добавьте этот код в newNumber
:
Установить десятичное число
Установка десятичной дроби может быть самой сложной частью.
Теперь нам нужно добавить логику переноса к двум вещам. До десятичной точки и нулей после запятой.
Десятичная точка
Наше свойство newNumber
имеет тип Decimal
. Так что не существует таких вещей, как 5.
или 15.
, это целые числа. Однако, если пользователь нажимает кнопку с десятичным числом, нам нужно предоставить визуальную очередь, в которой устанавливается десятичное число. Нам нужно обновить displayText
, пока мы не сможем установить newNumber
в соответствующее десятичное число. Для этого мы будем использовать новое свойство с именем carryingDecimal
.
Давайте запустим быстрый пример для ввода 5.2
- Установите цифру 5 (
newNumber = 5
,carryingDecimal = false
иdisplayText = “5”
) - Установите десятичное число (
newNumber = 5
,carryingDecimal = true
иdisplayText = “5.”
) - Установите цифру 2 (
newNumber = 5.2
,carryingDecimal = false
иdisplayText = “5.2”
)
Добавить свойство carryingDecimal
Добавить containsDecimal
вычисляемое свойство
Фактическая десятичная функция настройки очень проста:
- Проверить, содержит ли уже
number
десятичное число, если да, вернуть - Сделайте свойство
carryingDecimal
true
Вот код:
Наконец, сбросьте carryingDecimal
, когда установлено newNumber
Нали после запятой
Теперь нам предстоит решить похожую задачу. При установке нулей после десятичной точки наше свойство newNumber
, имеющее тип Decimal
, никогда их не добавит. Что касается 5.40000
, то он всегда будет преобразован обратно в 5.4
, так как это не String
.
Подобно тому, что мы сделали с десятичной точкой, нам нужно создать новое свойство с именем carryingZeroCount
, чтобы отслеживать нули после десятичной точки и добавлять их, когда установлена ненулевая цифра. Тем временем мы также покажем нули в displayText
, чтобы обеспечить визуальную очередь.
Давайте рассмотрим это на примере, чтобы было понятнее. Мы хотим ввести 2.003
- Установите цифру 2 (
newNumber = 2
,carryingDecimal = false
,carryingZeroCount = 0
,displayText = “2”
) - Установить десятичное число (
newNumber = 2
,carryingDecimal = true
,carryingZeroCount = 0
,displayText = “2.”
) - Установите цифру 0 (
newNumber = 2
,carryingDecimal = true
,carryingZeroCount = 1
,displayText = “2.0”
) - Установите цифру 0 (
newNumber = 2
,carryingDecimal = true
,carryingZeroCount = 2
,displayText = “2.00”
) - Установите цифру 3 (
newNumber = 2.003
,carryingDecimal = false
,carryingZeroCount = 0
,displayText = “2.003”
)
Я знаю, что это становится несколько запутанным, но, делая это, мы можем вообще избежать использования манипуляций со строками для ввода, что приносит свою долю проблем и сложностей.
Добавьте свойство carryingZeroCount
Обновите функцию setDigit()
до:
И, наконец, добавьте переносимые нули к getNumberString()
Все чисто
Очистить все просто. Нам просто нужно сбросить все наши используемые в настоящее время свойства
Вот код:
Прозрачный
Заметили, что при использовании Apple Calculator кнопка AC (Очистить все) меняется на кнопку C (Очистить) сразу после ввода цифры или десятичного числа? Эти кнопки могут выглядеть так, как будто они делают одно и то же, сбрасывая дисплей на 0, но на самом деле у них разные функции.
Все ясно сбросил все в том числе newNumber
, expression
, result
и возит.
Очистите только последнюю запись, в нашем случае newNumber
и ее переносы.
Итак, допустим, вы хотите посчитать 5 + 3. Вы вводите 5 + 2 — Ой! Нажмите кнопку очистки (5 + все еще сохраняется), введите 3, результат = 8. Все хорошо.
Давайте приступим к этому, но сначала добавим свойство pressedClear
, оно нам понадобится.
Аналогично нашей allClear()
функции добавьте этот код в clear()
И обновить newNumber
, чтобы при его установке мы сбрасывали pressedClear
Давайте добавим небольшое улучшение в number
для лучшего взаимодействия с пользователем.
Если пользователь только что нажал очистку или десятичное число, давайте покажем 0, даже если есть активный результат или выражение.
Обновите свойство number
с помощью этого кода:
Показывать кнопку AC или C в нашем представлении
Теперь, чтобы узнать, какая из двух кнопок очистки должна отображаться в нашем представлении, нам нужно создать вычисляемое свойство.
Добавьте вычисляемое свойство showAllClear
:
Вернитесь к CalculatorViewModel.swift
и обновите вычисляемое свойство buttonTypes
:
Окончательный калькулятор
Вот код нашей окончательной модели Calculator
:
Подведение итогов
Наконец-то мы закончили создание Apple Calculator. Спасибо, что вытащили.
Мы успешно инкапсулировали каждый компонент в свою собственную функциональность. ViewModel
не несет ответственности за выполнение вычислений, он просто информирует Calculator
о том, что вводит пользователь, и запрашивает результат.
Созданную нами модель Calculator
теперь можно использовать с любым видом. Мы даже можем создавать модульные тесты, не используя какой-либо другой файл.
Исходный код этой части вы можете найти здесь.
Спасибо за чтение, я был бы очень признателен, если бы вы подписались, чтобы я мог продолжать создавать больше подобного контента :)