Задание 5 cs193p Spring 2016 Smashtag Mentions Popularity (клиент Twitter). Решение.

Содержание

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

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

В Задании 5 вы должны еще больше усовершенствовать приложение Smashtag в плане проведения некоторого анализа всех меншенов, полученных в результате поиска в Twitter. Для этого нужно использовать Core Data.

 Основными идеями в этом Задании являются различные способы получения managedObjectContext — либо из UIManagedDocument, либо из AppDelegate, концептуальное и физическое конструирование схемы базы данных в Xcode, применение различных запросов NSFetchRequest c использованием небольшого встроенного языка для форматирования строк, а также различных дескрипторов сортировки. Необходимо свободно пользоваться классом NSFetchedResultsController или усовершенствованным на его основе классом CoreDataTableViewController , созданным Полом Хэгерти специально для этого курса.

Для выполнения Задания 5 нужно посмотреть видео и текстовые материалы Лекции 10 и Лекции 11.
Демонстрационное приложение Smashtag L11 находится на сайте Стэнфорда для Xcode 7 и Swift 2.2. Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 .0 — также на Github

В решении, представленном ниже, используется UIManagedDocument для получения контекста базы данных Core Data.
Код находится на Github для Xcode 7 и Swift 2.2.

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

Решение для дополнительного пункта  4 находится в посте «Задание 5 cs193p Spring 2016 Smashtag Mentions Popularity. Решение дополнительного пункта 4 (удаление старой информации)».

Будем следовать порядку разработки демонстрационного приложения Полом Хэгерти на Лекции 11.

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

В вашей закладке Recent Searches (последние строки поиска), добавьте кнопку Detail Disclosure к каждой строке. При ее нажатии вы должны “переезжать” (segue) на новый MVC с таблицей, которая показывает список всех меншенов с пользователями (users) и хэштегами (hashtags) во всех твитах, когда-либо выбранных с использованием поискового текста (search term) в этой строке таблицы. Предполагается, что меншены должны быть уникальны и нечувствительны к регистру.

Сначала сделаем всю необходимую подготовку на storyboard.

  1. Добавим кнопку Detail Disclosure (с буквой i в кружочке) к каждой строке экранного фрагмента Recents:

Screen Shot 2016-08-11 at 10.53.02 AM

2. Создадим новый MVC типа Table View Controller, как всегда вытянув его из Палитры Объектов. И назовем его Popularity.

3. Создадим пользовательский класс для нового MVC c именем PopularityTableViewController с superclass UITableViewController.

4. Установим пользовательский класс PopularityTableViewController для нового  MVC.

Screen Shot 2016-08-11 at 11.05.48 AM

5. Конфигурируем ячейку в новом MVC Popularity. Это будет стандартная ячейка стиля Subtitle с идентификатором PopoularMensionsCell:

Screen Shot 2016-08-11 at 11.22.59 AM

6. Создаем с помощью CTRL-перетягивания segue от экранного фрагмента Recents к новому MVC Popularity. Это будет Show segue, но он будет идти не непосредственно от строки в таблице, а от кнопки с буквой i в кружочке. Мы  знаем, что от строки можно создавать два segues. Один у нас уже есть. Давайте создадим второй.  Начинаем мы как всегда с CTRL-перетягивания от ячейки, а вот когда мы отпускаем клавишу CTRL, то нам будут предлагаться обычные segue от строки в разделе «Selection Segue» и от  кнопки с буквой i в кружочке в разделе «Accessory Action«.

Screen Shot 2016-08-11 at 11.52.46 AM

Мы выбираем Show в разделе «Accessory Action«.Дадим имя этому segue «ShowPopularMensions» и зафиксируем это в классе RecentsTableViewController:

Screen Shot 2016-08-11 at 12.08.13 PM

Вернемся к классу PopularityTableViewController, обслуживающему наш новый MVC, удалим весь пришедший с шаблоном код и подумаем, что будет Моделью этого MVC.

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

Таблица популярности меншенов во всех твитах, возвращенных при поиске по данной поисковой строке, должна быть отсортирована в порядке популярности от наиболее популярных меншенов в верху таблицы к менее популярным внизу.

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

