Задание 3 cs193p Winter 2017 «Графический» Калькулятор. Решение. Обязательные пункты.

Содержание

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

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

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

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

Код для отдельного Графического MVC на Github.
Код для обязательных пунктов находится на Github.
Код для обязательных и дополнительных пунктов находится на Github.

В данном посте представлено выполнение обязательных пунктов Задания 3.

На Лекции 5 и в подсказке №8 Задания 3 рекомендуется:

Это Задание стало доступно перед Лекцией, на которой демонстрируется как использовать множественные MVCs в вашем приложении. Вы можете начать выполнять это Задание перед Лекцией (которую очень рекомендую!) и подумать о том, чтобы создать абсолютно новое приложение, в котором находится единственный MVC: ваш новый графический MVC. Просто выберите некоторую удобную для рисования функцию с целью отладки приложения (например, cos(x) ). Затем после следующей Лекции, когда вы изучите как обращаться с множественными MVCs, вы можете просто добавить ваш повторно используемый графический MVC (включая его повторно используемый UIView) в приложение “Графический Калькулятор” для Задания № 3.

Поэтому мы будем выполнять обязательные пункты Задания 3 не в порядке нумерации.

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

Создайте полностью новый MVC, который строит график произвольной функции y = f(х). У него может быть любой, какой хотите, public API (учтите предостережения, указанные ниже). Выбирайте Model для этого MVC очень внимательно.

Давайте посмотрим, о каких предостережениях говорится в Задании 3:

  • К настоящему моменту вы уже должны очень четко понимать, что такое MVC. Проясните для себя, что является вашей Моделью для каждого из 2-х MVCs в этом Задании. Убедитесь, что вы не нарушаете правил MVC (например, у вас View не разговаривает с Моделью напрямую).
  • Помните, что Модель должна быть UI-независимым “сердцем” того, что ваше MVC делает и показывает. Прекрасным примером этого является CalculatorBrain, который, очевидно, является “сердцем”  MVC Калькулятор. Когда вы будете думать, что является Моделью для нового графического View Controller, спросите себя: “Что (фундаментально) действительно показывает этот MVC?”  Затем выберите что-то очень простое для представления этой Модели.
  • Модель необязательно должна быть классом class или структурой struct для того, что вы создаете (как CalculatorBrain). Она может быть чем угодно.

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

Ваш новый графический MVC должен быть полностью обобщенным (generic), готовым к повторному использованию компонентом. В нем не должно быть никаких упоминаний чего-либо относящегося к Калькулятору в реализации любой его части ( ни в Model, ни в Controller и ни в View).

Поступим точно также, как поступал профессор Пол Хэгерти при проектировании MVC «лицо» на  Лекции 4 и Лекции 5.

Создаем новый проект «с нуля» с помощью меню File -> New -> Project  и выбираем шаблон Single View Application. Я называю приложение Calculator и оно будет универсальным (Universal), потому что мы собираемся делать множество MVCs.

Вытягиваем из Палитры Объектов UIView на storyboard и размещаем на экранном фрагменте View Controller.

Затем «растянем» UIView с помощью механизма Autolayout на весь экран. Выравниваем его по голубым линиям и нажимаем на кнопку Resolve Auto Layout Issues (решить проблемы Autolayout). Из всплывающего меню выбираем пункт Reset to Suggested Constraints (установить предлагаемые ограничения):

Создадим пользовательские классы GraphViewController, который будет обслуживать наш новый MVC, и GraphView, который будет обслуживать наш UIView и устанавливаем их в Инспекторе Идентичности:
GraphViewController

GraphView

Создаем Outlet для UIView в классе GraphViewController:

Затем вставим новый View Controller в Navigation Controller и немного корректируем наши ограничения механизма Autolayout .

Нашему UIView зададим на storyboard режим перерисовки Content Mode равным Redraw, то есть «перерисовка» с помощью метода draw (CGRect):

Проверим ограничения (constrains), которые установила система Autolayout:

Обо всем этом говорилось на Лекциях 4 и 5.
Моделью нашего обобщенного MVC для рисования графиков функций y = f(x),  естественно, является Optional функция yForx типа ((Double) -> Double)?:

У нас есть View, представленный Outlet :

@IBOutlet weak var graphView: GraphView!

Public API у класса GraphView — это тоже Optional функция yForx типа ((Double) -> Double)?, для которой будет строиться функция y = f(x):

Каждый раз, когда меняется Модель или устанавливается Outlet graphVew, мы обновляем наш пользовательский интерфейс с помощью метода updateUI(), в котором мы транзитом передаем Optional функции yForx типа ((Double) -> Double)? нашему GraphView:

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

Как часть реализации нового MVC, вам необходимо написать обобщенный (generic), повторно используемый subclass UIView, который строит график функции y = f(х).

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

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

В классе GraphView нарисуем оси в методе draw (CGRect) с помощью вспомогательного класса AxesDrawer, который мы загружаем с сайта Stanford:

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

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

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

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

Этот прием Пол Хэгери будет часто использовать в дальнейшем.

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

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

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

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

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

.  .  .  .  .  .  .  .  .

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

Давайте вернемся в GraphViewController и зададим в его методе «жизненного цикла»  viewDidLoad в качестве тестового примера функцию:

Запускаем приложение и получаем график этой функции на нашем Графическом MVC в портретном и ландшафтном режимах:

Подсказка № 14 к обязательным пунктам

