Задание 6 Stanford CS 193P Fall 2017. Галерея изображений Image Gallery с постоянным хранением (persistent). Решение.

Содержание

Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Programming: Project 6: Persistent Image Gallery″. На русском языке вы можете скачать Задание 6 здесь: «Задание VI: Галерея изображений Image Gallery с постоянным хранением (persistent)»

Для решения Задания 6 необходимо ознакомиться с Лекцией 7, Лекцией 11Лекцией 12 , Лекции 13 и Лекцией 14.

Цель этого Задания 6 — понять  работу FileManager, URL, Codable, UIDocument и UIDocumentBrowserViewController и изучить, как пользоваться iOS API полностью самостоятельно по документации.

Это Задание использует код, который вы создали в Задании 5, но, возможно, вы захотите начать новый Xcode проект “с нуля” (так что вы сможете использовать Document Base App шаблон).

Мое решение обязательных и допонительных пунктов Задания 6 находится на Github  для iOS 11 и на Github для iOS 12:

ImageGallery_6_Requied_OLD — сохранение Модели в файловой системе без UIDocumentBrowserViewController

ImageGallery_6_Requed_Browser— подключение UIDocumentBrowserViewController, но пока присутствует кнопка «Sav

ImageGallery_6_Requed_Browser_No_Save_Button — работает UIDocumentBrowserViewController, но НЕТ кнопки «Save», используется URLCache,

ImageGallery_6_Requed_Browser_No_Save_Button_LocalImage — присутствует UIDocumentBrowserViewController, НЕТ кнопки «Save«, используется URLCache, сохранение изображений, для которых нет URL, производится в локальной файловой системе, документ имеет свой  UTI .imagegallery.

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

  1. Документы Image Gallery в вашем приложении теперь должны сохраняться постоянно.
  2. Вы можете полностью убрать поддержку таблицы UITableView, добавленную на прошлой неделе.

Приложение Image Gallery — это как раз такое приложение, которое естественно хочется превратить в приложение, основанное на документах, то есть Document based app. Потому что с помощью Image Gallery вы можете создавать тематические живописные Галереи Изображений, и определенно вы захотите их сохранить.

Что касается нашего основного MVC ImageCollectionViewController, то у него уже есть Модель ImageGallery:

, где ImageGallery — это класс:

В Задании 6 мы не будем использовать таблицу UITableView для поддержки множества Галерей с разными именами, поэтому мы можем сделать ImageGallery структурой struct и убрать оттуда имя name :

… , тем самым упростив Модель нашего  MVC ImageCollectionViewController для показа коллекции изображений :

Итак, у нас есть MVC, чью Модель мы можем устанавливать и это заставляет работать наш UI. Теперь все, что нам нужно сделать, это постоянно хранить нашу Модель ImageGallery на диске, то есть сделать ее persistent.

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

Используйте Codable для представления вашего документа в JSON или Property List формате.

Мы можем сделать Модель ImageGallery  persistent, если ТИП ImageGallery станет Codable. Тогда мы сможем превратить Модель ImageGallery  в JSON. Затем мы используем JSON формат нашего файла для постоянного хранения (persistent). Давайте вернемся в структуру ImageGallery и заставим генерировать ее JSON. И сделаем мы это, просто подтвердив протокол Codable для структуры ImageGallery. Затем мы перекомпилируем код и посмотрим, есть ли с этим какие-то проблемы и стала ли структура struct ImageGallery Codable или нет.

Компилятор “говорит” нам, что “Нет”, структура struct ImageGallery не стала Codable, потому что она не реализует протоколы Decodable и Encodable, которые являются двумя протоколами этого протокола Codable.

Почему нет? Ведь ТИП URLСodable, массив Array Сodable, но ТИП ImageModel не является Codable. Однако, нет проблем, ставим у него :Codable и опять перекомпилируем код:

Вы видите, что ошибки ушли, и мы так просто сделали структуру struct ImageGallery  CodableНовый способ архивирования Codable очень легко использовать, так как для 90% всех этих структур компилятор делает все вместо вас.

Как насчет JSON? Как нам получить JSON версию ImageGallery?

Я собираюсь добавить вычисляемую переменную var с именем json к моей структуре ImageGallery. Она возвращает Optional Data?, вполне возможно, что нам не удастся преобразовать себя в JSON:

Но я на 100% уверена, что нам удастся превратить структуру ImageGallery в JSON, но использовала Optional Data? просто для ясности кода. Я создам переменную json путем возвращения результата попытки преобразования себя self с помощью JSONEncoder.encode():

И это все. Будут возвращаться либо данные Data как результат преобразования self в формат JSON, либо nil, если не удастся выполнить это преобразование, но этого практичнски никогда не происходит.

Давайте сделаем небольшое добавление на наш UI в Image Gallery Collection View Controller, чтобы иметь кнопку, которую я собираюсь назвать “Save”. Когда я нажимаю на кнопку “Save”, то происходит сохранение этого json, которое является нашей галереей изображений ImageGallery, на диск в формате JSON.

Но сначала мы просто распечатаем  json на консоли, чтобы посмотреть на него.

Идем на storyboard и попробуем добавить кнопку “Save”.

Но если вы взгляните на storyboard, то увидите там много не нужных вещей. Убираем со storyboard таблицу Table View с редактируемыми именами Галерей Изображений. Мы собираемся существенно все это изменить, потому что будем использовать прекрасный компонент, связанный с приложением Files.

Так что я удаляю все ненужное, избавляюсь от всего, кроме Image Gallery Collection View Controller, обрамленного Navigation Controller, и Image View Controller:

 Я хочу на навигационной панеле UI Image Gallery Collection View Controller разместить кнопку “Save”. Идем в Палитру Объектов и ищем кнопку Bar Button, мы всегда используем кнопки Bar Button, а не регулярные кнопки UIButton,на таких навигационных панелях, .

Я инспектирую мою кнопку Bar Button и делаю ее стандартной кнопкой “Save”.

Давайте подсоединим эту кнопку “Save” к Target / Action в нашем Controller. Выполняем CTRL-перетягивание:

Это должен быть Action, я назову его save,  и это будет кнопка UIBarButtonItem:

Возвращаемся на полный экран к нашему Action save

Это save, и я просто хочу распечатать здесь мою Модель как JSON на консоли.  Для того, чтобы это сделать, я получу json из моей Модели imageGallery, если она установлена:

Теперь у меня есть json, но это Data?, а я хочу распечатать JSON как строку String. Когда у вас есть данные Data, а вы хотите распечатать их как строку String, то нужно сообщить системе, какая у данных кодировка. Это ASCII кодировка? Или это некоторый сорт Unicode кодировки? У формата JSON всегда UTF-8 кодировка, что означает Unicode Transformation Format, 8-bit («формат преобразования Юникода, 8-бит»).

Мы используем конструктор String, который берет json и сообщает о том, что будет кодировка .utf8. Хотя там множество кодировок типа .isoLatin1, .shiftJIS, .ascii 

… но у нас будет кодировка .utf8. Если мы смогли получить jsonString, то мы просто его распечатаем print (jsonString):

Итак, у нас есть кнопка “Save, которая распечатает нам Модель в виде JSON. Заметим, что кнопка “Save” — это левая кнопка на навигационной панели, раньше там была кнопка  splitViewController?.displayModeButtonItem, позволяющая в любой момент времени вернуть на экран таблицу с именами Галерей Изображений. Сейчас у нас нет ни Split View Controller, ни таблицы Table View, и эта кнопка больше не нужна:

Давайте запустим приложение и дадим ему шанс попробовать сохранить и распечатать сформированную нами коллекцию изображений, предварительно не забыв выбрать Navigation Controller “точкой входа” приложения. Для этого идем на storyboard и устанавливаем Navigation Controller свойство is initial View Controller:

Приложение запустилось и мы будем создавать нашу  коллекцию изображений imageGallery на тему «Осень»:

Наполняем нашу Галерею изображениями и кликаем на кнопке «Save»:

Смотрите! В нашем json мы получили “url”, который содержит очень очень длинный путь к Google изображениям,  и “aspectRatio для каждого из 6 изображений «Осень». Мы можем продолжать наполнять нашу Галерею и, нажав в конце кнопку “Save”, получить  json для всех изображений images:

Такие очень небольшие усилия — просто сделать Модель Codable — и мы преобразовали ее так, что можем сохранить ее в JSON формате.

