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

Содержание

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

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

iOS 10 Задания


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

Основой для решения Задания 4 является демонстрационный пример «Smashtag L9«, код которого доступен как на  iTunes название “Lecture 9 Demo Code: Smashtag«, так и на Github.

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

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

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

Задание 4 iOS 10.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, скаченный с 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

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

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

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

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

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

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

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

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

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

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

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

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

В классе для прототипа ячейки TweetTableViewCell загружается изображение профайла tweetProfileImageView из Интернета, которое должно выполняться за пределами man queue, поэтому используем все знания о многопоточности, полученные на Лекции 8 и напишем нужный код:

Отметим важность строки:

 if profileImageURL == tweet.user.profileImageURL,

Она позволяет предотвратить обработку данных, пришедших из сети и потерявших актуальность. Дело в том, что cells таблицы UITableView создаются только для видимых ячеек, и затем они используются повторно по мере того, как данные приходят на экран и уходят с экрана. Так что при скроллинге нашей таблицы твитов, данные о миниатюре могут подгрузиться, а сама ячейка уже «ушла» с экрана и нет смысла размещать ее на UI.
Эта функция используется при обновлении пользовательского интерфейса ячейки:

 

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

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

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

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

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

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

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

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

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

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

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

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

Первый вариант подходит для меншенов hashtagsurls и users, а второй — для images

Мы должны уметь преобразовывать  public API, которым у нас является твит tweetво внутреннюю структуру  [MentionSection], и мы сделаем это с помощью private функции initMensionSections (from tweet:), у которой на входе  tweet, а на выходе — массив  [MentionSection]:

Имея такую внутреннюю структуру очень просто реализовать методы делегата UITableViewDataSource для таблицы.

Особого внимания заслуживает последний метод cellForRowAt, в котором используются два различных прототипа ячеек: обычный UITableViewCell для меншенов, представленных строкой keyword, и ImageTableViewCell для меншенов, представленных изображением с соответствующим URL url.

Теперь осталась самая главная часть — загрузить нашу таблицу данными, как только кто-то извне установит нам новое значение переменной  tweet:

Надо сказать, что этот код с точки зрения паттерна конструирования MVC  выполняет типичную для Controller задачу — преобразование данных Модели в данные, требуемые View.

Чтобы покончить с методами делегата Table View, примем к рассмотрению подсказки № 19 — 21 Задания 4.

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

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

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

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

Еще нам нужно написать в классе  TweetsTableViewController метод prepare (for segue:, sender:):

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

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

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

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

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

Пункты 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, давайте взглянем на картину в целом:

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

  • 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».

В классе MentionsTableViewController, ОТ которого мы «переезжаем», выполняем метод prepare( for segue:, sender:)

Однако в случае с url, который также подпадает под прототип ячейки Keyword Cell, мы должны открыть этот url в Safari.Для этого мы воспользуемся подсказкой № 17:

Если у вас есть URL с именем url, вы можете открыть его в Safari следующим образом: UIApplication.sharedApplication().openURL(url). В iOS 10, это изменилось на open(url), но если вы используете более новую версию, то вы захотите защитить себя с помощью  if #available(iOS 10.0, *). Это может дать вам хороший шанс прочитать о  #available!

Но нам необходимо также предотвратить срабатывание segue. Это можно сделать с помощью метода shouldPerformSegue:

В результате кликая на URL в нашем MVC меншенов, мы открываем этот URL в а Safari, а затем возвращаемся  в наше приложение с помощью маленькой возвратной кнопки в левом верхнем углу:

Начиная с iOS 9 для вызова Safari появился новый API для вызова непосредственно SFSafariViewController

… при использовании которого придется импортировать SafariServices:

Screen Shot 2016-07-18 at 4.19.08 PM

В результате получается похожая картина, но возвратной кнопкой будет кнопка  Done в том же левом верхнем углу:

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

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

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

В классе MentionsTableViewController выполняем метод  prepare( for segue:, sender:):

Так как у нас уже есть изображение в ячейке ImageViewCell, то можно незначительно изменить ImageViewController, полученный из приложения Cassini, так, чтобы его public API был не только imageURL, но и просто image. Для этого достаточно сделать image НЕ private:

Тогда в методе prepare( for segue:, sender:) можно использовать не URL cell.imageURL, а уже полученное изображение из ячейки cell.tweetImage.image:

 

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

Для обязательного пункта, в котором пользователь может кликнуть на изображении (image) чтобы начать перемещать его с помощью жеста panning и масштабировать в новом MVC, вы можете практически полностью использовать код из демонстрационного приложения Cassini. Однако вам придется добавить в 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

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

Удовлетворяя подсказке № 23 Задания 4:

Было бы очень круто сделать так, чтобы автоматическая “подгонка” (autozooming-to-fit) работала бы и при изменении геометрии топового view этого MVC до тех пор,  пока пользователь не стал бы явно менять масштаб с помощью жеста pinching (есть метод делегата, который позволяет обнаружить явное изменение масштаба пользователем). Этот способ выполнит автоматическую “подгонку” изображения при повороте устройства.

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

Screen Shot 2015-07-09 at 6.39.10 PM

Переменная  autoZoomed будет сбрасываться, как только пользователь сам начнет выполнять zooming, то есть в методе scrollViewWillBeginZooming делегата ScrollView. Все методы делегата UIScrollViewDelegate  оформлены в extension класса ImageViewCintroller, как это сделано в Лекции 8:

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

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

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

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

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

  1. Здравствуйте. Спасибо за материал. Вопрос:
    Зачем в ImageViewController использовать модель imageURL и повторять загрузку данных через сеть, если у нас уже есть готовое загруженное изображение, от которого мы и переходим в новый контроллер? Может, просто передавать image в методе prepareForSegue? Единственное, что придется изменить автозум, потому что при задании image в таком случае геометрии еще нет и вызывать метод придется позже.

    • Наверно, можно сразу устанавливать свойство image, которое можно также сделать public. Вы пробовали это делать? По-моему никаких сложностей не должно быть. Если у вас все получится — пришлите ссылку на ваш код.
      Да, все работает:
      делаете image public в ImageViewController:

      var image: UIImage? {

      и в prepare в MensionsTableViewController используете другую строку:
      // ivc.imageURL = cell.imageUrl
      ivc.image = cell.tweetImage.image
      И автозум прекрасно работает.
      Наверно, так действительно рациональнее.

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