Задание 4. CS193P Spring 2016. Smashtag Mentions (клиент Twitter). Решение — обязательные пункты 1- 7.

Содержание

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

iOS 9 Задания


В Задании 4 вы должны усовершенствовать приложение Smashtag, которое мы создали на Лекции 9, чтобы обеспечить быстрый доступ к хэштэгам hashtags, URLs urls, изображениям images и пользователям users, упомянутым в твите. Основными идеями в этом Задании являются многопоточность, Table View, глубокое знание Navigation Controller, множественные MVC типа Tab Bar Controller и работа с изображениями с помощью Scroll View.

Для выполнения Задания 4 необходим материал  Лекции 8 и Лекции 9. Если вы установили Xcode 8, то исходное приложение Smashtag L9 находится для Swift 2.3  на Github, а для Swift 3 — также на Github.

В данном посте представлено решение Обязательных пунктов 1 — 7 Задания 4.
Код можно найти на Github для Xcode 7 и Swift 2.2.

Продолжение находится в посте Задание 4. CS193P Spring 2016. Smashtag Mentions (клиент Twitter). Решение — обязательные пункты 8 — 10.

Для того, чтобы начать с проекта, который профессор демонстрировал на Лекции 9, вам нужно создать workspace (рабочее пространство) в Xcode, которое содержит оба проекта:  Smashtag L9 project c Лекции 9 и поставляемый Стэнфордом Twitter фреймворк project. Так получилось, а может быть это сделано специально для учебных целей, но эти два проекта находятся в разных местах:  Smashtag L9 находится для Swift 2.3  на Github, а для Swift 3 — также на Github, а ссылка на проект для фрейворка Twitter дана непосредственно в

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

и может быть скачан отсюда. Вы должны создать рабочее пространство workspace в Xcode, поместить туда два ваши проекта и установить между ними связь. Поэтому в Задании 4 у нас появился отдельный этап — подготовка, которая описывается в подсказках № 2 и 3 Задания 4.

Подготовка

Подсказка № 2. Помните, что вы должны создать workspace в Xcode, который содержит оба проекта: ваш Smashtag project и поставляемый Twitter фреймворк project. Оба проекта должны быть равноправны (siblings) (не должно быть отношение наследования одного по отношению к другому) в рабочем простанстве (workspace)
Подсказка № 3. Вам необходимо перетянуть Twitter фреймворк (из навигационной панели вашего рабочего пространсва workspace) на закладку General установок (Settings) вашего Smashtag Project в раздел Embedded Binaries.

Это совсем необязательно, но для удобства работы я создаю папку Assignment_4, в которой размещу оба мои проекта : project Twitter, скаченный отсюда, и project Smashtag L9, скаченный для Swift 2.3  с Github, а для Swift 3 — также с Github. Сюда же я добавлю workspace.

Screen Shot 2016-07-16 at 11.47.39 AM

Действуем как на Лекции 9. Идем в Xcode и добавляем workspace с именем «TwitterClient» в ту же самую папку с помощью меню:

Screen Shot 2016-07-16 at 11.56.34 AM

В пустое workspace перетягиваем файлы проектов Smashtag.xcodeproj и Twitter.xcodeproj и стараемся их расположить равноправно (как siblings):

Screen Shot 2016-07-16 at 12.17.22 PM

Открываем проект Twitter и видим, что наш фреймворк окрашен в красный цвет, это значит, что он не сформирован:

Screen Shot 2016-07-16 at 12.20.23 PM

Формируем его как на Лекции 9, он становится черного цвета и перетягиваем в проект Smashtag в раздел Embedded Binaries закладки General. После этого фреймворк Twitter.framework появляется в проекте Smashtag

Screen Shot 2016-07-16 at 12.33.43 PM

… и мы можем начать работать над Заданием 4.

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

Усовершенствуйте приложение Smashtag, полученное на лекции, в части выделения  (разными цветами каждого) hashtags,urls и user screen names, упомянутых в тексте твита (они известны как “mentions” — меншены). Заметьте, что эти mentions уже обнаружены для вас в каждом твите и представлены как [Mension] в классе CS193pTwitter.Tweet в поставляемом коде Twitter.

Класс Twitter.Tweet обеспечивает нас всем необходимым – для каждого твита  для каждой категории меншенов (mentions) — hashtagsurls и userMensions -предоставляется массив структур [Mension].

Screen Shot 2016-07-16 at 3.49.44 PM

