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

Это решение Задания 3 cs193p Winter 2017 Графический Калькулятор — дополнительные пункты.

Обязательные пункты Задания 3, а также ссылки на текст самого Задания 3 можно посмотреть здесь:
Задание 3 cs193p Winter 2017 Графический Калькулятор. Решение обязательных пунктов.
Код можно посмотреть на Github:

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

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

У вас должна быть “графическая” кнопка в вашем основном экранном фрагменте Calculator, отражающая возможность построения графика по тем данным, которые введены в Калькулятор (например, является ли ваш результат отложенным (pending) или нет). Вам просто нужно сделать ее неработоспособной в этом случае, но, может быть, есть и другие случаи для такой неработоспособности: например, другой график или что-то еще? Это очень легкая задачка, которая не потребует много дополнительного кода!

Это действительно просто. Делаем Outlet для кнопки с изображение графика:

При установки этого Outlet мы делаем кнопку недоступной и изменяем цвет фона на серый:

А когда мы отображаем результат displayResult в CalculatorViewController, то в зависимости от того, является ли он «отложенным» или окончательным, мы делаем кнопку недоступной или доступной соответственно и меняем цвет фона:

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

2. Сохраните origin и scale между запусками вашего приложения. Где это следует сделать, оказывая наибольшее уважение MVC, как вы думаете? Нет однозначно “правильного ответа” на этот вопрос. Это требует некоторой утонченности знания  MVC.

 

3. При вращении прибора (или другом измении границ bounds), разместите origin вашего графика по отношению к центру графического View, а не по отношению к верхнему левому углу.  

Сначала мы решим дополнительный пункт 3.

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

Начало координат «уплывает» за пределы экрана. Давайте внимательно взглянем на код в классе Graph, в котором мы устанавливаем свойство origin, которое определяет положение начала координат нашего графика относительно левого верхнего угла:

До тех пор, пока originSet равна nil, свойство origin пересчитывается при изменении границ bounds нашего grapView и все прекрасно. Как только мы установили originSet, перемещая начало координат в любое другое место с помощью жеста «двойной tap«, оно не изменяется и при повороте мобильного устройства, например, в ландшафтный режим  продолжает сохранять свое значение, хотя границы bounds нашего grapView уже изменились. Учитывая существенное изменение размера экрана в портретном и ландшафтном режимах как на iPhone, так и на iPad, мы можем установить какое-то значение originSet, например, в портретном режиме, а перейдя в ландшафтный режим, обнаружить, что установленное значение originSet выходит за пределы экрана. 

Очевидно, что  originSet должен пересчитываться при изменении границ bounds нашего grapView. Но какую логику можно использовать для такого пересчета? Очевидно, необходимо отталкиваться от положения начала координат относительно центра экрана, который меняется при границы bounds нашего grapView. Однако нужно использовать некоторый нормированный показатель, который не зависит от размера экрана, но указывает смещение начала координат относительно центра экрана.
Нам необходимо сделать так, чтобы при повороте устройства была следующая картина:

Для того, чтобы этого достичь, нужно выполнить две вещи:

  1. указывать смещение originRelativeToCenter начала координат графика по отношению к центру нашего View.
  2. при изменении bounds нашего View пересчитывать originRelativeToCenter так, чтобы неизменным сохранялось соотношение по координате Х смещения  originRelativeToCenter.x к ширине bounds.size.width  и  соотношение по координате смещения originRelativeToCenter.y к высоте bounds.size.heignt.

Давайте рассмотрим класс GraphView нашего обобщенного пользовательского  View, которое строит графики. Вместо private свойства originSet, которое было в обязательных заданиях, определим  public свойство originRelativeToCenter в этом классе

Screen Shot 2015-05-14 at 5.55.45 PM

Это практически полная синтаксическая копия предыдущего свойства originSet, но семантический смысл у него другой — это смещение начала координат графика по отношению к центру нашего пользовательского View. Свойство originRelativeToCenter хорошо тем, что его значение по умолчанию равно  CGPointZero и не зависит от геометрии View. Мы можем задать его даже на этапе, когда «геометрия»  нашего пользовательского View не определена.

