Инженерные статьи CDNMac

Проверяем типы входных данных и временные файлы расширения iOS

Проверяем типы входных данных и временные файлы расширения iOS

Пользователь отправляет изображение из приложения «Фото» в iOS-приложение. Расширение появляется в меню «Поделиться», но после открытия сообщает, что не может прочитать файл. Проверки успешной сборки расширения здесь недостаточно: исходное приложение передаёт NSItemProvider, который может объявлять несколько типов данных, а файл иногда доступен по временному пути только во время вызова обработчика загрузки. Чтобы найти место сбоя, проверяйте отдельно появление расширения, чтение содержимого и передачу результата основному приложению.

Сначала определите контракт входных данных

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

Входные данные Что проверяет расширение Критерий приёмки
Одно изображение Есть ли представление, соответствующее типу изображения Содержимое прочитано, создана запись о передаче
Несколько изображений Типы, объявленные отдельно для каждого вложения Количество вложений равно сумме успешно обработанных и неудачных
Обычный текст Есть ли текстовое представление Текст принят, если это предусмотрено продуктом, либо явно отклонён
Неподдерживаемое содержимое Нет подходящего типа Показана понятная ошибка, пустая запись не создана

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

Сначала изучите объявленные типы, затем выбирайте представление

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

Копируйте файл внутри обработчика

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

Критерий приёмки — не «в обработчике удалось вывести путь», а «после завершения обработчика основное приложение по-прежнему может прочитать копию, к которой у него есть доступ».

Следующий фрагмент Swift показывает ключевую границу при работе с файлом изображения. Если расширению и основному приложению нужен общий доступ к файлам, настройте для них одну App Group и передавайте идентификатор группы через конфигурацию развёртывания. Не вставляйте пример идентификатора непосредственно в проект. Для копии создаётся новое имя файла, чтобы не перезаписать результат другого запроса на передачу.

import Foundation
import UniformTypeIdentifiers

enum ShareCopyError: Error {
    case unsupportedType
    case containerUnavailable
    case missingFile
}

func copySharedImage(
    from provider: NSItemProvider,
    groupID: String,
    completion: @escaping (Result<URL, Error>) -> Void
) {
    guard let typeID = provider.registeredTypeIdentifiers.first(where: {
        UTType($0)?.conforms(to: .image) == true
    }) else {
        completion(.failure(ShareCopyError.unsupportedType))
        return
    }

    provider.loadFileRepresentation(forTypeIdentifier: typeID) { source, error in
        if let error {
            completion(.failure(error))
            return
        }
        guard let source else {
            completion(.failure(ShareCopyError.missingFile))
            return
        }
        guard let container = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: groupID) else {
            completion(.failure(ShareCopyError.containerUnavailable))
            return
        }

        do {
            let directory = container.appendingPathComponent(
                "Incoming", isDirectory: true
            )
            try FileManager.default.createDirectory(
                at: directory, withIntermediateDirectories: true
            )
            let ext = UTType(typeID)?.preferredFilenameExtension ?? "bin"
            let target = directory.appendingPathComponent(
                UUID().uuidString + "." + ext
            )
            try FileManager.default.copyItem(at: source, to: target)
            completion(.success(target))
        } catch {
            completion(.failure(error))
        }
    }
}

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

Добавьте проверки границы чтения в автоматические тесты

Сначала напишите тесты для функций выбора типа и копирования, затем проверьте весь интерфейс передачи. Как минимум охватите тип изображения, неподдерживаемый тип, ошибку загрузки, недоступность общего контейнера, частичный сбой при нескольких вложениях и отсутствие перезаписи для входных файлов с одинаковыми именами. Тестовый объект-заменитель должен позволять управлять моментом вызова обработчика: предоставьте файл внутри обработчика, удалите исходный файл после его завершения и проверьте, что копия всё ещё читается. Так вы проверите жизненный цикл файла, а не просто наличие непустого URL.

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

: "${PROJECT:?set PROJECT to an existing .xcodeproj path}"
: "${SCHEME:?set SCHEME to a shared test scheme}"
: "${SIMULATOR_ID:?set SIMULATOR_ID to an available device ID}"

xcodebuild -list -project "$PROJECT"
xcrun simctl list devices available

RESULT_ROOT="$(mktemp -d)"
xcodebuild -project "$PROJECT" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
  -resultBundlePath "$RESULT_ROOT/share-input.xcresult" \
  test

В CI сохраняйте пакет результатов, а успех или неудачу задания определяйте по коду завершения тестового процесса. Не ограничивайтесь поиском «passed» в журнале: нужно также обнаруживать сбой запуска расширения и отсутствие нужных тестов в схеме. При первом подключении намеренно провалите одно утверждение, чтобы убедиться, что конвейер действительно блокирует дальнейшее выполнение.

Закройте пробелы тестов реальной передачей данных

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

При поиске причины сбоя двигайтесь по порядку. Расширение не появилось — проверьте правило активации. Появилось, но тип не подходит — запишите идентификаторы, предоставленные источником. Загрузка завершилась ошибкой — сохраните её категорию. Расширение прочитало файл, а основное приложение не смогло — проверьте права App Group, выполнение копирования внутри обработчика и то, указывает ли переданный основному приложению путь именно на копию. В тестовых записях оставляйте только необходимые типы, этапы и сведения об ошибках; не записывайте передаваемое содержимое в журналы сборки.

Что проверить перед выпуском

Перед публикацией проверьте четыре пункта: объявленные расширением типы совпадают с реально поддерживаемыми; каждый файл копируется внутри обработчика загрузки; основное приложение читает только копии из каталога, к которому у него есть доступ; отказ, отмена и частичный сбой дают видимый результат. Если конфигурация App Group менялась, повторите передачу на тестовом устройстве после новой установки приложения. Нельзя ограничиваться проверкой контейнера, уже существующего на машине разработчика.

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

Часто задаваемые вопросы

Можно ли передать приложению исходный URL файла из расширения?

Нет. Во время обратного вызова loadFileRepresentation скопируйте файл в доступное приложению место и передайте идентификатор копии.

Что проверить на устройстве после тестов в симуляторе?

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

CDNMac M4

Запустите следующую сборку iOS на выделенном облачном Mac

Выберите подходящую для задачи конфигурацию: M4, 16 ГБ ОЗУ, SSD на 256 ГБ, шесть узлов и четыре срока аренды.

Арендовать