Если два (или больше) меншенов имеют ту же самую популярность, эти меншены должны появляться в таблице в алфавитном порядке.

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

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

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

Не помещайте в таблицу меншены, которые упоминаются лишь один раз. Хотя, если вновь выбранный твит включает такой меншен, меншен должен немедленно появиться в таблице (потому что тогда он оказывается упомянутым уже больше, чем однократно).

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

Однажды сосчитанные в определенном твите меншены не должны вновь участвовать в подсчете количества упоминаний для того же самого твита, если при выборке этот твит появляется вновь. Другими словами, не должно быть быть дублирования меншенов в твитах, которые выбираются более одного раза.

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

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

Всякий раз, когда мы реализуем новый MVC, самая главная вещь, с которой вы должны начать, и я настаиваю на этом в этом курсе, это Модель данного MVC.  Вы должны понять, что делает этот MVC и какова его Модель.

В нашем случае Popularity MVC берет некоторую поисковую строку в виде меншена типа  #stanford и ищет и подсчитывает все мешены, связанные с выборкой твитов по этой поисковой строке.
Понятно, что очень важная часть этой Модели — mention, то есть строка типа #stanford. Но другая действительно важная часть нашей Модели — это база данных Core Data. Без нее мы не сможем найти меншены, которые появились в твитах, использующих строку этот mention. Так что, другая часть Модели — moc типа NSManagedObjectContext. Он будет Optional, но если он равен nil, то у меня будет пустая таблица.

Screen Shot 2016-08-11 at 1.27.34 PM

При изменении базы данных или поисковой строки мы будем обновлять пользовательский интерфейс с помощью метода update UI().
Прежде чем использовать Core Data, мы должны получить контекст базы данных moc типа NSManagedObjectContext , создать схему базы данных и разместить там данные согласно изобретенной нами схемы.
Все это мы должны делать там, где мы получаем выбранные твиты по поисковой строке, то есть в классе TweetTableViewController, в этом же классе находится поисковая строка в виде переменной searchText.
Далее действуем так, как показано в демонстрационном примере на Лекции 11.
Вот код, с помощью которого мы выполняли поиск новых твитов newTweets и вставляли их в таблицу. Здесь же я добавлю метод updateDatabase и передам ему в качестве аргумента newTweets, чтобы он разместил их в базе данных.

Screen Shot 2016-08-11 at 1.43.46 PM

Для работы с базой данных Core Data нужен контекст moc: NSManagedObjectContext?, который будет являться частью нашей Модели в MVC Tweet Table View Controller, обслуживаемым классом TweetTableViewController:

Screen Shot 2016-08-11 at 2.39.37 PM

Контекст moc  мы получим вторым способом, то есть через UIManagedDocument, но предварительно создадим расширение extension и subclass класса UIManagedDocument, который  назовем MyDocument. Все расширения находятся в файле Database.swift.

Screen Shot 2016-08-11 at 1.56.49 PM

Класс MyDocument создан исключительно для удобства использования базы данных в отладочном режиме. Явное расширение для файла . sqlite позволяет при клике на этот файл подробно просматривать базу данных с помощью любого стороннего приложения, работающего с базой данных SQLight. Я использую приложение Base на Mac.

Screen Shot 2016-08-11 at 2.06.43 PM
Кликаем на файле Twitter.sqlite

Screen Shot 2016-08-14 at 10.42.37 AM

и можем смотреть базу данных, использовать фильтры, запросы и т.д.

Screen Shot 2016-08-14 at 10.46.22 AM

и можем смотреть базу данных, использовать фильтры, запросы и т.д.
Метод contentsForType в subclass MyDocument позволяет нам видеть, когда происходит АВТОСОХРАНЕНИЕ. Метод handleError как понятно из названия обнаруживает конфликт и позволяет его разрешить, в нашем приложении он вряд ли пригодится, но если вы решите использовать ограничения на уникальность (unique constraints), которые появились в iOS 8, то он вам может пригодиться.

Но все это необязательно, можно использовать вместо subclass MyDocument  «чистый» класс UIManagedDocument.