Свойствами класса Mension являются сам текст мешена keyword, извлеченный из текста твита, и диапазон nsrange: NSRange символов в строке, для которых вы сможете установить фонт и/или цвет символов. Диапазон ns range не является Swift диапазоном Range. NSRange —  индексы в Objective-C строке NSString, но именно он нам и нужен. Причина, по которой мы используем именно этот диапазон связана с тем, что для раскрашивания строк используется Objective-C класс строк с атрибутами NSAttributedString, который еще не получил должной адаптации в Swift.
У большинства UIKit классов (таких как UILabel и UIButton) есть свойство attributedText, которое позволяет вам устанавливать и получать их текст с использованием NSAttributedString.
В классе для прототипа ячейки TweetTableViewCell сначала создадим палитру цветов для раскраски меншенов и объявим это свойство как public API …

Screen Shot 2016-07-16 at 4.23.21 PM

… а затем раскрасим метки для меншенов в специальном методе формирования текста твита

Screen Shot 2016-07-16 at 4.36.03 PM

с помощью класса NSMutableAttributedString, предварительно создав ему расширение для установки заданного цвета текста для заданного диапазона

Screen Shot 2016-07-16 at 4.38.14 PM

Обновление текста меток происходит в хорошо знакомом нам методе updateUI()

Screen Shot 2015-07-06 at 8.21.15 PM

.  .  .  .  .  .  .  .  .  .  .  .  .
Таким образом, мы  “не разрушили” preferred body фонт, используемый в демонстрационном примере Лекции 9, и выполнили подсказку № 8.

Подсказка № 8.Убедитесь, что вы “не разрушили” возможность, которая существует в текущей версии Smashtag, состоящая в том, что для показа твитов используется preferred body фонт (это позволяет делать текст в твитах меньшего или большего размера в зависимости от пожеланий пользователя, которые он выражает в Установках (Settings)).

Запускаем приложение  и получаем требуемый результат.
Код можно найти на Github.

Screen Shot 2016-07-16 at 5.14.01 PM

Пункты 2, 3, 4 обязательные

2. Когда пользователь кликает на твите, “переезжайте” (segue) на новый UITableViewController, у которого 4 секции, показывающие “mentions” в твите: images, hashtags, users и urls. Первая секция показывает (одно на строку) любые изображения, прикрепленные к твиту (найдены в переменной media в классе CS193pTwitter.Tweet). Последние 3 секции показывают элементы, описанные в Обязательном пункте  №1 (опять, по одному на строку).

3. Изображения images должны быть показаны в вышеупомянутой таблицы с их нормальным aspect ratio(соотношением сторон) и должны использовать всю щирину Table View (стандартная граница, окружающая изображение, является приемлемой).

4. Каждая секция в таблице “mentions” должна иметь соответствующие заголовки, но если в секции нет никаких элементов, то заголовок не должен быть виден для этой секции.

Добавляем из палитры объектов дополнительный Table View Controller на storyboard. В этой таблице нам будут нужны два прототипа ячеек таблицы: один для того, чтобы поместить туда строку urls, hashtags и users, а другой прототип — для размещения изображений : images.

Screen Shot 2015-07-07 at 8.36.14 AM

Один прототип — он имеет идентификатор Keyword Cell — это обычная табличная ячейка UITableViewCell, имеющая стиль Basic , который позволяет вывести на экран только заголовок Title, то есть разместить строку.
Другой прототип — он имеет идентификатор Image Cell — это ячейка,  настраиваемая пользователем, поэтому задаем ей стиль Custom. Для нее мы создадим специальный класс  ImageTableViewCell, который позволит загрузить в ячейку изображение. Изображение Image View нужно перетянуть из палитры объектов на storyboard в этот Custom прототип ячейки и сделать все необходимые настройки для механизма Autolayout.

Screen Shot 2015-07-07 at 8.55.25 AM

Не забудьте создать специальный класс  ImageTableViewCell для нашей ячейки и установить его в Инспекторе Идентичности (Identity Inspector) для прототипа  Image Cell.

Screen Shot 2015-07-07 at 9.08.53 AM

Но вернемся к нашей таблице и создадим для нее специальный класс MentionsTableViewController, который является subclass UITableViewController и не забудем указать его в Инспекторе Идентичности (Identity Inspector) для нашей таблице на storyboard.

Screen Shot 2015-07-07 at 9.52.08 AM

Давайте подумаем, что является для нашего класс MentionsTableViewController public API. Согласно заданию эта таблица должна отображать меншены для определенного твита, следовательно, сам твит и является public API класса MentionsTableViewController. То есть своего рода Моделью в терминологии паттерна проектирования MVC.

Screen Shot 2015-07-07 at 9.57.19 AM

