Задание 1 cs193p Winter 2017 Калькулятор.Решение. Обязательные и дополнительные пункты.

Содержание

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

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

Начинаем выполнять Задание 1 с кода, полученного в конце Лекции 2. Профессор настоятельно рекомендует не копировать код первых 2-х Лекций, а непосредственно печатать его в Xcode, так как это даст хороший опыт освоения среды разработки Xcode 8.

Я все-таки привела на Github коды демонстрационного примера, соответствующие окончанию Лекций 1 и 2. Это позволит совсем начинающим не «застрять» на самом первом этапе.

Решение Задания 1 находится на Github:

без кортежа в Моделе CalculatorBrain —  на Github
c кортежем в Моделе CalculatorBrain —  на Github

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

Ваш Калькулятор (Сalculator) уже умеет работать с числами с плавающей точкой (например, если вы последовательно будете нажимать на клавиши 3 ÷ 4 =,  то он покажет правильный результат  0.75), тем не менее, пока нет возможности ввести число с плавающей точкой. Это надо исправить. Разрешите вводить только правильные числа с плавающей точкой (например,  “192.168.0.1”  — неправильное число с плавающей точкой). Вам нужно добавить новую кнопку с точкой “.”. В обязательных пунктах этого Задания не беспокойтесь о точности и значащих цифрах (выполнение этой работы относится к дополнительным пунктам этого Задания).

Прежде всего надо сделать небольшое замечание относительно точки «.» как десятичного разделителя.

Замечание. Если вы будете использовать символ  точки «.» в качестве десятичного разделителя в Задании 1, то оно может работать на симуляторе и может не работать на вашем реальном устройстве, если Регион на вашем приборе установлен так, что там не применяется «.» для десятичных чисел. В этом случае приложение закончится аварийно. Мы будем проектировать приложение, которое должно работать на симуляторе. В конце выполнения всех пунктов (обязательных и дополнительных) Задания 1 в примечании я расскажу, что нужно делать, чтобы не зависеть от Региона.

Но вернемся к нашему пункту Задания и попытаемся заблокировать повторный ввод десятичного разделителя, который дальше по тексту мы будем условно называть «точкой», хотя, как мы уже знаем, это — не всегда точка.

Прежде всего необходимо проверить, что кнопка с точкой  «.» на вашем UI получена копированием цифры, а не операции. Это можно проверить, если вывести на экран одновременно ваш UI (storyboard) и класс ViewController, обслуживающий ваш UI.

Если мышку навести на маленький кружочек слева от метода touchDigit, то будут показаны все кнопки, «подвязанные» к этому методу, то есть цифровая клавиатура. Мы видим, что кнопка с точкой «.» находится среди них.
Если мышку навести на маленький кружочек слева от метода performOperation, то будут показаны все кнопки, «подвязанные» к этому методу, то все операции и мы не должны увидеть там кнопку с точкой «.» :

Если у вас так, как на рисунках выше, то для блокировки повторного ввода точки «.» проще всего использовать String метод contains (String), при вводе любой цифры (к которым относится и точка). Для этого нам понадобится всего одна строка:

Случай ввода в калькулятор числа “.5” работает правильно без дополнительного кода.

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

Добавьте побольше кнопок  с операциями к вашему Калькулятору, так чтобы общее их число равнялось, по крайней мере, 12 (у вас может быть и больше, если вы этого хотите). Вы можете выбрать любые подходящие вам операции. Кнопки должны быть правильно и красиво организованы как в портретном режиме, так и в ландшафтном режиме на любых моделях iPhones 6 и iPhones 7.

Пункт 4

Используйте цвета для придания вашему пользовательскому интерфейсу (UI) привлекательности. По крайней мере, цвета ваших кнопок с операциями должны отличаться от цвета кнопок цифровой клавиатуры, но в любом случае вы можете использовать любые цвета для придания хорошего вида вашему Калькулятору.

