FileManager / 1.1. Управление файлами


Видео
Problems with video



Сражаться из страха или злости — значит вести войну без конца.

Император Шаохао

В этом уроке


Файловая система

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

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

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

Для работы с файлами используется тип из Foundation - FileManager (корреспондирующий ссылочный тип NSFileManager). В большинстве случаев Вам не придётся создавать свой собственный объект - будет достаточно воспользоваться стандартным статическим FileManager.default. Плюсом данного подхода будет то, что такой объект заведомо потокобезопасен и Вы можете отправлять ему сообщения из множества потоков одновременно!

Ограничения на взаимодействие с файлами в разных операционных системах различается. Данный урок лучше всего выполнять в консольном приложении для macOS или использовать в каком-нибудь методе viewDidLoad для iOS. Мы будем демонстрировать работу программы на macOS и iOS.

Чтобы иметь возможность демонстрировать работу сразу на двух устройствах, мы создаем workspace - объединение нескольких проектов и сущностей в одном.
Используйте следующий путь для создания рабочего пространства File -> New -> Workspace

В нем создайте два проекта - для iOS и macOS, как Вы это делали ранее, однако на этапе выбора директории выберите Add To и укажите название текущего воркспейса.

Теперь создайте в папке c macOS Swift-файл для нашего класса-обёртки. Мы назвали его FileWrapping.

import Foundation

public class FileWrapping {
    static func doWork() {

    }
}

Теперь добавьте в main.swift вызов данной функции.
Нажмите правой кнопкой по проекту для iOS и выберите Add File to...

Затем пролистайте папки вверх, пока не найдёте файл-обёртку, добавьте его в данный проект. Теперь на него есть ссылка в обоих проектах.

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

Добавьте вызов doWork в метод viewDidLoad нашего единственного контроллера.


Сделаем же уже что-нибудь!

Весь дальнейший код будет писаться нами в теле doWork

Давайте создадим себе небольшую обёртку, дабы сократить количество кода:

let manager = FileManager.default

Давайте попробуем вывести временную директорию для файлов:

print(manager.temporaryDirectory)
print(manager.temporaryDirectory.path)

В первом случае мы получаем ссылку URL на директорию, во втором извлекаем путь до неё. Отличие ссылки от пути в том, что ссылка содержит также протокол доступа. В случае для файлов - это file://. URL - крайне универсальный способ работы с данными - он может ссылаться на самые разные ресурсы, не только файлы и сайты. В дальнейшем мы узнаем ещё несколько способов применить этот тип.

Также можно получить путь до текущей рабочей директории:

print(manager.currentDirectoryPath)
// /Users/gorloff/Library/Developer/Xcode/DerivedData/final@10@1_1-bdkbrmfqfxwmzfgmrfzjklaurcqo/Build/Products/Debug

Рабочую директорию для приложения-непесочницы можно изменить методом changeCurrentDirectoryPath(_:):

//manager.changeCurrentDirectoryPath("/")

Однако для приложений песочниц это не даст ничего хорошего...

Запустите программу на macOS, симуляторе и реальном устройстве, Вы получите три вывода соответственно:

//macOS
//file:///var/folders/6c/b82vsq25317f_9w0g07jf0qr0000gn/T/

//iOS simulator
//file:///Users/gorloff/Library/Developer/CoreSimulator/Devices/C4B5A863-9E2D-4DD3-883F-EF5823479DEC/data/Containers/Data/Application/37BF337D-2EAA-4405-A621-177D0A0B2030/tmp/

//iOS real device
//file:///private/var/mobile/Containers/Data/Application/F45C522C-22A9-478A-B2ED-39CBE498F307/tmp/

Папка для временных файлов управляется системой:

  1. В случае с macOS она выступает разделяемой,
  2. Для симулятора она находится внутри приложения, но доступна в macOS
  3. Для iOS она находится внутри приложения и по паттерну песочницы недоступна извне

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

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

