Задание 3 cs193p Spring 2016 Графический Калькулятор. Решение обязательных пунктов.

Screen Shot 2016-06-30 at 12.20.58 PM

Содержание

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

Задание 3 iOS 9.pdf

iOS 9 Задания

 В Задании 3 вы должны усовершенствовать свой калькулятор Calculator в плане создания графика для “программы”, которую пользователь ввел в ваш калькулятор. Этот график может масштабироваться (zoom in) с помощью жеста pinch и перемещаться по экрану с помощью жеста pan. Ваше приложение теперь будет работать не только на iPhone,  но также и на iPad.
Для выполнения Задания 3 необходимо освоение  Лекции 5Лекции 6 и Лекции 7.
В подсказке 4 Задания 3 предлагается некоторая методика разработки данного приложения, но каждый волен сам выбирать, какую часть приложения создавать в первую очередь. Поэтому обязательные пункты Задания 3 могут выполняться не в строгом порядке.

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

Продолжение решения Задания 3 — дополнительные пункты можно посмотреть здесь для Xcode 7 и Swift 2.2.

Код можно посмотреть на Github в окончательном варианте Задания 3 для Xcode 7 и Swift 2.2.

Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

Вы должны начать это Задание с кода вашего Задания 2, а не с какой-то демонстрации, находящейся на сайте. Изучение создания новых MVCs и segues требует опыта, поэтому не используйте copy / paste или редактирование уже существующей storyboard, на которой уже есть segues.

Этот пункт выполнен.

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

Переименуйте класс ViewController, c которым вы работали в Задании 1 и 2 в CalculatorViewController.

Переименование нужно выполнить в 3-х местах:
1. переименовать сам файл в Навигаторе с ViewController.swift на CalculatorViewController.swift
2. переименовать сам класс в файле CalculatorViewController.swift

Screen Shot 2016-06-26 at 9.17.40 PM

3. заменить класс на storyboard c  ViewController на CalculatorViewController в Инспекторе Идентичности (Identity Inspector)

Screen Shot 2016-06-26 at 9.30.40 PM

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

Добавьте новую кнопку к вашему пользовательскому интерфейсу калькулятора. Если вы ее нажимаете, то вы segues (“переезжаете”) на новый MVC (его вы должны будете написать). Этот MVC строит график для программы program в CalculatorBrain, сформированной во время нажатия кнопки, с использованием размещенной в стэке M, как независимой переменной. Например, если CalculatorBrain содержит sin(M), то вы рисуете синусоиду. Последующий ввод информации в калькулятор не должен оказывать эффект на график (до тех пор, пока графическая кнопка не будет снова нажата). Игнорируйте попытки пользователя построить график, если isPartialResult равен true в это время.

В данном пункте задания заменим кнопку с операцией Rand вычисления случайного числа на кнопку с изображением схематического графика.

Screen Shot 2016-06-28 at 11.54.04 AM

Следуем демонстрационному примеру, приведенному Полом Хэгарти в Лекции 5, применяя все его приемы к нашей задачи.
Идем в Палитру Объектов и первым же элементом этой палитры будет View Controller. Перетягиваем его на storyboard. Потом идем в меню File > New > File > Cocoa Touch Class и создаем subclass  UIViewController. Называем его GraphViewController. Устанавливаем класс для View Controller  в Инспекторе Идентичности (Identity Inspector) в Xcode.

Screen Shot 2015-05-06 at 9.20.15 PM

Пункты 7 (частично), 10  обязательные

7.Как часть вашей реализации (implementation), вам нужно написать обощенный (generic) y (x) графический UIView. Другими словами, UIView, который рисует графики, должен быть сконструирован таким способом, что он является полностью независимым от калькулятора (и мог бы использоваться в других совершенно различных приложениях для рисования графика y (x) ).
10. Ваш графический View должен быть @IBDesignable, и его масштаб должен быть @IBInspectable. Оси на графическом View должны появиться на storyboard в масштабе, установленно в Инспекторе Атрибутов

Теперь нам нужно добавить custom (пользовательское, далее я буду использовать слово custom для обозначения “пользовательское”, так как оно короче) View на наш Graph View Сontroller.
Как я буду это делать? Я иду в Палитру Объектов и вытягиваю оттуда обобщенный (generic) UIView.
Располагаем его на экране, так, чтобы появились пунктирные голубые направляющие линии и мой View заполнил бы собой весь мой экранный фрагмент.

Screen Shot 2015-05-06 at 9.38.40 PM

