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

Screen Shot 2016-07-03 at 8.13.07 PM

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

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

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

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

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

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

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

Screen Shot 2016-06-30 at 7.03.59 PM

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

Screen Shot 2016-06-30 at 7.09.19 PM
. . . . . . . . .
Screen Shot 2016-06-30 at 7.08.50 PM

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

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

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

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

 

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

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

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

Screen Shot 2016-07-03 at 4.21.12 PM

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

Screen Shot 2016-07-03 at 4.35.44 PM

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

  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 будет запрашиваться только в функции drawInRect, которая существенно упростилась и в которой уже все outlets установлены, и согласно подсказке №8 Задания 3, геометрия полностью определена. Следовательно, никаких проблем с использованием origin у нас не будет.

Screen Shot 2015-05-11 at 8.35.09 PM

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

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

Screen Shot 2016-07-28 at 6.01.08 PM

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

Screen Shot 2016-07-02 at 10.48.53 PM

и весь код размещаем в GrapViewController:

Screen Shot 2016-07-28 at 6.04.33 PM

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

Screen Shot 2016-07-03 at 6.08.45 PM

Но этого мало, потому что переменная originRelativeToCenter, как мы видим, зависит от «геометрии» graphView, а при установки outlets у нас еще нет данных о «геометрии» прибора. Эти данные появляются в методах viewDidLayoutSubviews() и viewDidLayoutSubviews()

Screen Shot 2016-07-28 at 6.15.10 PM

Теперь посмотрим, где мы записываем переменные originRelativeToCenter и scale в  NSUserDefaults.  Его значение может измениться при использовании жестов panning и  double-tapping (обязательный 11 пункт Задания 3). Запоминать в NSUserDefaults значения переменных originFactorDefault и scale будем в методе «жизненного цикла» ViewWillDisappear нашего Controller

Screen Shot 2016-07-28 at 6.17.36 PM

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

Screen Shot 2016-07-28 at 6.20.15 PM

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

Screen Shot 2016-07-03 at 8.59.33 PM

Это будет выполняться системой автоматически при соответствующем определении возможных положений прибора в установках проекта

Screen Shot 2016-07-03 at 6.41.58 PM

Код находится на  Github для Xcode 7 и Swift 2.2.

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

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

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

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

Screen Shot 2016-07-25 at 9.27.37 AM

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

Screen Shot 2016-07-25 at 9.36.10 AM

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

Screen Shot 2016-07-25 at 9.43.08 AM

и, наконец, останавливаем запись Time Profiler. В полученных данных устанавливаем участок, где пользователь применял жест pan, и устанавливаем его для анализа длительностью 2с. 

Screen Shot 2016-07-25 at 9.51.33 AM

Обратите внимание, что все опции Call Tree должны быть включены вами, так как при старте Time Profiler они почти все отключены по умолчанию. Большая часть времени (91,3%) расходуется на метод drawRect в GraphView.

Screen Shot 2016-07-25 at 9.57.59 AM

Дважды кликаем на строке с drawRect и получаем распределение времени в модуле drawRect: 6,5% приходится на рисование осей в функции axesDrawer.drawAxesInRect, 93,4% —  на рисование самого графика в функции drawCurveInRect.

Screen Shot 2016-07-25 at 10.02.48 AM

Мы можем пойти еще дальше и посмотреть, чем занята функция drawCurveInRect(…) :

Screen Shot 2016-07-25 at 10.07.45 AM

Мы видим, что она тратит 93,9% времени на вычисление значения y (x).
Итак, мы исследовали жест panning, и приходим к выводу, что для ускорения этого процесса нужно не рассчитывать значения функций y (x) или рассчитывать их пореже или кэшировать данные.
Исследования жеста pinchining для изменения масштаба показали, что
в модуле drawRect8,2% приходится на рисование осей в функции axesDrawer.drawAxesInRect, 91,8% —  на рисование самого графика в функции drawCurveInRect.

Screen Shot 2016-07-25 at 10.14.01 AM

Мы видим, что 93,6% времени занимает вычисление значения y (x).

Screen Shot 2016-07-25 at 10.16.21 AM

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

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

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

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

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

Первое, что сделаем — позволим не рисоваться самому графику, а рисоваться только осям. Для этого в классе GraphView добавим private свойство:

Screen Shot 2016-07-25 at 10.29.00 AM

которое по умолчанию выставлены в false и можем меняються на true и обратно только в обработке жеста panning. и/или жеста pinching.

Screen Shot 2016-07-25 at 10.33.36 AM

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

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

 private var snapshot:UIView?

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

Screen Shot 2016-07-25 at 10.36.51 AM
Это будет выглядеть так:

Screen Shot 2016-07-25 at 10.51.40 AM

Если мы опять обратимся к нашим инструментам, то увидим, что время обработки жеста  pan упала до 14% от максимального за весь сеанс по сравнению с 90% от предыдущего случая…

Screen Shot 2016-07-25 at 10.41.17 AM

…и теперь основное время занимает рисование осей:

Screen Shot 2016-07-25 at 10.48.49 AM

Можно заставить двигаться только мгновенный снимок:

Screen Shot 2016-07-25 at 11.03.07 AM

Если мы опять обратимся к нашим инструментам, то увидим, что время обработки жеста  pan упала до 7,5% от максимального за весь сеанс по сравнению с 14% от предыдущего случая…