Помните, что директория для временных файлов является директорией именно для временных файлов. Не используйте её для хранения долговременных файлов - система не гарантирует и даже не обещает, что файлы в ней будут живы в течение времени, после высвобождения управления ими.

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

print(manager.homeDirectoryForCurrentUser.path) // "/Users/gorloff"

Это сработает только на macOS, выдав ошибку компиляции на всём остальном. Это связано с тем, что данная часть API Foundation доступна только на macOS. Как же быть, если мы хотим заставить программу работать в случае отсутствия какого-то API на целевой платформе? Мы воспользуемся условными инструкциями компилятора:

#if !targetEnvironment(simulator)
print(manager.homeDirectoryForCurrentUser.path)
#endif

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

Обратите внимание, что при смене конфигурации для выполнения в верхнем коде меняется и обработка кода. Все конструкции внутри директив компилятора проверяются, даже если никогда не будут выполнены.

Боле правильным будет заменить условие на проверку текущей системы на macOS:

#if os(macOS)
print(manager.homeDirectoryForCurrentUser.path)
#endif

Есть также метод, позволяющий получить домашнюю директорию пользователя по его имени:

#if os(macOS)
print(manager.homeDirectory(forUser: "gorloff")?.path) // Optional("/Users/gorloff")
print(manager.homeDirectory(forUser: "Вячеслав Горлов")?.path) // Optional("/Users/gorloff")
print(manager.homeDirectory(forUser: "root")?.path) // Optional("/var/root")
print(manager.homeDirectory(forUser: "mistress")?.path) // nil
#endif

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

Имя пользователя это не Вячеслав Горлов, хотя macOS и позволяет использовать их взаимозаменяемо. Однако старайтесь использовать истинное латинское имя. Вы можете получить его, запустив терминал


Получение важных директорий

Вы можете получить важные директории для работы использовав метод менеджера url(for:in:appropriateFor:create:):

do {
    let documentsURL = try manager.url(for: .documentDirectory, in: .allDomainsMask, appropriateFor: nil, create: false)
    print(documentsURL)

} catch {
    print(error)
}
  1. FileManager.SearchPathDirectory - перечисление, содержашее привязки к типовым директориям. Мы получаем здесь директорию для документов. На macOS - это общая папка для пользователя, на iOS - используется каждым приложением отдельно. Её цель - хранение долговременных данных. Объектов данного типа крайне много, чтобы на данном этапе мы не останавливались на них, Вы можете прочесть о них подробнее на сайте документации Apple
  2. FileManager.SearchPathDomainMask - маска домена, в которой идёт поиск директории. Удовлетворяет OptionSet, а значит может быть использованы по-одному или в массиве. Выделяют:
    • userDomainMask - отсчёт идёт от домашней директории пользователя ~
    • localDomainMask - место, откуда объекты доступны всем пользователям системы
    • networkDomainMask - место, откуда ресурсы доступны по сети /Network
    • systemDomainMask - предоставляется Apple и не может быть изменена /System
    • allDomainsMask - все домены вместе
  3. Ссылка, которая добавится к новосозданной директории, если в функцию первым аргументом передаётся .itemReplacementDirectory, то есть запрос на создание временной директории.
  4. Следует ли создать директорию, если она не существует? Игнорируется, если сделан запрос на временную директорию.

Мы обернули вызов в do-catch, так как операция потенциально может выбросить исключение. Заметьте, что если Вы попробуете получить с помощью данного способа домашнюю директорию пользователя на iOS, то программа скомпилируется. Просто будет получено исключение.

Дальнейшая работа будет проходить внутри блока do-catch, чтобы отлавливать возможные ошибки.

Аналогично работает метод urls(for:in:) - он возвращает массив со всеми ссылками на директории по заданному домену (первый метод возвращает лишь первую):

let applicationURLs = manager.urls(for: .applicationDirectory, in: .allDomainsMask)
//[file:///Users/gorloff/Applications/, file:///Applications/, file:///Network/Applications/]
print(applicationURLs)

Возвращает ссылки на папки с приложениями.