Далее использую опцию “Reset to Suggested Constraints” (установка предлагаемых ограничений) системы Autolayout с помощью самой правой кнопки в нижнем правом углу.

Screen Shot 2016-06-26 at 9.58.38 PM

После этого, как всегда, мы должны пойти в Инспектор Размера и проверить, какие установились ограничения.

Screen Shot 2016-06-26 at 10.29.52 PM

Ограничения установились без «магических» чисел и теперь нам нужен custom (пользовательский) subclass UIView. Давайте сделаем это с помощью меню  File > New > File > Cocoa Touch Class и создаем subclass класса UIView. Мы назовем его GraphView. Будьте внимательны: не subclass класса UIViewController , а именно subclass класса.

Screen Shot 2016-06-26 at 10.32.16 PM

Получаем код для класса GraphView, в котором снимем комментарии и «освободим» метод drawRect, который нам необходим для рисования.

Screen Shot 2016-06-28 at 11.29.36 AM

Устанавливаем класс  GraphView  для пользовательского View в Инспекторе Идентичности (Identity Inspector) в Xcode.

Screen Shot 2015-05-06 at 9.51.35 PM

Загружаем для этого проекта класс AxesDraw, который рисует оси графика, с сайта Stanford.
В новом классе GraphView, который является subclass UIView, рисуем пока только оси, используя contentScaleFactor данного прибора, границы экрана bounds и координаты центра (graphCenter) нашего пользовательского  View, а также другие свойства, которые определены ниже:

Screen Shot 2016-06-28 at 12.17.58 PM

Обратите внимание на очень интересный способ расчета origin. Мы воспользовались подсказкой  № 11 Задания 3

Было бы прекрасно, если бы origin для вашего графика по умолчанию разместился в середине UIView. Но будьте внимательны где/ когда вы рассчитываете это, потому что границы (bounds) вашего UIView не будут установлены до тех пор, пока UIView не расположится на устройстве. Вы можете быть уверены, что границы (bounds) вашего UIView уже установлены в вашем drawRect. Но будьте внимательны, чтобы не переустановить заново origin, если он уже был кем-то установлен.

Для этого мы определили private переменную originSet, которая является Optional, то есть, если ее никто не установил, то она равняется nil. Но согласно подсказке мы должны ее установить в точку, соответствующую середине нашего UIView. В случае, если требуется сделать что-то дополнительное с «хранимой» переменной, то добавляем вычисляемую переменную origin, которая сама не имеет «хранения», но может управлять «чужим хранением». Это public переменная origin, которую можно устанавливать извне и в этом случае она просто установит нашу private переменную originSet, но если кто-то запросит эту переменную, то вместо nil он получит середину нашего view:

CGPoint(x: bounds.midX, y: bounds.midY)

Этот прием Пол Хэгери будет использовать часто, в частности, в конце Лекции 8 он устанавливает таким образом изображение image для ImageView с «подгонкой» размеров.
Специфицируем наш класс GraphView с помощью директивы @IBDesignable, и Xcode автоматически замечает это и рисует прямо на storyboard наши оси. Директивы @IBInspectable  дают возможность появится свойствам scale,  lineWidthcolor непосредственно в Инспекторе Атрибутов. Теперь мы можем устанавливать нами изобретенные свойства прямо на storyboard.
Устанавливаем режим перерисовке нашего пользовательского View в Redraw.

Screen Shot 2015-05-07 at 9.09.33 PM

Создаем с помощью CTRL-перетягивания от кнопки «Graph»  на Calculator View Controller до Graph View Controller

Screen Shot 2016-06-28 at 11.58.22 AM

… создаем segue типа Show с идентификатором «Show Graph»

Screen Shot 2016-06-28 at 5.16.29 PM

Выделяем наш Calculator View Controller и вставляем его в Navigation Controller c помощью меню Editor -> Embed in -> Navigation Controller

Screen Shot 2015-05-07 at 9.23.23 AM

Теперь на экранном фрагменте Calculator View Controller появился заголовок, который частично закрыл наш интерфейс. Необходимо привлечь для корректировки настроек Autolayout нашего старого друга — Схему UI (Document Outline)

Screen Shot 2016-06-28 at 5.24.29 PM

На другом экранном фрагменте Graph View Controller также появился заголовок, который частично закрыл наш график. Нам необходимо скорректировать там GraphView и его установки Autolayout. Для этого «оттягиваем» верхнюю границу GraphView вниз до голубой пунктирной линии и используем Reset to Suggested Contstraints для выбранного элемента.