Мы создадим расширениe extension класса UIManagedDocument для того, чтобы разместить там метод useDocument, аргументом которого является замыкание completion. Это замыкание выполняется асинхронно после открытия уже существующего документа document: MyDocument или после создания нового, если он не существует. В этом замыкании становится доступен работоспособный  document: MyDocument и мы можем воспользоваться этим, чтобы получить его managedObjectContext:

Screen Shot 2016-08-11 at 2.32.18 PM

Метод useDocument будет открывать уже существующий документ или создавать новый, если он не существует, или просто использовать документ, если он существует и уже открыт. Подробно об этом говорилось на Лекции 10. Вам не нужно об этом заботиться, вам будет послан уже готовый к использованию document, с которым вы в аргументе-замыкании можете делать все, что хотите, но нас интересует переменная managedObjectContext из документа document.
Установку контекста moc для базы данных выполним в методе «жизненного цикла» viewWillAppear:

Screen Shot 2016-08-11 at 2.43.37 PM

Итак, контекст у нас есть, можем работать с Core Data.
Но для нашей конкретной задачи нужна Модель Данных или схема базы данных.
Я приведу ее сразу в готовом виде и объясню, почему она именно такая:

Screen Shot 2016-08-11 at 2.50.12 PM

У нас в схеме базы данных будет три Сущности: твит TweetM, поисковый терм SearchTerm и меншен Mension. Сущности  Tweet и SearchTerm имеют Взаимосвязь типа «To Many» с обоих сторон, так как понятно, что один и тот же твит может оказаться в выборках для разных поисковых строк. Взаимосвязь terms в Сущности  TweetM, показывает, в каких поисковых темах присутствовал данных твит. Это очень важная взаимосвязь, с ее помощью мы можем контролировать повторно выбранные твиты для определенной строки поиска SearchTerm и не обрабатывать их, если они уже присутствуют в базе данных. Таким образом удается избегать дублирования. 

