Задание 2 cs193p Winter 2017 «Умный» Калькулятор. Решение. Обязательные и дополнительные пункты.

Содержание

Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Programming: Project 2: Calculator Brain″На русском языке вы можете скачать  текст Задания 2 здесь:

Задание 2 iOS 10.pdf

В Задании 2 вы усовершенствуете возможности Калькулятора, позволяя ему вводить “переменные” и поддерживать операцию Undo. Вы также подготовите Калькулятор к Заданию 3 следующей недели.
Для успешного выполнения Задания 2 нужно изучить материлы Лекций 1, 2 и 3.

Несмотря на схожесть названия Задания 2 в курсе «Developing  iOS 10 Apps with Swift» на название Задание 2 в предыдущем курсе  «Developing iOS 9 Apps with Swift», вам предлагается реализовать его совершенно по-другому, ближе к функциональному стилю программирование, а не объектно-ориентированному как было в прошлый раз. И начало этому положено в Задании 1 и на Лекциях 1 и 2, когда Модель CalculatorBrain, которая в прошлом курсе была  классом class, была заменена на структуру struct

В этом Задании вам придется кардинально поменять Модель вашего MVC и составить ее уже из двух совершенно отдельных структур данных: struct CalculatorBrain и Dictionary<String : Double> variableValues . Это абсолютно законно. Нет правила, которое бы говорило, что ваша Модель должна иметь единственную структуру данных.
Некоторые подсказки вы найдете в Лекции 3.

От вас потребуется знание всего спектра приемов работы со структурами struct, перечислениями enum, семантическое понимание Optional, использование замыканий и т.д.

Мое решение Задания 2 Зима 2017 г. Находится на Github:

Код для обязательных пунктов находится на Github.
Код для обязательных и дополнительных пунктов находится на Github.

Пункт 1 обязательный

Не изменяйте, не убирайте и не добавляйте ничего в public API (то есть НЕprivate func и vars) из вашего CalculatorBrainв Задании 1, за исключение undo ()и того, что определено в Обязательных пунктах 3 и 4. Продолжайте использовать Dictionary<String,Operation> в качестве основной внутренней структуры данных для выполнения операций в CalculatorBrain.

Пункт 2 обязательный

Ваш пользовательский интерфейс (UI) должен быть всегда синхронизирован с вашей Моделью (CalculatorBrain).

Пункт 3 обязательный

Наделите ваш CalculatorBrain способностью вводить “переменные”. Сделайте это путем реализации следующего API в вашем CalculatorBrain

func setOperand  (variable named: String)

Эта  функция должна делать в точности то, что вы можете себе представить: вводить “переменную” как операнд (например, setOperand (variable: “x”) будет вводить переменную с именем х). Установка операнда в x, а затем выполнение операции cos будет означать cos (x) в вашем CalculatorBrain.

У нас  есть подсказка № 1, которая гласит:

Несмотря на запрет в Обязательном пункте 1 этого Задания, вы можете изменять в private реализации CalculatorBrain все, что вам угодно. Основные изменения сводятся к тому, чтобы запоминать последовательность операндов и операций, которые вводятся в CalculatorBrain, потому что в дальнейшем вам придется многократно оценивать evaluate эту последовательность с произвольными значениями “переменных” (также вы должны уметь делать undo в этой последовательности на один шаг за раз).

Итак, мы должны придумать структуру данных для хранения наших операндов, операций и переменных, введенных в наш Калькулятор, и это будет массив internalProgram разнородных элементов, которые будут представлены очень простым перечислением OpStack:

Массив internalProgram является private переменной в нашей Модели — структуре struct CalculatorBrain и хранит разные элементы, это своего рода внутренняя программа действий нашего Калькулятора. Каждый элемент массива   internalProgram сопровождается определенным ассоциированным значением:

операнд  operand (Double) имеет значение типа Double
операция  operation (String) имеет символ операции типа String
переменная  variable (String) имеет символ  типа String

По мере того, как пользователь будет вводить операнды, операции и переменные на нашем UI, в Модели CalculatorBrain будут работать те же методы, что и прежде: setOperand и performOperation, но смысл этих методов будет другой: они будут заполнять мой массив internalProgram, который в последствие будет обрабатываться функцией evaluate для получения результатов расчета Калькулятора:

Мы добавили метод для ввода «переменных», для которых пока нет соответствующих элементов на нашем UI, но они будут добавлены позже :

func setOperand  (variable named: String)

Пункт 4 обязательный

Теперь, когда вы позволили вводить “переменные” как операнды, добавьте метод оценки CalculatorBrain (то есть получения результата расчета) путем подстановки значений для этих “переменных” из дополнительного словаря Dictionary


 func evaluate(using variables: Dictionary<String,Double>? = nil)
-> (result: Double?, isPending: Bool, description: String)

Заметьте, что этот метод берет Optional Dictionary (с ключами Strings и значениями Doubles) в качестве аргумента, значение которого по умолчанию равно nil, если при вызове метода словарь Dictionary не задается. Также заметьте, что этот метод возвращает кортеж (tuple) (первым элементом которого является Optional Double). Этот метод не является mutating (изменяющим) и вы не должны позволять компилятору делать его mutating. Если  “переменная”, установленная как операнд, не обнаружена в словаре Dictionary, то предполагается, что ее значение равно 0.

Мы перенесем в метод evaluate все методы и свойства, которые раньше находились в нашей структуре struct CalculatorBrain, но теперь они не будут  mutating методами и private переменными, они будут просто вложенными функциями и локальными свойствами:

Тело функции  evaluate очень простое — это последовательный анализ элементов массива internalProgram операндов, операций и «переменных», которые пользователь ввел в Калькулятор.

Метод evaluate не является mutating (это не разрешается согласно условиям Задания), она не вызывает никаких побочных эффектов в CalculatorBrain. Она использует функции с теми же именами setOperand и  performOperation, что и нынешняя структура   CalculatorBrain, но это вложенные в функцию evaluate функции, которые производят действительные расчеты на нашем Калькуляторе:

Это те самые функции, которые были в структуре CalculatorBrain, но теперь они находятся внутри метода evaluate, и хотя они продолжают использовать кортеж cache, который использовался в CalculatorBrain в Задании 1:

но теперь это локальная переменная метода evaluate, а не свойство в структуре struct CalculatorBrain.

Пункт 5 обязательный

Мы сделали result, description и resultIsPending НЕ-private переменными vars API CalculatorBrain в Задании 1. Это означает, что мы “подписались” продолжать поддерживать их, даже не смотря на то, что у нас появились новые возможности (концепция “переменных” как операндов) в этом Задании, что сделало их теперь неуместными. Реально мы должны упразднить (deprecate) эти vars (посмотрите все виды упраздненных iOS API в Xcode), но пока мы будем поддерживать result, description и resultIsPending НЕ-private vars и просто реализуем каждого из них простым вызовом функции evaluate с аргументом nil (то есть они дадут свои ответы в предположении, что все “переменные” равны 0). Тем не менее, не используйте никакие из этих vars где-нибудь в вашем коде в этом Домашнем Задании. Вместо этого используйте функцию evaluate.

В словаре слово “deprecate“ означает “выразить неодобрение”. Это означает, что вы просите людей прекратить использовать этот API, потому что собираетесь избавиться от него в будущем. Если вы посмотрите iOS документацию, то увидите много “deprecated” (упраздненных) методов. Мы поступим также, как поступает компания Apple, если она хочет избавиться от некоторого public API, которое она представляла в прошлом:

Теперь public API структуры struct CalculatorBrain выглядит следующим образом:

Пункт 6 обязательный

Вы не можете использовать Swift Типы Any> или AnyObject в этом Задании.

Мы действительно не использовали Типы Any или AnyObject в этом Задании. Массивы, содержащие  Any или AnyObject используются почти исключительно для обратной связи с Objective C. В Swift мы использовали массив Array enum элементов.

Пункт 7 обязательный