Добавлять кнопки в Stack View одно удовольствие — кнопка в стэке принимает ту форму, которая определяется свойствами стэка. Можно копировать целыми стэками-строками. В результате очень легко UI расширяется до следующего вида:

Пустые кнопки не являются кнопками операций и нужны для будущего Домашнего Задания № 2, а в этом Домашнем Задании № 1 не участвуют. Кнопка «» также не является кнопкой операции, она предназначения для “backspace” в случае ошибочного набора цифры при вводе числа, она упоминается в дополнительном пункте 1 этого Домашнего Задания и мы задействуем ее позже. Кнопка «С» также не является кнопкой операции, она нужна для обязательного пункта 8 этого Домашнего Задания и мы активизируем ее позже. Кнопка «Ran» также является кнопкой операции, которая генерирует случайное число в диапазоне от 0 до 1 и понадобиться нам в дополнительном пункте 3 этого Домашнего Задания.

В подсказках нам сообщают, что мы можем использовать любые Unicode символы в качестве математических символов ваших операций. Например, х² и  x⁻¹ прекрасные математические символы.

Посмотрим, как это будет выглядеть наш UI в ландшафтном режим.

О! В ландшафтном режиме у нас исчез наш дисплей! По вертикале стало меньше место и система  Autolayout предпочла «сжать» дисплей в пользу наших кнопок в цифровой клавиатуре. Давайте повысим у метки  display сопротивление к сжатию по вертикале и сделаем Content Compession Resistance Priority  (приоритет сопротивления сжатию) по вертикале равным 751, то есть выше, чем у стэка кнопок с цифровой клавиатурой равного 750.

Все нормально. Возвращаемся в портретный режим.

Все прекрасно работает в обоих режимах: портретном и ландшафтном.

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

Добавьте Bool свойство к вашему CalculatorBrain с именем resultIsPending, которое возвращает значение, показывающее, является ли бинарная операция отложенной  (если да, возвращает true, если нет, то false).

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

Добавьте String свойство к вашему CalculatorBrain с именем description, которое возвращает описание последовательности введенных операторов и операций, которые привели к значению, возвращаемому result  (или к результату, который сформировался на данный момент, если результат отложен и resultIsPending = true). Ни символ  равенства “=“, ни символ многоточия  “…” не должны появляться в этом описании.

Переменная resultIsPending является readonly (у нее есть только get{}) и она нужна в ViewController:

В отношении переменной description у нас есть подсказка № 10 в Задании №3 : 

Описание description нашего CalculatorBrain представляет собой строку String с операндами и операциями, которые привели к тому, что в данный момент находится на accumulator. Если вы будете думать таким образом, то все, что вы должны сделать — это создавать такое String представление каждый раз, когда вы устанавливаете на accumulator какое-то значение. Единственный “глюк” в этом алгоритме появляется, если у вас есть отложенная бинарная операция, то есть pendingBinaryOperation = true (вы должны обработать этот случай специальным образом).

Давайте при вычислении описания description будем следовать логике, которая заложена в вычислении значения accumulator.

Первое, что мы сделаем, это добавим ассоциированные значения во все операции enum Operation в классе CalculatorBrain. Подобно тому как для вычислений на нашем калькуляторе  существуют, например, для унарных операций функции типа (Double) -> Doubleдля описания  этих унарных операций введем функцию ((String) -> String)?, которая берет описание аргумента в виде строки и возвращает описание функции вместе со взятым аргументом также в виде строки. Точно также для бинарной операции добавляем для описания  функцию ((String, String) -> String)?, которая берет описание двух аргументов в виде строк и возвращает описание функции вместе с взятыми аргументами также в виде строки.

Заметьте, что для унарной и бинарной операций я использовала Optional функции и через секунду я объясню, почему. Согласно новым ассоциированным значениям модифицируем словарь операций operations:

Для некоторых операций значения функций описания  ((String) -> String)? и  ((String, String) -> String)? равны nil, а справа в комментариях приведено значение этих функций в виде замыкания, которое дает описание description этой операции. Если вы проанализируете все операции с комментариями, то поймете, что все описания за исключением тех, у которых нет комментариев, строятся по одному и тому же алгоритму:

для унарных операций — «<символ операции>(» + $0 + «)»
для бинарных операций — $0 + «<символ операции>» + $1

Мы присвоили этим операциям второе ассоциированное значение,  ((String) -> String)? для унарных операций или  ((String, String) -> String)? для бинарных операций, отвечающее за описание операции, равное nil, что говорит о том, что это описание будет строиться стандартным способом, указанным выше. Описания некоторых операций, например, таких как x⁻¹ или х² не подходят под стандартную схему  и требуют специальных функций:

 «x⁻¹» : Operation.unaryOperation( {1.0/$0},  {«(» + $0 + «)⁻¹»} ),

        «х²» : Operation.unaryOperation( {$0 * $0}, { «(» + $0 + «)²»} ),

  «xʸ» : Operation.binaryOperation( pow, { $0 + » ^ » + $1 } ),

Для них мы оставляем их специальные описания description. Optional значения функций ((String) -> String)? для унарных операций или  ((String, String) -> String)? для бинарных операций оказались очень удобными для случая, когда не нужно определять description функции  для всех операций, что утомительно, а можно просто поставить nil, если description операции соответствует стандартным правилам.

Далее при вычислении description действуем совершенно синхронно с вычислением accumulator.

Внутренней переменной accumulator будет соответствовать внутренняя переменная descriptionAccumulator, которая будет накапливать введенные операнды и операции.  Внешней readonly переменной result, отображающей результат вычислений, будет соответствовать внешней  readonly переменной description, показывающей описание того, что мы ввели для калькулятора.

Теперь в функциях  setOperand и performOperation  дополним код для формирования описания. Нам это будет сделать легко, так как там, где используется accumulator используем descriptionAccumulator.

Структура PendingBinaryOperation дополняется элементами описания операции descriptionFunction и первого операнда descriptionOperand, а также методом performDescription (with secondOperand:).

Функция performPendingBinaryOperation() также претерпела определенные изменения:

Возвращаемся к определению нашей внешней  readonly  переменной  description  и дадим некоторые пояснения.

Если у нас нет отложенной операции, то есть pending = nil, то  возвращается descriptionAccumulator, в чистом виде. Если есть отложенная операция, то описание формируется с помощью функции descriptionFunction для бинарной операции, у которой первым операндом будет описание первого операнда  descriptionAccumulator , а второй операнд этой функции будет зависеть от того, является ли второй операнд результатом какой-то другой операции (константы, унарной операции) descriptionAccumulator или он вообще еще не сформирован — тогда просто пустая строка «».

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

Используйте два вышеуказанных свойства для реализации метки UILabel на вашем пользовательском интерфейсе (UI), которая показывает последовательность операндов и операций, которые привели  (или приведут, если результат отложен resultIsPending) к тому, что показывается (или “будет показано”, если результат отложен resultIsPending) на дисплее display. Если resultIsPending равен значению true, то добавьте в конец UILabel, иначе добавьте =. Если вы находитесь в середине ввода числа и userIsInTheMiddleOfTyping = true, то вы можете оставить UILabel, показывающую то, что было там перед тем, как пользователь начал набирать число.

Очень легко добавляем метку в стэке и создаем Outlet history:

У этой метки есть одна особенность, которая отмечается в подсказке № 6 Задания:

Если вы установите text метки UILabel в nil или “” (пустая строка), то метка изменит свой размер и ее высота будет равна 0 (соответственно сдвинув оставшийся UI). Это может приводить ваших пользователей в замешательство. Если вы хотите, чтобы ваша метка UILabel оставалась пустой, но при этом высота ее не уменьшалась до нуля, то просто установите text метки UILabel в “ ” (пробел).