Атрибуты Сущности Mension диктуются тем, что нам нужно отобразить в таблице популярности меншенов. Сам мешен представлен своими атрибутами keyword и type («Нashtags'» или «Users»), кроме того, каждый меншен строго «маркируется» только одним поисковым термом term, это строго выделяет меншены, относящиеся к определенной поисковой строке term. Только в этом случае мы сможем корректно посчитать требуемое нам количество твитов count, в которых упоминается этот мешен при использовании строки выбора term.

Вы видите, что Сущность Mension никак не связана с Сущностью TweetM, хотя, конечно, экземпляры этой Сущности «рождаются» именно из твитов. Дело в том, что Сущность Mension  содержит агрегатную составляющую count в своих Атрибутах, поэтому с конкретной Сущностью TweetM она никак не может быть связана.

Конечно, мы создает subclasses NSManagedObject для наших Сущностей, как это описывалось на  Лекциях 10 и 11.

Screen Shot 2016-08-11 at 8.54.45 PM

Screen Shot 2016-08-11 at 8.56.13 PM

Screen Shot 2016-08-11 at 8.59.11 PM

Возвращаемся к методу updateDatabase () в классе TweetTableViewController. Мы будем использовать метод tweetWithTwittweInfo класса TweetM, который принимает решение о размещении данных конкретного твита в базе данных.

Screen Shot 2016-08-11 at 3.41.11 PM

Этот метод отличается от похожего метода, используемого в демонстрационном примере на Лекции 11 тем, что здесь присутствует дополнительный параметр andSearchTerm, который представляет строку поиска и который мы обязаны учитывать в нашей задаче. Это class метод для класса TweetM.

Screen Shot 2016-08-11 at 3.51.53 PM

Располагая информацией о твите tweeterInfo, полученной с сервера  Twitter, и текущей поисковой строкой term, первое, что мы делаем, это проверяем наличие твита с заданным tweeterInfo.id, у которого в множестве terms:NSSet содержится заданная поисковая строка.  Для этого мы формируем запрос с предикатом:

Screen Shot 2016-08-11 at 5.34.55 PM

Если такой твит в базе данных есть, то вся информация об этом твите, полученном в результате поиска по строке term, уже внесена в базу данных, и мы не будем его снова обрабатывать, а просто его вернем. Тем самым мы предотвратим дублирование информации в базе данных.

Если же такого твита нет, то это может произойти по двум причинам:  либо твита с заданным tweeterInfo.id вообще нет в базе данных, либо сам твит есть, но он не учтен для заданной поисковой строки term. В этом случае мы должны получить этот твит tweetM из базы данных ( либо существующий, либо вновь созданный ), получить из базы данных поисковую строку currentTerm  ( либо существующую, либо вновь созданную )…

Screen Shot 2016-08-11 at 5.38.00 PM

… и занести в множество terms:NSSet твита  tweetM заданную поисковую строку term … 

Screen Shot 2016-08-11 at 5.42.32 PM

а также записать в базу все его меншены, «маркированные» поисковой строкой  term:

Screen Shot 2016-08-11 at 5.43.37 PM

Мы видим, что у всех классов TweetM, SearchTerm и Mension, соответствующих нашим Сущностям и являющихся subclasses класса NSManagedObject, существуют однотипные методы возврата экземпляра этого класса, выбранного из базы данных по заданным значениям «ключевых» Атрибутов. Если этот экземпляр существует, то он возвращается, если нет — он создается заново и также возвращается. 

Screen Shot 2016-08-11 at 8.20.50 PM

Screen Shot 2016-08-11 at 8.32.05 PM

Screen Shot 2016-08-11 at 8.37.11 PM

Это class методы, так как они возвращают или создают определенный экземпляр класса по «ключевым» Атрибутам, и они подробно обсуждаются в демонстрационном примере на Лекции 11.
Эти методы очень удобно использовать для специфической обработки информации, например, для извлечения меншенов из данных о твите twitterInfo, полученных с сервера Twitter и представленных фреймворком Twitter

Screen Shot 2016-08-11 at 8.43.57 PM

… или для принятия решения о записи информации твита, «маркированного» определенной поисковой строкой term:

Screen Shot 2016-08-11 at 8.50.24 PM

Также как и в демонстрационном примере Лекции 11, мы создадим метод печати краткой информации о содержимом базы данных printDatabaseStatistics():

Screen Shot 2016-08-11 at 9.10.52 PM

С учетом печати метод обновления базы данных выглядит следующим образом:

Screen Shot 2016-08-11 at 9.13.28 PM

Итак, наши данные записаны в базу данных и сохранены.
Мы можем перейти ко второй части нашего приложения, а именно к наполнению данными нашего нового MVC.

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

Контент вашего нового MVC должен управляться NSFetchedResultsController (вы можете использовать CoreDataTableViewController в качестве superclass для Controller вашего нового MVC, если хотите).

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

Не наразрушайте функционирования остальной части приложения Smashtag. Нет требований преобразования уже существующих MVC из предыдущего Задания, в MVC,  использующие Core Data, только новый MVC. Конечно, вам придется модифицировать уже существующий код для запоминания данных, которые вы собираетесь использовать в новом MVC.

Класс PopularityTableViewController, обслуживающий наш новый MVC, делаем subclass класса CoreDataTableViewController, который скачиваем отсюда. Об этом классе подробно рассказывается на Лекции 11.

Screen Shot 2016-08-12 at 12.13.46 PM

Этот MVC будет работать только с данными из Core Data.  Поэтому нам нужен контекст moc базы данных, который мы получим также, как и ранее из UIManagedDocument в методе «жизненного цикла» viewWillAppear:

Screen Shot 2016-08-12 at 12.21.05 PM

Теперь вернемся к методу updateUI(), в котором мы должны устанавить наш fetchResultsController, унаследованный от  CoreDataTableViewController. Мы создадим новый NSFetchedResultsController с помощью инициализатора:

Screen Shot 2016-08-12 at 12.26.29 PM

Мы выбираем мешены, относящиеся к поисковой строке mension, которая является Моделью данного MVC. Сортируем результаты сначала по типу мешена typeHashtags» или «Users«), затем по количеству твитов count, в которых он упоминался, и по самому меншену keyword. В нашей таблице будут Секции, организованные по типу меншена  type.
Мне осталось реализовать метод cellForRowAtIndexPath, и я смогу запустить приложение. Для этого я должен установить “reuseIdentifier и сконфигурировать ячейку cell:

Screen Shot 2016-08-12 at 12.42.57 PM

Запускаем приложение.

Screen Shot 2016-08-12 at 12.55.53 PM

Screen Shot 2016-08-12 at 12.58.51 PM

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

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

В любое время вы НЕ должны блокировать main thread в вашем приложении. Вы можете предположить, что любые вызовы Core Data не займут слишком много времени, чтобы блокировать main thread (к счастью, вы сконструировали схему базы данных таким образом, что это действительно справедливо даже при загрузке большого числа твитов).

Код полностью настроен на работу приложения в многопоточной среде благодаря «обертывания» всех действий с базой данных в методы performBlock и  performBlockAndWait и любой момент может быть переведен на moc, функционирующий не на main queue.

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

Ваше приложение должно работать правильно как в портретном, так и в ландшафтном режимах на любом iPhone (это приложение только для iPhone).

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

Вы должны получить приложение, которое работает на реальном приборе, а не только на Симуляторе.

Все это выполнено.

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

Используйте UIManagedDocument для запоминания всей информации в Core Data. Будьте внимательны и правильно используйте асинхронность.

Выполнено.

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

Разделите вашу таблицу с популярностью меншенов на две Секции: хэштеги (Hashtags) и пользователи (Users). И опять, при правильной схеме базы данных, этот пункт может быть реализован несколькими (может быть, двумя) строками кода.

Выполнено.

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

Загрузка большого количества данных путем запроса существующего в базе данных экземпляра Сущности, а затем вставки его , если этот экземпляр не найден, снова и снова, по одному ( как мы делали на лекции), может оказаться очень не производительной. Ваше приложение можно улучшить и сделать его более эффективным по производительности, если проверять одним запросом существование в базе данных целой кучи объектов, которые мы хотим разместить, а затем создавать только те, которые там отсутствуют. Предикат с оператором IN может понадобиться в этом случае.

Создадим в классе TweetM метод, который будет обрабатывать сразу весь массив новых твитов [Twitter.Tweet].

Screen Shot 2016-08-14 at 11.13.19 AM

Идея этого метода состоит в том, что мы сначала определяем множество newsSet, состоящее из id новых твитов, полученных с сервера Twitter, затем формируем запрос с предикатом, содержащим оператор IN, для поиска в базе данных сразу всех твитов, входящих в множество  newsSet, и учтенных там с поисковой строкой term:

Screen Shot 2016-08-13 at 9.07.56 PM

В результате мы получаем массив твитов tweets, уже содержащихся в базе данных, который мы превращает в множество атрибутов unique для этих твитов:

Screen Shot 2016-08-14 at 11.20.53 AM

А затем вычитаем из множества newsSet id новых твитов множество uniquesSet твитов, находящихся в базе данных, и, таким образом получаем уточненное множество newsSet новых твитов, которых еще нет в базе данных и которые нужно записать:

Screen Shot 2016-08-14 at 11.25.45 AM

Далее мы записываем эти «действительно новые» твиты  в базу данных по одному:

Screen Shot 2016-08-14 at 11.27.48 AM

Естественно в методе updateDatabase () в классе TweetTableViewController мы будем использовать более эффективный метод tweetWithTwittweInfo класса TweetM, а старый для сравнения я оставлю закомментированным:

Screen Shot 2016-08-13 at 9.36.57 PM

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

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

Вам не требуется нигде что-либо уничтожать из базы данных, тем не менее, нам необходима информация лишь о наиболее поздних поисках в Twitter, так что со временем у нас напрасно занимается большой объем дискового пространства. Заставьте ваше приложение удалять из базы данных объекты, которые больше не представляют интереса (то есть доступ к этим данным не будет осуществляться в вашем UI) с тем, чтобы поддерживать регулируемый размер базы данных. Вы должны самостоятельно решить, когда наступает подходящее время для удаления уже неиспользуемых данных.

Решение для дополнительного пункта  4 находится в посте «Задание 5 cs193p Spring 2016 Smashtag Mentions Popularity. Решение дополнительного пункта 4 (удаление старой информации)».