Screen Shot 2016-07-02 at 10.07.07 PM

Предыдущее public свойство  origin, означающее смещение начала координат графика по отношению к левому верхнему углу нашего пользовательского View, остается, так как именно на него настроено рисование осей координат и самого графика. Но оно становится вычисляемым свойством на основе originRelativeToCenter

Screen Shot 2016-07-02 at 10.09.43 PM

Заметьте, что для вычисления  origin  понадобится центр graphCenter нашего  View, то есть геометрия должна быть уже определена к моменту использования  origin. Действительно,  origin будет запрашиваться только в функции draw(CGRect), которая существенно упростилась и в которой уже все outlets установлены, и согласно подсказке №8 Задания 3, геометрия полностью определена. Следовательно, никаких проблем с использованием origin у нас не будет.

Определенное заново вычисляемое свойство origin лучше сделать в классе GraphView  private.

Во внешнем «мире», например, в классе GraphViewController, будем оперировать с originRelativeToCenter. Значение именно этого свойство будет участвовать в запоминании и восстановлении информации в UserDefaults . Эти манипуляции мы производим в другом классе  GraphViewController, классе нашего Controller. Этим будет заниматься private вычисляемая переменная с таким же названием originRelativeToCenter.

Переменная originRelativeToCenter  будет вычисляться на основе двумерного нормированного «множителя» factor, который не зависит от размеров экрана и равен смещению начала оси координат originRelativeToCenter относительно центра, деленному соответственно на длину with и ширину height по координатам X и Y.

Этот множитель вместе с масштабом scale очень удобно хранить между запусками приложения и мы сможем правильно рассчитать положение начала по отношению у центру originRelativeToCenter при любой ориентации устройства. Для этого используем механизм UserDefault :

Когда мы используем переменные originRelativeToCenter и scale в правой части выражения — {get}, то вы «достаем» данные factor и scale из UserDefaults, а когда в левой части выражения — {set}, то «записываем» данные в UserDefaults.
Нам нужно «достать» из UserDefaults координаты начала координат графика относительно центра сразу после загрузки Outlet обобщенного (generic) графика graphView: GraphView!. Поэтому в Наблюдателе Свойства didSet {} автоматически подгружаемого  со storyboard свойства  graphView, мы устанавливаем значения свойствам originRelativeToCenter и  scale нашему графику, то есть используем  originRelativeToCenter  и scale переменные в правой части выражения, и следовательно, считываем из UserDefaults:

Screen Shot 2016-07-03 at 6.08.45 PM

Но этого мало, переменная originRelativeToCenter зависит от «геометрии» graphView, поэтому мы используем ее в методах viewDidLayoutSubviews() и viewDidLayoutSubviews():

Теперь посмотрим, где мы будем записывать переменные factor и scale в  UserDefaults. Их значения могут изменяться в процессе использования приложения, когда мы применяем жесты pan и  double-tap непосредственно на самом Графике. Запоминать в UserDefaults значения переменных factor и scale мы будем тогда, когда графический MVC покидает экран, то есть в методе «жизненного цикла» ViewWillDisappear нашего Controller:

Здесь мы используем переменные originRelativeToCenter и scale в левой части выражения, следовательно, идет «запись» переменных factor и scale в  UserDefaults.
Код получился простой и понятный.
Теперь займемся автовращением. Мы знаем, что если меняются границы bounds View, то нужно использовать пару методов «жизненного цикла» viewWillLayoutSubviews() и viewDidLayoutSubviews():

В методе viewWillLayoutSubviews() действуют «старые» границы bounds и мы там сохраняем «старую» ширину widthOld и действующий на текущий момент нормированный factor.
В методе viewDidLayoutSubviews() мы производим пересчет смещения начала координат графика graphView.originRelativeToCenter с учетом того же фактора  factor, который достаем из UserDefaults, и новых границ bounds. Мы знаем, что эта пара методов вызывается часто и не всегда при изменении границ, поэтому строго отслеживаем изменение с помощью «старой» ширины widthOld и новой ширины graphView.bounds.size.width.
Получился прекрасный результат.