Теперь добавим код для файловой системы, чтобы записать json в файл на диск. Это будет нашим следующим шагом. Возвращаемся в наш Controller, и вместо того, чтобы печатать json давайте запишем его на диск.

Переменная json имеет ТИП Data.

Я могу записать json на диск с помощью кода:

Здесь мы должны решить пару вопросов:

  • Первый — мы должны определить url того места, куда мы хотим записать наш json.
  • И второй вопрос — этот метод выбрасывает throws ошибку.

Мы должны решить обе эти проблемы.

Сначала поговорим о том, в какой url мы будем писать (write) наш json.

Каждый раз, когда мы говорим о файловой системе, что является шагом №1, который вы должны сделать для создания вашего url?

Вы должны найти в “песочнице” (Sandbox) директорию, с которой вы хотите начать. Это нужно делать ВСЕГДА. В 100% случаев, когда вы собираетесь что-то писать в файловую систему или что-то читать из файловой системы. Вы должны определиться с какой директории в “песочнице” (Sandbox) вы начнете. У нас будет документ, так что мы будем размещать его в Document Directory. Вот код, в котором мы ищем нужную директорию в “песочнице” (Sandbox).

Мы используем метод url файл-менеджера FileManager.default. Я ищу .documentDirectory и в iOS всегда использую .userDomainMask. Мы не замещаем никакие файлы в этой директории, и я совсем об этом не беспокоюсь, следовательно, appriateFor: nil, и мы хотим создать Document Directory, если ее нет в “песочнице” (Sandbox), следовательно, create: true.

Затем я добавляю имя моего файла “Untitled.json” к URL для Document Directory, именно так я собираюсь назвать мой файл.

Итак у меня есть URL моего файла url, и я могу писать в него с помощью метода write:

Но метод write выбрасывает throws ошибки, и я должна иметь дело с этими ошибками. Это значит, что перед вызовом метода write необходимо поставить try, и я действительно собираюсь “ловить” ошибки с помощью catch let error. В случае ошибки я распечатаю ошибку, чтобы бы мы могли видеть, что она собой представляет.

Если ошибки не произошло, то я напечатаю сообщение об успешном сохранении документа.

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

Давайте попробуем. Одна особенность этого приложения состоит в том, что каждый раз, когда мы стартуем это приложение, мы начинаем ВСЕГДА с пустой Галереи, так что нам нужно заново размещать все изображения для этой Галереи.

Я кликаю на кнопке “Save”, и мы видим на консоле saved successfully !!

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

И мы можем это сделать и увидеть, сохранен ли этот документ. Потому что у нас в iOS 11 есть ПРИЛОЖЕНИЕ Files, которое позволяет нам взглянуть на документы тех приложений, которые там выбираются. Ключевым моментом здесь являются приложения, которые видны в приложении Files и их документы, находящиеся в Document Directory, которые могут выбираться пользователем.

Для того, чтобы сделать “видимым” ваше приложение и его файлы в приложении Files, необходимо добавить строчку в ваш info.plist, о котором было сказано на Лекции 14. Она скажет о том, что мы являемся приложением, в Document Directory которого система может заглянуть и увидеть там наш документ.

Идем в наш info.plist, и там я сделаю то же самое, что мы делали, когда добавляли новую строку (Add Row), давая доступ к небезопасному http:// …URLs.

Мы добавляем новую строку и это будет строка Supports Document Browser:

Я собираюсь установить значение YES этому ключу.

Эта строка скажет системе, что она может заглядывать в мою директорию Document Directory и видеть там мои документы.

Я должна заново запустить мое приложение на iPad, чтобы передать этот новый info.plist нашему приложению на iPad. Но мне не нужно будет заново создавать наш документ “Untitled.json”, он уже там есть.

Я выполняю жест Swipe Up снизу вверх и появляется Dock, на котором я могу выбрать любое из этих приложений. И я кликаю на приложении Files.

Это приложение Files, и вы видите здесь его UI для работы с файлами.

Этот UI во многом похож на тот UI, который будет в нашем приложении.

Вы видите здесь прекрасные папки, которые можно организовать самым разным способом: по имени name, по дате date, по размеру size, по tags

Вы можете разместить вещи на iCloud Drive, вы можете разместить вещи на вашем iPad.

Я кликаю на On My iPad и вы видите там единственное приложение — ImageGallery. В данный момент только оно имеет директорию Document Directory с документами, файлы которых разрешается просматривать.

Это мое приложение.  Если я кликну на этой папке, то увижу мой документ “Untitled.json”.

И я даже могу кликнуть на нем и посмотреть его.

Нам показывают что-то типа JSON, видимо для показа используется некоторое другое приложение, показывающее стандартные файлы *.json, а вовсе не наше приложение и это смотрится очень странно.

Почему так?

Потому что мы не то приложение, которое показывает *.json файлы, наше приложение показывает *.imagegallery файлы, но приложение Files даже не знает о том, что мы то самое приложение, которое открывает подобные файлы. Мы можем это исправить чуть позже.  Для этого нам нужно будет определить новый ТИП файлов *.imagegallery . И тогда приложение Files будет знать, что такие файлы открывает наше приложение ImageGallery. Но сейчас наши документы имеют расширение *.json, и приложение Files “говорит”: “Хорошо, я буду использовать то приложение, которое открывает файлы с расширение *.json.”

Но тем не менее посмотрите, нам представлена правильная информация. У меня есть в файле “url”, у меня есть “aspect Ratio” для всех изображений нашей Галереи «Осень». Здорово! Теперь, по крайней мере, мы точно знаем, что наша Галерея сохранилась.

Давайте перейдем к следующему шагу, который заключается в чтении документа. Давайте заставим наше приложение каждый раз при появлении на экране искать файл “Untitled.json” и загружать его. Теперь при каждом запуске нашего приложения наша Галерея Изображений не будет пустой, будет загружаться последняя редакция нашей Галереи, над которым мы работали в прошлый раз и которая запомнилась в документе “Untitled.json”.

Вернемся к нашему Controller. Это наш  ImageGalleryCollectionViewController, и мы будем загружать документ в методе viewWillAppear.

Что нам необходимо сделать, чтобы наш документ появился на экране? Все очень просто. Нам необходимо получить это URL url для файла “Untitled.json”, затем посмотреть на его данные Data и превратить их обратно в Модель ImageGallery нашего Controller:

 Конечно, мне нужен url для файла “Untitled.json”, и я собираюсь сделать то же самое, что я делала перед этим:

Как и в прошлый раз в методе save, в методе viewWillAppear я получила Document Directory и добавила имя файла “Untitled.json”. Это в точности то, что было в прошлый раз, нет никакого отличия, потому что мне нужен в точности тот же самый url. Но дальше для получения данных jsonData я использую метод Data (contentsOf:), и мы получаем содержимое файла с URL url, которым является наш “Untitled.json” файл в Document Directory. Вот что собой представляет этот url.

Если мне удалось получить данные  jsonData из этого файла, мне необходимо создать из них экземпляр ImageGallery и установить это в качестве моей Модели imageGallery.

Итак, imageGallery — моя Модель, и я устанавливаю ее в ImageGallery ( json: jsonData), которую создаю из некоторых данных jsonData.

Правда у нас пока НЕТ ИНИЦИАЛИЗАТОРА для ImageGallery, который создает ImageGallery из JSON данных Data. Но мы действительно можем очень легко его написать. Возвращаемся в ImageGallery. Инициализатор похож на то, что мы писали для получения json, он будет “падающим” инициализатором (failable), он берет в качестве аргумента json, который является Data. Если он не сможет провести инициализацию, то он “падает” и возвращает nil.

Я просто хочу получить новое значение newValue с помощью декодера JSONDecoder, пытаясь раскодировать данные json, которые передаются в мой инициализатор. Во что я пытаюсь раскодировать Data? Я пытаюсь раскодировать данные в объект ImageGallery.self.

Если мне удалось это сделать, то я получаю новый ImageGallery объект newValue, и я могу просто написать:

self = newValue

Я просто пытаюсь инициализировать себя self, но если моя попытка заканчивается неудачей, то я возвращаю nil, так как моя инициализация “провалилась”.

Надо сказать, что здесь у нас намного больше причин “провалиться” (fail), потому что вполне возможно, что JSON данные json могут быть испорчены или указан неверный *.json файл или пустой *.json файл, все это может привести к “падению” (fail) инициализатора.

Теперь наш код работает:

Но нам сообщат, что мы не можем привлекать для инициализации Модели imageGallery инициализатор без аргументов, так как его нет у структуры struct ImageGallery:

Поэтому мы должны его создать для структуры struct ImageGallery:

Этот инициализатор дает возможность получить Галерею с пустой коллекцией изображений, и наша предыдущая ошибка исчезнет, но появится ошибка в методе viewWillAppear при инициализации Галереи imageGallery из данных json, так как это «падающий» иниуиализатор и он может вернуть nil:

Решение очень простое: если нам не удалось получить Галерею imageGallery из данных json, то мы создаем Галерею imageGallery с пустой коллекцией изображений с помощью инициализатора init() без параметров:

Мы реализовали чтение JSON файла, так что теперь, если мы уйдем из нашего приложения и вернемся опять в наше приложение Image Gallery, то документ уже будет на экране.

Давайте запустим приложение и оно покажет нам предыдущую Галерею Изображений «Осень«.

Да, это она, взгляните на нее, у нас уже выбран файл “Untitled.json” и если я добавлю еще пару изображений к коллекции и кликну на кнопке “Save”…

… а затем выйду из нашего приложения и запущу его заново, то увижу уже модифицированную Галерею изображений :

Теперь мы умеем сохранять нашу Галерею Изображений с помощью кнопки “Save” и загружать в методе viewWillAppear. Это здорово! Мы сильно продвинулись.

Но пришло время выйти на следующий уровень, который даст нам возможность воспользоваться UI по типу приложения Files. Для того, чтобы это сделать более легким способом и в тоже время упростить наш код, нам нужно использовать UIDocument. UIDocument инкапсулирует документ и позволяет с ним общаться совершенно замечательным способом с помощью очень удобного API. Вот что нам необходимо сделать.

Мы могли бы все это сделать в этом приложении. Все, что нам необходимо сделать, это создать subclass класса UIDocument и реализовать два метода, о которых говорилось на Лекции 14.

Но именно в этом месте выполнения Задания 6 мы сделаем следующий шаг наверх и используем шаблон. Мы собираемся следовать шаблону Document base app для создания основанного на документах приложения.

Поэтому я останавливаю дальнейшую разработку моего существующего приложения и переименовываю его в ImageGallery OLD. Код находится на Github   для iOS 11 и на Github для iOS 12 в папке ImageGallery_6_Requied_OLD.

Затем я возвращаюсь в Xcode и создам новый проект:

Для этого проекта я собираюсь использовать шаблон Document base app для создания основанного на документах приложения.

Я хочу, чтобы он по-прежнему назывался ImageGallery, так как по существу, это то же самое приложение, но построенное на основе этого шаблона.

Я хочу, чтобы оно размещалось в том же самом месте, где и все мои остальные приложения.

Давайте быстро взглянем на то, что нам создал этот шаблон. Это нормальное Xcode приложение и, конечно, в нем есть файлы LaunchScreen, Assets и AppDelegate, все эти 3 вещи я хочу разместить в папке Supporting File, как я делаю всегда:

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

Это метод open inputURL. Он посылается вашему приложению другими приложениями типа Files, которые говорят : “Я хочу открыть один из твоих документов”. Но он будет вызываться, если у вас только ImageGallery документ, а не JSON документ, так как мы не являемся открывающим JSON документы приложением. Но открытие вашего приложения другим приложением происходит именно благодаря этому методу.

Итак, у нас есть папка Supporting Files. Что еще у нас здесь есть?

У нас есть subclass класса UIDocument, который называется Document.

В итоге этот subclass класса UIDocument должен называется ImageGalleryDocument:

Мы также можем переименовать файл в ImageGalleryDocument.swift.

У класса ImageGalleryDocument есть две функции.

Одна из них, contents, преобразует нашу Модель в данные Data, а вторая — load, преобразует данные Data назад в Модель, то есть туда и обратно. Это все, что нам нужно реализовать, чтобы заставить работать этот документ. Мы поработаем немного над этими методами.

Затем у нас есть DocumentViewController, это в общих чертах напоминает нам наш ImageGalleryCollectionViewController , но это просто “заглушка”.

Конечно, у нас есть свой ImageGalleryCollectionViewController и DocumentViewController нам не нужен. Мы его удаляем.

У нас еще есть DocumentBrowserViewController. Это subclass класса UIDocumentBrowserViewController. У него есть метод viewDidLoad, в котором, как говорилось на Лекции 14, мы конфигурируем наш браузер документов.

Есть место, где мы вызываем importHandler для копирования шаблона нового документа.

Есть и другие методы, которых мы даже не будем касаться, они просто вызываются тогда, когда приходит время открыть документ.  И затем в самом низу есть метод presentDocument

… большую часть которого я удаляю:

Это внутренний метод, который вызывается из всех других методов и он “говорит” : ”Представьте ваш MVC для документа с этим URL” Мы его будем реализовывать его в ближайшее время.

Затем, конечно, мы получаем нашу storyboard . Давайте сделаем так, чтобы storyboard отображалась как iPad, это более привычно для нас форма, хотя, между прочим, наше приложение может также работать и на iPhone:

На нашей storyboard находятся два View Controllers. Левый, Document Browser View Controller, это и есть та самая вещь, которая выглядит как приложение Files. И заметьте, именно она имеет точку входа в приложение. При старте приложения мы сразу попадаем на нее.

Правый, Document View Controller, должен быть нашим Image Gallery Collection View Controller. Фактически, я его удаляю, потому что это просто местозаместитель (placeholder). Я собираюсь разместить на его месте Image Gallery Collection View Controller.

Мы должны вернуться в приложение ImageGallery OLD на его storyboard.

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

Поэтому я выберу все на этой storyboards и кликну Copy.

Затем я возвращаюсь в мое новое приложение ImageGallery на его storyboard и кликаю “Paste«:

Я знаю, что здесь тяжело увидеть что-то, но я уменьшаю масштаб, чтобы вы видели все в целом:

Вы видите, что между Document Browser View Controller и Image Gallery Collection View Controller нет Segue? Оба “живут” своей “жизнью”, поэтому мы должны делать презентацию нашего документа вручную. И еще одну вещь я сделаю: я перенесу точку входа приложения (маленькую стрелочку) на уже существующий Image Gallery Collection View Controller.

Мы включим в работу Document Browser View Controller, как только мы получим работающий UIDocument, потому что мы должны сначала заставить работать UIDocument прежде, чем привлекать к работе View Controller более высокого уровня.

Теперь у меня есть мой старый UI, и я собираюсь заставить его работать как и прежде, но только с UIDocument. Я собираюсь делать те же самые вещи, но мы теперь будем работать с UIDocument вместо того, чтобы иметь прямой доступ к файловой системе.

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

Я размещу оба приложения — старое и новое — одновременно на экране.

Я собираюсь перенести мой Controller, мою Модель, некоторые другие файлы типа ImageCollectionViewCell.swift для отображения ячеек коллекции, утилиты Utilities.swift. Вот что я собираюсь переносить в новое приложение. Я не буду переносить AppDelegate, потому что у меня уже есть AppDelegate, который “пришел” вместе с шаблоном. 

Я все это перетаскиваю в новое приложение и я копирую их все:

И это все. В итоге я перенесла все необходимое из моего старого приложения ImageGallery OLD в новое приложение ImageGallery.

Первое, что я должна сделать, это реализовать все необходимое для нашего  документа ImageGalleryDocument, это та вещь, которую я только что переименовала:

Здесь только два метода, и мы знаем как их реализовать, мы только что делали это. Заметьте, что метод contents возвращает Any, а НЕ Data:

Потому что этот Any может быть NSFileWrapper, директорией полной файлов, это точно такой же способ представления документа, как и Data. Но обычно возвращают Data, и видите, по умолчанию возвращаются пустые данные Data ().

Для того, чтобы реализовать любой из этих методов, нам действительно необходимо, чтобы нам передавали в документ Модель. И у меня будет переменная var imageGallery ТИПа ImageGallery?.

Каждый раз, когда я хочу сохранить мою Модель из моего Controller, я просто возьму эту Модель из моего Controller и размещу ее в документе ImageGalleryDocument, а дальше документ знает, что с ней нужно делать. Давайте пойдем дальше и в методе contents вернем imageGallery.json, а если у Модели imageGallery нет JSON представления, то вернутся пустые данные Data (), что соответствует пустому документу.

Мы знаем как обращаться с пустым документом, потому что наш документ всегда появлялся пустым.

Затем противоположное преобразование load, когда нам передаются данные Data, и мы должны превратить их в Модель imageGallery. Здесь я должна написать …