Имея твит tweet, мы должны извлечь все меншены и отобразить их в таблице. Для отображения данных в таблице в подсказке № 7 Задания 4 нам предлагают создать внутреннюю структуру данных, которая бы собирала данные по секциям (учитывая их похожесть и различия) и очень просто работала бы с  UITableViewDataSource таблицы Table View.
Давайте подробно остановимся на построением этой внутренней структуры данных, потому что простота и ясность решения нашей задачи очень сильно будет зависеть от этой структуры.

Screen Shot 2016-07-16 at 7.57.22 PM

На верхнем уровне будет массив секций mentionSections: [MentionSection], представленных структурой MentionSection, в которой задается тип секции type, который одновременно является и названием секции, и массив меншенов этой секции mentions : [MentionItem].
Нам в одной таблице нужно отобразить не только те меншены, которые представлены строкой, но и изображения, поэтому мы расширим понятие меншена по сравнению с тем, что представлен во фреймворке Twitter, и добавим туда и изображения images. Такой «расширенный» мешен мы будем использовать в качестве элемента Модели для MensionTableViewController и  представим его перечислением enum MentionItem, который включает в себя два варианта:

  • просто ключевое слово Keyword (String) c ассоциированным значением String в виде строки,
  • изображение Image (NSURL, Double) c ассоциированными значениями в виде NSURL (где взять изображение) и  Double ( aspect ratio или соотношение сторон).

Первый вариант подходит для меншенов hashtagsurls и users, а второй — для imagesИмея такую структуру очень просто реализовать методы делегата UITableViewDataSource для таблицы.

Screen Shot 2016-07-16 at 8.24.02 PM

Особого внимания заслуживает последний метод cellForRowAtIndexPath, в котором используются два различных прототипа ячеек: обычный UITableViewCell для меншенов, представленных строкой keyword , и ImageTableViewCell для меншенов, представленных изображением с соответствующим URL url.
Чтобы покончить с методами делегата Table View, примем к рассмотрению подсказки № 19 — 21 Задания 4.

Подсказка №19. Высота строки в вашем новом Сontroller не нуждается в “оценке” как высота строки в “списке твитов”, потому что у вас очень мало строк и производительность не играет решающего значения. Следовательно, вам захочется реализовать метод heightForRowAtIndexPath делегата UITableViewDelegate.
Подсказка №20. Для строк, содержащих изображение ( image), вам придется рассчитать подходящую высоту.Для других строк высоту можно рассчитывать автоматически путем возврата UITableViewAutomaticDimension из метода heightForRowAtIndexPath.
Подсказка №21. Вы можете рассчитать соотношение сторон (aspect ratio) изображения (image) в твите, не прибегая к реальной выборки image из своего url. Смотри класс MediaItem в поставляемом фреймворке Twitter.

Следуя этим рекомендациях получаем код для еще двух методов делегата Table View.

Screen Shot 2016-07-16 at 9.14.00 PM

Теперь осталась самая главная и интересная часть — загрузить нашу таблицу данными, а ведь у нас кроме твита tweet ничего нет в public API, поэтому следующая наша задача — преобразовать  tweet во внутреннюю структуру  mentionSections: [MentionSection], и сделать мы это должны как только кто-то извне установит нам новое значение переменной   tweet.

Screen Shot 2016-07-16 at 9.45.24 PM

Надо сказать, что этот код с точки зрения паттерна конструирования MVC выполняет типичную для Controller задачу — преобразование данных Модели в данные, требуемые View.
Осталось подсоединить вновь созданный Mentions Table View Controller к списку твитов Tweets Table View Controller, и мы сделаем это с помощью push segue от ячейки в таблице Tweets Table View Controller к таблице меншенов.

Screen Shot 2015-07-07 at 12.55.00 PM

Не забывайте указать идентификатор segue, он нам пригодится в коде. Мы задали идентификатор как «Show Mentions» и поместили его в специальную структуру в классе TweetsTableViewController, ОТ которого мы «переезжаем» в таблицу MensionsTableViewController

Screen Shot 2015-07-07 at 1.09.22 PM

Еще нам нужно написать в этом же классе метод prepareForSegue:

Screen Shot 2016-07-16 at 9.54.43 PM

Именно в выделенной выше строке происходит использование public API класса MentionsTableViewController и именно она позволит запустит механизм загрузки таблицы меншенов.
Осталась еще одна и последняя маленькая деталь — это пользовательский класс ImageTableViewCell, обслуживающий прототип ячейки Image Cell, отображающей изображения. Для этого класса  public API будет URL местонахождения изображения в сети — imageURL: NSURL. Нам нужно закачать оттуда данные и отобразить в ячейке. Причем выполнить все это мы должны, не блокируя main queue. Это задача нам уже знакома по приложению Cassini, которое рассматривалось на Лекции 8, поэтому я не буду останавливаться подробно на описании кода.