Этого же эффекта при автовращении iOS устройства можно добиться и с помощью метода viewWillTransition(to size: ,with coordinator: ) вместо  пары методов «жизненного цикла» viewWillLayoutSubviews() и viewDidLayoutSubviews():

Если вы хотите экспериментировать с запоминанием переменных factor и scale в  UserDefaults на симуляторе, то здесь есть  тонкость связанная с тем, как интерпретировать слова «между запусками вашего приложения». Если вы поставите «точку прерывания»  в методе «жизненного цикла» ViewWillDisappear нашего Controller:

И будете использовать в Xcode 8 кнопки «Run» и «Stop«:

То вы никогда не получите прерывания в методе ViewWillDisappear нашего Controller, потому что кнопка  «Stop» не дает полноценно завершится вашему приложению и вы не сможете произвести запись переменных factor и scale в  UserDefaults на симуляторе. Если вы нажмете просто на Home, то это приводит к приостановке (suspend) приложения, но не к его завершению ( close). Что нужно сделать, чтобы приложение на симуляторе завершилось ( close)?

Для полноценного завершения приложения на симуляторе нужно использовать клавиши  ++H, причем нажать H дважды, чтобы симулировать двойное нажатие на Home. После этого появляется список открытых приложений, из которого вы можете выбрать свое приложение, и выполнить жест swipe up для завершения приложения:

В этом случае будет выполнять метод  ViewWillDisappear вашего Controller, произойдет запись переменных factor и scale в  UserDefaults и ваше приложение корректно завершится.

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

Поймите, как работают Instruments,  анализируя производительность жестов  panning и pinching в вашем графическом View. Что делает перемещение графика по экрану таким “вялым”? Объясните в комментариях к вашему коду, что вы обнаружили и что с этим можно сделать.

Вместо запуска приложения запускаем Instruments для приложения с помощью  ⌘I и выбираем Time Profiler.

Нажимаем кнопку записи:

Возвращаемся к нашему приложению (оно уже запущено), создаем график sin(1÷M) и начинаем водить мышкой по экрану (жест pan):

И, наконец, останавливаем запись Time Profiler. Загрузка CPU во время выполнения жеста pan составляет в среднем 60% от максимальной за все время сеанса.

Обратите внимание, что почти все опции Call Tree должны быть включены вами, так как при старте Time Profiler они почти все отключены по умолчанию.

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

Большая часть времени (80,42%) расходуется на path.stroke() в методе drawCurveInRect в GraphView. Кстати, как и говорил профессор Пол Хэгерти в Лекции 5, время вычисления функции yForx пренебрежительно мало 0,9% по сравнению со временем рисования на экране.

Подобная же картина для жеста pinch:

Большая часть времени (75,32%) расходуется на path.stroke() в методе drawCurveInRect в GraphView, а время вычисления функции yForx невелико 2,6%.

Вот несколько предложений как можно уменьшить время для жестов panning и pinching:

  • Вы можете уменьшить «шаг»,с которым вычисляются графики, например , вычислять каждую 4-ую или каждую 10-ую точку …
  • Вы можете поступить, как поступает iOS при вращении прибора, — сохранять изображение view перед жестом и манипулировать его размерами и позицией вместо перерасчета графика…

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

Используйте информацию, которую вы обнаружили в 1-ом дополнительном пункте, для улучшения производительности жеста panningНЕ превращайте ваш код в “месиво”, выполняя это. Ваше решение должно быть простым и элегантным. Есть сильное искушение при оптимизации принести в жертву читабельность кода или преступить границы MVC, но вам НЕ разрешено это делать для этого дополнительного пункта!

Будем использовать «замороженное» изображения.

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

 private var snapshot:UIView?

Добавляем его к нашему view – график застывает, а позволяем скользить вслед за жестом pan только мгновенному снимку snapshot.  Как только мы останавливаем  жестом pan,  мгновенный снимок убирается с нашего view, и график восстанавливается с нужным положение начала координат origin:

Это будет выглядеть так:

Работает безукоризненно.

