Это решение Задания 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 для кнопки с изображение графика.
При установки этого outlet мы делаем кнопку недоступной. А когда мы получаем результат, то в зависимости от того, является ли он частичным или окончательным, мы делаем кнопку недоступной или доступной соответственно.
Этот пункт уже выполнен в обязательных пунктах и код можно посмотреть на 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.
Нам предлагают решить следующую проблему. Если я, находясь в портретном режиме, с помощью жеста двойной «тап» перемещаю начало осей моего графика из середины экрана в другое место, а затем поворачиваю мой прибор в ландшафтный режим, то получу следующую картину:
Начало координат «уплывает» за пределы экрана. Это происходит потому, что мы при рисовании осей устанавливаем свойство origin, которое определяет положение начала координат относительно левого верхнего угла и не изменяется при повороте в ландшафтный режим, хотя границы bounds нашего grapView изменились.
Нам необходимо сделать так, чтобы при повороте была следующая картина:
Для того, чтобы этого достичь, нужно выполнить две вещи:
- указывать смещение originRelativeToCenter начала координат графика по отношению к центру нашего View.
- при изменении bounds нашего View пересчитывать originRelativeToCenter так, чтобы неизменным сохранялось соотношение по координате Х смещения originRelativeToCenter.x к ширине bounds.size.width и соотношение по координате Y смещения originRelativeToCenter.y к высоте bounds.size.heignt.
Давайте рассмотрим класс GraphView нашего обобщенного пользовательского View, которое строит графики. Вместо private свойства originSet, которое было в обязательных заданиях, определим public свойство originRelativeToCenter в этом классе
Это практически полная синтаксическая копия предыдущего свойства originSet, но семантический смысл у него другой — это смещение начала координат графика по отношению к центру нашего пользовательского View. Свойство originRelativeToCenter хорошо тем, что его значение по умолчанию равно CGPointZero и не зависит от геометрии View. Мы можем задать его даже на этапе, когда «геометрия» нашего пользовательского View не определена.
Предыдущее public свойство origin, означающее смещение начала координат графика по отношению к левому верхнему углу нашего пользовательского View, остается, так как именно на него настроено рисование осей координат и самого графика. Но оно становится вычисляемым свойством на основе originRelativeToCenter
Заметьте, что для вычисления origin понадобится центр graphCenter нашего View, то есть геометрия должна быть уже определена к моменту использования origin. Действительно, origin будет запрашиваться только в функции drawInRect, которая существенно упростилась и в которой уже все outlets установлены, и согласно подсказке №8 Задания 3, геометрия полностью определена. Следовательно, никаких проблем с использованием origin у нас не будет.
Определенное заново вычисляемое свойство origin лучше сделать в классе GraphView private.
Во внешнем «мире», например, в классе GraphViewController, будем оперировать с originRelativeToCenter. Значение именно этого свойство будем участвовать в запоминании и восстановлении информации в NSUserDefaults . Эти манипуляции мы производим в другом классе GraphViewController, классе нашего Controller. Этим будет заниматься private вычисляемая переменная с таким же названием originRelativeToCenter.
Переменная originRelativeToCenter будет вычисляться на основе двумерного «множителя» factor, который не зависит от размеров экрана и равен смещению начала оси координат originRelativeToCenter относительно центра, деленному соответственно на длину with и ширину height по координатам X и Y. Двумерный множитель factor не является точкой, но формально мы представим его структурой CGPoint, так как он должен хранить два числа типа CGFloat, соответствующих координатам x и y. Этот множитель вместе с масштабом scale очень удобно хранить между запусками, так как повторный запуск возможен при другой ориентации прибора. Для этого используем механизм NSUserDefault
и весь код размещаем в GrapViewController:
Когда мы используем переменные originRelativeToCenter и scale в правой части выражения {get}, то вы «достаем» данные из NSUserDefaults, а когда в левой {set} — «записываем» данные в NSUserDefaults.
Нам нужно «достать» из NSUserDefaults координаты начала координат графика относительно центра сразу после загрузки outlet со storyboard. У нас один outlet — это обобщенный (generic) график graphView: GraphView! и в Наблюдателе Свойства автоматически подгружаемого со storyboard свойства, мы устанавливаем значения свойствам originRelativeToCenter и scale нашему графику, то есть используем originRelativeToCenter и scale переменные в правой части выражения.
Но этого мало, потому что переменная originRelativeToCenter, как мы видим, зависит от «геометрии» graphView, а при установки outlets у нас еще нет данных о «геометрии» прибора. Эти данные появляются в методах viewDidLayoutSubviews() и viewDidLayoutSubviews()
Теперь посмотрим, где мы записываем переменные originRelativeToCenter и scale в NSUserDefaults. Его значение может измениться при использовании жестов panning и double-tapping (обязательный 11 пункт Задания 3). Запоминать в NSUserDefaults значения переменных originFactorDefault и scale будем в методе «жизненного цикла» ViewWillDisappear нашего Controller
Здесь мы используем переменные originRelativeToCenter и scale в левой части выражения, следовательно, идет «запись» в NSUserDefaults.
Код получился простой и понятный.
Теперь займемся автовращением. Мы знаем, что если меняются границы bounds View, то нужно использовать пару методов «жизненного цикла» viewWillLayoutSubviews() и viewDidLayoutSubviews().
В методе viewWillLayoutSubviews() действуют «старые» границы bounds и мы там сохраняем «старую» ширину widthOld .
В методе viewDidLayoutSubviews() мы производим пересчет смещения начала координат графика graphView.originRelativeToCenter с учетом того же фактора новых границ bounds. Мы знаем, что эта пара методов вызывается часто и не всегда при изменении границ, поэтому строго отслеживаем изменение с помощью «старого» widthOld и нового.
Получился прекрасный результат.
Это будет выполняться системой автоматически при соответствующем определении возможных положений прибора в установках проекта
Код находится на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Пункт 4 дополнительный
Поймите, как работают Instruments, анализируя производительность жестов panning и pinching в вашем графическом View. Что делает перемещение графика по экрану таким “вялым”? Объясните в комментариях к вашему коду, что вы обнаружили и что с этим можно сделать.
Вместо запуска приложения запускаем Instruments для приложения с помощью ⌘I и выбираем Time Profiler.
Нажимаем кнопку записи:
Возвращаемся к нашему приложению (оно уже запущено), создаем график sin (1/M) и начинаем водить по экрану (жест pan)
и, наконец, останавливаем запись Time Profiler. В полученных данных устанавливаем участок, где пользователь применял жест pan, и устанавливаем его для анализа длительностью 2с.
Обратите внимание, что все опции Call Tree должны быть включены вами, так как при старте Time Profiler они почти все отключены по умолчанию. Большая часть времени (91,3%) расходуется на метод drawRect в GraphView.
Дважды кликаем на строке с drawRect и получаем распределение времени в модуле drawRect: 6,5% приходится на рисование осей в функции axesDrawer.drawAxesInRect, 93,4% — на рисование самого графика в функции drawCurveInRect.
Мы можем пойти еще дальше и посмотреть, чем занята функция drawCurveInRect(…) :
Мы видим, что она тратит 93,9% времени на вычисление значения y (x).
Итак, мы исследовали жест panning, и приходим к выводу, что для ускорения этого процесса нужно не рассчитывать значения функций y (x) или рассчитывать их пореже или кэшировать данные.
Исследования жеста pinchining для изменения масштаба показали, что
в модуле drawRect: 8,2% приходится на рисование осей в функции axesDrawer.drawAxesInRect, 91,8% — на рисование самого графика в функции drawCurveInRect.
Мы видим, что 93,6% времени занимает вычисление значения y (x).
Итак, мы исследовали жест pinching, и приходим к выводу, что для ускорения этого процесса нужно не не рассчитывать значения функций y (x) или рассчитывать их пореже или кэшировать данные..
Вот несколько предложений как можно уменьшить время для жестов panning и pinching:
- Вы можете уменьшить «шаг»,с которым вычисляются графики, например , вычислять каждую 4-ую или каждую 10-ую точку …
- Вы можете поступить, как поступает iOS при вращении прибора, — сохранять изображение view перед жестом и манипулировать его размерами и позицией вместо перерасчета графика…
Пункт 5 дополнительный
Используйте информацию, которую вы обнаружили в 1-ом дополнительном пункте, для улучшения производительности жеста panning. НЕ превращайте ваш код в “месиво”, выполняя это. Ваше решение должно быть простым и элегантным. Есть сильное искушение при оптимизации принести в жертву читабельность кода или преступить границы MVC, но вам НЕ разрешено это делать для этого дополнительного пункта!
Первое, что сделаем — позволим не рисоваться самому графику, а рисоваться только осям. Для этого в классе GraphView добавим private свойство:
которое по умолчанию выставлены в false и можем меняються на true и обратно только в обработке жеста panning. и/или жеста pinching.
Будем использовать «замороженное» изображения.
Как только жест начался, мы делаем мгновенный снимок нашего графика и помещаем его в
private var snapshot:UIView?
Мы добавляем его к нашему view – график застывает, а позволяем скользить вслед за жестом pan только осям. Как только мы останавливаем жестом pan, мгновенный снимок убирается с нашего view, и график восстанавливается:
Если мы опять обратимся к нашим инструментам, то увидим, что время обработки жеста pan упала до 14% от максимального за весь сеанс по сравнению с 90% от предыдущего случая…
…и теперь основное время занимает рисование осей:
Можно заставить двигаться только мгновенный снимок:
Если мы опять обратимся к нашим инструментам, то увидим, что время обработки жеста pan упала до 7,5% от максимального за весь сеанс по сравнению с 14% от предыдущего случая…
Последний вариант подойдет и для жеста pinch:
Таким образом, нам удалось добиться существенного (почти на 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.
И создаем для кнопки с графиком обычный Target / Action с помощью CTRL-перетягивания:
Метод, который мы назовем ShowGraph, во многом похож на метод prepareForSegue. Но мы можем что-то здесь делать только, если мы находимся внутри Split View. Поэтому я собираюсь сначала проверить, что мне удалось получить мой Detail как GraphViewСontroller, я затем я выполняю код, заимствованный из метода prepareForSegue:
В методе ShowGraph после if splitViewController… , мы продолим писать else{…}. То есть в противном случае (для iPhone) нам определенно нужен segue, и мы реализуем >segue в коде,используя метод performSegueWithIdentifier:
Но метод performSegueWithIdentifier требует, чтобы segue с таким идентификатором существовал на storyboard.
Мы eже избавились от segue. Нам нужно иметь на storyboard segue с таким идентификатором. Когда мы хотим иметь segue в коде, мы должны создать segue непосредственно от самого Calculator View Controller, от этой маленькой иконки, к Navigation Controller. Используем CTRL— перетягивание от Calculator ViewController к Navigation Controller.
Это должен быть Show segue, а не Show Detail, и даем ему то же имя.
Теперь приступаем непосредственно к пункту 6.
Будем хранить программу Калькулятора program в вычисляемой переменной program:
Эта переменная считывается в двух местах :
в методе viewDidLoad:
и в методе делегата UISplitViewControllerDelegate:
Переменная program записывается также в двух местах:
при нажатии кнопки с графиком:
и при «уходе» Calculator View Controller с экрана:
В результате, если это не первый запуск приложения и в прошлых сеансах строились графики, то будут построены графики с прошлого сеанса и на iPad, и на iPhone.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Добрый день!
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.
Мне кажется, что профессор имел ввиду именно этот вариант, то есть два независимых Калькулятора, две независимые программы program, которые сохраняются и восстанавливаются из NSUserDefaults.
Я уже делала этот вариант в Задании 3 курса iOS 8 и он описан в посте «Элегантный графический калькулятор без делегирования».
Я предполагаю выложить и это решение для Задании 3 курса iOS 9, но может быть чуть позже.
Спасибо.
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.
Спасибо за замечание.
И вам спасибо!