Последний вариант подойдет и для жеста pinch:

Screen Shot 2016-07-25 at 11.13.23 AM

Таким образом, нам удалось добиться существенного (почти на 60%) повышения быстродействия жеста pan и pinch.
Код находится на Github для Xcode 7 и Swift 2.2.

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

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

Когда ваше приложение запускается впервые, оно должно показывать последний график, который был построен (а не приходить пустым). Вам также следует переустановить свой Calculator при запуске в последнее состояние, в котором он был (которое может совпадать, а может и не совпадать с тем, что показывает график). Будьте внимательны и не нарушайте правила MVC, так как у каждого MVC есть свой собственный мир.

Это пункт выполнить значительно проще, если в случае с iPad мы не будем «переезжать» на новый  MVC, а будем повторно использовать Detail.Каждый раз, когда мы кликаем на кнопку, вызывающую построение графика, создается новый MVC GraphViewController, хотя в случае с iPad для этого нет никаких оснований.Об этом профессор подробно говорит на Лекции 8 (приблизительно на 65-ой минуте).
Для этого нам нужно отказаться от использования segues, потому что они всегда создают новый MVC.

Screen Shot 2016-07-26 at 12.52.56 PM

И создаем для кнопки с графиком обычный Target / Action с помощью CTRL-перетягивания:

Screen Shot 2016-07-26 at 12.58.38 PM

Метод, который мы назовем ShowGraph, во многом похож на метод prepareForSegue. Но мы можем что-то здесь делать только, если мы находимся внутри Split View. Поэтому я собираюсь сначала проверить, что мне удалось получить мой Detail как GraphViewСontroller, я затем я выполняю код, заимствованный из метода prepareForSegue:

Screen Shot 2016-07-26 at 1.26.53 PM

В методе ShowGraph после if splitViewController… , мы продолим писать else{…}. То есть в противном случае (для iPhone) нам определенно нужен segue, и мы реализуем >segue в коде,используя метод performSegueWithIdentifier:

Screen Shot 2016-07-26 at 1.37.52 PM

Но метод performSegueWithIdentifier требует, чтобы segue с таким идентификатором существовал на storyboard.
Мы eже избавились от segue. Нам нужно иметь на storyboard segue с таким идентификатором. Когда мы хотим иметь segue в коде, мы должны создать segue непосредственно от самого Calculator View Controller, от этой маленькой иконки, к Navigation Controller. Используем CTRL— перетягивание от Calculator  ViewController к Navigation Controller.

Screen Shot 2016-07-26 at 1.45.10 PM

Это должен быть Show segue, а не Show Detail, и даем ему то же имя.

Теперь приступаем непосредственно к пункту 6.

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

Screen Shot 2016-07-26 at 1.54.16 PM

Эта переменная считывается в двух местах :
в методе viewDidLoad:

Screen Shot 2016-07-26 at 1.58.40 PM

и в методе делегата UISplitViewControllerDelegate:

Screen Shot 2016-07-26 at 2.02.04 PM

Переменная program записывается также в двух местах:
при нажатии кнопки с графиком:

Screen Shot 2016-07-26 at 2.08.37 PM

и при «уходе» Calculator View Controller с экрана:

Screen Shot 2016-07-26 at 4.18.48 PM

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

Screen Shot 2016-07-26 at 4.25.17 PM

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

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

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

  1. Добрый день!
    graphVC.yForX = { [ weak weakSelf = self] x in
    weakSelf?.brain.variableValues[«M»] = x
    return weakSelf?.brain.result.0
    Как быть с реальным значением переменной М, которое мы могли ввести на стадии CalculatorViewController? Оно изменяется на последнее значение ‘x’ в GraphViewController. Может, стоить использовать другой dictionary?

    • Вы очень внимательны и абсолютно правы. Калькулятор и «построение графиков» должны быть развязаны. На самом деле должно быть два Калькулятора: один — в CalculatorViewController, а другой private — в GraphViewController, и у GraphViewController в качестве public API должно быть не замыкание уForx, а программа program: PropertyList?, которую должен интерпретирует private Calculator.
      Второй Калькулятор в GraphViewController
      Мне кажется, что профессор имел ввиду именно этот вариант, то есть два независимых Калькулятора, две независимые программы program, которые сохраняются и восстанавливаются из NSUserDefaults.
      Я уже делала этот вариант в Задании 3 курса iOS 8 и он описан в посте «Элегантный графический калькулятор без делегирования».
      Я предполагаю выложить и это решение для Задании 3 курса iOS 9, но может быть чуть позже.

  2. private var program: PropertyList? {
    get { return defaults.objectForKey(Keys.Program) as? [AnyObject] }
    set { defaults.setObject(newValue, forKey: Keys.Program) }
    }
    …..
    override func viewDidLoad() {
    …..
    if let savedProgram = program as? [AnyObject]{
    brain.program = savedProgram

    Татьяна, подскажите, пожалуйста, а для чего делается двойной кастинг?

    • Конечно, никакого смысла в этом нет:
      typealias PropertyList = AnyObject

      private var program: PropertyList? {
      get { return defaults.objectForKey(Keys.Program) /*as? [AnyObject]*/ }
      set { defaults.setObject(newValue, forKey: Keys.Program) }
      }

      Осталось от Swift 1.2 — там везде был этот AnyObject.
      Спасибо за замечание.

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