Если мы опять обратимся к нашим инструментам, то увидим, что загрузка CPU во время выполнения жеста pan упала до 6% от максимального за весь сеанс по сравнению с 60% для случая полного рисования графика и осей координат :

Вариант использования мгновенный снимка нашего графика  подойдет и для жеста pinch:

Это будет выглядеть так:

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

Когда ваше приложение запускается впервые, оно должно показывать последний график, который был построен (а не приходить пустым). Вам также следует переустановить свой Calculator при запуске в последнее состояние, в котором он был (которое может совпадать, а может и не совпадать с тем, что показывает график). Будьте внимательны и не нарушайте правила MVC, так как у каждого MVC есть свой собственный независимый мир. Наиболее простой механизм постоянного хранения в iOS — это UserDefaults, но только Property Lists могут быть там размещены, вы должны придумать, как превратить функцию, которую вы должны представить графически, в совмещенную с Property List форму. Для этого вам разрешается использовать Swift Типы Any или AnyObject для реализации именно этого дополнительного пункта Задания.

У нас в классе CalculatorBrain есть внутренняя программа Калькулятора internalProgram, которая запоминает последовательность операндов, операций и переменных, которые вводятся в CalculatorBrain, в виде массива перечислений enum OpStack:

Мы не можем использовать internalProgram для запоминания в UserDefaults, поэтому я добавлю в CalculatorBrain новую переменную, которую назову program. Это AnyObject. Но я также хочу сделать ее PropertyList, потому что это более информативно для того, кто будет читать мой код. Я собираюсь использовать очень крутую возможность в Swift, которая называется typealias, позволяющую создавать тип с именем PropertyList, который эквивалентен AnyObject:

Сделаем переменную program вычисляемой, поэтому у меня будут get{} и set{}. В get{} она должна превращать нашу внутреннюю программу Калькулятора  internalProgram типа [OpStack] в PropertyList типа AnyObject. И наоборот, если мы устанавливаем переменной  program значение в set{}, например, считанное из UserDefaults, то она должна восстанавливать из PropertyList нашу внутреннюю структуру  internalProgram.

В случае с  get{} все очень просто: извлекаем ассоциированные значения (это Double и String) для каждого элемента массива [OpStack] и размещаем их в другом массиве как Any элементы:

В случае с set{} мы «распаковываем» массив  Any элементов ( а на самом деле Double и String) и «запаковываем» их как ассоциированные значения в массив  [OpStack]:

Здесь все очищается, потому что мы запускаем новую программу. Далее идет проверка того, что новое значение newValue программы является массивом [Any], и если это не так, то установка program просто игнорируется. А если это действительно массив [Any], то мы получаем его в виде массива arrayOfAny, в котором буду анализировать все операции, операнды и переменные. Мы должны пройтись по всем элементам массива arrayOfAny.

В CalculatorViewController будем хранить программу Калькулятора в вычисляемой переменной program:

Эта переменная считывается в методе viewDidLoad и в этом случае мы восстанавливаем программу program из UserDefaults :

Мы тут же устанавливаем считанную программу savedProgram нашему Калькулятору brain, отображаем результаты displayResult и готовим Графический MVC для показа соответствующего графика.

Переменная program записывается в UserDefaults при «уходе» Calculator View Controller с экрана:

В результате, если это не первый запуск приложения и в прошлых сеансах строились графики, то будут построены графики с прошлого сеанса и на iPad, и на iPhone:

Кроме того, на Калькуляторе воспроизведена полностью программа, которую в прошлый раз набрал пользователь.

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

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

  1. Касательно первого пункта:
    Если кликнуть по кнопке С, кнопка построения графика станет активна.

  2. Сообщение выше касается всех «уникальных кнопок».
    Также перемещение снапшота (пункт 5) работает не корректно, т.к. двигается просто белое вью (без самого графика).

    P.S. Перепечатывал с примеров с сайта, и тестил проект с гитхаба.

    • Все работает прекрасно. Я не знаю, почему у вас не получается. Дайте ссылку на ваш код. Возможно что-то напутали.
      О каких «уникальных кнопках» вы говорите? Ничего не понятно.

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