Screen Shot 2015-05-07 at 9.11.44 AM

Теперь мы получили приложение, в котором дальше можно наращивать функциональные возможности Графического Калькулятора

Screen Shot 2016-06-28 at 6.55.35 PM

Полученное приложение прекрасно работает на iPhone в портретном и ландшафтном режимах: по кнопке с изображением графика мы переходим на экранный фрагмент «График», на котором пока только оси координат.

Screen Shot 2016-06-28 at 7.01.13 PM

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

Ваш графический View должен поддерживать следующие жесты:

    1. Pinching (увеличение и уменьшение масштаба целого графика, включая оси графика)
    2. Panning (перемещение целого графика, включая оси, вслед за передвижениями пальцев по экрану)
    3. Double-tapping (перемещение origin графика в точку, где вы дважды “тапнули”)

Будем добавлять «жесты» в GraphViewController, а функции обработки жестов будут в GraphView, так как жесты связаны с установкой его UI элементов. Но прежде создадим  outlet для GraphView с помощью  CTRL-перетягивания от GraphView в код.

Screen Shot 2016-06-28 at 7.16.03 PM

Добавлять жесты будем в setter, в Наблюдателе Свойства didSet { } нашего вновь созданного свойства graphView.

Screen Shot 2016-06-28 at 7.33.44 PM

Заметим, что жесты можно добавить и на storyboard. Этот вариант рассмотрен в Лекции 6. Поместим обработки жестов scaleoriginMoveorigin в класс GraphView, оставив их non-private.

Screen Shot 2015-05-07 at 12.15.01 PM

Код находится на Github для Xcode 7 и Swift 2.2. Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

Пункты 7 (полностью) и 8

 7. Как часть вашей реализации (implementation), вам нужно написать обощенный (generic)  y (x) графический UIView. Другими словами, UIView, который рисует графики, должен быть сконструирован таким способом, что он является полностью независимым от калькулятора (и мог бы использоваться в других совершенно различных приложениях для рисования графика y (x) ).

8. Графический View не должен владеть (то есть запоминать) данные, представляемые графически, даже временно. Он должен запрашивать данные, если это необходимо. Ваш графический view должен строить график функции y(x), он не должен графически отображать массив точек, не передавайте ему массив точек.

Мы должны научить наш GraphView рисовать графики типа y(x) в отсутствии данных и независимо от Калькулятора. Для этого мы представим функцию  y(x) в виде замыкания yForX, у которого на входе аргумент x типа Double и возвращает тоже Double. В Swift функции (замыкания) являются гражданами первого сорта, то есть могут объявляться как переменные, передаваться как параметры. Это сделает наш класс  GraphView обобщенным (generic) и его можно будет использовать в других приложениях, так как основной элемент API класса GraphView — просто замыкание определенного типа:

Screen Shot 2016-06-28 at 7.54.45 PM

Это Optional функция — вы не обязаны задавать эту функцию в своем экземпляре класса GraphView. Получилось нечто похожее на optional метод в протоколе старого Objective-C стиля. Такие optional методы не допускаются в протоколах нового Swift стиля.
Используя замыкание yForX как элемент API класса GraphView, который устанавливается извне, рисуем график с помощью drawCurveInRect:

Screen Shot 2016-07-20 at 3.53.04 PM

Логика этого метода очень простая и описана она в Задании 3 в подсказке № 13

Не переусложните ваш drawRect. Просто делайте итерации по каждой пикселе (pixel) (не point) по всей ширине (width) вашего UIView, и рисуйте линию (или просто перемещайтесь, если последняя точка была “неправильной”) к следующей “правильной” точке.»

Ширина нашего UIView bounds.size.width в точках (points), а нам сказали итерировать по пикселям, следовательно мы должны умножить на contentScaleFactor, который содержит количество пикселей на одну точку в зависимости от разрешения дисплея вашего прибора (1— обычный 2 — Retina, — iPhone 6+)
Поэтому общее количество итераций (или пикселей) будет bounds.size.width * contentScaleFactor.
.  .  .  .  .  .  .

Screen Shot 2016-07-20 at 3.57.43 PM