Не забудьте повысить у метки history сопротивление к сжатию по вертикале и сделайте Content Compession Resistance Priority  (приоритет сопротивления сжатию) по вертикале равным 751, то есть выше, чем у стэка кнопок с цифровой клавиатурой равного 750.

Добавляем формирование текста этой метки в класс ViewController при нажатии кнопки с операцией и срабатывании метода performOperation:

Как указывалось в Задании используются переменные description и resultIsPending нашего Калькулятора brain.

Нам предлагают тесты для проверки формирования нашего description:

  1. касаемся  7 +     будет показано “7 + …” ( 7, которая все еще на display)
  2. 7 + 9        будет показано “7 + …” (9 на display)
  3. 7 + 9 =     будет показано “7 + 9 =” (16 на display)
  4. 7 + 9 = √  будет показано  “√(7 + 9) =” (4 на display)
  5. 7 + 9  = √ + 2     будет показано “7 + √(9) …” (3 на display)
  6. 7 + 9 √      будет показано “7 + √(9) …” (3 на display)
  7. 7 + 9 √ =   будет показано “7 + √(9) =“ (10 на display)
  8. 7 + 9 = + 6 + 3 =   будет показано “7 + 9 + 6 + 3 =” (25 на display)
  9. 7 + 9 = √ 6 + 3 =   будет показано “6 + 3 =” (9 на display)
  10. 5 + 6 = 7 3    будет показано “5 + 6 =” (73 на display)
  11. 4 × π =         будет показано “4 × π =“ (12.5663706143592 на display)

Для этого в тестовом файле CalculatorBrainTests.swift формируем тесты на проверку  description и не только:

В тестовом файле CalculatorUITests.swift формируем тесты на проверку пользовательского интерфейса (UI), то есть на соответствие заголовков кнопок и символов для операций и не только:

Запускаем приложение на тестирование

Получаем положительный результат обоих тестов:

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

Добавьте кнопку C, которая очищает все ( ваш дисплей display, новую только что добавленную метку UILabel и т.д.). Калькулятор после нажатия кнопки C должен быть в том же состоянии, что и при старте приложения.

Кнопка C пустая в том смысле, что к ней ничего не привязано: ни операций, на цифр на цифровой клавиатуре, и мы создадим Action, который назовем

@IBAction func clearAll (sender: UIButton) :

Еще добавим метод в нашу Модель —  public API класса CalculatorBrain:

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

Дайте возможность пользователю нажимать кнопку  “backspace”, если он ввел неверную цифру. Это вовсе не кнопка “undo,” так что если пользователь нажал неверную кнопку с операцией , то его ждет неудача ! Вам решать, как вы будете разруливать случай, когда пользователь кнопкой “backspace” полностью удалил число и все еще находится в процессе ввода числа (in the middle of entering), но оставлять дисплей display абсолютно пустым было бы не очень дружелюбно. Может оказаться, что раздел Strings and Characters Руководства по Swift будет очень полезно для решения этой проблемы.

Кнопка со знаком «» символизирует операцию “backspace” и она не привязана ни к цифровой клавиатуре, ни к операциям. Создадим для нее свой Action и соответственно метод  @IBAction func backspace(sender: UIButton) :

В методе func backspace(sender: UIButton)  мы удаляем последний из набранных пользователем символов при вводе числа:

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

Изучите документацию класса NSNumberFormatter с тем, чтобы использовать его для форматирования display, который должен показывать 6 цифр после десятичной точки  (вместо показа всех цифр, представляющих Double). Это уничтожит необходимость использования Autoshrink в display. Использование  класса NSNumberFormatter поможет избавиться от лишних “.0” , подсоединяемых к целочисленным значениям (например, показываем “4”, в не  “4.0” при извлечении квадратного корня из 16). Вы можете это использовать и для описания descriptionв классе CalculatorBrain.

Из-за того, что инициализация  NSNumberFormatter может быть экстремально дорогой операцией в iOS, способной «поставить на колени» отдельные приложения, не хотелось бы, чтобы производительность приложения зависела от этого, особенно если используется одна и та же конфигурацию  NSNumberFormatter всюду в моем приложении.