Добавьте две новых кнопки на UI калькулятора Calculator: →M и M. Не приносите в жертву никакие кнопки с требуемыми операциями из Задания 1 (хотя вы можете добавить больше операций, если хотите). Эти две кнопки будут соответственно устанавливать и получать “переменную” с именем M в CalculatorBrain.

  1. →M вызывает функцию evaluate в вашей Model со словарем Dictionary,  у которого единственный элемент с ключом M и значением, равным текущему значению на display. Затем display модернизируется для показа результата result, полученного из функции evaluate. До тех пор,  пока не будет повторно нажата либо эта кнопка, либо кнопка С, тот же самый словарь Dictionary будет использоваться при вызове функции evaluate.
  2. →M не должна выполнять setOperand.
  3. Нажатие M должно вызвать setOperand(“M”) в brain и затем показать на display результат result вызова evaluate со значением display.
  4. →M и M являются механизмом Controller, а не механизмом Model (хотя они оба используют механизм “переменных” в Model).
  5. Это не выдающаяся кнопка “memory” на нашем калькуляторе, но она является хорошим инструментом для тестирования, правильно ли работает концепция “переменных”, реализованная выше.  Примеры …

9 +  M = √ description имеет вид √(9+M), display показывает 3, так как M не установлена (то есть равна 0)

7 →M >⇒ display теперь показывает 4 (квадратный корень 16), description все еще показывает (9+M)

+14  = display показывает 18, description теперь (9+M)+14

Располагаем на storyboard  кнопки →M  и M и с помощью CTRL-перетягивания создаем для них @IBAction функции setM и pushM:

Хотя методы @IBAction в названии ориентируются на переменную M, реализация методов  setM и pushM предполагает произвольную переменную, которую мы читаем с заголовка кнопки:

Для хранения значений «переменных» мы используем словарь variableValues, который находится в классе ViewController и является такой же составной частью Модели этого MVC, как и brain:

Теперь наша Модель сделана из двух различных и совершенно отдельных структур данных: struct CalculatorBrain и Dictionary<String : Double> variableValues (того, который содержит значения для M и других «переменных»). Это абсолютно законно. Нет правила, которое бы говорило, что ваша Модель должна иметь единственную структуру данных.

Проверяем работоспособность на примерах, представленных в Задании:

Пункт 8 обязательный

Покажите значение M (если оно установлено) где-нибудь на вашем UI.

Добавим метку displayM для отображения значения M в один StackView вместе с меткой history для описания действий пользователя на Калькуляторе :

Метки history и displayM разделят горизонтальное пространство в соответствие с множителем  0.3. Вот как будет выглядеть результат последнего теста:

Пункт 9 обязательный

Убедитесь, что кнопка С вашего Задания 1 работает правильно в этом задании. В дополнение она должна упразднять использование словаря Dictionary для “переменной” M (не устанавливать M в 0 или какое-то другое значение, просто останавливать использование словаря Dictionary до тех пор, пока не будет нажата кнопка →M повторно). Это позволит вам тестировать случай “неустановленной” переменной.

Так как у нас две части Модели, мы должны уметь очистить обе ее части. В CalculatorBrain мы добавляем в public API метод clear, который  очищает массив internalProgram:

Это эквивалентно коду internalProgram = [].

Словарь variableValues очищается одной строкой кода, так что используем очистку обеих частей Модели в реализации кнопки С:

Вот как выглядит Action clearAll:

 

Пункт 10 обязательный

Добавьте “Undo” к вашему калькулятору: в дополнительном пункте Задания 1 вы добавляли  кнопку  “backspace”, если пользователь ввел неверную цифру. Теперь мы говорим о комбинации “backspace” и реального “Undo” в единой кнопке. Если пользователь находится в середине ввода числа, то это кнопка работает как  backspace. Если пользователь не находится  в середине ввода числа, то должно сделать Undo последней вещи, которая была выполнена в CalculatorBrain.  Не отменяйте запоминания значений переменной (но ДЕЛАЙТЕ отмену установки “переменной” как операнда).

Добавляем в API CalculatorBrain новый метод undo(), который позволяет удалять последний элемент из массива internaProgram:

И используем его в нашем Controller на кнопке «backspace«:

Вот как выглядит Action backspace:

Из текста метода backspace видно, что мы работаем только с меткой display, если идет набор числа, и с внутренней программой  internaProgram Калькулятора brain., мы не трогаем напрямую словарь variableValues

После реализации операции «undo» нам необходимо проверить работоспособность ситуации, указанной в подсказке № 10:

Без undo можно вводить только числовые значения M. Если вы попытаетесь запомнить что-то еще в качестве значения M (например, ? или 23÷2), то выражение становится новым активным выражение для Калькулятора, замещая предыдущее, которое вы хотели оценивать первоначально. Хотя, как только вы сделаете undo, то вы сможете вернуться к назад к предыдущему выражению после того, как установили значение для M. Например, ввод M cos, затем π, затем →M, затем undo (чтобы избавиться от ?) … Теперь ваш Калькулятор будет показывать значение cos(M), которое будет равно -1.

Для  этого вводим:
 M cos  >⇒ description имеет вид cos(M) =display показывает 1, так как M не установлена (то есть равна 0)
Затем вводим:
 π →M  ⇒ description имеет вид π =display показывает 3.141593
Undo   ⇒ description имеет вид cos (M) =display показывает -1, так как M установлена в π.

Добавляем вышеприведенный тест:

Получаем результат теста:

Код для обязательных пунктов находится на Github.

Пункт 1 дополнительный

Ваш Калькулятор должен сообщать об ошибках. Например, √  из отрицательного числа или деление на нуль. Есть несколько способов “обнаружения” этих ошибок  (может быть добавить ассоциированное значение к Unary/ BinaryOperation вариантам, которые являются функциями, которые обнаруживают ошибку, или, возможно, заставить функцию, которая ассоциируется с Unary/ BinaryOperation возвращать кортеж как с результатом result, так и с ошибкой  error (если она есть) или ???). То, как вы будете сообщать о любых обнаруженных ошибках пользователям CalculatorBrain API, потребует от вас некоторых изменений API, но не заставляйте пользователей CalculatorBrain API иметь дело с ошибками, если они этого не хотят (то есть позвольте Controllers, которые хотят показывать errors, делать это, а другим, которые не хотят показывать ошибки,  позвольте иметь дело просто с NaN или +∞, появляющимися на вашем UI). Другими словами, не изменяйте никакие из существующих методов и свойств в non-private API CalculatorBrain для того, чтобы поддерживать эти возможности  (вместо этого добавьте методы / свойства, если это необходимо).

Необходимо уметь спрашивать операции binaryOperation и unaryOperation, какие ошибки (если они есть) будут генерироваться при передачи в них  операнда (ов).
Один из способов сделать это — иметь для binaryOperation и unaryOperation операций ассоциированное значение , которое представляют собой функцию, анализирующую потенциальные аргументы и возвращающую соответствующее String сообщение об ошибке error, если выполнение операции генерирует ошибку (или nil  в противном случае).
Большинство операций не могут сообщать о каких-то ошибках, да мы и не обязаны заставлять их это делать. В этом случае мы будем использовать  nil в качестве функции, тестирующей ошибки. Можно сделать Optional целиком тип функции, размещая тип функции в круглых скобках и ставя знак ? после круглых скобок. Например, ((Double, Double ) -> String?)? — это  Optional  функция, которая берет два Double и возвращает Optional <String>.
Добавляем к операциям binaryOperation и unaryOperation новое ассоциированное значение в виде типа тестирующей ошибку функции.

 Здесь интересны два варианта:

  • первый — если сама тестирующая ошибки функция не установлена, то есть сама функция errorTest -это nil,
  • второй — функция errorTest не nil, но она декларирована так, что возвращает nil. Это очень удобно.

В нашей реализации только некоторые унарные операции типа lnx⁻¹, sin⁻¹cos⁻¹ и бинарная операция «деление»  ÷ получают тестирующую ошибку функцию.

Ошибку будем фиксировать в локальной переменной error: String? в методе evaluate :

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
Прежде, чем выполнить унарную или бинарную операцию, тестируем полученные операнды на ошибку. Если ошибка обнаружена, то фиксируем ошибку в error:


.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и здесь интересны два варианта:

  • первый — если  функция validator не установлена, например, для операций «sin«,»cos«,»tan«, то error равно nil,
  • второй — функция validator не nil, но она декларирована так, что возвращает nil в случае отсутствия ошибки. Это очень удобно.

Здесь действительно очень удачно работают два перечисленных выше варианта. Если у вашей операции, например, «sin» или «cos» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции ««, а аргумент не дает «Отрицательное значение», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator является очень удобным при обработке ошибок.
Бинарная операция может быть в отложенном состоянии, поэтому тестирующую функцию нужно запомнить в отложенной структуре PendingBinaryOperationInfo:

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