if let json = contents as? Data { …

… потому что я должна быть уверена, что мне передаются в качестве contents данные Data, я НЕ хочу иметь дело с FileWrapper. Затем я устанавливаю свою Модель imageGallery, используя конструктор ImageGallery, который берет данные Data и превращает их в экземпляр ImageGallery:

Конечно, конструктор может “падать” и Модель ImageGallery будет равна nil и документ не сохранится. Не может сохраняться nil документ.

Что касается аргумента ofType для обоих этих методов, то это UTI ( Uniform Type Identifier) — уникальный идентификатор, используемый Apple для идентификации разных объектов: документов, изображений, данных фильмов и т.д.. Наподобие public.json, которое является UTI, или edu.stanford.cs193p.imagegallery, которое тоже может быть UTI. Это уникальный идентификатор типа. В действительности, мы не будем беспокоиться об аргументе ofType, потому что мы открываем только одну вещь, это либо public.json, либо edu.stanford.cs193p.imagegallery, но в любом случае для нас это будут JSON данные. Так что мы не будем заботиться об аргументе ofType, но некоторые приложения могут открывать множество различных типов, и им нужно знать, какой нужен тип для успешного открытия файла или его сохранения.

И на этом все, это все, что мы должны сделать для ImageGalleryDocument.

Теперь можем использовать весь API класса UIDocument типа асинхронного открытия или сохранения, работы с iCloud, автоматического сохранения Autosave, весь этот богатый арсенал будет работать без каких либо усилий с нашей стороны, так как мы реализовали эти две вещи в ImageGalleryDocument.

Давайте вернемся в наш ImageGalleryCollectionViewController туда, где мы обращаемся к файловой системе напрямую.

Я хочу избавиться от всего этого, так что давайте это удалим:

Я хочу заменить этот код кодом, использующем те же самые примитивы, но с применением API документа UIDocument.

Для того, чтобы это сделать, мне, очевидно, нужна переменная var, которая является документом, потому что мы хотим иметь возможность разговаривать с нашим собственным документом. Мы назовем переменную var document и ее ТИП будет ImageGalleryDocument.

Переменная var document будет устанавливаться в коде компонента DocumentBrowserViewController, в той его части, где мы выбираем файл. Когда этот компонент выбирает файл, мы будем устанавливать переменную var document в нашем новом MVC, и он волшебным образом будет показывать содержимое документа, используя API документа UIDocument.

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

Мы сделаем это, просто написав document?.open. Это все, что мы должны написать. Метод open имеет замыкание completionHandler, в котором вам сообщается, успешно прошло открытие документа или нет. Обычно мы хотим это проверить, и если все прошло успешно, то нам нужно выполнить некоторые действия.

Возможно, мы захотим установить заголовок нашему MVC:

self.title = self.document?.localizedName

Этот localizedName просто приходит из URL как последняя часть URL без расширения файла, и мы разместим это в качестве заголовка. Конечно, мы находимся внутри замыкания, поэтому нужен self. Но наиболее важная вещь, которую я хочу сделать, если мы успешно открыли этот документ, это установка Модели нашего Controller в Модель того документа, который мы только что открыли.

У документа document, который мы получаем из файла, есть Модель imageGallery. И есть Модель self.imageGallery нашего Controller,  которую мы устанавливаем и тем самым заставляем наш UI отображать сохраненную в файле Галерею Изображений. Основные операции, которые мы делаем при работе со всеми этими документами, это установка и получение нашей Модели self.imageGallery.

А как насчет save? По идее мы не выполняем сохранение save, так как у нас действует Автосохранение. Единственно важная вещь, связанная с Автосохранением, состоит в том, что вам нужно сообщить UIDocument, что что-то изменилось. В противном случае, он не собирается тратить время на автосохранение того, что не изменилось.

И вы делаете это, во-первых, присвоив Модели документа document.imageGallery вашу Модель imageGallery, так как вы определенно захотите установить Модель документа равной вашей Модели после того, как поработали с Галереей Изображений на экране. А затем вы должны сообщить документу document, что произошло изменение, то есть вы вызываете метод updateСhangeСount (.done) :

Указанная нами опция .done “говорит” о том, что изменения произведены. И, конечно, я смогу это сделать только в том случае, если Модель документа document?.imageGallery не равна nil, так как если я даже не начинала работать над этим документом, то не имеет смысла сообщать о его изменениях.

На самом деле это выглядит очень странно: мы говорим: ”Сохранить!”, кликая на кнопке Save, а ничего не происходит.

По-хорошему мы должны замечать любые изменения, происходящие с Галереей Изображений на экране: кто-то перетащил Drag еще одно изображение в нашу Галерею или изменил в ней порядок изображений. Именно в этом случае мы должны сказать: ”Сохранить!” Тогда у нас не будет необходимости в вызове пользователем метода save, должно происходить Автосохранение при любых изменениями в документе document.

Но так как мы этого не сделали, то вынуждены пока использовать кнопку Save, с помощью которой пользователь каждый раз будет сообщать об изменениях в документе  document.

Что еще мы должны здесь сделать? Как насчет закрытия нашего документа document?

Сейчас у нас один документ, “Untitled.json”, но мы сейчас находимся  в процессе добавления в наше приложение компонента DocumentBrowserViewController, который позволит нам выбирать множество различных документов. Так что нам необходим способ закрытия одного документа для того, чтобы мы могли открыть другой документ.

Я возвращаюсь на storyboard и добавляю кнопку “Close” на наш UI. Я размещу ее с той же стороны, что и кнопка “Save”. Это опять будет кнопка Bar Button. И это будет одна из стандартных кнопок с именем “Done”, потому что я закончила работать с этим документом.

Что мы будем делать в нашем методе close? Для приложений, основанных на документах, это очень легко:

У метода close также есть замыкание completionHandler с индикатором success успешного завершения операции закрытия документа, но я этим не интересуюсь, потому что я ничего не собираюсь с этим делать. Закончилась ли моя попытка закрыть документ успешно или нет, я надеюсь, что она закончилась успешно, и в 99% случаев так оно и происходит. Я могла бы “ловить” ошибки errors и все такое, выясняя причины не закрытия документа, но лучшее, что мы можем сделать, это просто закрыть документ и посмотреть, что произойдет.

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

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

Теперь заметьте, я вызвала метод save() без аргумента и получила ошибку, так как у метода save (_ sender: UIBarButtonItem) есть аргумент ТИПА UIBarButtonItem, а я его не указала. 

Есть такой замечательный трюк. Я сделаю ТИП sender Optional, и присвою ему значение по умолчанию, равное nil.

Проделав это, я теперь могу вызывать метод save () без аргументов, так как по умолчанию sender равен nil, и я могу не использовать этот аргумент.

Теперь необходимо протестировать все это.

Посмотрите на этот код, которым мы заменили предыдущее прямое обращение к файловой системе. Он очень понятный и хорошо читаемый код:

Мы открываем документ document?.open, закрываем наш документ document?.close, сообщаем об изменении документа document?.updateChangeCount.

В нашем методе viewDidLoad я устанавливаю мой документ document в “Untitled.json”. Вообще-то это дело компонента DocumentBrowserViewController, именно он должен устанавливать мою переменную document, но пока я хочу, чтобы он загружался в методе viewDidLoad. Этот viewDidLoad мне нужен только для тестовых целей.

Я опять использую знакомый нам код для получения URL для файла “Untitled.json”, а затем устанавливаю мой документ document в ImageGalleryDocument с этим URL:

У UIDocument есть единственный инициализатор, это UIDocument (fileURL: URL). Но все это я сделала исключительно для тестирования.

Запускаем приложение, оно должно загрузить этот “Untitled.json” файл, потому что мы немедленно устанавливаем его в качестве нашего документа document, а затем в viewWillAppear наш документ открывается и сделает все необходимые вещи.

Не забудьте проверить и сделать необходимые настройки в вашем файле info.plist:

Давайте запустим приложение и посмотрим, что происходит.

Вы видите, что наше приложение загрузило документ “Untitled.json”.

Теперь, когда мы поняли, что при повторном запуске нашего нового приложения ImageGallery загружается файл “Untitled.json”, давайте попробуем на реальном устройстве добавить еще изображений и кликнуть на кнопке  “Done”:

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

Итак, UIDocument делает за нас всю работу по взаимодействию с файловой системой.

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

Давайте добавим еще один кусочек в наш паззл.

Прежде, чем я что-то сделаю, я должна убедиться, что я убрала код для получения URL для файла “Untitled.json” и установки моего документа document в ImageGalleryDocument с этим URL в методе viewDidLoad.

Я собираюсь убрать этот метод потому, что я не буду каждый раз устанавливать мой документ document равным “Untitled.json”, я собираюсь устанавливать его где-то, где компонент DocumentBrowserViewController попросит меня это сделать.

Итак, это наш DocumentBrowserViewController, где собственно и происходит все самое “магическое”. Мы собираемся реагировать на то, что получаем из Document Browser View Controller, чтобы представлять наш MVC поверх Document Browser View Controller для отображения наших документов.

Давайте посмотрим, как все это можно сделать. Реализуем все части, о которых говорилось на Лекции 14.

Начнем с конфигурации viewDidLoad в DocumentBrowserViewController. Конечно, мы хотим сделать себя делегатом:

delegate = self

Конечно, мы также НЕ хотим разрешать выбирать многочисленные элементы items, потому что на данный момент нам известен лишь один тип документов ImageGalleryDocument. Свойство allowsDocumentCreation будет равно true пока. И затем все эти вещи, связанные с цветами отображения я не собираюсь менять, поэтому избавляюсь от лишнего кода:

Позже мы вернемся к viewDidLoad и изменим allowsDocumentCreation через мгновение.

Но пока идем ниже к методу didRequestDocumentCreationWithHandler. Именно здесь мы “говорим”: “Дай мне URL пустого документа, потому что кто-то хочет создать новый документ.»

Я хочу сильно упроcтить реализацию этого метода. Я удалю весь этот код и просто вызову importHandler, который мне передается в качестве аргумента, с аргументами: URL и тем, что принимает значения .copy или .move. Я вызываю importHandler с аргументом template, который является URL, и аргументом .copy.

Все, что нам необходимо сделать, это создать переменную…

var template: URL?

… сделав ее указателем на некоторый пустой документ, и я буду это делать в viewDidLoad. Я размещаю здесь обычный код для получения URL, который на этот раз будет находиться НЕ в директории Document Directory, потому что это пустой документ, который я каждый раз буду копировать, и который не должен появляться в документах, когда кто угодно может открыть его и посмотреть. Я собираюсь разместить его в директории ApplicationSupport Directory, это прекрасное место для размещения подобного рода вещей, действующих, так сказать, “за кулисами”, но хранящихся постоянно.

Я хочу, чтобы шаблон template всегда был под рукой. Я не хочу, чтобы template располагался в директории Caches Directory и постоянно удалялся, хотя я мог бы сохранять его и в директории Caches Directory и каждый раз создавать его в viewDidLoad.

В любом случае у меня есть URL, и я по старой памяти назову этот шаблон пустого документа “Untitled.json” .

Я размещаю шаблон пустого документа в моей директории .applicationSupportDirectory, и все, что я хочу сделать дальше, это убедиться, что я создала файл для шаблона пустого документа, а также получить его URL.

Поэтому я могу написать:

Если template не равен nil, то есть другими словами, я смогла создать URL, который я должна была создать, я устанавливаю разрешение на создание документа allowDocumentCreation равное true. Вместо этого я запрашиваю FileManager.default о создании файла с помощью метода createFile, который берет путь atPath к файлу, в нашем случае это будет template!.path, некоторый контент файла contents, который я сделаю пустым Data(), потому что это пустой документ, и мне не нужны никакие атрибуты файла attribute, поэтому я избавляюсь от них. В результате я получаю хитроумный код:

Метод createFile — это прекрасный способ создания файла, потому что он не “выбрасывает” throws ошибку и возвращает булевское значение Bool, то есть удалось создать файл или он уже существует, что, собственно, мне и нужно. Если это значение равно true, то я разрешаю создавать новый документ, в противном случае, я не разрешаю создавать документ, так как я не смог создать шаблон для пустого документа.

Видите? Это прекрасный маленький “трюк” при создании шаблона, если вы “проваливаете” создание шаблона по каким-то причинам. Вы можете писать этот код.

Теперь importHandler будет работать. Я передаю ему этот template, который я создала в viewDidLoad, и он должен скопировать его из директории ApplicationSupport Directory в директорию Document Directory.

Следующих методов я даже не буду касаться. Они вызываются тогда, когда люди кликают на документах didPickDocumentURLs, когда некоторые другие приложения типа Files просят вас открыть один из ваших документов didImportDocumentAt.

Заметьте, что оба этих метода выполняют одну и ту же строку кода:

presentDocument (at: destinationURL)

Это означает размещение (представление) на экране вашего View Controller для показа документа.

У нас еще есть метод failedToImportDocumentAt получения ошибки error.

На самом деле вам следует в этом методе размещать “экстренное уведомление” Alert. Пока мы этого делать не будем, но можно действительно разместить “экстренные уведомления” Alerts типа “Не могу загрузить документ!”, “Не могу открыть документ!” или что-то еще.

Еще у нас есть метод presentDocument, который представляет документ на экране:

Я собираюсь делать в этом методе то, что было показано на слайдах на Лекции 14, то есть я собираюсь получить storyBoard, у меня уже есть нужный код для получения storyBoard, затем я собираюсь использовать storyBoard для получения View Controller, который я хочу представлять (present) на экране.

Но есть небольшая хитрость в этом представлении. Дело в то, что View Controller, который я собираюсь представлять, это на самом деле НЕ ImageGallery Collection View Controller. Это Navigation Controller, в котором находится наш ImageGallery Collection View Controller.

Я должна как-то получить в коде экземпляр этого Navigation Controller, который выводит на экран мой MVC.

Я должна дать имя этому Navigation Controller, так что идем в Инспектор Идентичности и даем ему имя. Я собираюсь назвать его DocumentMVC. Это могло бы быть любое имя, но DocumentMVC — это своего рода то, что он собой представляет:

Теперь у меня есть имя, и я могу вернуться в мой код и получить documentVC:

Теперь у меня есть documentVC, я собираюсь его конфигурировать, а затем представить на экране с помощью

present(documentVC, animated: true)

Я действительно хочу, чтобы была анимация, если кто-то кликнет на этом документе, содержание должно открываться путем скольжения вверх, мы не хотим, чтобы оно появлялось “прыжком”, мы хотим, чтобы происходило плавное скольжение снизу вверх. Существует даже такой способ анимации, когда содержимое документа получается путем увеличения иконки файла, что очень круто и это не так уж и сложно. У нас будет нормальная презентация нашего MVC со скольжением снизу вверх:

Для того, чтобы мы могли установить documentVC наш документ document, он должен быть ImageGalleryCollectionViewController. Я сделаю сейчас вещь, очень похожую на то, что мы делали при подготовке (prepare) нашего documentVC.

Если я смогу получить содержимое contents нашего documentVC, а это мой ImageGalleryCollectionViewController, то …

Что такое contents? Во вспомогательном файле Utilities.swift  есть код для contents:

contents  — это вычисляемая переменная var класса UIViewController, которая просто “говорит”, что если это UINavigationController, то возвращается его содержимое, то есть navcon.visibleViewController ?? navcon. В противном случае, когда я не являюсь UINavigationController, возвращается просто self

Этот код работает независимо от того, “обернут” ли ваш UIViewController в UINavigationController  или нет. Так что мы получаем содержимое documentVC как documentVC.contents и проверяем, является ли оно ImageGalleryCollectionViewController с помощью оператора as?.

Если это так, то я устанавливаю свойство document для моего imageGalleryCollectionViewController. Это новый документ ImageGalleryDocument, и мы создаем его с помощью единственного доступного конструктора ImageGalleryDocument, который берет в качестве аргумента fileURL. Откуда нам взять этот URL? Это тот URL, который мы хотим представлять и который нам передается в качестве аргумента, то есть documentURL:

Другая вещь, которую мы должны сделать, — это типы файлов.

Если мы посмотрим установки (settings) нашего проекта для TARGETS на закладке Info в разделе Document Types, то есть типы файлов, которые мы можем открывать, то увидим, что это только изображения images.

Мы НЕ открываем изображения images, мы открываем JSON файлы, поэтому public.JSON.


Если мы ничего не забыли, то все должно работать.

Вы видите, что наше приложение выглядит совершенно по-другому, оно показывает нам UI, который похож на приложение Files. Мы видим все эти папки и можем даже пойти на iCloud Drive. Мы можем пойти на “On My iPad” и увидеть там папку ImageGallery

Давайте заглянем внутрь этой папки:

Здесь находится наш файл “Untitled.json”, давайте откроем его:

Здорово! Все работает!  Давайте закроем этот документ с помощью кнопки “Done”, но документ не закрывается.

Когда MVC выходит на экран, он занимает целый экран, потому что мы представляем его с помощью кода:

present(documentVC, animated: true)

Как мы можем заставить его “уйти” (dismiss) с экрана?

Мы будем это делать в самом Image Gallery Collection View Controller, который хочет “уйти” с экрана, в методе close() при нажатии кнопки “Done”.

Сразу после строки с save () мы напишем следующий код:

Мы использовали метод dismiss (animated: true), именно таким способом можно заставить самостоятельно “уйти” с экрана MVC,  если вы выводили его на экран с помощью present (documentVC, animated: true).

У метода dismiss есть замыкание completionHandler,  и в этом замыкании мы разместили код закрытия документа. Таким образом, я буду ждать, пока я покину экран, и при этом будет происходить анимация, и только после этого я закрою документ self.document?.close(). Так как теперь я оказалась внутри замыкания, то я должна использовать self.

Давайте попробуем запустить приложение.

Опять кликаем на “Untitled.json” и я попробую его закрыть, нажав на кнопку “Done«:

Документ “уходит” с экрана, и мы возвращаемся к нашему UI.

Теперь я хочу создать новый документ, я кликаю на “Create”…

… происходит копирование шаблона, который представляет собой пустой документ. Располагаем справа Safari с поиском в Google изображений другой тематики, например, ocean, и мы можем начать заполнять наш новый документ, нашу новую Галерею Изображений:

Наш новый документ назыввется “Untitled 2” :

Теперь я кликну на кнопке “Save”, чтобы дать знать, что у нас изменения, и опять, я говорила вам, сохранение изменений может быть выполнено автоматически. Затем я кликаю на кнопке “Done”. И у меня два документа:

Я могу вернуться к первому документу, кликнув на документе “Untitled

Теперь к документу “Untitled 2”:

А что если я хочу размещать мои Галереи Изображений на iCloud Drive?

Смотрите, что произойдет, если я перейду на iCloud Drive в приложении ImageGallery?

У нас есть кнопка “Create Document”, я ее кликаю и смотрите, что происходит:

Мы можем создать документ на iCloud Drive, что означает в действительности создание документа в “сети” (network). Это позволит увидеть документ на других моих устройствах.

Давайте создадим еще одну Галерею Изображений «sunset«:

Кликаем “Save”, а затем “Done”. И теперь на iCloud Drive у нас появился “Untitled” документ.

У нас все еще есть “Untitled” документ на нашем устройстве On My iPad

Но у нас есть “Untitled” документ и на iCloud Drive. Давайте кликнем на нем…

Мы видим нашу прежнюю, только что созданную на  iCloud Drive Галерею Изображений «sunset«:

Следующая вещь, которую я хочу сделать, это существенно улучшить внешний вид этого UI.

Посмотрите на эти иконки, это худшие иконки в Мире. Мы перейдем от наихудших иконок к наилучшим иконкам, потому что с помощью класса UIDocument мы можем для каждого документа иметь свою собственную иконку. И я установлю в качестве иконки для документа, например, миниатюрную версию первого изображения в самом документе, thumbnail. Таким образом, я очень легко смогу увидеть, что за Галерея Изображений находится в этом документе.  И я смогу увидеть это прямо в моем приложении.

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

Установите понятное изображение для миниатюры ваших документов. Возможно, не имеет смысла делать действительно мгновенный снимок (snapshot) полностью вашей коллекции Collection View! Сделайте что-то другое.

Давайте сделаем это. Это супер просто делается в UIDocument. Так что я возвращаюсь в класс ImageGalleryDocument. Это очень простой класс. И я добавлю туда еще один метод, который я переопределю (override). Он называется fileAttributesToWrite:

Этот метод в итоге просто возвращает словарь attributes с атрибутами файла типа “этот файл скрыт” и т.д.. Я получаю эти атрибуты attributes из своего super класса:

var attributes = try super.fileAttributesToWrite (to: url,  for: saveOperation)

, потому что я переопределяю (override) этот метод.

После того, как я их получила, я добавляю один атрибут, который представляет собой достаточно сложную вещь и называется thumbnailDictionaryKey. Для этого сложного ключа, значением в словаре является другой словарь с другим ключом NSThumbnail1024x1024SizeKey. Но не позволяйте такому названию одурачить вас. Миниатюра thumbnail может быть любого размера, какого вы хотите. Хотя если она слишком маленькая, то используется опять иконка этого документа. Необходимо убедиться, что она достаточно большого размера для того, чтобы наше улучшение UI прошло успешно и я смогла бы разместить миниатюру thumbnail вместо иконки.

Для миниатюры я добавляю переменную var thumbnail ТИПА UIImage. Миниатюра thumbnail — это просто UIImage.

Я буду устанавливать переменную thumbnail с изображением миниатюры в моем документе ImageGalleryDocument каждый раз, когда закрываю документ. Каждый раз, когда я закрываю документ, я буду делать “мгновенный снимок” (snapshot) первого изображения в моем документе.

Давайте сделаем это в моем Controller в его методе close, сразу после вызова save().

Первое изображение firstImage — это вычисляемая переменная:

Код для переменной snapshot, которую я здесь использовала для firstImage, размещен в файле Utilities.swift. Вы можете взглянуть на него. Это всего 3-4 строки кода, в которых вы берете ваш UIView и делаете его “мгновенный снимок” (snapshot), затем вы “захватываете” его как UIImage.

И опять, между прочим, я делаю это только в том случае, если Модель imageGallery моего документа document НЕ nil, потому что я не хочу делать “мгновенные снимки” пустых документов.

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

У нас два документа:  “Untitled” и “Untitled 2”.

Помните, что мы устанавливаем миниатюру thumbnail при закрытии документа. Кликаем на первом документе и закрываем его с помощью кнопки “Done”.

Смотрите! Мы получили прекрасную миниатюру первого изображения этого документа. А как насчет другого документа “Untitled 2”?

Закрываем этот документ с помощью кнопки “Done”.

Мы опять получили прекрасную миниатюру.

Это даже работает на iCloud Drive. Правда возможна небольшая задержка на  iCloud Drive, потому что это требует загрузки миниатюры, но давайте попробуем.

Открываем на iCloud Drive документ “Untitled”.

Закрываем этот документ с помощью кнопки “Done”.

Смотрите, как быстро мы получили миниатюру для этого документа, очень скоростная “сеть”. Я не знаю, удалось ли вам заметить маленькое “облачко”, которое появляется, когда идет загрузка на  iCloud Drive, что очень здорово.

Тот UI, который вы здесь видите, полностью соответствует UI приложения Files. Здесь вы можете делать не только получать информацию о вашем документе, кликая на Info, но также можете перемещать файлы, например, с iCloud Drive назад на локальное устройство или копировать файлы.

Нас спрашивают, куда я хочу переместить мой файл

Я хочу переместить на “On My iPad” в папку приложения ImageGallery и кликаем на кнопке “Copy”.

Меня спрашивают, хочу ли я сохранить оба этих файла или заменить уже существующий там файл, так как оба эти файла имеют имя “Untitled”. Я выбираю сохранение обоих этих файлов. Возвращаюсь на  “On My iPad”в папку приложения ImageGallery, то там я обнаружу уже три документа, включая и только что скопированный из iCloud Drive.

Вы также можете переименовывать файлы. Например, переименуем “Untitled” файл в файл “Осень”.

Вы можете переименовывать файлы прямо в этом приложении, не открывая для этого специальное приложение Files.

Если мы запустим приложение ImageGllery на симуляторе, то на iCloud Drive мы увидим наш Untitled документ:

Оно там присутствует с «облачком», которое говорит о том, что пока идет загрузка этой Галереи Изображений на iCloud Drive.

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

5. Кэшируйте локально изображения во всех ваших Галереях изображений (Image Gallery), используя URLCache, до разумных пределов файловой системы (объясните, какой предел вы выбрали в комментариях где-нибудь в коде).

7. На этой неделе требуется, чтобы приложение запускалось на реальном устройстве (а не просто на симуляторе).

Все дальнейшие эксперименты мы будем проводить на реальных устройствах iPad  и iPhone.

Нас намеренно просят сделать то, о чем даже не упоминалось на Лекциях (URLCache).

Класс URLCache реализует кэширование реакций (responses) сервера в ответ на запрос о загрузке данных по заданному URL. Это осуществляется путем отображения (mapping) URLRequest объектов в CachedURLResponse объекты и выполняется путем комбинацией кэширования в памяти и на диске, позволяя манипулировать обоими порциями этого кэша.

Объект CachedURLResponse снабжает нас метаданными реакции (response) сервера в виде реакции URLResponse и данных Data, являющихся актуальным кэшируемым контентом.

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

Мы могли бы инициировать кэш с определенными размерами кэша в «памяти» и на диске:

Но если нет срециальных требований к кэшированию, то Apple рекомендует использовать URLCache.shared.

Загрузку изображений коллекции мы производим в классе ImageCollectionViewCell, который является subclass класса UICollectionViewCell, в методе updateUI():

  1. Сначала мы анализируем, задан ли imageURL. Если imageURL не равен nil и мы смогли получить url, то мы продолжим выполнение дальнейших операций, а иначе — возврат return.
  2. Мы уже создали наш кэш cache либо как URLCache.shared, либо с помощью инициализатора URLCache (memoryCapacity: 5*1024*1024, diskCapacity: 30*1024*1024, diskPath: nil) с конкретными размерами кэша а «памяти» и на диске. Заданный по умолчанию кэш  URLCache.shared  подходит для большинства случаев.
  3. Затем мы создаем запрос request с помощью инициализатора URLRequest из  URL url.
  4. Мы используем для выполнения нашей работы глобальную фоновую (backhround) .userInitiated очередь (queue). Это необходимо для того, чтобы не блокировать main thread при загрузке изображения. Сначала мы проверяем, а не находятся ли данные нашего запроса на выборку уже изображения в кэше cache.
  5. Если они в кэше cache, то мы показываем изображение image.
  6. Если изображение image не обнаружено в кэше cache, то мы идем дальше и делаем запрос на получение изображения image с помощью  URLSession. Проверяем, что у нас есть данные data и реакция response. Убеждаемся, что у нас имеется положительная реакция response на выборку данных, прежде, чем мы продолжим выполнение.
  7. Наконец, мы создаем cachedData из рекции response и данных data и запоминаем в кэше cache.

Запускаем приложение, выбираем документ «Осень»:

Мы видим, что сначала работает «сеть» и идет накопление запросов в кэше cache, но если мы дойдем до конца и вернемся назад, то начнет работать кэш cache:

Если мы кликнем на кнопке «Done» и снова откроем документ «Осень«, то мы увидим, что работает только кэш cache:

Иногда, чтобы избежать резкого появления на экране («прыжком») изображения image, выбранного из кэша cache, используют «сглаживающую» анимацию:

Мы можем ее использовать при обнаружении изображения image в кэше cache:

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

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

Это полный браузер для просмотра документов, и он также очень хорошо работает как на iPhone, так и на iPad. Но есть пара особенностей, которые мы должны учесть при работе на iPhone, и вы их увидите.

Итак, вы видите, что у нас на iCloud Drive есть документ, который мы недавно создали. Я могу открыть его на iPhone. Но как бы я не старался, я не смогу перетащить Drag никакие изображения на мой документ, так как на iPhone у меня не работает многозадачный режим. Кроме того, еще одна вещь, которую я не смогу сделать, — это создать новый документ. Почему? Потому что я не смогу перетаскивать Drag из Safari новые изображения для коллекции. Поэтому нет смысла вводить пользователя в заблуждение и предлагать ему на iPhone кнопку “Create Document”.

Я действительно хочу, чтобы кнопка “Create Document” не появлялась на iPhone. Еще я хочу, чтобы моя коллекция изображений Collection View позволяла бы использовать механизм Drag & Drop хотя бы для реорганизации изображений внутри Галереи.

Почему на iPhone не работает перетаскивание Drag в коллекции изображений CollectionView? Потому что CollectionView по умолчанию на iPhone не позволяет выполнять перетаскивание Drag. Но мы можем “включить” эту возможность.

Давайте исправим обе эти вещи в нашем коде.

Сначала уберем кнопку “Create Document” на iPhone и сделаем это в той части нашего кода, где мы создавали шаблон template для новых документов.

Я просто не хочу выполнять этот код для iPhone, и тогда останется

allowsDocumentCreation = false

Если я смогу определить, что я нахожусь на iPad, то я буду выполнять выделенный код только для iPad. Я использовала для этого сравнение UIDevice.current.userInterfaceIdiom  с опцией .pad:

Итак, я буду создавать шаблон template для нового документа только, если UIDevice.current.userInterfaceIdiom == .pad, то есть я нахожусь на iPad. Я никогда не буду создавать новый документ на iPhone. С этой особенностью работы на iPhone мы справились.

Как насчет того, чтобы перетаскивание Drag работало на с Collection View?

Это мой ImageGalleryCollectionViewController, и я иду в метод viewDidLoad и там я установлю переменную dragInteractionEnabled для коллекции collectionView! в true:

На iPad по умолчанию эта переменная равна true, а на iPhone она по умолчанию равна false. Я установил ей значение true для любого случая. Теперь я могу выполнять перетаскивание Drag в моей коллекции Collection View также и на iPhone.

Запускаем приложение. Мы не должны увидеть кнопку “Create Document” и должны получить возможность с помощью механизма Drag & Drop реорганизовывать изображения внутри коллекции Collection View.

Да, мы НЕ видим кнопки “Create Documentё”, но, конечно, можем открывать уже имеющиеся документы. Открываем документ “Sunrise”.Теперь я могу перемещать изображения внутри коллекции Collection View. Если я кликну на кнопке «Done«, то наша Галерея Изображений “Sunrise” сохранится и ее иконка поменяется:

Это приложение работает также и на симуляторе iPhone X:

Но есть одно небольшое замечание относительно симуляторов в подсказках :

Подсказка № 4

Симуляторы не оказывают надежной поддержки Documents на локальном устройстве, так что залогиньтесь в iCloud на ваших Симуляторах и используйте iCloud Drive как место хранения для тестирования вашего приложения.

На этом все обязательные  и один дополнительный пункты Задание 6 выполнены, Код находится на Github   для iOS 11 и на Github для iOS 12 в папке ImageGallery_6_Requed_Browser.

Избавляемся от кнопки «Save«.

Используя кнопку «Save» мы по идее  не выполняем сохранение, так как для документов действует Автосохранение. Единственно важная вещь, связанная с Автосохранением, состоит в том, что вам нужно сообщить UIDocument, что что-то изменилось. В противном случае, он не собирается тратить время на автосохранение того, что не изменилось.

Мы сообщаем документу document, что произошло изменение, вызывая метод updateСhangeСount (.done). И, конечно, я смогу это сделать только в том случае, если Модель документа document?.imageGallery не равна nil:

В действительности мы должны замечать любые изменения в нашем документе: кто-то перетащил Drag еще одно изображение в нашу Галерею Изображений или переместил изображение на другую позицию или удалил его, бросив его в «мусорный бак». Именно в этом случае мы должны сказать: ”Сохранить!” Тогда нет необходимости в вызове пользователем метода save. Поэтому переименуем метод save () в documentChanged(): и отменим вызов метода save () в close():

Теперь нам следует отслеживать изменения в документе и вызывать метод documentChanged() при изменениях. Это необходимо сделать при сбросе Drop изображения как извне, так и при внутренней реорганизации, в методе делегата performDrop:

Еще изменения нашей коллекции происходят при сбросе Drop изображения в «мусорный бак», то есть в GabageView, и мы также должны сообщить нашему Controller о том, что произошли изменения в нашем документе document, но View не может “разговаривать” с Controller иначе, чем “слепым” структурированным способом. Есть много способов это сделать. Один из них — это классическое делегирование. Другой способ — использовать замыкание вместо делегирования. И, наконец, третий способ — это уведомления Notification. Я выбрала самый компактный способ — с замыканием. На Лекции 14 рассматриваются способы классического делегирования и  с Notification.

Добавляем замыкание garbageViewDidChanged в класс GarbageView, управляющий сбросом Drop замыканий в «мусорный бочок»:

Далее при изменении коллекции (collection.dataSource as? ImageGalleryCollectionViewController)? мы вызываем этот метод :

В нашем Controller, ImageGalleryCollectionViewController, там, где мы создают кнопку «мксорный бочок» мы определяем замыкание garbageViewDidChanged, в котором сообщаем нашему документу с помощью функции documentChanged(),  что  документ изменился:

И это все. Теперь мы убираем кнопку «Save» с нашего UI. И это правильно.

Не следует создавать кнопку Save«в приложении, ориентированном на работу с документами UIDocument. В таких приложениях у вас НИКОГДА НЕ должно быть кнопки Save«. В таком приложении вы должны вызвать метод  updateСhangeСount (.done), если что-то изменилось.

Стартуем приложение. Открываем Галерею Изображений «Sunrise»:

Мы можем делать с изображениями этой Галереи «Sunrise» что угодно:

  • добавлять новые изображения:

  • выбрасывать изображения в «мусорный бак»:

  • перемещать на новое место

В результате мы получим Галерею «Sunrise» в следующем виде:

К тому моменту, как мы кликнем на кнопке «Done«, все изменения будут уже сохранены. Повторный выбор Галереи «Sunrise«…

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

Код нахоится на Github   для iOS 11 и на Github для iOS 12 в папке ImageGallery_6_Requed_Browser_No_Save_Button.

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

Сделайте так, чтобы ваши документы открывались из приложения Files.

У нас будет свой собственный тип документа .imagegallery. Мы устанавливаем свой собственный тип в разделе, идущем сразу за разделом Document types, который называется Exported UTIs, где за UTI стоит сокращение Uniform Type Identifier:

Здесь мы изобрели свой собственный Uniform Type Identifier (то есть UTI) с уникальным именем “edu.stanford.cs193p.PersistentImageGallery.imageGallery”:

 Внизу, я должна указать расширение имени файла .imagegallery, которое присуще ImageGallery документу. После установки этого расширения вы можете вернуться назад, к разделу Document Types, который был выше, и добавить новый UTI в качестве документа, который это приложение открывает:

ТИП “edu.stanford.cs193p.PersistentImageGallery.imageGallery” —  это наш “UTI”, на который мы ссылаемся. Наподобие public.json для JSON.  Мы добавляем его к разделу Document Types, как поддерживаемый Тип Документа. Мы также указываем опцию «Editor» для параметра CFBundleTypeRole, а это означает, что наше приложение может открывать и сохранять документы этого ТИПА. Указываем опцию «Owner» для параметра LSHandlerRank, а это означает, что наше приложение является основным при открытии файлов этого ТИПА.

Сравните значения параметров CFBundleTypeRole  и LSHandlerRank в разделе Document Types, с аналогичными, которые поступили к нам вместе с шаблоном Document Based App, в котором в разделе Document Types мы чуть-чуть подправили ТИП открываемого файла, заменив .images на .json:

Мы видим здесь указана опция «Viewer» для параметра CFBundleTypeRole, а это означает, что наше приложение может открывать документы этого ТИПА для просмотра. Указываем опцию «Alternate» для параметра LSHandlerRank, а это означает, что наше приложение является лишь альтернативным приложением при открытии файлов этого ТИПА.

Кроме изменений в настройках нашего проекта, мы должны предусмотреть в DocumentBrowserViewController, что шаблоном нашего ImageGallery документа будет файл с расширением .imagegallery:

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

Запускаем наше приложение, кликаем на «Create Document«:

Наполняем нашу коллекцию изображениями прекрасной осени и кликаем на кнопке «Done«:

Мы получим «Untitle« ImageGallery документ с иконкой в виде первого элемента коллекции изображений:

Если мы выполним жест Long Press, то появится меню с действиями, которые можно выполнить над этим документом.

Давайте для начала его переименуем в «Прекрасную осень«:

Опять выполним жест Long Press и получим информацию info об этом документе:

Мы видим, что этот документ имеет расширение .imagegallery и ТИП ImageGallery:

Поэтому, если мы запустим приложение Files и кликнем на этом файле….

… то должно открыться наше приложение ImageGallery, как основное приложение, показывающее этот ТИП файла с расширением .imagegallery:

Да, наше приложение ImageGallery открыло Галерею Изображений «Прекрасная осень«.  Цель достигнута.

Код нахоится на Github  для iOS 11 и на Github для iOS 12 в папке ImageGallery_6_Requed_Browser_No_Save_Button.

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

Поддержите перетаскивание Drag изображений UIImages, у которых нет правильного URL для изображения. Сохраните изображение UIImage, которое вы перетащили, в файловой системе и создайте свой собственный URL к нему. Это нормально, если изображения будут не видны на других устройствах (если документ запоминается в iCloud Drive). Будьте внимательны и помните, что абсолютный путь (absolute path) URL (то есть тот, что начинается с /) не работает от запуска к запуску вашего приложения. Вы ВСЕГДА должны запрашивать FileManager об URL вашей “Песочницы” напрямую и создавать относительные пути (relative paths) оттуда. Вы должны принимать во внимание это в структуре данных вашего документа.

Такая ситуация, когда вы перетаскиваете изображение UIImage вместе с URL  из Google, а в последствии не можете его воспроизвести по этому URL, бывает.

Давайте запустим наше приложение из папки ImageGallery_6_Requed_Browser_No_Save_Button и попробуем перетащить изображение в нашу Галерею «Осень«:

Мы получим ошибку при загрузке этого изображения по сброшенному URL:

Пока мы находимся в процессе сброса Drop и нам доступны и изображение image и URL url, мы можем делать своеобразную асинхронную проверку на то, что выбранная по этому url информация является изображением UIImage:

Метод берет check, который является методом UIImage и расположен в расширении extension класса UIImage в файле Utilities.swift. Этот метод берет ваш URL на проверку, идет в интернет, асинхронно выбирает изображение и убеждается, что это действительно изображение UIImage. Если он убеждается, что это действительно изображение UIImage, то происходит обратный вызов (callback) с помощью замыкания handler, которое ВЫ ему предоставляете. В этом случае замыкание handler возвращает вам исходный правильный url. Если методу check не удается сформировать изображение UIImage по заданному url, то данные изображения становятся self, они записываются в локальный URL в файловой системе и замыкание handler возвращает вновь сформированный локальный url:

Возвращенный методом check URL url, полученный при сбросе Drop или новый, полученный для локального файла, в котором сохранено изображение image, мы записываем в Модель и передаем placeholderContext  для вставки нового изображения в коллекцию collectionView: и сообщаем о том, что наш документ изменен:

Давайте повторим наш «трудный» сброс изображения:

На этот раз сброс изображения прошел удачно:

Мы даже можем закрыть наш документ и открыть его вновь и увидеть, что наше «трудное» изображение приходит на экран из кэша:

Но давайте закроем наше приложение и запустим его вновь из Xcode:

Мы видим, что приложение пыталось считать из кэша наше трудное изображение, но нично не вышло. Оно продолжает считывать их «сети» ( то есть из нашего локального файла) и получает ошибку. Почему?

Потому что абсолютный путь (absolute path) URL (то есть тот, что начинается с /) для локального файла не работает от запуска к запуску вашего приложения. Вы ВСЕГДА должны запрашивать FileManager об URL вашей “Песочницы” напрямую и создавать относительные пути (relative paths) оттуда. То есть имя файла осталось прежним, а путь к нему поменялся, поэтому мы должны знакового его сформировать и пристыковать в нему уже имеющееся имя файла. 

Это делает метод changeLocalURL(), расположенный в расширении extension  класса URL в файле Utilities.swift:

Мы заменяем неправильные URL для «трудных» изображений на правильные URL прямо после открытия нашего документа и перед загрузкой коллекции collectionView. Изменения мы делаем по месту с помощью метода mutateEach:

Метод  mutateEach является расширением extension массива Array:

Запустим приложение вновь из Xcode: и видим, что «трудное» изображение восстановилось благодаря замене URL локального имени на правильное:

Мы видим, что идентификаторы приложений, участвующие в имени локального файла отличаются и мы ВСЕГДА должны запрашивать FileManager об URL нашей “Песочницы” напрямую и создавать правильные относительные пути. 

Есть еще одно место, где нам придется различать url ТИПА //https: (интернет) и file/// (локальная файловая система). Это запоминание данных выборки из интернета в кэше URLCache в классе ImageCollectionViewCell :

Так как наш документ «Untitled 2» находится на iCloud Drive, мы можем обнаружить и открыть его  на симуляторе:

Однако наше «трудное» изображение не загрузилось из интернета и показывается с ошибкой, так как их URL соответствует локальному файлу на реальном iPad:

На реальном iPad на iCloud Drive «трудное» изображение по-прежнему существует:

Код нахоится на Github   для iOS 11 и на Github для iOS 12 в папке ImageGallery_6_Requed_Browser_No_Save_Button_LocalImage.

Все Задание 6, включая обязательные и дополнительные пункты, выполнено.