Экземпляр класса NSNumberFormatter нужен и в CalculatorBrain (нужно преобразовывать операнды из Double в String  и размещать их в описании description), и в ViewController (для формирования displayValue).

Поэтому создаем глобальную константу formatter , которая настраивается на представление чисел с 6-ю значащими цифрами, и создавать ее мы будем очень интересным способом — путем мгновенного выполнения замыкания. В руководстве “The Swift Programming Language.”  нужно искать раздел «Setting a Default Property Value with a Closure or Function«. Указание на этот раздел есть и в Задании на чтение 2.

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

Обратите внимание на круглые скобки ( ) в конце. Это и есть инициализация переменной (или константы) с помощью мгновенного выполнения замыкания, оно выполняется один раз при инициализации переменной или константы. Это очень крутая вещь и о ней стоит почитать в руководстве по Swift.
Использование такого formatter не потребует указания класса ни в displayValue  в ViewController.swift:

ни в setOperand в CalculatorBrain.swift :

Делаем соответствующие изменения в CalculatorBrain.swift и в ViewController.swift:

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

Определите одну из ваших кнопок для операции, которая бы “генерировала случайное число с двойной точностью в диапазоне между 0 и 1”. Кнопка для этой операции не является константой (так как меняет свое значение при каждом нажатии). Она не является и унарной операцией (так как ни с чем не оперирует). Возможно, наиболее простым способом генерации случайного числа в iOS является глобальная Swift функция arc4random(), которая генерирует случайное число в диапазоне  между 0 и наибольшим возможным 32-разрядным целым (UInt32.max). Конечно, вам нужно преобразовать полученное случайное число в число с плавающей точкой двойной точности.

  Для генерации случайного числа добавляем в  enum Operation тип операции nullaryOperation c ассоциированным значение виде функции, у которой нет никаких аргументов (нуль аргументов), а на выходе у нее Double. В качестве второго ассоциированного значения, связанного с формированием описания description,  используется просто строка String:

В методе performOperation будем в случае nullaryOperation просто вызывать функцию без аргументов.

Пополним словарь с операциями operatons:

Добавляем кнопку с загловком Ran, подсоединенную к Action performOperation:

Таким образом, мы видим, что класс CalculatorBrain сконструирован так, что его очень просто функционально расширить добавлением любого числа операций.

Дополнение 1. Работа с региональным десятичным разделителем.

Если вы будете использовать символ  точки «.» в качестве десятичного разделителя в Задании 1, то оно может работать на симуляторе и не работать на вашем реальном устройстве, если Регион на вашем приборе установлен так, что там не применяется «.» для десятичных чисел. В этом случае приложение закончится аварийно. Но приложение должно работать в любом Регионе.

Для того, чтобы не зависеть от Региона, лучше получать разделитель для десятичных чисел непосредственно в вашем калькуляторе:

Если мы пойдем немного дальше, исследуя десятичный разделитель, то нам захочется иметь кнопку не с точкой «.», а с тем знаком, который принят в этом Регионе. Для этого сделаем outlet для кнопки с условным названием «точка»

Screen Shot 2016-05-10 at 11.26.36 AM

Мы должны установить заголовок этой кнопки равным decimalSeparator и это мы сделаем с использованием Наблюдателя didSet{} Свойста @IBOutlet weak var tochka; этот метод вызывается один раз при  установке этого outlet  со  storyboard:

Для проверки в Interface Builder можно поставить на кнопке не «.«, а три символа «.PO«, которые на реальном устройстве или на симуляторе будут заменены на реальный десятичный разделитель decimalSeparator, взятый из NumberFormatter().

Если мы хотим, чтобы наш Калькулятор работал для любого региона, то мы должны сделать преобразования числа в строку и наоборот с учетом локализации:

В этом случае мы сможет на симуляторе испытать его для любой страны :

И получим правильный результат:

Дополнение 2.  Сделаем вычислямую переменную displayValue экземпляра класса ViewController не Double, а Double?.

Сейчас в get{ } для вычисляемой переменной displayValue класса ViewController используется принудительное «разворачивание» Optional результата преобразования строки String в Double:

Да, сейчас мы гарантируем, что принудительное  «разворачивание» Optional не приведет к аварийному завершению приложения наличием четко определенных кнопок, но, возможно, в будущем нам придется добавить дополнительные кнопки и тогда нам придется все время возвращаться к этой строке кода.

Давайте сделаем  переменную displayValue не Double, а Double?. Кроме того,  set{ } переменной displayValue тесно связан с переменной result Модели CalculatorBrain, который является Double?. В  set{ } переменной displayValue можно перенести и код для метки history, потому что изменение на результата на дисплее display происходит синхронно с изменение описания действий Калькулятора на history:

Немного изменится метод performOperation в классе ViewController :

Код для Задания № 1для вышеперечисленных пунктов находится на  Github.

Дополнение 3.  Использование кортежа для объединения двух переменных vars (accumulator и его String представления).

В подсказке № 11 говорится :

Если вы в реализации description будете использовать подход, представленный в предыдущем пункте, то обнаружите, что значения для двух переменных vars (accumulator и его String представления) устанавливаются всегда в одно и то же время и никогда не бывают рассинхронизированы друг с другом. У Swift есть структура данных для переменных vars, которые всегда “ходят вместе” наподобие описанных выше: это кортеж (tuple). Мы его еще не изучали, но рассмотрите вариант использования кортежа (tuple), если он вам понятен.

На Лекции 3 говорилось, что мы будем использовать именованные кортежи (tuples), поэтому в Модель CalculatorBrain определим кортеж cache c двумя именованными переменными, причем имена совпадают с private локальными переменными accumulator и descriptionAccumulator:

А дальше — впереди бывших  accumulator и descriptionAccumulator просто ставим «cache.» и все. Например, в методе setOperand:

В простейших случаях вычисления частей, составляющих кортеж cache, можно вообще не именовать части кортежа в правой части. Например, в  методе performOperation:

или в методе clear():

Мы могли бы пойти еще дальше и использовать идею объединения в кортеж оставшихся public переменных result, description и resultIsPending в кортеж:

но это уже предмет Задания № 2, а в этом Задании № 1 мы прошли небольшую подготовку по использованию кортежей.

Код для Задания № 1 для варианта с кортежем (tuple) находится на  Github.

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

Задание 1 cs193p Winter 2017 Калькулятор.Решение. Обязательные и дополнительные пункты.: 5 комментариев

  1. не знаете почему не работает строка с descriptionAccumulator = formatter.string( … )
    unresolved identifier ‘formatter’

  2. К пункту 2 (Разрешите вводить только правильные числа с плавающей точкой). Необходимо строку кода 34 userInTheMiddleOfTiping = true вставить в условие, что точка не введена, иначе при вводе, например (.π.), или (π.π), будет возникать ошибка.

    23 @IBAction func touchDigit(_ sender: UIButton) {
    24 let digit = sender.currentTitle!
    25 if userInTheMiddleOfTiping { // это не первый цифровой символ?
    26 let textCurrentlyInDisplay = display.text!
    27
    28 if !(textCurrentlyInDisplay.contains («.»)) || (digit != «.») {
    29
    30 display.text = textCurrentlyInDisplay + digit
    31 }
    32 } else { // это первый вводимый символ с цифровой клавиатуры
    33 display.text = digit
    34 if digit != «.» {
    35 userInTheMiddleOfTiping = true
    36 }
    37 }
    38 }

    • При вводе π срабатывает не метод touchDigit, а метод performOperation, потому что кнопка с заголовком «π» отнесена к операциям и не привязана к методу touchDigit, поэтому вы никогда не увидите на дисплее «.π.»

      Это цифровая клавиатура:

      цифровая клавиатура
      Это операции:

      Операции

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