Ваш графический калькулятор должен обладать способностью графически представлять разрывные свойства функций (например, он должен только пытаться нарисовать линии от или к точкам, у которых значение соответствует .isNormal или .isZero). Для того, чтобы упростить эту ситуацию, вполне допустимо, чтобы ваш Графический View рисовал бы график функции, которая катастрофически быстро нарастает при изменении на одну пикселю по оси х, в виде почти вертикальной линии между этими двумя точками, даже если эта функция действительно является разрывной (например, tan(x)). Было бы здорово, если бы вы определяли эти случаи с некоторым допуском и не рисовали бы вертикальные линии ( но это на ваше усмотрение).

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

Мы рисуем в координатах XgYg области  draw(CGRect), поэтому у меня есть вычисляемая переменная х, которая дает нам координату Х в системе координат источника данных:

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

Если мы теперь нарисуем график разрывной функции sin (M) ÷ cos (M), то у нас появятся вертикальные линии, которых нам хотелось бы избежать:

Для этого мы будем запомнить yGraf предыдущей точки в переменной oldYGraph и подсчитывать приращение координаты Yg,  которое не должно превышать в 1.5 раза размер экране по большей стороне:

В случае если у нас наблюдается катастрофически  быстрое нарастание координаты Yg при изменении на одну пикселю по оси Xg (фиксируем вычисляемой переменной disContinuity), то мы не будем рисовать линию, а скажем, что следующая точка опять должна быть «первой»:

Построим графики различных функций: разрывных и неразрывных.

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

Ваш графический View должен быть @IBDesignable, и его масштаб должен быть @IBInspectable. Оси на графическом View должны появиться на storyboard в масштабе, установленном в Инспекторе Атрибутов.

Специфицируем наш класс GraphView с помощью директивы @IBDesignable, и Xcode автоматически замечает это и рисует прямо на storyboard наши оси. Директивы @IBInspectable  дают возможность появится свойствам scale,  lineWidthcolor, colorAxes непосредственно в Инспекторе Атрибутов:

Теперь мы можем устанавливать нами изобретенные свойства прямо на storyboard:

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

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

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

Будем добавлять «жесты» в GraphViewController, а функции обработки жестов будут в GraphView, так как все жесты связаны с установкой его UI элементов. Добавлять жесты будем в Наблюдателе Свойства didSet { } нашего свойства graphView:

Поместим обработки жестов scaleoriginMoveorigin в класс GraphView, оставив их non-private:

Код находится на Github.

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

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

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

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

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

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

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

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

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

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

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

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

Убираем Action performOperation :

И включаем Calculator View Controller и Graph View Controller  в Split View Controller как описано в Лекции 6:

У нас уже есть segue, который реализует «переезд» с Калькулятора на графический MVC. Это segue типа Show Detail и я дала ему имя «Show Graph».

Нам осталось написать метод prepareForSegue в классе CalculatorViewController, в котором мы и подготовим MVC— пункт назначения (в нашем случае это графический MVC) к «переезду», а именно установим замыкание yForX и сформируем заголовок графического MVC:

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

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

Запускаем на iPhone 7 + в портретном режиме:

Запускаем на iPhone 7 + в ландшафтном режиме:

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


Запускаем на iPad  в ландшафтном режиме:

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

Продолжение следует…

Задание 3 cs193p Winter 2017 «Графический» Калькулятор. Решение. Обязательные пункты.: 4 комментария

  1. Можно улучшить калькулятор сделав так, чтобы при запуске открывался не GraphViewController, а CalculatorVIewController:

    Необходимо добавить в CalculatorVIewController:

    override func awakeFromNib() {
    super.awakeFromNib()
    self.splitViewController?.delegate = self
    }

    func splitViewController(_ splitViewController: UISplitViewController,
    collapseSecondary secondaryViewController: UIViewController,
    onto primaryViewController: UIViewController) -> Bool {
    return true
    }

    А также имплементировать UISplitViewControllerDelegate:

    class CalculatorViewController: UIViewController, UISplitViewControllerDelegate

    Код CalculatorViewController здесь:

    https://gist.github.com/Darkhonbek/3dbe4c1ebf1239edf27981161124193f#file-calculatorviewcontroller-swift

    • Об этом говорит Пол Хэгерти на 65-ой минуте Лекции 8 и предлагает это решение, но немного в другом виде:

      func splitViewController(_ splitViewController: UISplitViewController,
      collapseSecondary secondaryViewController: UIViewController,
      onto primaryViewController: UIViewController) -> Bool {
      if primaryViewController.contents == self {
      if let gvc = secondaryViewController.contents as? GraphViewController,
      gvc.yForx == nil {
      return true
      }
      }
      }

      Если делегат splitViewController спрашивает меня, буду ли я сам выполнять “падение” (collapse) Detail поверх Master в случае, когда на Detail находится пустой график, то я возвращаю true, таким образом, сообшая Split View, что сам об этом позабочусь, но на самом деле не делаю ничего, ибо мне это не нужно. Это своего рода “фальсификация”, “обман” Split View.
      В остальных случаях, то есть когда на Detail находится НЕ пустой график, я говорю Split View, чтобы он делал это как считает нужным, и возвращаю false.

  2. Не совсем понятно с решением для точек разрыва функции. Переменная oldGraph всегда будет оставаться равной нулю. Возможно она должна получать значение в цикле по пикселям?

    • Возможно. Но опыт подсказывает, что этот вариант работает, так что лучше вообще убрать oldGraph.
      Все равно мы условно (приблизительно) с помощью коэффициента 1.5 определяем сильный выход за границу экрана координаты Y.
      Но любой другой разумный вариант приветствуется.

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