Screen Shot 2016-07-17 at 7.13.02 PM

Запускаем приложение и смотрим вариант с изображением.

Screen Shot 2016-07-17 at 7.37.13 PM

Смотрим вариант без изображения — эта секция отсутствует.

Screen Shot 2015-07-07 at 1.50.28 PM

Проверим выполнение пункта № 20 в подсказках

Подсказка №24. Замечательной возможностью вашего приложения является (должно быть!) то, что если пользователь захочет увеличить немного масштаб изображения (image) в твите без того, чтобы кликать на нем и “переезжать” на MVC для подробного показа image, пользователь может просто перевернуть прибор в ландшафтный режим. Если все выполнено правильно, то вы получите эту возможность бесплатно (то есть не написав ни строчки кода).

Screen Shot 2016-07-17 at 7.40.08 PM copy

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

5. Если пользователь выбирает какой-то hashtag или user в таблице “mentions”, созданной в вышеприведенном Обязательном пункте № 2,  то вы должны куда-то “переехать”  (segue), чтобы показать результаты поиска в Twitter этого hashtag или user. Это должен быть поиск именно hashtags или  users, а не просто строки с именем hashtag или user (например, поиск  “#stanford”, а не “stanford”). View Controller, куда вы “переедите” (segue), должен работать точно также, как ваш главный View Controller, показывающий твиты  (TweetTableViewController).

6. Если пользователь кликает на “меншене” url в вашем вновь созданном View Controller, вы должны открыть этот url в Safari (смотри раздел “Подсказки”, приведенный ниже, и узнай как это сделать).

Прежде, чем приступать к обязательным пунктам 5 — 7 Задания 4, давайте взглянем на картину в целом:

Screen Shot 2016-07-18 at 3.45.05 PM

В таблице «Меншены» у нас два типа прототипа ячеек:

  • Keyword Cell (для  hashtags, users или urls)
  • Image Cell (для images).

От прототипа Keyword Cell пользователь “переезжаем” (segue) на наш старый  MVC «Выборка твитов«, но с новой строкой поиска, которая соответствует выбранным в таблице «Меншены»  hashtags и users, а для urls  предлагается открыть браузер Safari.
От прототипа Image Cell пользователь “переезжаем” (segue) на новый  MVC «Изображение«, который позволит пользователю прокручивать изображение и изменять его масштаб.
От прототипа ячейки Keyword Cell создаем нормальный “Show” segue на старый TweetTableViewController и задаем идентификатор segue «From Keyword».
Screen Shot 2016-07-18 at 3.57.57 PM
В классе MentionsTableViewController, ОТ которого мы «переезжаем», выполняем метод prepareForSegue

Screen Shot 2016-07-18 at 4.04.25 PM

Однако в случае с url, который также подпадает под прототипа ячейки Keyword Cell, мы должны открыть этот url в Safari, то есть предотвратить срабатывание segue. Это можно сделать с помощью метода

Screen Shot 2016-07-18 at 4.07.56 PM

Для вызова Safari мы использовали новый в iOS 9 API для SFSafariViewController, а для этого придется импортировать SafariServices:

Screen Shot 2016-07-18 at 4.19.08 PM

Код можно найти на Github.

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

Если пользователь кликнет на изображении (image) в вашем вновь созданном View Controller, “переезжайте” на новый MVC, который позволит пользователю прокручивать (scroll) изображение и изменять его масштаб. Когда изображение впервые появляется в MVC, оно должно быть показано в увеличенном масштабе (но со своим нормальным соотношением сторон (aspect ratio)) и так, чтобы занять как можно больше экранного пространства без “белых зазоров” вокруг изображения.

Используя подсказку № 22, копируем ImageViewController из проекта «Cassini» на нашу storyboard и создаем нормальный “Show” segue от прототипа ячейки Image Cell к только что скопированному ImageViewController. Задаем идентификатор segue «Show Image«.

Screen Shot 2016-07-18 at 6.03.44 PM

В классе MentionsTableViewController выполняем метод prepareForSegue

Screen Shot 2016-07-18 at 6.06.15 PM

Согласно подказкам № 22 нам следует добавить в ImageViewController автоматическую “подгонку” (autozooming-to-fit) изображения под полный размер экрана путем изменения масштаба. Что это значит? Давайте посмотрим на рисунок.

Screen Shot 2015-07-08 at 7.20.40 PM

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

  • масштаб по вертикали  = H экрана /H изобр. ≈ 1.2
  • масштаб по горизонтали  = W экрана /W изобр.≈ 0.6

Берем большее значение 1.2 делаем «подгонку» по вертикали. Это означает, что по вертикали скроллинга не будут, скроллить можно по горизонтали, если вы хотите посмотреть все изображение.
Теперь посмотрим другую ситуацию, которая может появиться при ландшафтном режиме просмотра.

Screen Shot 2015-07-08 at 7.49.43 PM

Опять рассчитываем две величины:

масштаб по вертикали  = H экрана /H изобр. ≈ 0.6
масштаб по горизонтали  = W экрана /W изобр.≈ 1.1

Берем большее значение 1.1 делаем «подгонку» по горизонтали.Это означает, что по горизонтали скроллинга не будут, скроллить можно по вертикали, если вы хотите посмотреть все изображение.
Как видно из рисунков, желательно иметь «подгонку» не только по масштабу, но и по центру изображения. Все это обеспечивается методом zoomScaleToFit и переменной autoZoomed

Screen Shot 2015-07-09 at 6.28.25 PM

Переменная autoZoomed устанавливается в true при установке image и тут же запускается режим автоматической «подгонки»

Screen Shot 2015-07-09 at 6.34.38 PM

Удовлетворяя подсказке № 23 Задания 4, запускаем режим автоматической «подгонки» при изменении геометрии прибора, то есть в методе viewDidLayoutSubviews «жизненного цикла» View Controller.

Screen Shot 2015-07-09 at 6.39.10 PM

Переменная  autoZoomed будет сбрасываться, как только пользователь сам начнет выполнять zooming, то есть в методе scrollViewWillBeginZooming делегата ScrollView

Screen Shot 2015-07-09 at 6.43.50 PM

Запускаем приложение, находим твит с image и кликаем на этом image.

Screen Shot 2016-07-18 at 6.20.37 PM

Все прекрасно работает.

Код можно найти на Github.

Продолжение находится в посте Задание 4. CS193P Spring 2016. Smashtag Mentions (клиент Twitter). Решение — обязательные пункты 8 — 10.

Задание 4. CS193P Spring 2016. Smashtag Mentions (клиент Twitter). Решение — обязательные пункты 1- 7.: 10 комментариев

  1. Добрый день!
    Как вызывается в коде метод previousViewController() ?
    Расширение в контролере TweetTableViewController.

    extension UINavigationController {
    //Get previous view controller of the navigation stack
    func previousViewController() -> UIViewController?{
    let lenght = self.viewControllers.count
    let previousViewController: UIViewController? = lenght >= 2 ? self.viewControllers[lenght-2] : nil
    return previousViewController
    }
    }

    • Я так понимаю, это предложение?
      А зачем?
      Eсть прекрасный метод
      navigationController?.popViewControllerAnimated(true)
      который сбрасывает «верхнюю карту» со стэка.

      • В вашем примере с github есть такое расширение. При отладке этот метод вызывается. Я так и не смог понять каким образом.

          • Спасибо. Буду смотреть.Я вижу, что отладчик туда ходит. Но если это extension закомментировать, то будет работать прекрасно, даже не вспомнит о методе previousViewController().
            Так что он нигде не вызывается, это остался какой-то атавизм.
            Пока не знаю, почему туда заходит отладчик. Если что-то прояснится — сообщу. Очень интересно.
            Кстати, если метод переименовать в previousViewController1(), то отладчик не заходит, но все работает.
            Значит что-то с именем.
            Буду смотреть.

  2. Спасибо за ответ!
    Метод срабатывает без вызова, думал упустил что то из теории.
    Спасибо!

  3. Добрый день!
    При переходе на ImageViewController мы грузим из сети те же данные картинки второй раз. Не лучше ли будет открыть переменную image у ImageViewController и передавать целые данные UIImage между контроллерами? Все-таки проект Cassini грузил данные один раз.

    • Да, справедливое замечание, но лучше использовать класс NSCache. Это как NSDictionary (objectForKey и setObject:forKey), но добавляется концепция “стоимости” того, что будет в кэше, с помощью setObject:forKey:cost:. “Стоимостью” изображения мог бы быть размер в kb, например. NSCache будет выбрасывать из кэша элементы в любое время, когда ему захочется, так что вы всегда будете искать нужный NSURL (чтобы найти соответствующее изображение UIImage), использовать его, если вы нашли, или опять загрузить его, если вы не нашли.
      А Модель для класса ImageViewController лучше оставить прежней — imageURL.
      Кстати в последнем дополнительном пункте этого Задания 4 для Collection как раз делается что-то подобное.

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