а затем использовать при вычислении на предмет проверки ошибки:

И опять используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и опять здесь удачно работают два перечисленных выше варианта. Если у вашей операции, например, «+» или «» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции деления «÷«,а аргументы не дают «Деление на нуль», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator позволяет нам вести обработку ошибок одной строкой.
Для того, чтобы передать обнаруженную или не обнаруженную ошибку в ViewController, расширим кортеж, возвращаемый методом evaluate и добавим сообщение об ошибке error типа String?:

В ViewController добавим переменную кортеж displayResult, которая будет «встречать» кортеж, вернувшийся из метода evaluate в CalculatorBrain, производить необходимую обработку кортежа с помощью оператора switch c паттернами  соответствия образцу (pattern matching) и осуществлять всю необходимую настройку UI:

Если Controllers не хотят показывать ошибки error,а хотят иметь дело просто с NaN  или +∞, появляющимися на вашем UI, то можно просто убрать последний вариант case в switch операторе для displayResult. и не выводить на display сообщение error об ошибке.

Давайте напомним, что для соответствующих кнопок, где результат отображается на дисплее display, мы устанавливаем значение для переменной displayResult с помощью метода evaluate в CalculatorBrain:
кнопка с операциями

кнопка backspace

кнопка C

кнопка →M

кнопка M

Пункт 2 дополнительный

Добавьте иконки в ваш файл Assets.xcassets (который вы переместили в папку Supporting Files на Лекции 1). Единственная хитрость в этом пункте состоит в обеспечении версии вашей иконки для всех правильных размеров.

Иконки задаются в файле Assets.xcassets, в котором есть необходимые «кармашки» для каждого устройства (будем использовать иконки только для iPhones  и iPads iOS 7 и позже):

Для получения иконок я использую Mac приложение   AppIcon, которое имеет экран для приема изображения и кнопку «Export»:

При нажатии кнопки «Export» получаем все иконки необходимого размера, которые затем перетягиваем в нужные «кармашки».

Пункт 3 дополнительный

Создайте экран запуска для Калькулятора в LaunchScreen.storyboard. Скорее всего, вы захотите использовать некоторое простое взаимное расположение элементов экрана, чтобы заставить экран запуска выглядеть хорошо на всех платформах.

Устанавливаем Launch файл в закладке > General:

Выбираем LaunchScreen.storyboard в Навигаторе проекта и устанавливаем опцию  «Use as Launch Screen» в Инспекторе Файла:

Перетягиваем из Палитры Объектов в Области ImageView и «бросаем» внутрь экранного фрагмента Views Controller:

Выравниваем по голубым линиям в верхнем левом углу и растягиваем на весь экран также по голубым линиям:

Используем установку ограничений (constrains) механизма Autolayout по голубым линиям «Reset to Suggested Constrains» с помощью кнопки в нижнем правом углу и получаем  ограничения (constrains) в Инспекторе Размера:

Мы растянули изображение на весь экран, какого бы размера оно ни было. Теперь перетягиваем файл с изображением в наш проект. Опять выбираем изображение на storyboard,  выбираем закладку Инспектор Атрибутов и устанавливаем это изображение, а также опцию «Aspect Fit«:

Теперь все будет работать на всех приборах и различных ориентациях.

Дополнение. Учет приоритета бинарных операций при получении description для  потока бинарных операций

Еще в Задании 1 мы научились выполнять поток бинарных операций без промежуточной операции «=«. Для этого мы добавили метод performPendingBinaryOperation() в обработку бинарной операции:

Это работает прекрасно, если это поток операций с одинаковым приоритетом. Например, 6 + 7 + 11 + 4 или 6 × 7 × 11 × 4. Мы получаем прекрасный description «6 + 7 + 11 + 4 =» или «6 × 7 × 11 × 4 =«.

Описание должно содержать скобки согласно приоритету операций:

Для этого мы должны наделить бинарные операции еще одним свойством — приоритетом preference:

Для операций умножения и деления устанавливаем precedence1, а для сложения и вычитания — 0, а для бинарной операции возведения в степень «» — 2:

Для остальных операций , а также для операнда значение precedence принимает значение по умолчанию:

Мы отслеживаем приоритет prevPrecedence предыдущей операции (или операнда) и приоритет  precedence текущей бинарной операции с помощью структуры PendingBinaryOperation:

И составляем description  бинарной операции с учетом этой информации — если приоритет  prevPrecedence предыдущей бинарной информации меньше приоритета  precedence текущей операции, то заключаем первый операнд в круглые скобки:

При выполнении отложенной бинарной операции делаем предыдущим приоритетом приоритете текущей бинарной операции :

Давайте проверим работу нашего калькулятора на примере  функцию (sin(M)+M)÷M, которую мы создадим с помощью  переменной M. Получим, между прочим, сообщение об ошибке  «Деление на нуль«, так как если значение M не присвоено значение, то оно равно 0. Eсли мы затем пошлем в переменную M значение π/2, а с помощью ⌫ вернемся назад к нашей  формуле, то значение выражения (sin(M)+M)÷будет равно 1.63662.

Код для обязательных и дополнительных пунктов находится на Github.

ОБСУЖДЕНИЕ МАТЕРИАЛОВ курса «Разработка iOS приложений с Swift» проводится на private новом форуме на Piazza. Делиться своими решениями и задавать вопросы можно там.
Для регистрации вам необходимо пройти по ссылке:

http://piazza.com/moscow_physical_engineering_institute_bestkora.com/spring2017/mf141
и набрать private  код mf141.