.  .  .  .  .  .  .  .  .
А рисовать мы должны в точках (points), поэтому переводим полученное значение пикселя i в х-координату точки xGraph, разделив на contentScaleFactor и опять возвращаясь в пространство точек. Понятно, что чем больше разрешение дисплея прибора, тем больше мы будем делать расчетов, но графики всегда строятся в точках (points).
Теперь нам нужно получить y-координату точки yGraph,  имея функцию yForX. Это не так просто, учитывая, что система координат, в которой вы рисуете внутри вашего drawRect не та же самая, что система координат вашего источника данных,  обеспечивающего данными ваш график  (потому что, например, у системы координат, в которой вы рисуете, есть origin в левом верхнем углу, а origin данных, возможно, находится где-то еще на view; не говоря уже о масштабировании scale).
Не будем сейчас в этом увязать (я подробно расскажу об этом позже) , просто акцентируйте внимание на том, что график строиться на основе public переменной  yForX, которая является функцией и представляет собой API класса GraphView:

Screen Shot 2016-07-20 at 4.13.41 PM

Пункты 3 (полностью), 4  и 6 обязательные

3. Добавьте новую кнопку к вашему пользовательскому интерфейсу калькулятора. Если вы ее нажимаете, то вы segues (“переезжаете”) на новый MVC (его вы должны будете написать). Этот MVC строит график для программы program в CalculatorBrain, сформированной во время нажатия кнопки, с использованием размещенной в стэке M, как независимой переменной. Например, если CalculatorBrain содержит sin(M), то вы рисуете синусоиду. Последующий ввод информации в калькулятор не должен оказывать эффект на график (до тех пор, пока графическая кнопка не будет снова нажата). Игнорируйте попытки пользователя построить график, если isPartialResult равен true в это время.

4. Никакому из ваших MVCs в этом Задании не разрешается иметь в non-private API CalculatorBrain. (За буквой I в API стоит interface, а не implementation)
6. В любое время, пока график находится на экране, должно быть также показано описание (description) того, что рисуется на графике, например, если строится график sin (M), то строка “sin(M)” должна показываться где-то на экране.

Моделью для графического MVC  GraphViewController является функция y(x), которую мы должны нарисовать и которую мы представим в обобщенном (generic) виде как замыкание yForX, у которого на входе аргумент x типа Double и на выходе     Double?. Оно транзитом будет устанавливаться для для нашего View, обслуживаемое классом GraphView:

Screen Shot 2016-06-28 at 10.08.16 PM

Таким образом мы получили обобщенный класс GrapViewController, Моделью для которого является Optional замыкание yForX, у которого на входе аргумент x типа Double и на выходе  Double?.
У нас уже есть segue, который реализует «переезд» с Калькулятора на графический MVC. Нам осталось написать метод prepareForSegue в классе CalculatorViewController, в котором мы и подготовим MVC- пункт назначения (а это графический MVC) к «переезду», а именно установим замыкание yForX и сформируем заголовок графического MVC:

Screen Shot 2016-06-28 at 11.55.52 PM

Принимая во внимание, что замыкания (closures) захватывают любые переменные из внешнего контекста для использования внутри замыкания, мы можем заставить Калькулятор brain работать на нас в замыкании при построении графика.
Для того, чтобы игнорировать попытки пользователя построить график, если isPartialResult равен true используем метод shouldPerformSegueWithIdentifier:

Screen Shot 2016-06-28 at 11.49.40 PM

Запускаем приложение и набираем на калькуляторе следующую последовательность 1 ÷ M = cos  × M =. Не обращаем внимание на сообщение «Nan», так как нам важна формула нашего графика, а не расчетный результат в одной точке, и нажимаем кнопку с графиком. Получаем график с заголовком, на котором написана функция y(x) и можем использовать жесты. В частности использован жест pinch  для изменения масштаба.

Screen Shot 2016-06-29 at 11.40.43 PM

Код находится на Github  для Xcode 7 и Swift 2.2.Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

9. Ваш графический калькулятор должен обладать способностью графически представлять разрывные свойства функций (то есть он должен рисовать линии только от/ к точкам, которые для фиксированного  значения M program, задающая рисование графиков, оценивает как Double (то есть не nil) что соответствует .isNormal или .isZero).

Я хочу воспользоваться этим пунктом, чтобы подробнее рассказать, как построить график функции y(x) в drawRect.
Сначала проясним ситуацию с системами координат:

Screen Shot 2016-06-30 at 12.02.23 AM

Мы рисуем в координатах X‘Y′ области  drawRect, поэтому у меня есть вычисляемая переменная х, которая дает нам координату Х в системе координат источника данных:

Screen Shot 2016-07-20 at 4.17.55 PM

Переменная y вычисляет значение координату Y  в системе координат источника данных согласно функции y(x), представленной замыканием yForX, и здесь требуется проверка, что мы получили конечное (finite) значение. Это осуществляется с помощью метода isFinite типа Double, который проверяет, что она не  InFinite (бесконечность) и не NaN. Мы сохраняем сведение о точке в небольшой структуре OldPoint:

Screen Shot 2016-07-20 at 4.21.43 PM

Свойство normal нужно для того, чтобы пометить, что точка «правильная», потому что линию можно провести только между двумя «правильными» точками, а свойство yGraph  сохраняет координату Y для точки в системе координат координатах X‘Y′  области  drawRect для вычисления приращения и определения точек «разрыва» функции. «Разрыв» устанавливается с некоторым допущением в вычисляемом свойстве disContinuity:

Screen Shot 2016-07-20 at 4.23.57 PM

Поэтому даже если обе точки, через которые нужно провести линию, — «правильные», но приращение координаты  Y слишком велико, то линия не рисуется.

Screen Shot 2016-07-20 at 4.25.18 PM

Ниже на рисунке видно различие графиков, в которых точки «разрыва» не обрабатываются — слева, и обрабатываются — справа. На графике слева присутствуют вертикальные линии, которые не являются графиком функции y(x).

Screen Shot 2016-06-30 at 8.24.12 AM

Код находится на Github для Xcode 7 и Swift 2.2.Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

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

На iPad и в ландшафтном режиме на iPhone6+ устройствах, график должен быть (или может быть) на экране одновременно с вашим пользовательским интерфейсом существующего калькулятора (например, в Split View). На других iPhones график следует “выталкивать” (“push”) на экран через Navigation Controller.

Добавляем Split View Controller на storyboard и убираем «сопутствующие» View Controllers. Сделаем Сalculator View Controller — Master, а Graph View Controller — Detail. Добавим еще один Navigation Controller  для Detail. И segue теперь у нас будет не Show, а ShowDetail (лучше уничтожить старый segue и создать новый, не забудьте указать идентификатор segue «Show Graph»):

Screen Shot 2016-06-30 at 11.43.33 AM

Запускаем на iPad.

Screen Shot 2016-06-30 at 11.37.31 AM

Код находится на Github для Xcode 7 и Swift 2.2. Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.

Но этого недостаточно, дело в том, что на iPad в портретном режиме появляется экран с нарисованными осями, но без графика —  это Detail (в нашем случае GraphViewController) для SplitViewController, и это было обычное поведение SplitViewController. Но если на iPhone 6+ есть возвратная кнопка к Master (в нашем случае это Калькулятор), то на iPad даже непонятно, что делать. Пользователь должен каким-то магическим способом догадаться, что работает жест swipe, который покажет нам Master, то есть Калькулятор.

Screen Shot 2016-07-26 at 11.44.03 AM

Для того, чтобы появилась возвратная кнопка, нужно добавить код настройки Split View Controller в файл AppDelegate.swift, который у нас находится в папке Supporting Files:

Screen Shot 2016-07-26 at 11.50.21 AM

После этого появляется возвратная кнопка при старте iPad в портретном режиме:

Screen Shot 2016-07-26 at 11.57.09 AM

Но мы «переезжаем» на Detail с помощью segue, а он предполагает при каждом таком «переезде» создание нового MVC, поэтому мы должны добавить возвратную кнопку в метод prepareForSegue:

Screen Shot 2016-07-26 at 12.05.35 PM

Код можно посмотреть в окончательном варианте на Github.
Еще одну вещь нам нужно поправить в Split View Controller. Когда на iPhone запускается Графический Калькулятор, то нам показывают пустой график. Мы хотим, чтобы вначале был показан Калькулятор, потому что пустой график абсолютно бесполезен, он лишь принуждает нас нажать кнопку возврата и попасть в Калькулятор.
Как мы будем решать эту проблему? Нам нужно использовать делегата UISplitViewControllerDelegate. Я размещу код для него в CalculatorViewController. И первая вещь, которую я сделаю — я сделаю CalculatorViewController делегатом его собственного UISplitViewController.
Другими словами, ему необходимо быть делегатом того Split View, в котором он находится. Для этого мне понадобится viewDidLoad.

Screen Shot 2016-07-26 at 4.41.04 PM

Потом нужно сообщить компилятору, что CalculatorViewController  является UISplitViewControllerDelegate.

Screen Shot 2016-07-26 at 4.43.44 PM

И реализовать метод делегата:

Screen Shot 2016-07-26 at 4.46.23 PM

который в случае пустого графика выведет на экран iPhone Калькулятор.

Код можно посмотреть на Github в окончательном варианте Задания 3 для Xcode 7 и Swift 2.2. Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.