Задание 2 cs193p Winter 2017 «Умный» Калькулятор. Решение. Обязательные и дополнительные пункты.: 21 комментарий

  1. В методе clearAll() ViewController’а надо бы userInTheMiddleOfTyping в false ставить, а то лидирующий ноль при сбросе будет.

      • словарь для М, также не надо сбрасывать (it should not set M to zero or any other value, just stop using that Dictionary until →M is pressed again)

        • Интересно, какой же есть другой способ «остановить» использование словаря Dictionary?

          • Остановить использование словаря вообще нельзя, потому, что даже вводя в него 0, что пункт 9 запрещает (не устанавливать M в 0 или какое-то другое значение), мы не продолжаем использование словаря, но только с 0 (нулем), который появляется при первой же операции (опять же установка М в 0). Если только профессор не ошибся в формулировке, то надо как бы удалять М из формулы, но примерами это он не подтвердил, что и как должно быть, поэтому, наверное этот вариант единственно реальный. Немного жалко только времени из-за этой формулировки потраченного…

          • Все не так печально, как вам кажется. Профессор вовсе не ошибся, он подталкивает вас к тому, чтобы вы умели очень легко использовать Optional словарь, чтобы останавливать поиск в словаре значения по ключу с помощью цепочки Optional:
            cache.accumulator = variables?[named] ?? 0
            Видите?
            Если variables = nil, то поиск по named не происходит.
            Можно это использовать и в правой части в ViewController :
            variableValues?[symbol] = displayValue
            У меня этого не сделано, но стоило бы и в ViewController сделать словарь переменных Optional.
            Попробуйте.
            Это отличное упражнение.
            В Swift 4 это еще лучше усовершенствовали.

          • Я и ищу, буду пробовать и через Optional.
            А пока промежуточное решение (костыль) «приостанавливающее»словарь:
            // кнопка
            private var parityflag = false
            @IBAction func setVariable(_ sender: UIButton){
            if !parityflag {
            parityflag = true
            variableCollection[«W»] = displayValue
            variableCollection[«Ⓜ️»] = displayValue
            } else {
            parityflag = false
            variableCollection[«Ⓜ️»] = 0
            }
            displResult()
            userInTheMiddleOfTyping = false
            }
            и
            displayM.text = numberFormatter.string(from: NSNumber(value:variableCollection[«W»] ?? 0))

          • Вы меня не поняли: Optional является не только значение variableValues[«M»], но и сам словарь variableValues — Optional:

            private var variableValues : [String: Double]?

            Здесь как бы два уровня Optional:
            один на уровне целиком словаря variableValues,
            а другой на уровне значения для ключа «M».
            Останавливает использование словаря именно Optional на уровне словаря :
            variableValues = nil
            И именно равенство variableValues = nil останавливает поиск в словаре благодаря цепочке:
            variableValues?[«M»]
            Сначала смотрим, а не равен ли nil сам словарь, и если это так, то поиска по «M» не будет.
            Понимаю, что сложно, но надо «врубаться». Именно на это рассчитывал профессор.

          • Через Optional тоже работает:
            } else {
            parityflag = false
            variableCollection[«Ⓜ️»] = nil // Работает
            }
            displResult()
            userInTheMiddleOfTyping = false
            }
            Только сбрасывает в 0 displayM

          • Ещё раз перечитал текст задания, в принципе запрета на установку в 0 кнопкой →M нет, ( в отличие от кнопки С. Поэтому решение выглядит так:
            // Сброс кнопкой С
            @IBAction func reset(_ sender: UIButton) {
            userInTheMiddleOfTyping = false
            brain.reset ()
            displResult()
            // sequence.text = String() // пустая строка ленты (, что и при включении симулятора)
            variableCollection[«Ⓜ️»] = nil
            parityFlag = false
            }
            private var variableCollection = [ String: Double] ()
            private var parityFlag = false
            @IBAction func setVariable(_ sender: UIButton) { // кнопка
            if !parityFlag {
            parityFlag = true
            variableCollection[«Ⓜ️»] = displayValue
            } else {
            parityFlag = false
            variableCollection[«Ⓜ️»] = nil
            }
            displResult()
            userInTheMiddleOfTyping = false // зафиксировать окончание ввода операнда ( — тоже операнд)
            }
            // вычисление с «переменной»
            @IBAction func evaluteVariable(_ sender: UIButton) { // кнопка
            brain.setOperand(variable: «Ⓜ️»)
            displResult()
            }
            где:
            private func displResult() {
            displayResult = brain.evaluate(using: variableCollection) }
            Таким образом словарь кнопкой С не устанавливается в какое-либо значение, в частности в 0.

          • Вы извините, но это полная «каша».
            Как можно писать variableCollection[«Ⓜ️»] = nil, если ваш словарь
            private var variableCollection = [ String: Double] ()
            не имеет Optional значения.
            Говоря об Optional словаре , я имела ввиду следующее:

  2. Если вводить бинарные операции подряд, то операнды будут перезаписываться.
    Например при наборе:
    12 + 14 — 26 =
    Результат получается:
    12 + 26 =

    • Исправил, оказывается на «Storyboard» не реагировала кнопка «-«, т.к. это был другой символ похожий на «-«, но разный по Unicode.

    • Исправил. Оказывается не реагировала кнопка «-» на Storyboard, т.к. это был другой символ похожий на минус, но разный по Unicode.

      • А Вы не задумывались, как же тогда моя программа работает? Если нет, тогда цитата:

        «You can also use subscript syntax to retrieve a value from the dictionary for a particular key. Because it is possible to request a key «for which no value exists, a dictionary’s subscript returns an optional value of the dictionary’s value type. If the dictionary contains a value for the requested key, the subscript returns an optional value containing the existing value for that key. Otherwise, the subscript returns nil…
        You can use subscript syntax to remove a key-value pair from a dictionary by assigning a value of nil for that key:

        airports[«APL»] = «Apple International»
        // «Apple International» is not the real airport for APL, so delete it
        airports[«APL»] = nil
        // APL has now been removed from the dictionary »

        Отрывок из книги: Apple Inc. «The Swift Programming Language (Swift 4)». iBooks. https://itunes.apple.com/il/book/the-swift-programming-language-swift-4/id881256329?mt=11

        И самое главное: в вашей реализации словарь не приостанавливается кнопкой →M, а продолжает записывать новые значения из строки результата на дисплее.

        • Извините, но вы опять все перепутали.
          Вот как в Задании 2 звучит обязательный пункт 9. относительно кнопки «С» (ни «M» и ни «→M»):

          «Make sure your C button from Assignment 1 works properly in this assignment. In
          addition, it should discard the Dictionary it was using for the M variable (it should not
          set M to zero or any other value, just stop using that Dictionary until →M is pressed
          again). This will allow you to test the case of an “unset” variable.»

          А вот русский перевод:

          9 . Убедитесь, что кнопка С​ вашего Задания 1 работает правильно в этом задании. В
          дополнение она должна упразднять использование словаря Dictionary для “переменной” M
          (не устанавливать M в 0 или какое-то другое значение, просто останавливать использование
          словаря Dictionary ДО ТЕХ ПОР, пока не будет нажата кнопка →M​ повторно). Это позволит вам
          тестировать случай “неустановленной” переменной.
          Так что кнопка →M и НЕ обязана останавливать использование словаря,
          наоборот, ее использование приводит к записи значения в словарь, что тут же делает словарь рабочим.

          Если вы еще не поняли смысла обязательного пункта 9 Задания 2, то задайтесь вопросом: «А почему это в функции
          func evaluate(using variables: Dictionary? = nil)
          -> (result: Double?, isPending: Bool, description: String) словарь указан как Optional?»
          Если вы ответите для себя на этот вопрос, надеюсь, поймете смысл обязательного пункта 9 Задания 2.

          • Согласен, в пункте 9 кнопка →M​ только вызывает использование словаря. Но в пункте 7а.→M​ сказано ( в вашем же переводе) : … До тех пор, пока не будет повторно нажата либо эта кнопка либо кнопка С, тот же самый словарь Dictionary будет использоваться при вызове функции evaluate .
            Т.е. при повторном нажатии →M​ уже не должен использоваться тот же словарь ( с одной переменной). Тут не сказано, что значение словаря не должно быть использовано, а только, что словарь НЕ должен использоваться.

            Теперь, что касается func evaluate.
            ? и nil вводятся для выполнения условий пункта 4.
            Если убрать все deprecated — (Первое условие пункта 4:
            «…этот метод берет Optional Dictionary (с ключами Strings и значениями Doubles ) в качестве аргумента, значение которого по умолчанию равно nil , если при вызове метода словарь Dictionary не задается»)
            и
            не передавать невоспринимаемого значения variable[M] = nil ( Второе условие пункта 4:
            «Если “переменная”, установленная как операнд, не обнаружена в словаре Dictionary , то предполагается, что её значение равно 0 «),
            то
            Вы можете убрать и ? и nil , и всё, за исключением отсутствия передаваемого значения, будет работать.
            Например: 2+M7→M​= появится результат 9.
            Если дальше +M² = результат станет равным 58, а кнопка С всё так же будет влиять на приостановку словаря, не сбрасывая его значения в 0, если конечно в функции кнопки С останется строка кода:
            имя Словаря [«M»] = nil

          • К сожалению, все мои попытки как-то показать вам интересные синтаксические особенности Swift пока не встречают понимания.
            Вам нужно работать дальше, возможно, в конце курса вы почувствуете их.

  3. Вопрос немного оффтоп: а возможно с такой моделью реализовать функционал скобок в вычислении? Или тут уже нужно будет использовать специальные алгоритмы?

    • Скобки — это знак равенства, означает рассматривайте выражение целиком. Например, вы можете составить такое выражение:
      выражение со скобками
      Но это не полноценный скобочный функционал, так как в бинарной операции во втором операнде не удается расставить скобки.
      Это лишь демонстрационный пример упрощенного Калькулятора.
      Раньше профессор демонстрировал полноценный Калькулятор с использованием стека, записанного в форме обратной польской записи (RPN), не требующей применения скобок, и преобразование его в инфиксную форму со скобками https://bestkora.com/IosDeveloper/zadanie-2-reshenie-ubiraem-lishnie-skobki-dopolnitelnyj-punkt-1-swift-1-2-i-swift-2-0/. Инфиксная форма нужна только для представления описания стека в привычной для пользователя форме var description и не влияет на вычисления калькулятора. Этот демонстрационный пример требовал рекуррентных вычислений, что заслоняло суть программирования на Swift. И все вздохнули с облегчением, когда он перешел на эту явно упрощенную, хотя и неполноценную форму Калькулятора.
      Но если вас интересует полноценный Калькулятор, то можете посмотреть решение здесь, кстати, очень интересное, хотя и на Swift 2, но это не сильно меняет код.

      • Спасибо! Обязательно ознакомлюсь. Просто я дошел до алгоритма сортировочной станции, а потом отдельных вычислений в RPN, но мне показалось, что должен быть более лаконичное решение.

Обсуждение закрыто.